修改产品导入
This commit is contained in:
@@ -63,3 +63,92 @@ export function delProduct(productIds) {
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function getProductImportTask(taskId) {
|
||||
return request({
|
||||
url: '/price/product/import/task/' + taskId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
function pickImportTaskId(res) {
|
||||
if (!res) return null
|
||||
const d = res.data
|
||||
if (d && typeof d === 'object' && d.taskId) return String(d.taskId)
|
||||
if (typeof d === 'string' && d.trim()) return d.trim()
|
||||
if (typeof res.msg === 'string') {
|
||||
const m = res.msg.trim()
|
||||
if (/^[a-f0-9]{32}$/i.test(m)) return m
|
||||
if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(m)) {
|
||||
return m.replace(/-/g, '')
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function pollProductImportTask(taskId, onProgress) {
|
||||
const started = Date.now()
|
||||
const maxMs = 30 * 60 * 1000
|
||||
while (Date.now() - started < maxMs) {
|
||||
const res = await getProductImportTask(taskId)
|
||||
const task = res.data || {}
|
||||
if (typeof onProgress === 'function') {
|
||||
onProgress(task)
|
||||
}
|
||||
if (task.status === 'SUCCESS') {
|
||||
return { task }
|
||||
}
|
||||
if (task.status === 'FAIL') {
|
||||
const err = new Error(task.error || task.message || 'import failed')
|
||||
err.task = task
|
||||
throw err
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
}
|
||||
throw new Error('import timeout')
|
||||
}
|
||||
|
||||
/**
|
||||
* Async all-country product import; poll until done.
|
||||
* @param {File|Blob} file
|
||||
* @param {(task: object) => void} [onProgress]
|
||||
*/
|
||||
export async function importProductExcelAsync(file, onProgress) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const submitRes = await request({
|
||||
url: '/price/product/importData?updateSupport=0',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
const taskId = pickImportTaskId(submitRes)
|
||||
if (!taskId) {
|
||||
throw new Error('missing import taskId')
|
||||
}
|
||||
return pollProductImportTask(taskId, onProgress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Async single-country product import; poll until done.
|
||||
* @param {File|Blob} file
|
||||
* @param {string} countryCode
|
||||
* @param {(task: object) => void} [onProgress]
|
||||
*/
|
||||
export async function importProductCountryExcelAsync(file, countryCode, onProgress) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const params = new URLSearchParams()
|
||||
params.set('countryCode', countryCode)
|
||||
const submitRes = await request({
|
||||
url: '/price/product/importCountryData?' + params.toString(),
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
const taskId = pickImportTaskId(submitRes)
|
||||
if (!taskId) {
|
||||
throw new Error('missing import taskId')
|
||||
}
|
||||
return pollProductImportTask(taskId, onProgress)
|
||||
}
|
||||
|
||||
@@ -60,25 +60,24 @@
|
||||
<excel-import-dialog
|
||||
ref="importRef"
|
||||
title="产品 Excel 导入"
|
||||
action="/price/product/importData"
|
||||
width="560px"
|
||||
:show-update-support="false"
|
||||
template-url="/templates/product/product-import-all-template.xlsx"
|
||||
template-file-name="产品多国家价表导入模板"
|
||||
:import-desc="allImportDesc"
|
||||
:submit-handler="runImportAllAsync"
|
||||
@success="() => tableRef.refresh()"
|
||||
/>
|
||||
<excel-import-dialog
|
||||
ref="countryImportRef"
|
||||
title="按国家导入"
|
||||
action="/price/product/importCountryData"
|
||||
width="560px"
|
||||
:show-update-support="false"
|
||||
:extra-query="countryImportQuery"
|
||||
: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>
|
||||
@@ -96,6 +95,21 @@
|
||||
</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>
|
||||
|
||||
@@ -105,7 +119,7 @@ import ProductFormDialog from './components/ProductFormDialog.vue'
|
||||
import ProductCountryDrawer from './components/ProductCountryDrawer.vue'
|
||||
import ProductSurchargeDrawer from './components/ProductSurchargeDrawer.vue'
|
||||
import ProductExpiredDrawer from './components/ProductExpiredDrawer.vue'
|
||||
import { listProduct } from '@/api/price/product'
|
||||
import { listProduct, importProductExcelAsync, importProductCountryExcelAsync } from '@/api/price/product'
|
||||
import { listRegionOptions } from '@/api/data/region'
|
||||
|
||||
const { bms_product_effect_status } = useDict('bms_product_effect_status')
|
||||
@@ -120,15 +134,18 @@ const importRef = ref()
|
||||
const countryImportRef = ref()
|
||||
const importCountryCode = ref('')
|
||||
const regionOptions = ref([])
|
||||
const countryImportQuery = computed(() => ({ countryCode: importCountryCode.value }))
|
||||
const importProgressVisible = ref(false)
|
||||
const importProgressPercent = ref(0)
|
||||
const importProgressMessage = ref('')
|
||||
const importProgressStatus = ref(undefined)
|
||||
const allImportDesc = [
|
||||
'适用多国家价表(B2C 小包价表等),按目录 sheet 匹配国家导入。',
|
||||
'生效日期必填。新版导入时上一版失效日=本次生效日,复制上一版后按国家/分区/重量段匹配:命中更新,未命中插入。',
|
||||
'生效日期必填。产品+国家有新生效日时,先失效该国家下全部旧重量段,再插入 Excel 重量段。',
|
||||
'请在弹窗底部下载模板,按模板格式整理后上传。相同产品代码+生效日期不覆盖主数据。'
|
||||
]
|
||||
const countryImportDesc = [
|
||||
'选择目的国家后上传专线成本价表,目录中的产品都会记到该国家。',
|
||||
'生效日期必填。新版会复制上一版全部国家明细再与导入行匹配更新/插入;其他国家行会转结到新版。',
|
||||
'生效日期必填。产品+国家有新生效日时,先失效该国家下全部旧重量段,再插入 Excel 重量段。',
|
||||
'邮编、赔偿标准等 sheet 不导入。可在弹窗底部下载模板。'
|
||||
]
|
||||
|
||||
@@ -138,7 +155,6 @@ const columns = [
|
||||
{ prop: 'serviceLevel', label: '时效等级', width: 90, align: 'center' },
|
||||
{ prop: 'cargoAttr', label: '走货属性', minWidth: 120, slot: 'cargoAttr' },
|
||||
{ prop: 'destRegion', label: '目的区域', width: 90, align: 'center' },
|
||||
// { prop: 'taxMode', label: '税模式', width: 90, align: 'center', dict: 'bms_tax_mode' },
|
||||
{ prop: 'effectiveDate', label: '生效日期', width: 120, align: 'center' },
|
||||
{ prop: 'expireDate', label: '失效日期', width: 120, align: 'center' },
|
||||
{ prop: 'countryCount', label: '国家数', width: 80, align: 'center' },
|
||||
@@ -202,8 +218,80 @@ function validateCountryImport() {
|
||||
}
|
||||
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('请先选择文件')
|
||||
}
|
||||
return runImportWithProgress((onProgress) => importProductExcelAsync(raw, onProgress))
|
||||
}
|
||||
|
||||
async function runImportCountryAsync(uploadFile) {
|
||||
const raw = uploadFile && (uploadFile.raw || uploadFile)
|
||||
if (!raw) {
|
||||
throw new Error('请先选择文件')
|
||||
}
|
||||
if (!importCountryCode.value) {
|
||||
throw new Error('请先选择目的国家')
|
||||
}
|
||||
const code = importCountryCode.value
|
||||
return runImportWithProgress((onProgress) => importProductCountryExcelAsync(raw, code, 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>
|
||||
|
||||
Reference in New Issue
Block a user