Files
BMS/bms-front/src/views/price/set/utils/priceSet.js
T
2026-10-28 21:42:45 +08:00

480 lines
14 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.
/**
* 价格套餐 · 纯函数与数据工厂
*
* 只做数据转换,不依赖组件实例 / 字典 / 接口,
* create.vue、detail.vue 及 set/components 下的组件共用。
*/
import { parseTime } from '@/utils/bms'
/** 计价类型枚举 */
export const SET_TYPE = {
FIXED: 'FIXED',
DISCOUNT: 'DISCOUNT'
}
/**
* Version create-price mode
* PULL: from product library
* IMPORT: Excel + preload latest prior version
* IMPORT_FRESH: Excel only, no prior version data
*/
export const PRICE_BUILD_MODE = {
PULL: 'PULL',
IMPORT: 'IMPORT',
IMPORT_FRESH: 'IMPORT_FRESH'
}
export function isPullBuildMode(mode) {
return mode === PRICE_BUILD_MODE.PULL
}
export function isImportBuildMode(mode) {
return mode === PRICE_BUILD_MODE.IMPORT
}
export function isImportFreshBuildMode(mode) {
return mode === PRICE_BUILD_MODE.IMPORT_FRESH
}
/** Either Excel mode (with or without prior version) */
export function isExcelBuildMode(mode) {
return isImportBuildMode(mode) || isImportFreshBuildMode(mode)
}
/** 套餐状态枚举:D 待启用 / A 审核中 / E 生效 / R 驳回 / S 已停用 */
export const SET_STATUS = {
DRAFT: 'D',
AUDIT: 'A',
EFFECTIVE: 'E',
REJECT: 'R',
STOPPED: 'S'
}
const STATUS_LABEL = {
D: '待启用',
A: '审核中',
E: '生效',
R: '驳回',
S: '已停用'
}
const STATUS_TAG = {
D: 'warning',
A: 'primary',
E: 'success',
R: 'danger',
S: 'info'
}
/** 默认取值方式 */
export const DEFAULT_ROUND_MODE = 'HALF_UP'
/** version status: 0 draft / 3 pending / 1 active / 2 expired */
export const VER_STATUS = {
DRAFT: '0',
PENDING: '3',
EFFECT: '1',
EXPIRED: '2'
}
export function versionStatusLabel(status) {
const map = {
'0': '草稿',
'3': '待生效',
'1': '生效',
'2': '失效'
}
return map[status] || status || '-'
}
export function versionStatusTag(status) {
const map = { '0': 'warning', '3': 'primary', '1': 'success', '2': 'info' }
return map[status] || 'info'
}
/** True draft only (not pending) */
export function isDraftVersion(row) {
return !!row && String(row.status) === VER_STATUS.DRAFT
}
export function isPendingVersion(row) {
return !!row && String(row.status) === VER_STATUS.PENDING
}
export function isEffectVersion(row) {
return !!row && String(row.status) === VER_STATUS.EFFECT
}
/** Draft or pending: may continue editing price details */
export function isPriceEditableVersion(row) {
return isDraftVersion(row) || isPendingVersion(row)
}
/**
* Build-mode cards stay visible; locked for enabled/expired versions (已生效/已失效).
* Draft / pending / create wizard remain selectable.
*/
export function isBuildModeLockedVersion(rowOrStatus) {
const s = rowOrStatus && typeof rowOrStatus === 'object' ? rowOrStatus.status : rowOrStatus
if (s == null || s === '') return false
const status = String(s)
return status === VER_STATUS.EFFECT || status === VER_STATUS.EXPIRED
}
export function nextVersionNo(versions) {
const seqs = (versions || []).map(v => Number(v.versionSeq) || 0)
const max = seqs.length ? Math.max.apply(null, seqs) : 0
return 'V' + (max + 1)
}
function versionHasProducts(v) {
return !!(v && (v.products || []).some(p => p && p.productCode))
}
/**
* Latest prior version for Excel build-price preload (exclude current draft).
* Prefer EFFECT / PENDING / highest versionSeq that actually has products,
* so an empty EFFECT does not hide an older version that still has data.
*/
export function pickLatestPriorVersion(versions, excludePsvId) {
const list = (versions || []).filter(v => {
if (!v || v.psvId == null) return false
if (excludePsvId != null && String(v.psvId) === String(excludePsvId)) return false
return true
})
if (!list.length) return null
const ranked = []
const effect = list.find(v => String(v.status) === VER_STATUS.EFFECT)
if (effect) ranked.push(effect)
const pending = list.find(v => String(v.status) === VER_STATUS.PENDING)
if (pending && (!effect || String(pending.psvId) !== String(effect.psvId))) ranked.push(pending)
const bySeq = list.slice().sort((a, b) => (Number(b.versionSeq) || 0) - (Number(a.versionSeq) || 0))
bySeq.forEach(v => {
if (!ranked.some(x => String(x.psvId) === String(v.psvId))) ranked.push(v)
})
const withProducts = ranked.find(versionHasProducts)
return withProducts || ranked[0] || null
}
/** Package status label */
export function setStatusLabel(set) {
if (set && set.expired) return '已过期'
if (set && set.status && STATUS_LABEL[set.status]) return STATUS_LABEL[set.status]
return set && set.status ? String(set.status) : '-'
}
/** Package status tag type */
export function setStatusTagType(set) {
if (set && set.expired) return 'danger'
if (set && set.status && STATUS_TAG[set.status]) return STATUS_TAG[set.status]
return 'info'
}
/** 明细 / 客户是否可维护:待启用、驳回、生效 */
export function isChildEditable(status) {
return [SET_STATUS.DRAFT, SET_STATUS.REJECT, SET_STATUS.EFFECTIVE].includes(status)
}
/** 日期显示 yyyy-MM-dd */
export function formatDate(val) {
return val ? parseTime(val, '{y}-{m}-{d}') : ''
}
/** 有效期区间文案,结束为空表示长期 */
export function formatDateRange(begin, end) {
return `${formatDate(begin) || '-'} ~ ${end ? formatDate(end) : '长期'}`
}
/** 是否固定价模式 */
export function isFixedType(setType) {
return setType === SET_TYPE.FIXED
}
/** 是否折扣模式 */
export function isDiscountType(setType) {
return setType === SET_TYPE.DISCOUNT
}
/** 计价类型展示文案 */
export function setTypeLabel(setType) {
return isFixedType(setType) ? 'FIXED 固定价' : 'DISCOUNT 折扣'
}
/**
* 数字转字符串,抹掉浮点误差尾巴
* @param {number|string} v
* @returns {string} 0.1 + 0.2 -> '0.3'
*/
export function trimNum(v) {
if (v == null || v === '') return ''
const n = Number(v)
if (Number.isNaN(n)) return String(v)
return String(parseFloat(n.toFixed(6)))
}
/** Weight label: left-open right-closed, e.g. 0.3<W\u22641 */
export function formatWeightLabel(min, max) {
return `${trimNum(min)}<W≤${trimNum(max)}`
}
/** 国家唯一键:优先国家三字码,其次国家名称 */
export function countryKeyOf(row) {
if (!row) return ''
if (row.countryCode) return row.countryCode
if (row.countryName) return 'N:' + row.countryName
return ''
}
/** 明细行的国家 与 产品国家配置行 是否指向同一国家 */
export function matchCountry(detail, item) {
if (detail.countryCode && item.countryCode) {
return detail.countryCode === item.countryCode
}
return !!detail.countryName && item.countryName === detail.countryName
}
/** detail row zone vs product-country row */
export function matchZone(detail, item) {
const detailHas = !!(detail.zoneCode || detail.zoneId)
const itemHas = !!(item.zoneCode || item.zoneId)
if (!detailHas && !itemHas) return true
if (detailHas !== itemHas) return false
if (detail.zoneId != null && item.zoneId != null) return Number(detail.zoneId) === Number(item.zoneId)
if (detail.zoneCode && item.zoneCode) return String(detail.zoneCode) === String(item.zoneCode)
return false
}
/** country + zone match */
export function matchCountryZone(detail, item) {
return matchCountry(detail, item) && matchZone(detail, item)
}
/**
* 国家下拉选项(按产品的国家配置去重)
* @param {Array} rows product_country 行
* @returns {Array<{key, code, name, label}>}
*/
export function buildCountryOptions(rows) {
const map = new Map()
;(rows || []).forEach(item => {
const key = countryKeyOf(item)
if (!key || map.has(key)) return
map.set(key, {
key,
code: item.countryCode || undefined,
name: item.countryName,
label: item.countryCode ? `${item.countryName} ${item.countryCode}` : item.countryName
})
})
return [...map.values()]
}
/**
* 重量段下拉选项(按已选国家过滤去重)
* @param {Array} rows product_country 行
* @param {Object} row 当前明细行(含国家)
* @returns {Array<{value, label, detailId, freightPrice, registrationFee}>}
*/
/**
* Zone options for selected country (from product-country rows).
* @returns {Array<{key, zoneId, zoneCode, zoneName, label}>}
*/
export function buildZoneOptions(rows, row) {
const map = new Map()
;(rows || []).filter(item => matchCountry(row, item)).forEach(item => {
if (!item.zoneCode && item.zoneId == null) return
const key = item.zoneCode || String(item.zoneId)
if (map.has(key)) return
map.set(key, {
key,
zoneId: item.zoneId,
zoneCode: item.zoneCode || undefined,
zoneName: item.zoneName || undefined,
label: item.zoneName || item.zoneCode || key
})
})
return [...map.values()]
}
/** Whether selected country has any zone rows */
export function countryHasZones(rows, row) {
return buildZoneOptions(rows, row).length > 0
}
export function buildWeightOptions(rows, row) {
const list = rows || []
if (countryHasZones(list, row) && !(row && (row.zoneCode || row.zoneId))) {
return []
}
const map = new Map()
list.filter(item => matchCountryZone(row, item)).forEach(item => {
if (item.weightMin == null && item.weightMax == null) return
const value = formatWeightLabel(item.weightMin, item.weightMax)
if (map.has(value)) return
map.set(value, {
value,
label: value,
detailId: item.detailId,
zoneId: item.zoneId,
zoneCode: item.zoneCode,
zoneName: item.zoneName,
freightPrice: item.freightPrice,
registrationFee: item.registrationFee,
weightMin: item.weightMin,
weightMax: item.weightMax,
effectiveDate: item.effectiveDate,
expireDate: item.expireDate
})
})
return [...map.values()]
}
/** 附加费金额展示:RATE 转百分比,其余带币种 */
export function formatChargeValue(row) {
if (!row || row.chargeValue == null) return '-'
const v = Number(row.chargeValue)
if (row.chargeType === 'RATE') return (v * 100).toFixed(2) + '%'
return v + ' ' + (row.currency || 'CNY')
}
/** 附加费作用范围文案 */
export function surchargeScopeLabel(s) {
// 主表(prc_surcharge)数据:country_code 有值 = 该国专属(计费归属),空 = 全部国家
if (s.detailId == null && s.psdId == null) {
return s.countryCode ? `国家 ${s.countryCode}` : '全部国家'
}
const w = s.weightMin != null && s.weightMax != null ? `${s.weightMin}-${s.weightMax}kg` : '-'
return `${s.countryName || '-'} · ${w}`
}
/**
* 产品关联附加费(prc_surcharge_rel)是否作用于该明细行
* 产品关联:国家 + productCountryId;SALES:detailId=psdId
*/
export function surchargeAppliesToRow(rel, row) {
if (!rel || !row) return false
if (rel.countryCode && row.countryCode
&& String(rel.countryCode).toUpperCase() !== String(row.countryCode).toUpperCase()) {
return false
}
// SALES rel: detail_id = psd_id
if (rel.priceType === 'SALES') {
if (rel.detailId != null && row.psdId != null) {
return Number(rel.detailId) === Number(row.psdId)
}
return true
}
// PRODUCT / REDLINE: product_country detail or product-level
if (rel.detailId == null) return true
return row.productCountryId != null && Number(rel.detailId) === Number(row.productCountryId)
}
/** 附加费标签文案 */
export function surchargeTagLabel(s) {
return `${s.scName} ${formatChargeValue(s)}`
}
/** 新增一条空明细行 */
export function createEmptyDetailRow() {
return {
productCode: undefined,
productName: undefined,
countryCode: undefined,
countryName: undefined,
_countryKey: undefined,
zoneId: undefined,
zoneCode: undefined,
zoneName: undefined,
weightLabel: undefined,
weightMin: undefined,
weightMax: undefined,
productCountryId: undefined,
fvValue: undefined,
fvRegistrationFee: undefined,
/** product default freight when copied from product_country */
_baseFreight: undefined,
/** product default registration fee */
_baseReg: undefined,
discountValue: undefined,
roundMode: DEFAULT_ROUND_MODE,
effectiveDate: undefined,
expireDate: undefined
}
}
/** 明细行 -> 可编辑行(补齐取值方式与国家键) */
export function toEditableDetailRow(row) {
return {
...row,
roundMode: row.roundMode || DEFAULT_ROUND_MODE,
_countryKey: countryKeyOf(row),
_baseFreight: row._baseFreight != null ? row._baseFreight : undefined,
_baseReg: row._baseReg != null ? row._baseReg : undefined
}
}
/** FIXED row changed vs product default */
export function isDetailPriceChanged(row) {
if (!row) return false
if (row._baseFreight != null && Number(row.fvValue) !== Number(row._baseFreight)) return true
if (row._baseReg != null && Number(row.fvRegistrationFee) !== Number(row._baseReg)) return true
return false
}
/** 新增一条空客户绑定行 */
export function createEmptyCustomerRow({ effectiveDate, expireDate, seq = 1 } = {}) {
return {
customerId: undefined,
customerCode: undefined,
customerName: '',
bindPriority: seq,
prBegindate: effectiveDate,
prEnddate: expireDate
}
}
/** 新建套餐的基本信息默认值 */
export function createEmptyBasicInfo() {
return {
priceSetName: '',
setType: SET_TYPE.FIXED,
defaultDiscount: 88,
priceBuildMode: undefined,
effectiveDate: undefined,
expireDate: undefined,
priceSetNote: ''
}
}
/** 明细行 -> 提交后端的 payload */
export function toDetailPayload(d) {
return {
psdId: d.psdId || undefined,
psvId: d.psvId || undefined,
productCode: d.productCode,
productName: d.productName,
countryCode: d.countryCode || undefined,
countryName: d.countryName || undefined,
zoneId: d.zoneId || undefined,
zoneCode: d.zoneCode || undefined,
zoneName: d.zoneName || undefined,
weightLabel: d.weightLabel || undefined,
weightMin: d.weightMin,
weightMax: d.weightMax,
productCountryId: d.productCountryId || undefined,
fvValue: d.fvValue,
fvRegistrationFee: d.fvRegistrationFee,
discountValue: d.discountValue,
roundMode: d.roundMode || DEFAULT_ROUND_MODE,
effectiveDate: d.effectiveDate || undefined,
expireDate: d.expireDate || undefined,
/** 勾选保存的附加费:带数组即按勾选落快照,空数组=一条都不选 */
surcharges: Array.isArray(d.surcharges) ? d.surcharges.map(s => ({ scId: s.scId })) : undefined
}
}
/** 客户绑定行 -> 提交后端的 payload */
export function toCustomerPayload(c) {
return { ...c }
}