/** * 价格套餐 · 参考数据与明细级联 * * 页面内共享一份(在父页面调用一次,作为 prop 传给各步骤组件), * 避免产品 / 产品国家 / 附加费 被重复请求。 */ import { listProductOptions, listProductCountries } from '@/api/price/product' import { listSurchargeByProduct, listSurchargeOptions } from '@/api/price/surcharge' import { buildCountryOptions, buildZoneOptions, buildWeightOptions, countryHasZones, countryKeyOf, formatWeightLabel, matchCountryZone, surchargeAppliesToRow, toEditableDetailRow } from '../utils/priceSet' /** * 产品 / 产品国家 / 附加费 参考数据(带缓存) */ export function usePriceSetRefData() { /** 产品下拉选项 */ const productOptions = ref([]) /** productCode -> product_country 行 */ const countryCache = reactive({}) /** productCode -> 进行中的国家明细请求(去重,避免 hydrate 按行打爆接口) */ const countryPending = {} /** productCode -> 有效附加费关联 */ const surchargeCache = reactive({}) /** productCode -> 进行中的请求 */ const surchargePending = {} /** 价格基础 → 附加费配置 主表全量(勾选弹窗数据源,页面内共享一份) */ const allSurchargeList = ref(null) let allSurchargePending = null const productMap = computed(() => new Map(productOptions.value.map(p => [p.value, p]))) /** 加载可选产品(已生效),保留列表展示字段 */ function loadProducts() { return listProductOptions({ status: '1' }).then(res => { productOptions.value = (res.data || []).map(p => ({ value: p.productCode, label: `${p.productCode} ${p.productName || ''}`.trim(), name: p.productName, serviceLevel: p.serviceLevel, cargoAttr: p.cargoAttr, destRegion: p.destRegion, taxMode: p.taxMode, countryCount: p.countryCount })) return productOptions.value }) } /** 产品名称 */ function productNameOf(productCode) { const hit = productMap.value.get(productCode) return hit ? hit.name : productCode } /** product meta for step-3 product list */ function productMetaOf(productCode) { return productMap.value.get(productCode) || null } /** YYYY-MM-DD from datetime / date */ function dateOnly(v) { if (!v) return null return String(v).slice(0, 10) } /** * Country row active at asOf (version effective time). * Window: effective_date <= asOf AND (expire_date is null OR expire_date > asOf) */ function isCountryActiveAt(item, asOf) { const t = dateOnly(asOf) if (!t) return true const eff = dateOnly(item.effectiveDate) const exp = dateOnly(item.expireDate) if (eff && eff > t) return false if (exp && exp <= t) return false return true } /** currently open country rows (not expired) */ function isCountryOpen(item) { if (String(item.status) === '2') return false const exp = dateOnly(item.expireDate) if (!exp) return true const today = dateOnly(new Date().toISOString()) return exp > today } /** * Load product country×weight rows. * Cache keeps full list; pass asOf (version effective_time) to filter the active window. * If asOf match is empty, fall back to currently open rows so step-3 can still show full product prices. * In-flight requests are deduped per productCode (hydrateRow is per-detail-row). */ function ensureCountries(productCode, asOf) { if (!productCode) return Promise.resolve([]) const filterRows = (rows) => { const all = rows || [] if (!asOf) return all.filter(isCountryOpen) const hit = all.filter(item => isCountryActiveAt(item, asOf)) if (hit.length) return hit return all.filter(isCountryOpen) } if (countryCache[productCode]) { return Promise.resolve(filterRows(countryCache[productCode])) } if (countryPending[productCode]) { return countryPending[productCode].then(filterRows) } countryPending[productCode] = listProductCountries(productCode).then(res => { const rows = res.data || [] countryCache[productCode] = rows return rows }).catch(() => { // do not cache failure; allow later retry without stampeding while in-flight return [] }).finally(() => { delete countryPending[productCode] }) return countryPending[productCode].then(filterRows) } /** 确保产品的有效附加费已加载(剔除 OF 燃油,结果缓存 + 请求去重) */ function ensureSurcharges(productCode) { if (!productCode) return Promise.resolve([]) if (surchargeCache[productCode]) return Promise.resolve(surchargeCache[productCode]) if (surchargePending[productCode]) return surchargePending[productCode] surchargePending[productCode] = listSurchargeByProduct(productCode).then(res => { const rows = (res.data || []).filter(item => String(item.ssCode || '').toLowerCase() !== 'of') surchargeCache[productCode] = rows return rows }).catch(() => { // 失败不写缓存,后续可重试(避免一次抖动就让该产品永久显示无附加费) return [] }).finally(() => { delete surchargePending[productCode] }) return surchargePending[productCode] } /** 产品已关联的附加费全量(不按国家/重量段过滤),供套餐勾选使用 */ function allSurchargesOf(productCode) { if (!productCode) return [] return surchargeCache[productCode] || [] } /** 确保附加费配置主表已加载(剔除 OF 燃油,页面内共享一份缓存) */ function ensureAllSurcharges() { if (allSurchargeList.value) return Promise.resolve(allSurchargeList.value) if (!allSurchargePending) { allSurchargePending = listSurchargeOptions().then(res => { // /options 返回 AjaxResult(data),兼容分页接口的 rows 写法 const raw = res.data || res.rows || [] const rows = raw.filter(item => String(item.ssCode || '').toLowerCase() !== 'of') allSurchargeList.value = rows return rows }).catch(() => { // 失败不缓存,允许下次重试;调用方拿空数组也不至于报错中断 return [] }).finally(() => { allSurchargePending = null }) } return allSurchargePending } /** 附加费配置主表全量(需先 ensureAllSurcharges) */ function allSurchargeConfigs() { return allSurchargeList.value || [] } /** 明细行命中的附加费(按产品 + 国家 + 重量段过滤) */ function surchargesOf(row) { if (!row || !row.productCode) return [] return (surchargeCache[row.productCode] || []).filter(s => surchargeAppliesToRow(s, row)) } /** 明细行可选国家 */ function countryOptionsOf(row) { return buildCountryOptions(countryCache[row?.productCode] || []) } function zoneOptionsOf(row) { return buildZoneOptions(countryCache[row?.productCode] || [], row || {}) } function hasZonesOf(row) { return countryHasZones(countryCache[row?.productCode] || [], row || {}) } /** 明细行可选重量段 */ function weightOptionsOf(row) { return buildWeightOptions(countryCache[row?.productCode] || [], row || {}) } return { productOptions, loadProducts, productNameOf, productMetaOf, ensureCountries, ensureSurcharges, allSurchargesOf, ensureAllSurcharges, allSurchargeConfigs, surchargesOf, countryOptionsOf, zoneOptionsOf, hasZonesOf, weightOptionsOf } } /** * 明细行级联:产品 → 国家 → 分区 → 重量段,并带出默认运费 / 挂号费 * * @param {Object} refData usePriceSetRefData() 的返回值(页面内共享同一份) */ export function useDetailCascade(refData) { /** 切换产品后清空下游字段 */ function resetRow(row) { row.countryCode = undefined row.countryName = undefined row._countryKey = undefined row.zoneId = undefined row.zoneCode = undefined row.zoneName = undefined row.weightLabel = undefined row.weightMin = undefined row.weightMax = undefined row.productCountryId = undefined row.fvValue = undefined row.fvRegistrationFee = undefined row.effectiveDate = undefined row.expireDate = undefined } /** 选择产品:回填名称、清空级联、预取附加费与国家配置 */ function onProductChange(row, setType) { row.productName = row.productCode ? refData.productNameOf(row.productCode) : undefined resetRow(row) if (!row.productCode) return refData.ensureSurcharges(row.productCode) if (setType === 'FIXED') refData.ensureCountries(row.productCode) } /** 选择国家:回填国家码 / 名称,清空重量段与价格 */ function onCountryChange(row) { const opt = refData.countryOptionsOf(row).find(c => c.key === row._countryKey) row.countryCode = opt ? opt.code : undefined row.countryName = opt ? opt.name : undefined row.zoneId = undefined row.zoneCode = undefined row.zoneName = undefined row.weightLabel = undefined row.weightMin = undefined row.weightMax = undefined row.productCountryId = undefined row.fvValue = undefined row.fvRegistrationFee = undefined row.effectiveDate = undefined row.expireDate = undefined } function onZoneChange(row) { const opt = refData.zoneOptionsOf(row).find(z => z.key === row.zoneCode) row.zoneId = opt ? opt.zoneId : undefined row.zoneCode = opt ? opt.zoneCode : undefined row.zoneName = opt ? opt.zoneName : undefined row.weightLabel = undefined row.weightMin = undefined row.weightMax = undefined row.productCountryId = undefined row.fvValue = undefined row.fvRegistrationFee = undefined row.effectiveDate = undefined row.expireDate = undefined } /** 选择重量段:绑定产品国家明细,并带出默认运费 / 挂号费 */ function onWeightChange(row) { const opt = refData.weightOptionsOf(row).find(w => w.value === row.weightLabel) if (!opt) { row.productCountryId = undefined row.weightMin = undefined row.weightMax = undefined row.effectiveDate = undefined row.expireDate = undefined return } row.productCountryId = opt.detailId if (opt.zoneId != null) row.zoneId = opt.zoneId if (opt.zoneCode) row.zoneCode = opt.zoneCode if (opt.zoneName) row.zoneName = opt.zoneName if (opt.freightPrice != null) row.fvValue = Number(opt.freightPrice) if (opt.registrationFee != null) row.fvRegistrationFee = Number(opt.registrationFee) row.weightMin = opt.weightMin row.weightMax = opt.weightMax row.effectiveDate = opt.effectiveDate row.expireDate = opt.expireDate } /** hydrate detail row; normalize weight label to min { if (!row.weightLabel) return const hit = rows.find(item => { if (row.productCountryId != null && Number(item.detailId) === Number(row.productCountryId)) return true if (!matchCountryZone(row, item)) return false return formatWeightLabel(item.weightMin, item.weightMax) === row.weightLabel || (row.weightLabel.includes(String(item.weightMin)) && row.weightLabel.includes(String(item.weightMax))) }) if (!hit) return row.weightLabel = formatWeightLabel(hit.weightMin, hit.weightMax) row._countryKey = countryKeyOf(hit) row.productCountryId = hit.detailId if (!row.countryCode) row.countryCode = hit.countryCode if (!row.countryName) row.countryName = hit.countryName if (hit.zoneId != null) row.zoneId = hit.zoneId if (hit.zoneCode) row.zoneCode = hit.zoneCode if (hit.zoneName) row.zoneName = hit.zoneName row.weightMin = hit.weightMin row.weightMax = hit.weightMax row.effectiveDate = hit.effectiveDate row.expireDate = hit.expireDate if (row._baseFreight == null && hit.freightPrice != null) row._baseFreight = Number(hit.freightPrice) if (row._baseReg == null && hit.registrationFee != null) row._baseReg = Number(hit.registrationFee) }) return row } return { resetRow, onProductChange, onCountryChange, onZoneChange, onWeightChange, hydrateRow } }