376 lines
14 KiB
Vue
376 lines
14 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<el-alert
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
class="mb16"
|
||
title="导入前须先选择生效时间;红线价与国家明细均按此时间落库,上一版产品/明细失效时间设为该时间。多国家价表用「全部导入」,单国专线成本价用「按国家导入」。相同产品代码+生效日期不覆盖主数据。"
|
||
/>
|
||
|
||
<basic-table
|
||
ref="tableRef"
|
||
:extra-params="extraParams"
|
||
:columns="columns"
|
||
:search-columns="searchColumns"
|
||
:query="queryList"
|
||
row-key="productId"
|
||
>
|
||
<template #toolbar="{ selection }">
|
||
<el-col :span="1.5">
|
||
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['price:redline:add']">新建红线价</el-button>
|
||
</el-col>
|
||
<el-col :span="1.5">
|
||
<el-dropdown @command="handleImportCommand">
|
||
<el-button type="info" plain icon="Upload" v-hasPermi="['price:redline:import']">
|
||
导入 Excel<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||
</el-button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item command="all">全部导入(多国家价表)</el-dropdown-item>
|
||
<el-dropdown-item command="country">按国家导入(单国专线成本价)</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</el-col>
|
||
<el-col :span="1.5">
|
||
<el-button plain icon="Clock" @click="expiredRef.open()" v-hasPermi="['price:redline:list']">失效价格</el-button>
|
||
</el-col>
|
||
</template>
|
||
|
||
<template #col-cargoAttr="{ row }">
|
||
<el-tag type="primary" size="small" effect="plain">{{ row.cargoAttr || '-' }}</el-tag>
|
||
</template>
|
||
|
||
<template #col-status="{ row }">
|
||
<dict-tag :options="bms_product_effect_status" :value="row.status" />
|
||
</template>
|
||
|
||
<template #col-action="{ row }">
|
||
<el-button link type="primary" title="价格" icon="Document" @click="excelFileRef.open(row)" v-hasPermi="['price:redline:query']"></el-button>
|
||
<el-button link type="primary" title="红线价明细" icon="View" @click="countryRef.open(row)" v-hasPermi="['price:redline:query']"></el-button>
|
||
<el-button link type="primary" title="附加费" icon="Money" @click="surchargeRef.open(row)" v-hasPermi="['price:surcharge:query']"></el-button>
|
||
<el-button title="编辑" link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:redline:edit']"></el-button>
|
||
</template>
|
||
</basic-table>
|
||
|
||
<redline-form-dialog ref="formRef" @success="() => tableRef.refresh()" />
|
||
<redline-excel-file-drawer ref="excelFileRef" />
|
||
<redline-country-drawer ref="countryRef" />
|
||
<redline-surcharge-drawer ref="surchargeRef" @success="() => tableRef.refresh()" />
|
||
<redline-expired-drawer ref="expiredRef" @view-country="(row) => countryRef.open(row)" />
|
||
<excel-import-dialog
|
||
ref="importRef"
|
||
title="红线价 Excel 导入"
|
||
width="560px"
|
||
:show-update-support="false"
|
||
template-url="/templates/product/product-import-all-template.xlsx"
|
||
template-file-name="产品多国家价表导入模板"
|
||
:import-desc="allImportDesc"
|
||
:before-submit="validateImportEffectiveDate"
|
||
:submit-handler="runImportAllAsync"
|
||
@success="() => tableRef.refresh()"
|
||
>
|
||
<template #extra>
|
||
<el-form label-width="90px">
|
||
<el-form-item label="生效时间" required>
|
||
<el-date-picker
|
||
v-model="importEffectiveDate"
|
||
type="datetime"
|
||
format="YYYY-MM-DD HH:00"
|
||
value-format="YYYY-MM-DD HH:00:00"
|
||
popper-class="redline-import-hour-picker"
|
||
placeholder="请选择生效时间"
|
||
style="width: 100%"
|
||
@change="onImportEffectiveTimeChange"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</template>
|
||
</excel-import-dialog>
|
||
<excel-import-dialog
|
||
ref="countryImportRef"
|
||
title="按国家导入"
|
||
width="560px"
|
||
:show-update-support="false"
|
||
:before-submit="validateCountryImport"
|
||
:import-desc="countryImportDesc"
|
||
template-url="/templates/product/product-import-country-template.xlsx"
|
||
template-file-name="产品按国家导入模板"
|
||
:submit-handler="runImportCountryAsync"
|
||
@success="() => tableRef.refresh()"
|
||
>
|
||
<template #extra>
|
||
<el-form label-width="90px">
|
||
<el-form-item label="生效时间" required>
|
||
<el-date-picker
|
||
v-model="importEffectiveDate"
|
||
type="datetime"
|
||
format="YYYY-MM-DD HH:00"
|
||
value-format="YYYY-MM-DD HH:00:00"
|
||
popper-class="redline-import-hour-picker"
|
||
placeholder="请选择生效时间"
|
||
style="width: 100%"
|
||
@change="onImportEffectiveTimeChange"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="目的国家" required>
|
||
<el-select v-model="importCountryCode" filterable clearable placeholder="请选择国家" style="width: 100%">
|
||
<el-option
|
||
v-for="item in regionOptions"
|
||
:key="item.countryCode2"
|
||
:label="regionLabel(item)"
|
||
:value="item.countryCode2"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
</template>
|
||
</excel-import-dialog>
|
||
|
||
<el-dialog
|
||
v-model="importProgressVisible"
|
||
title="正在导入产品"
|
||
width="420px"
|
||
:close-on-click-modal="false"
|
||
:close-on-press-escape="false"
|
||
:show-close="false"
|
||
append-to-body
|
||
>
|
||
<div class="import-progress">
|
||
<el-progress :percentage="importProgressPercent" :status="importProgressStatus" />
|
||
<div class="import-progress-msg">{{ importProgressMessage }}</div>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup name="PriceProduct">
|
||
import ExcelImportDialog from '@/components/ExcelImportDialog'
|
||
import RedlineFormDialog from './components/RedlineFormDialog.vue'
|
||
import RedlineExcelFileDrawer from './components/RedlineExcelFileDrawer.vue'
|
||
import RedlineCountryDrawer from './components/RedlineCountryDrawer.vue'
|
||
import RedlineSurchargeDrawer from './components/RedlineSurchargeDrawer.vue'
|
||
import RedlineExpiredDrawer from './components/RedlineExpiredDrawer.vue'
|
||
import { listRedline, importRedlineExcelAsync, importRedlineCountryExcelAsync } from '@/api/price/redline'
|
||
import { listRegionOptions } from '@/api/data/region'
|
||
|
||
const { bms_product_effect_status } = useDict('bms_product_effect_status')
|
||
|
||
const extraParams = { statusGroup: 'active' }
|
||
const tableRef = ref()
|
||
const formRef = ref()
|
||
const excelFileRef = ref()
|
||
const countryRef = ref()
|
||
const surchargeRef = ref()
|
||
const expiredRef = ref()
|
||
const importRef = ref()
|
||
const countryImportRef = ref()
|
||
const importCountryCode = ref('')
|
||
const importEffectiveDate = ref('')
|
||
|
||
function pad2(n) {
|
||
return String(n).padStart(2, '0')
|
||
}
|
||
|
||
function formatImportHour(d) {
|
||
if (!d || Number.isNaN(d.getTime())) return undefined
|
||
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
|
||
+ ' ' + pad2(d.getHours()) + ':00:00'
|
||
}
|
||
|
||
function normalizeImportToHour(val) {
|
||
if (val == null || val === '') return val
|
||
if (val instanceof Date) return formatImportHour(val)
|
||
const s = String(val).trim().replace('T', ' ')
|
||
const m = s.match(/^(\d{4}-\d{2}-\d{2})[ ](\d{2})/)
|
||
if (m) return m[1] + ' ' + m[2] + ':00:00'
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s + ' 00:00:00'
|
||
return formatImportHour(new Date(s.replace(/-/g, '/'))) || val
|
||
}
|
||
|
||
function onImportEffectiveTimeChange(val) {
|
||
importEffectiveDate.value = normalizeImportToHour(val) || ''
|
||
}
|
||
const regionOptions = ref([])
|
||
const importProgressVisible = ref(false)
|
||
const importProgressPercent = ref(0)
|
||
const importProgressMessage = ref('')
|
||
const importProgressStatus = ref(undefined)
|
||
const allImportDesc = [
|
||
'适用多国家价表(B2C 小包价表等),按目录 sheet 匹配国家导入。',
|
||
'必选生效时间:产品与 product_country 明细均按此时间计费;上一版产品/明细失效时间设为该时间。',
|
||
'请在弹窗底部下载模板,按模板格式整理后上传。相同产品代码+生效日期不覆盖主数据。'
|
||
]
|
||
const countryImportDesc = [
|
||
'选择目的国家后上传专线成本价表,目录中的产品都会记到该国家。',
|
||
'必选生效时间:产品与 product_country 明细均按此时间计费;上一版产品/明细失效时间设为该时间。',
|
||
'邮编、赔偿标准等 sheet 不导入。可在弹窗底部下载模板。'
|
||
]
|
||
|
||
const columns = [
|
||
{ prop: 'productCode', label: '产品代码', minWidth: 110 },
|
||
{ prop: 'productName', label: '产品名称', minWidth: 140, showOverflowTooltip: true },
|
||
{ prop: 'serviceLevel', label: '时效等级', width: 90, align: 'center' },
|
||
{ prop: 'cargoAttr', label: '走货属性', minWidth: 120, slot: 'cargoAttr' },
|
||
{ prop: 'destRegion', label: '目的区域', width: 90, align: 'center' },
|
||
{ prop: 'effectiveDate', label: '生效时间', width: 160, align: 'center' },
|
||
{ prop: 'expireDate', label: '失效时间', width: 160, align: 'center' },
|
||
{ prop: 'countryCount', label: '国家数', width: 80, align: 'center' },
|
||
{ prop: 'surchargeCount', label: '附加费数', width: 90, align: 'center' },
|
||
{ prop: 'status', label: '状态', width: 90, align: 'center', slot: 'status' },
|
||
{ prop: 'updateNote', label: '更新内容', minWidth: 160, showOverflowTooltip: true },
|
||
{ prop: 'action', label: '操作', width: 240, fixed: 'right', slot: 'action' }
|
||
]
|
||
|
||
const searchColumns = [
|
||
{ prop: 'productCode', label: '产品代码', component: 'input', placeholder: '产品代码' },
|
||
{ prop: 'productName', label: '产品名称', component: 'input', placeholder: '产品名称' }
|
||
]
|
||
|
||
function queryList(params) {
|
||
return listRedline(params)
|
||
}
|
||
|
||
function handleAdd() {
|
||
formRef.value.open()
|
||
}
|
||
|
||
function handleUpdate(row) {
|
||
formRef.value.open(row)
|
||
}
|
||
|
||
function handleImportCommand(command) {
|
||
if (command === 'country') {
|
||
handleCountryImport()
|
||
return
|
||
}
|
||
handleImport()
|
||
}
|
||
|
||
function handleImport() {
|
||
importEffectiveDate.value = ''
|
||
importRef.value.open()
|
||
}
|
||
|
||
function regionLabel(item) {
|
||
const name = item.countryNameCn || item.countryNameEn || ''
|
||
const code = item.countryCode2 || ''
|
||
return name && code ? name + ' (' + code + ')' : (name || code)
|
||
}
|
||
|
||
function loadRegionOptions() {
|
||
if (regionOptions.value.length) return
|
||
listRegionOptions().then(res => {
|
||
regionOptions.value = res.data || []
|
||
})
|
||
}
|
||
|
||
function handleCountryImport() {
|
||
importCountryCode.value = ''
|
||
importEffectiveDate.value = ''
|
||
loadRegionOptions()
|
||
countryImportRef.value.open()
|
||
}
|
||
|
||
function validateImportEffectiveDate() {
|
||
if (!importEffectiveDate.value) {
|
||
return '请先选择生效时间'
|
||
}
|
||
return ''
|
||
}
|
||
|
||
function validateCountryImport() {
|
||
const effErr = validateImportEffectiveDate()
|
||
if (effErr) return effErr
|
||
if (!importCountryCode.value) {
|
||
return '请先选择目的国家'
|
||
}
|
||
return ''
|
||
}
|
||
|
||
function applyImportProgress(t) {
|
||
importProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||
if (t.status === 'PENDING') importProgressMessage.value = '排队中…'
|
||
else if (t.status === 'RUNNING') {
|
||
const m = t.message || ''
|
||
if (m === 'parsing excel') importProgressMessage.value = '正在解析价表…'
|
||
else if (m === 'finishing') importProgressMessage.value = '收尾中…'
|
||
else if (m === 'queued') importProgressMessage.value = '排队中…'
|
||
else if (m.indexOf('importing ') === 0) {
|
||
const rest = m.slice('importing '.length)
|
||
importProgressMessage.value = '正在导入产品 ' + rest
|
||
} else importProgressMessage.value = m || '正在导入…'
|
||
} else if (t.status === 'SUCCESS') {
|
||
importProgressMessage.value = t.message || '导入完成'
|
||
importProgressStatus.value = 'success'
|
||
} else if (t.status === 'FAIL') {
|
||
importProgressMessage.value = t.error || t.message || '导入失败'
|
||
importProgressStatus.value = 'exception'
|
||
}
|
||
}
|
||
|
||
async function runImportWithProgress(runner) {
|
||
importProgressVisible.value = true
|
||
importProgressPercent.value = 0
|
||
importProgressMessage.value = '上传并解析中…'
|
||
importProgressStatus.value = undefined
|
||
try {
|
||
const { task } = await runner(applyImportProgress)
|
||
importProgressPercent.value = 100
|
||
importProgressStatus.value = 'success'
|
||
return (task && task.message) ? task.message : '导入完成'
|
||
} catch (e) {
|
||
importProgressStatus.value = 'exception'
|
||
const tip = (e && e.task && e.task.error) || (e && e.message) || '导入失败'
|
||
importProgressMessage.value = tip
|
||
await new Promise(resolve => setTimeout(resolve, 600))
|
||
throw e
|
||
} finally {
|
||
importProgressVisible.value = false
|
||
}
|
||
}
|
||
|
||
async function runImportAllAsync(uploadFile) {
|
||
const raw = uploadFile && (uploadFile.raw || uploadFile)
|
||
if (!raw) {
|
||
throw new Error('请先选择文件')
|
||
}
|
||
if (!importEffectiveDate.value) {
|
||
throw new Error('请先选择生效时间')
|
||
}
|
||
const eff = importEffectiveDate.value
|
||
return runImportWithProgress((onProgress) => importRedlineExcelAsync(raw, eff, onProgress))
|
||
}
|
||
|
||
async function runImportCountryAsync(uploadFile) {
|
||
const raw = uploadFile && (uploadFile.raw || uploadFile)
|
||
if (!raw) {
|
||
throw new Error('请先选择文件')
|
||
}
|
||
if (!importEffectiveDate.value) {
|
||
throw new Error('请先选择生效时间')
|
||
}
|
||
if (!importCountryCode.value) {
|
||
throw new Error('请先选择目的国家')
|
||
}
|
||
const code = importCountryCode.value
|
||
const eff = importEffectiveDate.value
|
||
return runImportWithProgress((onProgress) => importRedlineCountryExcelAsync(raw, code, eff, onProgress))
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.mb16 { margin-bottom: 16px; }
|
||
.import-progress { padding: 8px 4px 4px; }
|
||
.import-progress-msg {
|
||
margin-top: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
word-break: break-word;
|
||
max-height: 160px;
|
||
overflow: auto;
|
||
}
|
||
</style>
|