修改套餐导入功能

This commit is contained in:
wuweihua
2026-10-10 11:16:03 +08:00
parent 9d7218795a
commit fab2ec7d9c
12 changed files with 578 additions and 127 deletions
@@ -13,11 +13,31 @@
:disabled="(livePersist && !priceSetId) || pulling"
@click="handlePullLibrary"
>拉取产品列表</el-button>
<el-dropdown
v-if="showImportAction && !isModifyImport"
trigger="click"
@command="handleImportCommand"
>
<el-button
type="primary"
size="small"
:disabled="(livePersist && !priceSetId) || pulling || (livePersist && buildModeDirty)"
>
{{ importActionLabel }}
<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-button
v-if="showImportAction"
v-else-if="showImportAction"
type="primary"
size="small"
:disabled="(livePersist && !priceSetId) || pulling || buildModeDirty"
:disabled="(livePersist && !priceSetId) || pulling || (livePersist && buildModeDirty)"
@click="openImport"
>{{ importActionLabel }}</el-button>
</div>
@@ -127,6 +147,32 @@
@success="onImportSuccess"
/>
<excel-import-dialog
ref="countryImportRef"
title="按国家导入"
width="560px"
:show-update-support="false"
:import-desc="countryImportDesc"
:before-submit="validateCountryImport"
:submit-handler="runCountryImportAsync"
@success="onImportSuccess"
>
<template #extra>
<el-form label-width="90px">
<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="progressDialogTitle"
@@ -162,6 +208,7 @@ import {
toDetailPayload
} from '../../utils/priceSet'
import { getPriceSet, listSetDetailByProduct, importSetDetailPricesAsync, pullSetDetailsFromLibraryAsync, previewPullFromLibrary, previewImportDetailPrices, updateSetDetail, updateSetDetailSurchargeAsync, delSetDetail } from '@/api/price/set'
import { listRegionOptions } from '@/api/data/region'
const props = defineProps({
details: { type: Array, required: true },
@@ -219,6 +266,14 @@ const discTitle = ref('编辑价格')
const discValue = ref(undefined)
const discCode = ref('')
const importRef = ref()
const countryImportRef = ref()
const importCountryCode = ref('')
const regionOptions = ref([])
const countryImportDesc = [
'选择目的国家后上传专线成本价表(含「产品目录」),目录中的产品都会记到该国家。',
'按产品+国家覆盖:仅替换已选国家的明细,其它产品/国家保留。',
'邮编、赔偿标准等 sheet 不导入。仅套餐「Excel 导入建价」可用。'
]
const importProgressVisible = ref(false)
const importProgressPercent = ref(0)
const importProgressMessage = ref('')
@@ -251,8 +306,8 @@ const importDesc = computed(() => {
}
return [
'适用威狮B2C小包价表(含「产品目录」)。',
'按产品+国家+分区(有则匹配)+重量段:命中则更新运费/挂号费,未命中则新增套餐明细。',
'仅固定价套餐可导入。'
'按产品+国家覆盖:Excel 中出现的产品+国家会替换已有同范围明细,其它产品/国家保留。',
'仅固定价套餐「导入 Excel 建价」可用;不影响导入修改价格/拉取产品库。'
]
})
@@ -381,6 +436,22 @@ async function chooseBuildMode(mode) {
let localKeySeq = 0
/** Apply preview products/details into parent reactive arrays (create defer mode). */
function mergeLocalProducts(existing, incoming) {
const map = new Map()
for (const p of existing || []) {
if (p && p.productCode) map.set(String(p.productCode).toUpperCase(), { ...p })
}
for (const p of incoming || []) {
if (!p || !p.productCode) continue
const key = String(p.productCode).toUpperCase()
const prev = map.get(key) || {}
const next = { ...prev, ...p }
if (p.pefId == null && prev.pefId != null) next.pefId = prev.pefId
map.set(key, next)
}
return Array.from(map.values())
}
function applyLocalDraft(products, details) {
clearDetailCache()
const hydrated = (details || []).map(d => {
@@ -393,10 +464,12 @@ function applyLocalDraft(products, details) {
return row
})
if (Array.isArray(props.details)) {
props.details.splice(0, props.details.length, ...hydrated)
props.details.splice(0, props.details.length)
hydrated.forEach(row => props.details.push(row))
}
if (Array.isArray(props.products)) {
props.products.splice(0, props.products.length, ...((products || []).map(p => ({ ...p }))))
props.products.splice(0, props.products.length)
;(products || []).forEach(p => props.products.push({ ...p }))
}
hydrated.forEach(d => {
if (!d || !d.productCode) return
@@ -405,74 +478,109 @@ function applyLocalDraft(products, details) {
})
}
async function openImport() {
if (props.buildModeDirty) {
function regionLabel(item) {
const name = (item && (item.countryNameCn || item.countryNameEn)) || ''
const code = (item && item.countryCode2) || ''
return name && code ? name + ' (' + code + ')' : (name || code)
}
function loadRegionOptions() {
if (regionOptions.value.length) return
listRegionOptions().then(res => {
regionOptions.value = res.data || []
})
}
async function canOpenImport() {
// Edit live: mode change must be saved before import. Create draft: import is local preview.
if (props.livePersist && props.buildModeDirty) {
proxy.$modal.msgWarning("请先点「完成」保存建价方式")
return
return false
}
if (props.livePersist && !props.priceSetId) {
proxy.$modal.msgWarning('缺少套餐ID')
return
return false
}
if (!isFixedType(props.setType)) {
proxy.$modal.msgWarning('仅固定价套餐支持导入')
return
return false
}
if (props.livePersist) {
const psvId = await resolvePsvId(true)
if (!psvId) {
proxy.$modal.msgWarning('缺少套餐版本,请先保存基本信息')
return
return false
}
}
return true
}
async function openImport() {
if (!(await canOpenImport())) return
importRef.value && importRef.value.open()
}
async function runImportAsync(uploadFile) {
function handleImportCommand(cmd) {
if (cmd === 'country') openCountryImport()
else openImport()
}
async function openCountryImport() {
if (!(await canOpenImport())) return
importCountryCode.value = ''
loadRegionOptions()
countryImportRef.value && countryImportRef.value.open()
}
function validateCountryImport() {
if (!importCountryCode.value) return '请先选择目的国家'
return ''
}
async function runImportAsync(uploadFile, countryCode) {
const raw = uploadFile && (uploadFile.raw || uploadFile)
if (!raw) {
throw new Error('请先选择文件')
}
progressDialogTitle.value = '正在导入价格'
importProgressVisible.value = true
importProgressPercent.value = 0
importProgressMessage.value = '上传并解析中…'
importProgressStatus.value = undefined
try {
if (!props.livePersist) {
importProgressMessage.value = '正在解析价表…'
importProgressPercent.value = 30
const res = await previewImportDetailPrices(raw, {
updateOnly: isModifyImport.value,
details: isModifyImport.value ? (props.details || []) : undefined
})
const data = res.data || {}
if (data.pefId != null) {
emit('update:pefId', data.pefId)
}
const products = isModifyImport.value ? (props.products || []) : (data.products || [])
applyLocalDraft(products, data.details || [])
importProgressPercent.value = 100
importProgressStatus.value = 'success'
return data.message || '导入完成'
// Do not open a second el-dialog over ExcelImportDialog: closing the overlay
// click-throughs into el-upload and EP crashes (inputRef.value.value = '').
// Excel build-price (IMPORT mode): merge by product+country. Modify-price / other modes unchanged.
const replaceByProductCountry = !isModifyImport.value || !!countryCode
if (!props.livePersist) {
const res = await previewImportDetailPrices(raw, {
updateOnly: isModifyImport.value && !countryCode,
replaceByProductCountry,
countryCode: countryCode || undefined,
details: props.details || []
})
const data = res.data || {}
if (data.pefId != null) {
emit('update:pefId', data.pefId)
}
const { task } = await importSetDetailPricesAsync(
raw,
{ priceSetId: props.priceSetId, psvId: await resolvePsvId(false), updateOnly: isModifyImport.value },
applyBuildProgress
)
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
const products = replaceByProductCountry
? mergeLocalProducts(props.products, data.products || [])
: (isModifyImport.value ? (props.products || []) : (data.products || []))
applyLocalDraft(products, data.details || [])
return data.message || '导入完成'
}
const { task } = await importSetDetailPricesAsync(
raw,
{
priceSetId: props.priceSetId,
psvId: await resolvePsvId(false),
updateOnly: isModifyImport.value && !countryCode,
replaceByProductCountry,
countryCode: countryCode || undefined
}
)
return (task && task.message) ? task.message : '导入完成'
}
async function runCountryImportAsync(uploadFile) {
if (!importCountryCode.value) {
throw new Error('请先选择目的国家')
}
return runImportAsync(uploadFile, importCountryCode.value)
}
function applyBuildProgress(t) {
@@ -316,14 +316,14 @@ export function useDetailCascade(refData) {
// Do NOT prefetch /surcharge/byProduct here: applySetData hydrates every detail row
// on step-1 load; use SALES rels on row.surcharges instead. Fetch on demand in picker.
if (setType !== 'FIXED') return row
// Bulk load from DB already has productCountryId + weight bounds: normalize locally,
// do not fire /countries per detail row (was stampeding thousands of identical requests).
if (row.productCountryId != null && (row.weightMin != null || row.weightMax != null || row.weightLabel)) {
// Excel preview / DB bulk already has country + weight: normalize locally.
// Do not fire /countries per detail row (import can be hundreds of rows).
if (row.productCountryId != null
|| ((row.weightMin != null || row.weightMax != null) && (row.countryCode || row.countryName))) {
if (row.weightMin != null || row.weightMax != null) {
row.weightLabel = formatWeightLabel(row.weightMin, row.weightMax)
}
row._countryKey = countryKeyOf(row)
// Keep package prices in fv*; product base is stamped later from product_country.
return row
}
refData.ensureCountries(row.productCode).then(rows => {
+5 -1
View File
@@ -26,7 +26,7 @@
:set-type="form.basic.setType"
:price-build-mode="form.version.priceBuildMode"
@update:price-build-mode="onPriceBuildModeUpdate"
@update:pef-id="v => { draftPefId.value = v }"
@update:pef-id="onPefIdUpdate"
:version-no="form.version.versionNo"
:version-status="form.version.status"
:ref-data="refData"
@@ -338,6 +338,10 @@ function onPsvIdUpdate(psvId) {
if (psvId != null && psvId !== '') form.version.psvId = psvId
}
function onPefIdUpdate(pefId) {
draftPefId.value = pefId
}
/** Step2 chose PULL / IMPORT: edit live-saves mode; create only local + optional preview pull. */
function onPriceBuildModeUpdate(mode) {
if (!mode) return