From c6358608261d28365638f04d19ce5aaf734168c3 Mon Sep 17 00:00:00 2001 From: xiaojibeier Date: Tue, 7 Jul 2026 00:12:27 +0800 Subject: [PATCH] 0010 --- src/api/customerPrice.js | 633 +++++++++- src/config/buildId.json | 4 +- src/data/appChangelog.js | 10 + src/utils/warehouseGlobalTabSchema.js | 53 + src/views/base/WhSrfRdcRightPane.vue | 2 +- src/views/dealer/DealerPriceBook.vue | 531 +++++++-- .../dealer/DealerPriceSpecialPriceDlg.vue | 1040 +++++++++++++++++ 7 files changed, 2103 insertions(+), 170 deletions(-) create mode 100644 src/views/dealer/DealerPriceSpecialPriceDlg.vue diff --git a/src/api/customerPrice.js b/src/api/customerPrice.js index 9adecc7..e4f7945 100644 --- a/src/api/customerPrice.js +++ b/src/api/customerPrice.js @@ -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 }} [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} */ + 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} */ + 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} */ + 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} */ + 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 { - 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 : {}), - } + 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, + }, + ) } /** @@ -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), + } +} diff --git a/src/config/buildId.json b/src/config/buildId.json index edeeaa4..cadd8b8 100755 --- a/src/config/buildId.json +++ b/src/config/buildId.json @@ -1,4 +1,4 @@ { - "buildId": "20260706004", - "generatedAt": "2026-07-06T11:25:37.842Z" + "buildId": "20260707001", + "generatedAt": "2026-07-06T16:11:57.042Z" } \ No newline at end of file diff --git a/src/data/appChangelog.js b/src/data/appChangelog.js index 7193610..574766f 100755 --- a/src/data/appChangelog.js +++ b/src/data/appChangelog.js @@ -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', diff --git a/src/utils/warehouseGlobalTabSchema.js b/src/utils/warehouseGlobalTabSchema.js index f008751..a300d14 100644 --- a/src/utils/warehouseGlobalTabSchema.js +++ b/src/utils/warehouseGlobalTabSchema.js @@ -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) diff --git a/src/views/base/WhSrfRdcRightPane.vue b/src/views/base/WhSrfRdcRightPane.vue index 54cae2b..a8d1409 100755 --- a/src/views/base/WhSrfRdcRightPane.vue +++ b/src/views/base/WhSrfRdcRightPane.vue @@ -3,7 +3,7 @@
可售卖区域 - 可调配仓 + 源采购 RDC
diff --git a/src/views/dealer/DealerPriceBook.vue b/src/views/dealer/DealerPriceBook.vue index c964d05..6e9afbe 100644 --- a/src/views/dealer/DealerPriceBook.vue +++ b/src/views/dealer/DealerPriceBook.vue @@ -84,10 +84,10 @@
物料
分类
承兑
-
返利月
+
返利月
超额绩效(率/额)
超额利润(率/额)
-
议价率
+
客户砍价率
好处费(率/额)
特价
状态
@@ -115,7 +115,10 @@ />
- {{ row.goods_code }} +
@@ -133,14 +136,47 @@
{{ fmtBool(row.is_support_acceptance) }}
-
{{ fmtNum(row.rebate_delay_base_months) }}
-
{{ fmtNum(row.excess_performance_rate) }} / {{ fmtNum(row.excess_performance_amount) }}
-
{{ fmtNum(row.excess_profit_rate) }} / {{ fmtNum(row.excess_profit_amount) }}
-
{{ fmtNum(row.habitual_bargain_ratio) }}
-
{{ fmtNum(row.benefit_rate) }} / {{ fmtNum(row.benefit_fee) }}
+
{{ formatCustomerPriceMonths(row.rebate_delay_base_months) }}
+
+ + + {{ metricSummary(row, 'excess_performance').display }} + {{ metricSummary(row, 'excess_performance').count }}种 + + + + {{ metricSummary(row, 'excess_performance').display }} + +
+
{{ formatCustomerPriceRate(row.excess_profit_rate) }} / {{ formatCustomerPriceAmount(row.excess_profit_amount) }}
+
{{ formatCustomerPriceRate(row.habitual_bargain_ratio) }}
+
+ + + {{ metricSummary(row, 'benefit').display }} + {{ metricSummary(row, 'benefit').count }}种 + + + + {{ metricSummary(row, 'benefit').display }} + +
- 特价 - + {{ fmtBool(row.is_special_price) }}
+ 特殊定价 编辑 删除
@@ -188,7 +225,7 @@

将以下定价参数应用到已选 {{ batchEditRows.length }} 条价格本(客户:{{ activeCompanyLabel }})。

- + + + 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>} */ +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} */ +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() +}) + + diff --git a/src/views/dealer/DealerPriceSpecialPriceDlg.vue b/src/views/dealer/DealerPriceSpecialPriceDlg.vue new file mode 100644 index 0000000..6583038 --- /dev/null +++ b/src/views/dealer/DealerPriceSpecialPriceDlg.vue @@ -0,0 +1,1040 @@ + + + + +