Files
BMS/bms-front/src/views/price/set/composables/usePriceSetRefData.js
T

358 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 价格套餐 · 参考数据与明细级联
*
* 页面内共享一份(在父页面调用一次,作为 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<W\u2264max */
function hydrateRow(d, setType) {
const row = toEditableDetailRow(d)
if (!row.productCode) return row
// 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)) {
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 => {
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 }
}