This commit is contained in:
xiaojibeier
2026-07-07 00:12:27 +08:00
parent b575b9c193
commit c635860826
7 changed files with 2103 additions and 170 deletions
+576 -51
View File
@@ -1,4 +1,5 @@
import request from '@/utils/request'
import { pickGoodsNameFromListedRow } from '@/utils/goodsCategorys'
function unwrapListed(res) {
const data = res?.data
@@ -22,8 +23,8 @@ function unwrapListed(res) {
export function pickCustomerPriceCode(row) {
if (!row || typeof row !== 'object') return ''
const candidates = [
row.price_code,
row.customer_price_code,
row.price_code,
row.price_book_code,
row.priceCode,
row.code,
@@ -45,6 +46,362 @@ function flattenPriceListedRow(raw) {
return raw
}
function toPriceNum(v, fallback = 0) {
if (v == null || v === '') return fallback
const n = Number(v)
return Number.isFinite(n) ? n : fallback
}
function parseJsonObject(raw) {
if (raw == null || raw === '') return null
if (typeof raw === 'object' && !Array.isArray(raw)) return raw
if (typeof raw === 'string') {
const text = raw.trim()
if (!text) return null
try {
const parsed = JSON.parse(text)
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null
} catch {
return null
}
}
return null
}
/** @returns {{ rate: number, amount: number }} */
export function createEmptyExcessPerformance() {
return { rate: 0, amount: 0 }
}
/** @returns {{ rate: number, fee: number }} */
export function createEmptyBenefit() {
return { rate: 0, fee: 0 }
}
/** 超额绩效 JSON{ rate, amount } 或 { [bill_code]: { rate, amount } } */
function isNestedMetricEntry(value) {
return value && typeof value === 'object' && !Array.isArray(value)
&& ('rate' in value || 'amount' in value || 'fee' in value)
}
/** @returns {Array<{ bill_code: string, rate: number, amount: number }>} */
export function parseExcessPerformanceGroups(src) {
const row = src && typeof src === 'object' ? src : {}
const json = parseJsonObject(row.excess_performance)
if (json) {
if (isNestedMetricEntry(json) && !Object.keys(json).some((k) => isNestedMetricEntry(json[k]))) {
return [{
bill_code: '',
rate: toPriceNum(json.rate),
amount: toPriceNum(json.amount),
}]
}
const groups = Object.entries(json)
.filter(([, val]) => isNestedMetricEntry(val))
.map(([bill_code, val]) => ({
bill_code: String(bill_code || '').trim(),
rate: toPriceNum(val.rate),
amount: toPriceNum(val.amount),
}))
if (groups.length) return groups
}
if (row.excess_performance_rate != null || row.excess_performance_amount != null) {
return [{
bill_code: '',
rate: toPriceNum(row.excess_performance_rate),
amount: toPriceNum(row.excess_performance_amount),
}]
}
return []
}
/** @returns {Array<{ bill_code: string, rate: number, fee: number }>} */
export function parseBenefitGroups(src) {
const row = src && typeof src === 'object' ? src : {}
const json = parseJsonObject(row.benefit)
if (json) {
if (isNestedMetricEntry(json) && !Object.keys(json).some((k) => isNestedMetricEntry(json[k]))) {
return [{
bill_code: '',
rate: toPriceNum(json.rate),
fee: toPriceNum(json.fee ?? json.amount),
}]
}
const groups = Object.entries(json)
.filter(([, val]) => isNestedMetricEntry(val))
.map(([bill_code, val]) => ({
bill_code: String(bill_code || '').trim(),
rate: toPriceNum(val.rate),
fee: toPriceNum(val.fee ?? val.amount),
}))
if (groups.length) return groups
}
if (row.benefit_rate != null || row.benefit_fee != null) {
return [{
bill_code: '',
rate: toPriceNum(row.benefit_rate),
fee: toPriceNum(row.benefit_fee),
}]
}
return []
}
/** 列表展示:0.05 → 5% */
export function formatCustomerPriceRate(rate) {
if (rate == null || rate === '') return '—'
const n = Number(rate)
if (!Number.isFinite(n)) return '—'
const pct = n !== 0 && Math.abs(n) <= 1 ? n * 100 : n
const text = Number.isInteger(pct)
? String(pct)
: String(Number(pct.toFixed(4))).replace(/\.?0+$/, '')
return `${text}%`
}
export function formatCustomerPriceMetricAmount(v) {
if (v == null || v === '') return '—'
const n = Number(v)
return Number.isFinite(n) ? String(n) : '—'
}
/** 展示用:金额 + 元 */
export function formatCustomerPriceAmount(v) {
const text = formatCustomerPriceMetricAmount(v)
return text === '—' ? '—' : `${text}`
}
/** 展示用:月数 + 月 */
export function formatCustomerPriceMonths(v) {
const text = formatCustomerPriceMetricAmount(v)
return text === '—' ? '—' : `${text}`
}
/** @param {{ rate?: number, amount?: number, fee?: number }} group */
export function formatCustomerPriceMetricPair(group, amountKey = 'amount') {
if (!group) return '—'
const amt = group[amountKey]
const rateText = formatCustomerPriceRate(group.rate)
const amtText = formatCustomerPriceAmount(amt)
if (rateText === '—' && amtText === '—') return '—'
return `${rateText} / ${amtText}`
}
function metricGroupSignature(group, amountKey) {
return `${group?.rate ?? ''}|${group?.[amountKey] ?? ''}`
}
/**
* 列表单元格:摘要 + 多种悬停
* @param {Array<{ bill_code?: string, rate?: number }>} groups
* @param {{ amountKey?: string, billNameMap?: Record<string, string> }} [opts]
*/
export function summarizeCustomerPriceMetricGroups(groups, opts = {}) {
const amountKey = opts.amountKey || 'amount'
const billNameMap = opts.billNameMap || {}
const list = Array.isArray(groups) ? groups : []
if (!list.length) {
return { display: '—', multi: false, count: 0, tooltip: '' }
}
const fmt = (g) => formatCustomerPriceMetricPair(g, amountKey)
const pickBillLabel = (code) => {
const c = String(code || '').trim()
if (!c) return '默认'
return billNameMap[c] || c
}
if (list.length === 1) {
return { display: fmt(list[0]), multi: false, count: 1, tooltip: '' }
}
const sig0 = metricGroupSignature(list[0], amountKey)
const allSame = list.every((g) => metricGroupSignature(g, amountKey) === sig0)
if (allSame) {
return { display: fmt(list[0]), multi: false, count: list.length, tooltip: '' }
}
const tooltip = list
.map((g) => `${pickBillLabel(g.bill_code)}${fmt(g)}`)
.join('\n')
return {
display: fmt(list[0]),
multi: true,
count: list.length,
tooltip,
}
}
/** 超额绩效 JSON{ rate, amount } */
export function normalizeExcessPerformance(raw) {
const json = parseJsonObject(raw)
if (json) {
const groups = parseExcessPerformanceGroups({ excess_performance: json })
if (groups.length) return { rate: groups[0].rate, amount: groups[0].amount }
}
return createEmptyExcessPerformance()
}
/** 让利 JSON{ rate, fee } */
export function normalizeBenefit(raw) {
const json = parseJsonObject(raw)
if (json) {
const groups = parseBenefitGroups({ benefit: json })
if (groups.length) return { rate: groups[0].rate, fee: groups[0].fee }
}
return createEmptyBenefit()
}
function normalizeExcessPerformanceFromRow(row) {
const groups = parseExcessPerformanceGroups(row)
if (!groups.length) return createEmptyExcessPerformance()
return { rate: groups[0].rate, amount: groups[0].amount }
}
function normalizeBenefitFromRow(row) {
const groups = parseBenefitGroups(row)
if (!groups.length) return createEmptyBenefit()
return { rate: groups[0].rate, fee: groups[0].fee }
}
/** @returns {{ rate: number, amount: number }} */
export function createEmptyExcessPerformanceBucket() {
return { rate: 0, amount: 0 }
}
/** @returns {{ rate: number, fee: number }} */
export function createEmptyBenefitBucket() {
return { rate: 0, fee: 0 }
}
function groupsToPerformanceBuckets(groups) {
/** @type {Record<string, { rate: number, amount: number }>} */
const buckets = {}
for (const g of groups || []) {
const code = String(g.bill_code ?? '').trim()
buckets[code] = {
rate: toPriceNum(g.rate),
amount: toPriceNum(g.amount),
}
}
if (!Object.keys(buckets).length) {
buckets[''] = createEmptyExcessPerformanceBucket()
}
return buckets
}
function groupsToBenefitBuckets(groups) {
/** @type {Record<string, { rate: number, fee: number }>} */
const buckets = {}
for (const g of groups || []) {
const code = String(g.bill_code ?? '').trim()
buckets[code] = {
rate: toPriceNum(g.rate),
fee: toPriceNum(g.fee),
}
}
if (!Object.keys(buckets).length) {
buckets[''] = createEmptyBenefitBucket()
}
return buckets
}
function mergeCustomerPriceBucketKeys(excessBuckets, benefitBuckets) {
const keys = new Set([
...Object.keys(excessBuckets || {}),
...Object.keys(benefitBuckets || {}),
])
for (const key of keys) {
if (!excessBuckets[key]) excessBuckets[key] = createEmptyExcessPerformanceBucket()
if (!benefitBuckets[key]) benefitBuckets[key] = createEmptyBenefitBucket()
}
}
/** 编辑 Tab:仅 JSON 里实际存在的分桶 key(空 key 表示单组,不出 Tab) */
export function listCustomerPriceJsonBucketKeys(excessBuckets, benefitBuckets) {
const ordered = []
const seen = new Set()
const add = (raw) => {
const key = String(raw ?? '').trim()
if (seen.has(key)) return
seen.add(key)
ordered.push(key)
}
for (const k of Object.keys(excessBuckets || {})) add(k)
for (const k of Object.keys(benefitBuckets || {})) add(k)
const named = ordered.filter((k) => k !== '')
return named.length ? named : []
}
/** 编辑保存:buckets → 接口 JSON(有命名 key 则分桶,否则 { rate, amount } */
export function serializeExcessPerformanceBuckets(buckets) {
const src = buckets && typeof buckets === 'object' ? buckets : {}
const namedKeys = Object.keys(src).filter((k) => String(k).trim() !== '')
if (!namedKeys.length) {
const b = src[''] || createEmptyExcessPerformanceBucket()
return { rate: toPriceNum(b.rate), amount: toPriceNum(b.amount) }
}
/** @type {Record<string, { rate: number, amount: number }>} */
const out = {}
for (const code of namedKeys) {
const b = src[code] || createEmptyExcessPerformanceBucket()
out[code] = { rate: toPriceNum(b.rate), amount: toPriceNum(b.amount) }
}
return out
}
/** 编辑保存:buckets → 接口 JSON(有命名 key 则分桶,否则 { rate, fee } */
export function serializeBenefitBuckets(buckets) {
const src = buckets && typeof buckets === 'object' ? buckets : {}
const namedKeys = Object.keys(src).filter((k) => String(k).trim() !== '')
if (!namedKeys.length) {
const b = src[''] || createEmptyBenefitBucket()
return { rate: toPriceNum(b.rate), fee: toPriceNum(b.fee) }
}
/** @type {Record<string, { rate: number, fee: number }>} */
const out = {}
for (const code of namedKeys) {
const b = src[code] || createEmptyBenefitBucket()
out[code] = { rate: toPriceNum(b.rate), fee: toPriceNum(b.fee) }
}
return out
}
/** 表单 → 新增/修改请求体 */
export function buildCustomerPriceWriteBody(form, extras = {}) {
const excess_performance = serializeExcessPerformanceBuckets(form?.excess_performance_buckets)
const benefit = serializeBenefitBuckets(form?.benefit_buckets)
const body = {
is_support_acceptance: !!form?.is_support_acceptance,
rebate_delay_base_months: toPriceNum(form?.rebate_delay_base_months),
excess_performance,
excess_profit_rate: toPriceNum(form?.excess_profit_rate),
excess_profit_amount: toPriceNum(form?.excess_profit_amount),
habitual_bargain_ratio: toPriceNum(form?.habitual_bargain_ratio),
benefit,
is_special_price: !!form?.is_special_price,
is_valid: form?.is_valid == null ? true : !!form?.is_valid,
}
if (extras.sales_policy_type != null) {
body.sales_policy_type = toPriceNum(extras.sales_policy_type)
} else if (form?.sales_policy_type != null) {
body.sales_policy_type = toPriceNum(form.sales_policy_type)
}
if (extras.pos != null) body.pos = toPriceNum(extras.pos)
else if (form?.pos != null) body.pos = toPriceNum(form.pos)
const pc = String(
extras.customer_price_code ?? extras.price_code
?? form?.customer_price_code ?? form?.price_code ?? '',
).trim()
const cc = String(extras.company_code ?? form?.company_code ?? '').trim()
const gc = String(extras.goods_code ?? form?.goods_code ?? '').trim()
if (pc) body.customer_price_code = pc
if (cc) body.company_code = cc
if (gc) body.goods_code = gc
return body
}
/** POST /api/customer/price/listed */
export async function customerPriceListed(body = {}) {
const payload = { ...(body || {}) }
@@ -71,23 +428,19 @@ export function customerPriceBatchAdd(body) {
export function buildCustomerPriceAddItem(company_code, goods_code, overrides = {}) {
const cc = String(company_code ?? '').trim()
const gc = String(goods_code ?? '').trim()
return {
const o = overrides && typeof overrides === 'object' ? overrides : {}
return buildCustomerPriceWriteBody(
{
excess_performance_buckets: groupsToPerformanceBuckets([]),
benefit_buckets: groupsToBenefitBuckets([]),
...o,
},
{
company_code: cc,
goods_code: gc,
sales_policy_type: 0,
is_support_acceptance: false,
rebate_delay_base_months: 0,
excess_performance_rate: 0,
excess_performance_amount: 0,
excess_profit_rate: 0,
excess_profit_amount: 0,
habitual_bargain_ratio: 0,
benefit_rate: 0,
benefit_fee: 0,
is_special_price: false,
is_valid: true,
...(overrides && typeof overrides === 'object' ? overrides : {}),
}
},
)
}
/**
@@ -146,49 +499,32 @@ export function customerPriceBatchModify(body) {
return request.post('/api/customer/price/batch_modify', body)
}
export function buildCustomerPriceModifyPayload(form, { price_code, company_code, goods_code } = {}) {
const pc = String(price_code ?? '').trim()
const body = {
is_support_acceptance: !!form?.is_support_acceptance,
rebate_delay_base_months: Number(form?.rebate_delay_base_months) || 0,
excess_performance_rate: Number(form?.excess_performance_rate) || 0,
excess_performance_amount: Number(form?.excess_performance_amount) || 0,
excess_profit_rate: Number(form?.excess_profit_rate) || 0,
excess_profit_amount: Number(form?.excess_profit_amount) || 0,
habitual_bargain_ratio: Number(form?.habitual_bargain_ratio) || 0,
benefit_rate: Number(form?.benefit_rate) || 0,
benefit_fee: Number(form?.benefit_fee) || 0,
is_special_price: !!form?.is_special_price,
is_valid: !!form?.is_valid,
}
if (pc) body.price_code = pc
const cc = String(company_code ?? form?.company_code ?? '').trim()
const gc = String(goods_code ?? form?.goods_code ?? '').trim()
const cat = String(form?.category_code ?? '').trim()
if (cc) body.company_code = cc
if (gc) body.goods_code = gc
if (cat) body.category_code = cat
return body
export function buildCustomerPriceModifyPayload(form, { customer_price_code, company_code, goods_code, price_code } = {}) {
return buildCustomerPriceWriteBody(form, {
customer_price_code: customer_price_code ?? price_code,
company_code,
goods_code,
})
}
/**
* 修改一条或批量修改价格本
* @param {object[]} rows 含 price_code 的行
* @param {object[]} rows 含 customer_price_code 的行
* @param {object} form 表单字段
*/
export async function persistCustomerPriceModify(rows, form) {
const list = (Array.isArray(rows) ? rows : [])
.map((row) => {
const price_code = pickCustomerPriceCode(row)
if (!price_code) return null
const customer_price_code = pickCustomerPriceCode(row)
if (!customer_price_code) return null
return buildCustomerPriceModifyPayload(form, {
price_code,
customer_price_code,
company_code: row.company_code,
goods_code: row.goods_code,
})
})
.filter(Boolean)
if (!list.length) throw new Error('缺少 price_code,无法修改')
if (!list.length) throw new Error('缺少 customer_price_code,无法修改')
if (list.length === 1) {
await customerPriceModify(list[0])
} else {
@@ -199,8 +535,117 @@ export async function persistCustomerPriceModify(rows, form) {
/** POST /api/customer/price/remove */
export function customerPriceRemove(body) {
const price_code = pickCustomerPriceCode(body)
return request.post('/api/customer/price/remove', { price_code })
const customer_price_code = pickCustomerPriceCode(body)
return request.post('/api/customer/price/remove', { customer_price_code })
}
/** POST /api/customer/price/modify_custom */
export function customerPriceModifyCustom(body) {
return request.post('/api/customer/price/modify_custom', body)
}
/** POST /api/customer/price/remove_custom */
export function customerPriceRemoveCustom(body) {
const customer_price_code = pickCustomerPriceCode(body)
return request.post('/api/customer/price/remove_custom', { customer_price_code })
}
function normalizeCustomerPriceCustom(raw) {
if (raw == null || typeof raw !== 'object') return null
const src = raw
const excess_performance_groups = parseExcessPerformanceGroups(src)
const benefit_groups = parseBenefitGroups(src)
return {
...src,
sales_policy_type: src.sales_policy_type == null ? 0 : toPriceNum(src.sales_policy_type),
is_support_acceptance: src.is_support_acceptance === true
|| src.is_support_acceptance === 1
|| src.is_support_acceptance === '1',
rebate_delay_base_months: toPriceNum(src.rebate_delay_base_months),
excess_performance_groups,
benefit_groups,
excess_performance: normalizeExcessPerformanceFromRow(src),
benefit: normalizeBenefitFromRow(src),
excess_profit_rate: toPriceNum(src.excess_profit_rate),
excess_profit_amount: toPriceNum(src.excess_profit_amount),
habitual_bargain_ratio: toPriceNum(src.habitual_bargain_ratio),
}
}
/** 特殊定价表单默认值(无 custom 时全部为 0) */
export function createEmptyCustomerPriceCustomForm() {
return {
sales_policy_type: 0,
is_support_acceptance: false,
rebate_delay_base_months: 0,
excess_performance_buckets: groupsToPerformanceBuckets([]),
excess_profit_rate: 0,
excess_profit_amount: 0,
habitual_bargain_ratio: 0,
benefit_buckets: groupsToBenefitBuckets([]),
}
}
/** 列表行 custom → 特殊定价表单;custom 为空则全 0 */
export function customerPriceCustomFormFromRow(row) {
const src = row && typeof row === 'object' ? row : {}
const custom = src.custom != null && typeof src.custom === 'object' ? src.custom : null
if (!custom) return createEmptyCustomerPriceCustomForm()
const excess_performance_buckets = groupsToPerformanceBuckets(
Array.isArray(custom.excess_performance_groups) && custom.excess_performance_groups.length
? custom.excess_performance_groups
: parseExcessPerformanceGroups(custom),
)
const benefit_buckets = groupsToBenefitBuckets(
Array.isArray(custom.benefit_groups) && custom.benefit_groups.length
? custom.benefit_groups
: parseBenefitGroups(custom),
)
mergeCustomerPriceBucketKeys(excess_performance_buckets, benefit_buckets)
return {
...createEmptyCustomerPriceCustomForm(),
sales_policy_type: custom.sales_policy_type == null ? 0 : toPriceNum(custom.sales_policy_type),
is_support_acceptance: custom.is_support_acceptance === true
|| custom.is_support_acceptance === 1
|| custom.is_support_acceptance === '1',
rebate_delay_base_months: toPriceNum(custom.rebate_delay_base_months),
excess_performance_buckets,
excess_profit_rate: toPriceNum(custom.excess_profit_rate),
excess_profit_amount: toPriceNum(custom.excess_profit_amount),
habitual_bargain_ratio: toPriceNum(custom.habitual_bargain_ratio),
benefit_buckets,
}
}
/** 特殊定价 → modify_custom 请求体 */
export function buildCustomerPriceCustomWriteBody(form, { customer_price_code } = {}) {
const pc = String(customer_price_code ?? '').trim()
return {
customer_price_code: pc,
sales_policy_type: toPriceNum(form?.sales_policy_type),
is_support_acceptance: !!form?.is_support_acceptance,
rebate_delay_base_months: toPriceNum(form?.rebate_delay_base_months),
excess_performance: serializeExcessPerformanceBuckets(form?.excess_performance_buckets),
excess_profit_rate: toPriceNum(form?.excess_profit_rate),
excess_profit_amount: toPriceNum(form?.excess_profit_amount),
habitual_bargain_ratio: toPriceNum(form?.habitual_bargain_ratio),
benefit: serializeBenefitBuckets(form?.benefit_buckets),
}
}
/** 保存特殊定价(modify_custom */
export async function persistCustomerPriceModifyCustom(row, form) {
const customer_price_code = pickCustomerPriceCode(row)
if (!customer_price_code) throw new Error('缺少 customer_price_code,无法保存自定义')
await customerPriceModifyCustom(buildCustomerPriceCustomWriteBody(form, { customer_price_code }))
}
/** 删除自定义(remove_custom */
export async function persistCustomerPriceRemoveCustom(row) {
const customer_price_code = pickCustomerPriceCode(row)
if (!customer_price_code) throw new Error('缺少 customer_price_code,无法删除自定义')
await customerPriceRemoveCustom({ customer_price_code })
}
function normalizeGoodsCategorys(raw) {
@@ -239,25 +684,105 @@ export function formatCustomerPriceGoodsDisplay(row) {
export function normalizeCustomerPriceRow(raw) {
const src = flattenPriceListedRow(raw)
if (!src || typeof src !== 'object') return null
const price_code = pickCustomerPriceCode(src)
const customer_price_code = pickCustomerPriceCode(src)
const company_code = String(src.company_code ?? '').trim()
const goods_code = String(src.goods_code ?? '').trim()
if (!price_code && !company_code && !goods_code) return null
if (!customer_price_code && !company_code && !goods_code) return null
const goods_categorys_list = normalizeGoodsCategorys(src)
const goods_categorys_text = formatCustomerPriceGoodsDisplay({ goods_categorys_list })
const excess_performance_groups = parseExcessPerformanceGroups(src)
const benefit_groups = parseBenefitGroups(src)
const excess_performance = normalizeExcessPerformanceFromRow(src)
const benefit = normalizeBenefitFromRow(src)
const has_custom = src.custom != null && typeof src.custom === 'object'
const custom = has_custom ? normalizeCustomerPriceCustom(src.custom) : null
return {
...src,
price_code,
customer_price_code,
price_code: customer_price_code,
company_code,
goods_code,
category_code: String(src.category_code ?? '').trim(),
sales_policy_type: src.sales_policy_type == null ? 0 : toPriceNum(src.sales_policy_type),
pos: src.pos == null || src.pos === '' ? null : toPriceNum(src.pos),
excess_performance_groups,
benefit_groups,
excess_performance,
benefit,
has_custom,
custom,
custom_excess_performance_groups: custom?.excess_performance_groups ?? [],
custom_benefit_groups: custom?.benefit_groups ?? [],
excess_profit_rate: toPriceNum(src.excess_profit_rate),
excess_profit_amount: toPriceNum(src.excess_profit_amount),
habitual_bargain_ratio: toPriceNum(src.habitual_bargain_ratio),
goods_categorys_list,
goods_categorys_text,
goods_name: String(
src.goods_name ?? src.goods?.name ?? src.name ?? '',
).trim(),
goods_name: pickGoodsNameFromListedRow(src)
|| String(src.goods_name ?? src.goods?.name ?? src.name ?? '').trim(),
company_name: String(
src.company_name ?? src.customer_name ?? src.full_name ?? '',
).trim(),
}
}
function pickExcessPerformanceGroupsFromRow(row) {
const src = row && typeof row === 'object' ? row : {}
const groups = src.excess_performance_groups
if (Array.isArray(groups) && groups.length) return groups
return parseExcessPerformanceGroups(src)
}
function pickBenefitGroupsFromRow(row) {
const src = row && typeof row === 'object' ? row : {}
const groups = src.benefit_groups
if (Array.isArray(groups) && groups.length) return groups
return parseBenefitGroups(src)
}
/** 编辑表单默认值 */
export function createEmptyCustomerPriceForm() {
return {
company_code: '',
goods_code: '',
category_code: '',
goods_categorys_text: '',
sales_policy_type: 0,
is_support_acceptance: false,
rebate_delay_base_months: 0,
excess_performance_buckets: groupsToPerformanceBuckets([]),
excess_profit_rate: 0,
excess_profit_amount: 0,
habitual_bargain_ratio: 0,
benefit_buckets: groupsToBenefitBuckets([]),
is_special_price: false,
is_valid: true,
pos: null,
}
}
/** 列表行 → 编辑表单(JSON 有几组就加载几组) */
export function customerPriceFormFromRow(row) {
const src = row && typeof row === 'object' ? row : {}
const excess_performance_buckets = groupsToPerformanceBuckets(pickExcessPerformanceGroupsFromRow(src))
const benefit_buckets = groupsToBenefitBuckets(pickBenefitGroupsFromRow(src))
mergeCustomerPriceBucketKeys(excess_performance_buckets, benefit_buckets)
return {
...createEmptyCustomerPriceForm(),
company_code: src.company_code || '',
goods_code: src.goods_code || '',
category_code: src.category_code || '',
goods_categorys_text: src.goods_categorys_text || formatCustomerPriceGoodsDisplay(src) || '',
sales_policy_type: src.sales_policy_type == null ? 0 : toPriceNum(src.sales_policy_type),
is_support_acceptance: src.is_support_acceptance === true || src.is_support_acceptance === 1 || src.is_support_acceptance === '1',
rebate_delay_base_months: toPriceNum(src.rebate_delay_base_months),
excess_performance_buckets,
excess_profit_rate: toPriceNum(src.excess_profit_rate),
excess_profit_amount: toPriceNum(src.excess_profit_amount),
habitual_bargain_ratio: toPriceNum(src.habitual_bargain_ratio),
benefit_buckets,
is_special_price: src.is_special_price === true || src.is_special_price === 1 || src.is_special_price === '1',
is_valid: src.is_valid == null ? true : (src.is_valid === true || src.is_valid === 1 || src.is_valid === '1'),
pos: src.pos == null || src.pos === '' ? null : toPriceNum(src.pos),
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
{
"buildId": "20260706004",
"generatedAt": "2026-07-06T11:25:37.842Z"
"buildId": "20260707001",
"generatedAt": "2026-07-06T16:11:57.042Z"
}
+10
View File
@@ -50,6 +50,16 @@ export const APP_CHANGELOG = [
{
date: '2026-07-06',
items: [
{
icon: 'mdi:warehouse',
kind: 'change',
text: '「仓库管理」RDC 物料右侧面板 Tab「可调配仓」更名为「源采购 RDC」。',
},
{
icon: 'mdi:notebook-outline',
kind: 'add',
text: '「客户管理 → 客户价格本 → 特殊定价」:批量值只读展示主记录,「自定义」单独对接 `modify_custom` / `remove_custom`(确定保存、已有 custom 时可删);交易利润区保留费用票据切换;超额绩效/好处费 JSON 按接口实际 key 动态 Tab,无 custom 默认全 0。',
},
{
icon: 'mdi:fence',
kind: 'change',
+53
View File
@@ -206,6 +206,59 @@ function isRdcEntryEmpty(rdcEntry) {
return bills.every((b) => isRdcBucketEmpty(b))
}
/** @param {unknown} rdcEntry warehouse[rdcCode] */
export function pickWarehouseMetricsForChannel(rdcEntry, sectionKey, channelKey) {
if (!rdcEntry || typeof rdcEntry !== 'object') {
return normalizeChannelMetrics(null)
}
if (isLegacyRdcMetricsBucket(rdcEntry)) {
return normalizeChannelMetrics(rdcEntry[sectionKey]?.[channelKey])
}
let fallback = null
for (const billBucket of Object.values(rdcEntry)) {
if (!isLegacyRdcMetricsBucket(billBucket)) continue
const m = normalizeChannelMetrics(billBucket[sectionKey]?.[channelKey])
if (m.rate != null || m.amount != null) return m
if (!fallback) fallback = m
}
return fallback ?? normalizeChannelMetrics(null)
}
/** @param {unknown} rdcEntry warehouse[rdcCode] */
export function listWarehouseRdcBillKeys(rdcEntry) {
if (!rdcEntry || typeof rdcEntry !== 'object' || Array.isArray(rdcEntry)) return []
if (isLegacyRdcMetricsBucket(rdcEntry)) return []
return Object.keys(rdcEntry)
.map((k) => String(k || '').trim())
.filter(Boolean)
}
/** @param {unknown} rdcEntry warehouse[rdcCode] @param {unknown} billCode */
export function pickWarehouseMetricsForBillChannel(rdcEntry, billCode, sectionKey, channelKey) {
if (!rdcEntry || typeof rdcEntry !== 'object') {
return normalizeChannelMetrics(null)
}
if (isLegacyRdcMetricsBucket(rdcEntry)) {
return normalizeChannelMetrics(rdcEntry[sectionKey]?.[channelKey])
}
const bill = String(billCode ?? '').trim()
if (!bill) {
return pickWarehouseMetricsForChannel(rdcEntry, sectionKey, channelKey)
}
const billBucket = rdcEntry[bill]
if (isLegacyRdcMetricsBucket(billBucket)) {
return normalizeChannelMetrics(billBucket[sectionKey]?.[channelKey])
}
return normalizeChannelMetrics(null)
}
/** 客户 sale_type → 仓库渠道 key(忽略 internal */
export function warehouseChannelKeyFromSaleType(saleType) {
const n = Number(saleType)
if (n === 2) return 'retail'
return 'channel'
}
/** 保存前剔除全空的 RDC / 票据条目 */
export function pruneEmptyWarehouseRdcEntries(bucket) {
const normalized = normalizeWarehouseGoodsBucket(bucket)
+1 -1
View File
@@ -3,7 +3,7 @@
<header class="wh-srf-rdc-right-hd">
<el-radio-group v-model="activeTab" size="small" class="wh-srf-rdc-right-tabs">
<el-radio-button value="region">可售卖区域</el-radio-button>
<el-radio-button value="deploy">可调配仓</el-radio-button>
<el-radio-button value="deploy">源采购 RDC</el-radio-button>
</el-radio-group>
</header>
<div class="wh-srf-rdc-right-body">
+407 -102
View File
@@ -84,10 +84,10 @@
<div class="dpb-col dpb-col--goods dpb-head-col dpb-head-col--goods">物料</div>
<div class="dpb-col dpb-col--cats dpb-head-col dpb-head-col--cats">分类</div>
<div class="dpb-col dpb-col--flag dpb-head-col dpb-head-col--accept">承兑</div>
<div class="dpb-col dpb-col--num dpb-head-col dpb-head-col--rebate">返利月</div>
<div class="dpb-col dpb-col--num dpb-col--rebate dpb-head-col dpb-head-col--rebate">返利月</div>
<div class="dpb-col dpb-col--pair dpb-head-col dpb-head-col--perf">超额绩效<span class="dpb-head-sub">/</span></div>
<div class="dpb-col dpb-col--pair dpb-head-col dpb-head-col--profit">超额利润<span class="dpb-head-sub">/</span></div>
<div class="dpb-col dpb-col--num dpb-head-col dpb-head-col--bargain">价率</div>
<div class="dpb-col dpb-col--num dpb-col--bargain dpb-head-col dpb-head-col--bargain">客户砍价率</div>
<div class="dpb-col dpb-col--pair dpb-head-col dpb-head-col--benefit">好处费<span class="dpb-head-sub">/</span></div>
<div class="dpb-col dpb-col--flag dpb-head-col dpb-head-col--special">特价</div>
<div class="dpb-col dpb-col--flag dpb-head-col dpb-head-col--valid">状态</div>
@@ -115,7 +115,10 @@
/>
</div>
<div class="dpb-col dpb-col--goods">
<span v-if="row.goods_code" class="dpb-code dpb-code--inline">{{ row.goods_code }}</span>
<template v-if="row.goods_code">
<span class="dpb-goods-name">{{ pickGoodsDisplayName(row) }}</span>
<span v-if="pickGoodsDisplayCode(row)" class="dpb-code dpb-code--inline">{{ pickGoodsDisplayCode(row) }}</span>
</template>
<span v-else class="dpb-muted"></span>
</div>
<div class="dpb-col dpb-col--cats dpb-cats-cell">
@@ -133,14 +136,47 @@
<div class="dpb-col dpb-col--flag">
<span class="dpb-flag-text">{{ fmtBool(row.is_support_acceptance) }}</span>
</div>
<div class="dpb-col dpb-col--num">{{ fmtNum(row.rebate_delay_base_months) }}</div>
<div class="dpb-col dpb-col--pair">{{ fmtNum(row.excess_performance_rate) }}<span class="dpb-pair-sep"> / </span>{{ fmtNum(row.excess_performance_amount) }}</div>
<div class="dpb-col dpb-col--pair">{{ fmtNum(row.excess_profit_rate) }}<span class="dpb-pair-sep"> / </span>{{ fmtNum(row.excess_profit_amount) }}</div>
<div class="dpb-col dpb-col--num">{{ fmtNum(row.habitual_bargain_ratio) }}</div>
<div class="dpb-col dpb-col--pair">{{ fmtNum(row.benefit_rate) }}<span class="dpb-pair-sep"> / </span>{{ fmtNum(row.benefit_fee) }}</div>
<div class="dpb-col dpb-col--num dpb-col--rebate">{{ formatCustomerPriceMonths(row.rebate_delay_base_months) }}</div>
<div class="dpb-col dpb-col--pair dpb-metric-cell">
<el-tooltip
v-if="metricSummary(row, 'excess_performance').tooltip"
:content="metricSummary(row, 'excess_performance').tooltip"
placement="top"
:show-after="280"
popper-class="dpb-metric-tooltip"
:popper-options="{ strategy: 'fixed' }"
>
<span class="dpb-metric-line">
<span class="dpb-metric-text">{{ metricSummary(row, 'excess_performance').display }}</span>
<span class="dpb-metric-multi">{{ metricSummary(row, 'excess_performance').count }}</span>
</span>
</el-tooltip>
<span v-else class="dpb-metric-line">
<span class="dpb-metric-text">{{ metricSummary(row, 'excess_performance').display }}</span>
</span>
</div>
<div class="dpb-col dpb-col--pair">{{ formatCustomerPriceRate(row.excess_profit_rate) }}<span class="dpb-pair-sep"> / </span>{{ formatCustomerPriceAmount(row.excess_profit_amount) }}</div>
<div class="dpb-col dpb-col--num dpb-col--bargain">{{ formatCustomerPriceRate(row.habitual_bargain_ratio) }}</div>
<div class="dpb-col dpb-col--pair dpb-metric-cell">
<el-tooltip
v-if="metricSummary(row, 'benefit').tooltip"
:content="metricSummary(row, 'benefit').tooltip"
placement="top"
:show-after="280"
popper-class="dpb-metric-tooltip"
:popper-options="{ strategy: 'fixed' }"
>
<span class="dpb-metric-line">
<span class="dpb-metric-text">{{ metricSummary(row, 'benefit').display }}</span>
<span class="dpb-metric-multi">{{ metricSummary(row, 'benefit').count }}</span>
</span>
</el-tooltip>
<span v-else class="dpb-metric-line">
<span class="dpb-metric-text">{{ metricSummary(row, 'benefit').display }}</span>
</span>
</div>
<div class="dpb-col dpb-col--flag">
<el-tag v-if="isTruthy(row.is_special_price)" size="small" type="warning" effect="plain">特价</el-tag>
<span v-else class="dpb-muted"></span>
<span class="dpb-flag-text">{{ fmtBool(row.is_special_price) }}</span>
</div>
<div class="dpb-col dpb-col--flag">
<el-tag
@@ -152,6 +188,7 @@
</el-tag>
</div>
<div class="dpb-col dpb-col--op">
<span class="dpb-op-link dpb-op-link--special" @click="openSpecialPrice(row)">特殊定价</span>
<span class="dpb-op-link" @click="openEdit(row)">编辑</span>
<span class="dpb-op-link dpb-op-link--muted" @click="removeRow(row)">删除</span>
</div>
@@ -188,7 +225,7 @@
<p v-if="formMode === 'batch'" class="dpb-form-batch-tip">
将以下定价参数应用到已选 <strong>{{ batchEditRows.length }}</strong> 条价格本客户{{ activeCompanyLabel }}
</p>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="88px" size="small" class="dpb-form">
<el-form ref="formRef" :model="form" :rules="formRules" label-width="120px" size="small" class="dpb-form">
<template v-if="formMode !== 'batch'">
<el-row :gutter="16" class="dpb-form-meta-row">
<el-col :span="16">
@@ -234,31 +271,101 @@
<div class="dpb-form-block">
<div class="dpb-form-block-hd">定价参数</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="返利延迟月数">
<div class="dpb-form-param-shell">
<div class="dpb-form-param-top">
<div class="dpb-form-param-pair">
<label class="dpb-form-param-label">返利延迟月数</label>
<div class="dpb-form-rate-field">
<el-input-number
v-model="form.rebate_delay_base_months"
:min="0"
:controls="false"
class="dpb-form-num"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="习惯性议价比率">
<span class="dpb-form-unit"></span>
</div>
</div>
<div class="dpb-form-param-pair">
<label class="dpb-form-param-label">客户砍价率</label>
<div class="dpb-form-rate-field">
<el-input-number
v-model="form.habitual_bargain_ratio"
:controls="false"
class="dpb-form-num"
/>
</el-form-item>
</el-col>
</el-row>
<span class="dpb-form-unit">%</span>
</div>
</div>
</div>
<div class="dpb-form-rate-grid">
<div v-for="block in formRateBlocks" :key="block.key" class="dpb-form-rate-row">
<span class="dpb-form-rate-name">{{ block.label }}</span>
<div v-if="editJsonBucketKeys.length" class="dpb-form-bill-tabs">
<button
v-for="code in editJsonBucketKeys"
:key="code"
type="button"
class="dpb-form-bill-tab"
:class="{ 'is-active': activeEditBillCode === code }"
@click="activeEditBillCode = code"
>
<span class="dpb-form-bill-tab-name">{{ editBillTabLabel(code) }}</span>
</button>
</div>
<template v-if="form.excess_performance_buckets[activeEditBucketKey]">
<div class="dpb-form-param-rate">
<label class="dpb-form-param-label">超额绩效</label>
<div class="dpb-form-rate-fields">
<div class="dpb-form-rate-field">
<span class="dpb-form-rate-field-label"></span>
<el-input-number
v-model="form.excess_performance_buckets[activeEditBucketKey].rate"
:controls="false"
class="dpb-form-num"
/>
<span class="dpb-form-unit">%</span>
</div>
<div class="dpb-form-rate-field">
<span class="dpb-form-rate-field-label"></span>
<el-input-number
v-model="form.excess_performance_buckets[activeEditBucketKey].amount"
:controls="false"
class="dpb-form-num"
/>
<span class="dpb-form-unit"></span>
</div>
</div>
</div>
<div class="dpb-form-param-rate">
<label class="dpb-form-param-label">好处费</label>
<div class="dpb-form-rate-fields">
<div class="dpb-form-rate-field">
<span class="dpb-form-rate-field-label"></span>
<el-input-number
v-model="form.benefit_buckets[activeEditBucketKey].rate"
:controls="false"
class="dpb-form-num"
/>
<span class="dpb-form-unit">%</span>
</div>
<div class="dpb-form-rate-field">
<span class="dpb-form-rate-field-label"></span>
<el-input-number
v-model="form.benefit_buckets[activeEditBucketKey].fee"
:controls="false"
class="dpb-form-num"
/>
<span class="dpb-form-unit"></span>
</div>
</div>
</div>
</template>
<div
v-for="block in formRateBlocks"
:key="block.key"
class="dpb-form-param-rate"
>
<label class="dpb-form-param-label">{{ block.label }}</label>
<div class="dpb-form-rate-fields">
<div class="dpb-form-rate-field">
<span class="dpb-form-rate-field-label"></span>
@@ -267,6 +374,7 @@
:controls="false"
class="dpb-form-num"
/>
<span class="dpb-form-unit">%</span>
</div>
<div class="dpb-form-rate-field">
<span class="dpb-form-rate-field-label"></span>
@@ -275,6 +383,7 @@
:controls="false"
class="dpb-form-num"
/>
<span class="dpb-form-unit"></span>
</div>
</div>
</div>
@@ -287,6 +396,14 @@
</template>
</el-dialog>
<DealerPriceSpecialPriceDlg
v-model="specialPriceDlgVisible"
:company-label="activeCompanyLabel"
:customer="activeCustomer"
:price-row="specialPriceRow"
@saved="onSpecialPriceSaved"
/>
<DealerPriceSceneGoodsDlg
v-model="sceneGoodsDlgVisible"
:customer="activeCustomer"
@@ -310,23 +427,48 @@
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/request'
import { customerListed, pickCustomerDisplayName, pickOrgBlocCode, extractIgnoreSceneCodes } from '@/api/customer'
import { normalizeOrgSaleType } from '@/api/company'
import DealerPriceSceneGoodsDlg from './DealerPriceSceneGoodsDlg.vue'
import DealerPriceCategoryGoodsDlg from './DealerPriceCategoryGoodsDlg.vue'
import DealerPriceSpecialPriceDlg from './DealerPriceSpecialPriceDlg.vue'
import {
customerPriceAdd,
customerPriceListed,
customerPriceRemove,
buildCustomerPriceWriteBody,
createEmptyCustomerPriceForm,
customerPriceFormFromRow,
fetchCustomerPriceGoodsCodes,
formatCustomerPriceAmount,
formatCustomerPriceGoodsDisplay,
formatCustomerPriceMonths,
formatCustomerPriceRate,
listCustomerPriceJsonBucketKeys,
normalizeCustomerPriceRow,
persistCustomerPriceGoods,
persistCustomerPriceModify,
pickCustomerPriceCode,
summarizeCustomerPriceMetricGroups,
} from '@/api/customerPrice'
import {
FINANCE_BILL_TYPE_EXPENSE,
expenseBillTabParts,
normalizeFinanceBillType,
} from '@/utils/financeBillTypes'
import { pickDisplayCode, pickDisplayName } from '@/utils/warehouseOpsDisplay'
defineOptions({ name: 'DealerPriceBook' })
function pickGoodsDisplayName(row) {
return pickDisplayName(row?.goods_name, row?.goods_code)
}
function pickGoodsDisplayCode(row) {
return pickDisplayCode(row?.goods_name, row?.goods_code)
}
const loading = ref(false)
const saving = ref(false)
const loadError = ref('')
@@ -350,12 +492,18 @@ const activeCompanyLabel = computed(() => {
})
const existingPriceGoodsCodes = ref([])
/** @type {import('vue').Ref<Record<string, string>>} */
const expenseBillNameMap = ref({})
const activeEditBillCode = ref('')
const sceneGoodsDlgVisible = ref(false)
const categoryGoodsDlgVisible = ref(false)
const addingPriceBook = ref(false)
const dialogVisible = ref(false)
const specialPriceDlgVisible = ref(false)
/** @type {import('vue').Ref<object|null>} */
const specialPriceRow = ref(null)
const formMode = ref('add') // add | edit | batch
const editPriceCode = ref('')
const batchEditRows = ref([])
@@ -376,9 +524,7 @@ const formCategoryLabel = computed(() => {
})
const formRateBlocks = [
{ key: 'excess_performance', label: '超额绩效', rateKey: 'excess_performance_rate', amountKey: 'excess_performance_amount' },
{ key: 'excess_profit', label: '超额利润', rateKey: 'excess_profit_rate', amountKey: 'excess_profit_amount' },
{ key: 'benefit', label: '好处费', rateKey: 'benefit_rate', amountKey: 'benefit_fee' },
]
const selectableRows = computed(() =>
@@ -426,30 +572,47 @@ function clearSelection() {
selected.value = new Set()
}
const form = reactive(emptyForm())
const form = reactive(createEmptyCustomerPriceForm())
const editJsonBucketKeys = computed(() =>
listCustomerPriceJsonBucketKeys(form.excess_performance_buckets, form.benefit_buckets),
)
const activeEditBucketKey = computed(() => {
const keys = editJsonBucketKeys.value
if (keys.length) {
const cur = String(activeEditBillCode.value || '').trim()
if (cur && keys.includes(cur)) return cur
return keys[0]
}
return ''
})
const formRules = {
goods_code: [{ required: true, message: '请填写物料编码', trigger: 'blur' }],
}
function emptyForm() {
return {
company_code: '',
goods_code: '',
category_code: '',
goods_categorys_text: '',
is_support_acceptance: false,
rebate_delay_base_months: 0,
excess_performance_rate: 0,
excess_performance_amount: 0,
excess_profit_rate: 0,
excess_profit_amount: 0,
habitual_bargain_ratio: 0,
benefit_rate: 0,
benefit_fee: 0,
is_special_price: false,
is_valid: true,
function assignFormFromRow(row) {
Object.assign(form, customerPriceFormFromRow(row), {
company_code: row.company_code || activeCompanyCode.value,
})
pickInitialEditBill()
}
function editBillTabLabel(code) {
const c = String(code || '').trim()
return expenseBillNameMap.value[c] || c || '—'
}
function pickInitialEditBill() {
const keys = editJsonBucketKeys.value
if (!keys.length) {
activeEditBillCode.value = ''
return
}
const cur = String(activeEditBillCode.value || '').trim()
if (cur && keys.includes(cur)) return
activeEditBillCode.value = keys[0]
}
function flattenDealers(list, parentGroup = null, out = []) {
@@ -463,6 +626,7 @@ function flattenDealers(list, parentGroup = null, out = []) {
name: pickCustomerDisplayName(item),
parent_code: String(item?.parent_code ?? '').trim(),
bloc_code: pickOrgBlocCode(item, groupRow),
sale_type: normalizeOrgSaleType(item?.sale_type ?? item?.sales_type),
ignore_scenes: extractIgnoreSceneCodes(item),
raw: item,
groupRaw: groupRow,
@@ -498,41 +662,47 @@ function fmtNum(v) {
return Number.isFinite(n) ? String(n) : '—'
}
function buildFormPayload() {
return {
company_code: String(activeCompanyCode.value || '').trim(),
goods_code: String(form.goods_code || '').trim(),
is_support_acceptance: !!form.is_support_acceptance,
rebate_delay_base_months: Number(form.rebate_delay_base_months) || 0,
excess_performance_rate: Number(form.excess_performance_rate) || 0,
excess_performance_amount: Number(form.excess_performance_amount) || 0,
excess_profit_rate: Number(form.excess_profit_rate) || 0,
excess_profit_amount: Number(form.excess_profit_amount) || 0,
habitual_bargain_ratio: Number(form.habitual_bargain_ratio) || 0,
benefit_rate: Number(form.benefit_rate) || 0,
benefit_fee: Number(form.benefit_fee) || 0,
is_special_price: !!form.is_special_price,
is_valid: !!form.is_valid,
function metricSummary(row, kind) {
const billNameMap = expenseBillNameMap.value
if (kind === 'benefit') {
return summarizeCustomerPriceMetricGroups(row?.benefit_groups, {
amountKey: 'fee',
billNameMap,
})
}
return summarizeCustomerPriceMetricGroups(row?.excess_performance_groups, {
amountKey: 'amount',
billNameMap,
})
}
async function loadExpenseBillNames() {
try {
const res = await request.post('/api/bill/listed', {
page: 1,
limit: 500,
type: FINANCE_BILL_TYPE_EXPENSE,
})
const list = Array.isArray(res?.data)
? res.data
: (Array.isArray(res?.data?.list) ? res.data.list : [])
const map = {}
for (const bill of list) {
if (normalizeFinanceBillType(bill?.type) !== FINANCE_BILL_TYPE_EXPENSE) continue
const code = String(bill?.bill_code ?? '').trim()
if (!code) continue
map[code] = expenseBillTabParts(bill).name
}
expenseBillNameMap.value = map
} catch {
expenseBillNameMap.value = {}
}
}
function assignFormFromRow(row) {
Object.assign(form, emptyForm(), {
company_code: row.company_code || activeCompanyCode.value,
goods_code: row.goods_code || '',
category_code: row.category_code || '',
goods_categorys_text: row.goods_categorys_text || formatCustomerPriceGoodsDisplay(row) || '',
is_support_acceptance: isTruthy(row.is_support_acceptance),
rebate_delay_base_months: Number(row.rebate_delay_base_months) || 0,
excess_performance_rate: Number(row.excess_performance_rate) || 0,
excess_performance_amount: Number(row.excess_performance_amount) || 0,
excess_profit_rate: Number(row.excess_profit_rate) || 0,
excess_profit_amount: Number(row.excess_profit_amount) || 0,
habitual_bargain_ratio: Number(row.habitual_bargain_ratio) || 0,
benefit_rate: Number(row.benefit_rate) || 0,
benefit_fee: Number(row.benefit_fee) || 0,
is_special_price: isTruthy(row.is_special_price),
is_valid: row.is_valid == null ? true : isTruthy(row.is_valid),
function buildFormPayload() {
return buildCustomerPriceWriteBody(form, {
company_code: String(activeCompanyCode.value || '').trim(),
sales_policy_type: 0,
})
}
@@ -689,14 +859,17 @@ function openAdd() {
formMode.value = 'add'
editPriceCode.value = ''
batchEditRows.value = []
Object.assign(form, emptyForm(), { company_code: activeCompanyCode.value })
Object.assign(form, createEmptyCustomerPriceForm(), {
company_code: activeCompanyCode.value,
})
pickInitialEditBill()
dialogVisible.value = true
}
function openEdit(row) {
const price_code = pickCustomerPriceCode(row)
if (!price_code) {
ElMessage.warning('缺少 price_code,无法编辑(请确认列表接口返回 price_code')
ElMessage.warning('缺少 customer_price_code,无法编辑(请确认列表接口返回 customer_price_code')
return
}
formMode.value = 'edit'
@@ -706,6 +879,15 @@ function openEdit(row) {
dialogVisible.value = true
}
function openSpecialPrice(row) {
specialPriceRow.value = row
specialPriceDlgVisible.value = true
}
async function onSpecialPriceSaved() {
await loadData()
}
function openBatchEdit() {
const list = selectedEditableRows.value
if (!list.length) {
@@ -714,7 +896,7 @@ function openBatchEdit() {
}
const missing = list.filter((row) => !pickCustomerPriceCode(row))
if (missing.length) {
ElMessage.warning('部分记录缺少 price_code,无法批量修改')
ElMessage.warning('部分记录缺少 customer_price_code,无法批量修改')
return
}
formMode.value = 'batch'
@@ -755,7 +937,7 @@ async function saveForm() {
async function removeRow(row) {
const price_code = pickCustomerPriceCode(row)
if (!price_code) {
ElMessage.warning('缺少 price_code,无法删除')
ElMessage.warning('缺少 customer_price_code,无法删除')
return
}
const label = row.goods_categorys_text
@@ -771,7 +953,7 @@ async function removeRow(row) {
return
}
try {
await customerPriceRemove({ price_code })
await customerPriceRemove(row)
ElMessage.success('已删除')
await loadData()
} catch (e) {
@@ -781,7 +963,10 @@ async function removeRow(row) {
defineExpose({ openAdd })
onMounted(() => loadCustomerList())
onMounted(() => {
void loadExpenseBillNames()
loadCustomerList()
})
</script>
<style scoped>
@@ -940,7 +1125,7 @@ onMounted(() => loadCustomerList())
}
.dpb-flex-table {
--dpb-cols: 36px 7fr 10fr 4fr 5fr 7fr 7fr 5fr 7fr 4fr 4fr 88px;
--dpb-cols: 36px 12fr 7fr minmax(2.5em, 48px) minmax(3.5em, 68px) 7fr 7fr minmax(5em, 84px) 7fr minmax(2.5em, 48px) 4fr 196px;
flex: 1;
min-height: 0;
display: flex;
@@ -995,6 +1180,22 @@ onMounted(() => loadCustomerList())
.dpb-head-col--valid { border-bottom-color: #67c23a; }
.dpb-head-col--ops { border-bottom-color: #909399; }
.dpb-flex-head .dpb-col--num { text-align: right; padding-right: 20px; }
.dpb-flex-head .dpb-col--rebate,
.dpb-flex-data .dpb-col--rebate {
padding-right: 6px;
white-space: nowrap;
}
.dpb-flex-head .dpb-col--bargain,
.dpb-flex-data .dpb-col--bargain {
padding-right: 6px;
white-space: nowrap;
}
.dpb-flex-head .dpb-col--rebate {
justify-content: flex-end;
}
.dpb-flex-head .dpb-col--bargain {
justify-content: flex-end;
}
.dpb-flex-head .dpb-col--pair { text-align: right; padding-right: 20px; }
.dpb-flex-head .dpb-col--flag,
.dpb-flex-head .dpb-col--op,
@@ -1084,6 +1285,16 @@ onMounted(() => loadCustomerList())
overflow: hidden;
padding-left: 12px;
}
.dpb-goods-name {
font-family: 'DingTalk_JinBuTi_Regular', sans-serif;
font-size: calc(13px + var(--wh-fs-delta, 0px));
font-weight: 700;
color: #1d3461;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.dpb-col--cats {
display: flex;
align-items: center;
@@ -1135,6 +1346,29 @@ onMounted(() => loadCustomerList())
color: #606266;
}
.dpb-pair-sep { color: #c0c4cc; }
.dpb-metric-cell {
overflow: hidden;
min-width: 0;
}
.dpb-metric-line {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 100%;
min-width: 0;
}
.dpb-metric-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.dpb-metric-multi {
flex-shrink: 0;
font-size: calc(11px + var(--wh-fs-delta, 0px));
color: #e6a23c;
font-weight: 600;
}
.dpb-col--flag {
display: flex;
align-items: center;
@@ -1149,7 +1383,9 @@ onMounted(() => loadCustomerList())
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
gap: 8px 18px;
padding: 0 4px;
}
.dpb-name {
@@ -1191,6 +1427,11 @@ onMounted(() => loadCustomerList())
white-space: nowrap;
}
.dpb-op-link:hover { color: #66b1ff; }
.dpb-op-link--special {
color: #e6a23c;
font-weight: 600;
}
.dpb-op-link--special:hover { color: #f0b84d; }
.dpb-op-link--muted { color: #909399; }
.dpb-op-link--muted:hover { color: #606266; }
@@ -1203,11 +1444,15 @@ onMounted(() => loadCustomerList())
}
.dpb-form-num {
width: 100%;
width: 108px;
max-width: 100%;
}
.dpb-form :deep(.el-form-item) {
margin-bottom: 16px;
}
.dpb-form :deep(.el-form-item__label) {
white-space: nowrap;
}
.dpb-form-meta-row :deep(.el-form-item) {
margin-bottom: 12px;
}
@@ -1239,37 +1484,84 @@ onMounted(() => loadCustomerList())
font-weight: 600;
color: #606266;
}
.dpb-form-rate-grid {
.dpb-form-bill-tabs {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 14px;
}
.dpb-form-bill-tab {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fff;
font-size: 12px;
color: #606266;
cursor: pointer;
}
.dpb-form-bill-tab.is-active {
border-color: #409eff;
background: #ecf5ff;
color: #409eff;
font-weight: 600;
}
.dpb-form-bill-tab-tax {
font-size: 11px;
color: #e6a23c;
}
.dpb-form-bill-tab.is-active .dpb-form-bill-tab-tax {
color: #cf9236;
}
.dpb-form-param-shell {
--dpb-param-label-w: 7.5em;
--dpb-param-input-w: 108px;
--dpb-param-gap: 10px;
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 12px;
}
.dpb-form-rate-row {
display: grid;
grid-template-columns: 88px 1fr;
.dpb-form-param-top {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
padding: 10px 12px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 6px;
gap: 8px 32px;
}
.dpb-form-rate-name {
.dpb-form-param-pair {
display: grid;
grid-template-columns: var(--dpb-param-label-w) var(--dpb-param-input-w);
align-items: center;
column-gap: var(--dpb-param-gap);
}
.dpb-form-param-label {
justify-self: start;
text-align: left;
font-size: 13px;
font-weight: 500;
color: #303133;
line-height: 1.4;
color: #606266;
white-space: nowrap;
cursor: default;
}
.dpb-form-param-rate {
display: grid;
grid-template-columns: var(--dpb-param-label-w) 1fr;
align-items: center;
column-gap: var(--dpb-param-gap);
}
.dpb-form-rate-fields {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 20px;
}
.dpb-form-rate-field {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
gap: 6px;
flex: 0 0 auto;
}
.dpb-form-rate-field-label {
flex: 0 0 18px;
@@ -1277,6 +1569,11 @@ onMounted(() => loadCustomerList())
color: #909399;
text-align: center;
}
.dpb-form-unit {
flex-shrink: 0;
font-size: 12px;
color: #909399;
}
.dpb-form-num :deep(.el-input__inner) {
text-align: left;
}
@@ -1290,3 +1587,11 @@ onMounted(() => loadCustomerList())
line-height: 1.5;
}
</style>
<style>
.dpb-metric-tooltip {
white-space: pre-line;
max-width: 320px;
line-height: 1.55;
}
</style>
File diff suppressed because it is too large Load Diff