diff --git a/src/api/costShareListed.js b/src/api/costShareListed.js new file mode 100644 index 0000000..01c2b16 --- /dev/null +++ b/src/api/costShareListed.js @@ -0,0 +1,117 @@ +import request from '@/utils/request' + +function str(v) { + return String(v ?? '').trim() +} + +function unwrapListed(res) { + const root = res?.data ?? res + if (Array.isArray(root)) { + return { list: root, total: root.length } + } + if (root && typeof root === 'object') { + const list = Array.isArray(root.list) + ? root.list + : Array.isArray(root.data) + ? root.data + : Array.isArray(root.rows) + ? root.rows + : [] + const total = Number(root.total ?? root.count ?? list.length) + return { list, total: Number.isFinite(total) && total >= 0 ? total : list.length } + } + return { list: [], total: 0 } +} + +export function pickApiErrorMessage(err, fallback = '操作失败') { + return String( + err?.data?.message + || err?.data?.data?.message + || err?.message + || fallback, + ).trim() || fallback +} + +function buildShareListedPayload(body = {}, filterKeys = []) { + const payload = { ...body } + if (payload.page != null) payload.page = Math.max(1, Number(payload.page) || 1) + if (payload.limit != null) payload.limit = Math.min(50, Math.max(1, Number(payload.limit) || 20)) + for (const key of filterKeys) { + if (!str(payload[key])) delete payload[key] + else payload[key] = str(payload[key]) + } + if (str(payload.start_time)) payload.start_time = str(payload.start_time) + else delete payload.start_time + if (str(payload.end_time)) payload.end_time = str(payload.end_time) + else delete payload.end_time + return payload +} + +function buildAmortizePayload(body = {}) { + const payload = { ...body } + if (str(payload.start_time)) payload.start_time = str(payload.start_time) + else delete payload.start_time + if (str(payload.end_time)) payload.end_time = str(payload.end_time) + else delete payload.end_time + return payload +} + +function createCostShareListed(path, filterKeys) { + return async function listed(body = {}) { + const payload = buildShareListedPayload(body, filterKeys) + const res = await request.post(path, payload) + const { list, total } = unwrapListed(res) + return { list: Array.isArray(list) ? list : [], total } + } +} + +function createCostShareAmortize(path) { + return async function amortize(body = {}) { + return request.post(path, buildAmortizePayload(body)) + } +} + +const GOODS_WAREHOUSE = ['goods_code', 'warehouse_code'] +const GOODS_ONLY = ['goods_code'] + +/** POST /api/cost/fixed_assets/share/listed */ +export const fixedAssetsShareListed = createCostShareListed( + '/api/cost/fixed_assets/share/listed', + GOODS_WAREHOUSE, +) +/** POST /api/cost/fixed_assets/amortize */ +export const fixedAssetsShareAmortize = createCostShareAmortize('/api/cost/fixed_assets/amortize') + +/** POST /api/cost/low_value_consumables/share/listed */ +export const lowValueConsumablesShareListed = createCostShareListed( + '/api/cost/low_value_consumables/share/listed', + GOODS_WAREHOUSE, +) +/** POST /api/cost/low_value_consumables/amortize */ +export const lowValueConsumablesShareAmortize = createCostShareAmortize( + '/api/cost/low_value_consumables/amortize', +) + +/** POST /api/cost/purchasing_labor/share/listed */ +export const purchasingLaborShareListed = createCostShareListed( + '/api/cost/purchasing_labor/share/listed', + GOODS_ONLY, +) +/** POST /api/cost/purchasing_labor/amortize */ +export const purchasingLaborShareAmortize = createCostShareAmortize('/api/cost/purchasing_labor/amortize') + +/** POST /api/cost/sales_doc_labor/share/listed */ +export const salesDocLaborShareListed = createCostShareListed( + '/api/cost/sales_doc_labor/share/listed', + GOODS_ONLY, +) +/** POST /api/cost/sales_doc_labor/amortize */ +export const salesDocLaborShareAmortize = createCostShareAmortize('/api/cost/sales_doc_labor/amortize') + +/** POST /api/cost/warehouse_labor/share/listed */ +export const warehouseLaborShareListed = createCostShareListed( + '/api/cost/warehouse_labor/share/listed', + GOODS_WAREHOUSE, +) +/** POST /api/cost/warehouse_labor/amortize */ +export const warehouseLaborShareAmortize = createCostShareAmortize('/api/cost/warehouse_labor/amortize') diff --git a/src/api/customerPrice.js b/src/api/customerPrice.js index e4f7945..5ab255f 100644 --- a/src/api/customerPrice.js +++ b/src/api/customerPrice.js @@ -68,6 +68,109 @@ function parseJsonObject(raw) { return null } +function parseJsonArray(raw) { + if (Array.isArray(raw)) return raw + if (typeof raw === 'string') { + const text = raw.trim() + if (!text) return null + try { + const parsed = JSON.parse(text) + return Array.isArray(parsed) ? parsed : null + } catch { + return null + } + } + return null +} + +function parseCustomerPriceCustomRaw(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 +} + +/** 特殊售价:兼容 custom 内/行级及历史字段名 */ +export function pickCustomerPriceSpecialAmount(...sources) { + for (const src of sources) { + if (!src || typeof src !== 'object') continue + for (const key of ['special_amount', 'special_price', 'special_sale_price']) { + const v = src[key] + if (v == null || v === '') continue + const n = Number(v) + if (Number.isFinite(n)) return n + } + } + return 0 +} + +/** modify_custom 分桶 key(bill_label + bill_tax) */ +export function customerPriceBillBucketKey({ bill_label, bill_tax, bill_code } = {}) { + const label = String(bill_label ?? '').trim() + const taxRaw = bill_tax + const hasTax = taxRaw != null && taxRaw !== '' && Number.isFinite(Number(taxRaw)) + if (label && hasTax) return `${label}|${Number(taxRaw)}` + if (label) return label + return String(bill_code ?? '').trim() +} + +/** Tab / 悬停展示:优先 bill_label(含税率) */ +export function formatCustomerPriceBillBucketLabel(key, meta) { + const label = String(meta?.bill_label ?? '').trim() + if (label) { + const tax = meta?.bill_tax + if (tax != null && tax !== '' && Number.isFinite(Number(tax))) { + return `${label}(${Number(tax)}%)` + } + return label + } + const k = String(key ?? '').trim() + if (!k) return '默认' + if (k.includes('|')) { + const [name, tax] = k.split('|') + if (name && tax) return `${name}(${tax}%)` + return name || k + } + return k +} + +function mapExcessPerformanceArrayItem(item, idx) { + if (!item || typeof item !== 'object') return null + const bill_label = String(item.bill_label ?? '').trim() + const bill_tax = toPriceNum(item.bill_tax) + const bill_code = customerPriceBillBucketKey({ bill_label, bill_tax }) || String(idx) + return { + bill_code, + bill_label, + bill_tax, + rate: toPriceNum(item.rate), + amount: toPriceNum(item.fees ?? item.amount), + } +} + +function mapBenefitArrayItem(item, idx) { + if (!item || typeof item !== 'object') return null + const bill_label = String(item.bill_label ?? '').trim() + const bill_tax = toPriceNum(item.bill_tax) + const bill_code = customerPriceBillBucketKey({ bill_label, bill_tax }) || String(idx) + return { + bill_code, + bill_label, + bill_tax, + rate: toPriceNum(item.rate), + fee: toPriceNum(item.fees ?? item.fee ?? item.amount), + } +} + /** @returns {{ rate: number, amount: number }} */ export function createEmptyExcessPerformance() { return { rate: 0, amount: 0 } @@ -84,9 +187,13 @@ function isNestedMetricEntry(value) { && ('rate' in value || 'amount' in value || 'fee' in value) } -/** @returns {Array<{ bill_code: string, rate: number, amount: number }>} */ +/** 超额绩效:数组 [{ bill_label, bill_tax, rate, fees }] 或对象分桶 */ export function parseExcessPerformanceGroups(src) { const row = src && typeof src === 'object' ? src : {} + const arr = parseJsonArray(row.excess_performance) + if (arr?.length) { + return arr.map(mapExcessPerformanceArrayItem).filter(Boolean) + } const json = parseJsonObject(row.excess_performance) if (json) { if (isNestedMetricEntry(json) && !Object.keys(json).some((k) => isNestedMetricEntry(json[k]))) { @@ -115,9 +222,13 @@ export function parseExcessPerformanceGroups(src) { return [] } -/** @returns {Array<{ bill_code: string, rate: number, fee: number }>} */ +/** 好处费:数组 [{ bill_label, bill_tax, rate, fees }] 或对象分桶 */ export function parseBenefitGroups(src) { const row = src && typeof src === 'object' ? src : {} + const arr = parseJsonArray(row.benefit) + if (arr?.length) { + return arr.map(mapBenefitArrayItem).filter(Boolean) + } const json = parseJsonObject(row.benefit) if (json) { if (isNestedMetricEntry(json) && !Object.keys(json).some((k) => isNestedMetricEntry(json[k]))) { @@ -186,10 +297,6 @@ export function formatCustomerPriceMetricPair(group, amountKey = 'amount') { return `${rateText} / ${amtText}` } -function metricGroupSignature(group, amountKey) { - return `${group?.rate ?? ''}|${group?.[amountKey] ?? ''}` -} - /** * 列表单元格:摘要 + 多种悬停 * @param {Array<{ bill_code?: string, rate?: number }>} groups @@ -204,24 +311,19 @@ export function summarizeCustomerPriceMetricGroups(groups, opts = {}) { } const fmt = (g) => formatCustomerPriceMetricPair(g, amountKey) - const pickBillLabel = (code) => { - const c = String(code || '').trim() + const pickBillLabel = (g) => { + if (g?.bill_label) return formatCustomerPriceBillBucketLabel(g.bill_code, g) + const c = String(g?.bill_code || '').trim() if (!c) return '默认' - return billNameMap[c] || c + return billNameMap[c] || formatCustomerPriceBillBucketLabel(c, g) } 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)}`) + .map((g) => `${pickBillLabel(g)}:${fmt(g)}`) .join('\n') return { @@ -275,13 +377,15 @@ export function createEmptyBenefitBucket() { } function groupsToPerformanceBuckets(groups) { - /** @type {Record} */ + /** @type {Record} */ const buckets = {} for (const g of groups || []) { - const code = String(g.bill_code ?? '').trim() + const code = String(g.bill_code ?? customerPriceBillBucketKey(g) ?? '').trim() buckets[code] = { rate: toPriceNum(g.rate), amount: toPriceNum(g.amount), + bill_label: String(g.bill_label ?? '').trim(), + bill_tax: g.bill_tax == null || g.bill_tax === '' ? undefined : toPriceNum(g.bill_tax), } } if (!Object.keys(buckets).length) { @@ -291,13 +395,15 @@ function groupsToPerformanceBuckets(groups) { } function groupsToBenefitBuckets(groups) { - /** @type {Record} */ + /** @type {Record} */ const buckets = {} for (const g of groups || []) { - const code = String(g.bill_code ?? '').trim() + const code = String(g.bill_code ?? customerPriceBillBucketKey(g) ?? '').trim() buckets[code] = { rate: toPriceNum(g.rate), fee: toPriceNum(g.fee), + bill_label: String(g.bill_label ?? '').trim(), + bill_tax: g.bill_tax == null || g.bill_tax === '' ? undefined : toPriceNum(g.bill_tax), } } if (!Object.keys(buckets).length) { @@ -350,6 +456,57 @@ export function serializeExcessPerformanceBuckets(buckets) { return out } +function bucketBillMeta(key, bucket) { + const b = bucket && typeof bucket === 'object' ? bucket : {} + let bill_label = String(b.bill_label ?? '').trim() + let bill_tax = b.bill_tax == null || b.bill_tax === '' ? undefined : toPriceNum(b.bill_tax) + if (!bill_label) { + const k = String(key ?? '').trim() + if (k.includes('|')) { + const [name, tax] = k.split('|') + bill_label = String(name || '').trim() + if (tax !== '' && bill_tax == null) bill_tax = toPriceNum(tax) + } else { + bill_label = k + } + } + return { bill_label, bill_tax: bill_tax ?? 0 } +} + +/** modify_custom:buckets → [{ bill_label, bill_tax, rate, fees }] */ +export function serializeExcessPerformanceBucketsAsCustomArray(buckets) { + const src = buckets && typeof buckets === 'object' ? buckets : {} + const namedKeys = Object.keys(src).filter((k) => String(k).trim() !== '') + const keys = namedKeys.length ? namedKeys : [''] + return keys.map((key) => { + const b = src[key] || createEmptyExcessPerformanceBucket() + const { bill_label, bill_tax } = bucketBillMeta(key, b) + return { + bill_label, + bill_tax, + rate: toPriceNum(b.rate), + fees: toPriceNum(b.amount), + } + }) +} + +/** modify_custom:buckets → [{ bill_label, bill_tax, rate, fees }] */ +export function serializeBenefitBucketsAsCustomArray(buckets) { + const src = buckets && typeof buckets === 'object' ? buckets : {} + const namedKeys = Object.keys(src).filter((k) => String(k).trim() !== '') + const keys = namedKeys.length ? namedKeys : [''] + return keys.map((key) => { + const b = src[key] || createEmptyBenefitBucket() + const { bill_label, bill_tax } = bucketBillMeta(key, b) + return { + bill_label, + bill_tax, + rate: toPriceNum(b.rate), + fees: toPriceNum(b.fee), + } + }) +} + /** 编辑保存:buckets → 接口 JSON(有命名 key 则分桶,否则 { rate, fee }) */ export function serializeBenefitBuckets(buckets) { const src = buckets && typeof buckets === 'object' ? buckets : {} @@ -569,6 +726,27 @@ function normalizeCustomerPriceCustom(raw) { excess_profit_rate: toPriceNum(src.excess_profit_rate), excess_profit_amount: toPriceNum(src.excess_profit_amount), habitual_bargain_ratio: toPriceNum(src.habitual_bargain_ratio), + special_amount: pickCustomerPriceSpecialAmount(src), + } +} + +/** 列表刷新后合并刚保存的 custom(listed 未回传 special_amount 时兜底) */ +export function applyCustomerPriceCustomSnapshot(row, snapshot) { + if (!row || typeof row !== 'object' || !snapshot || typeof snapshot !== 'object') return row + const customRaw = { + ...(row.custom && typeof row.custom === 'object' ? row.custom : {}), + ...snapshot, + special_amount: pickCustomerPriceSpecialAmount(snapshot, row.custom, row, snapshot), + } + const custom = normalizeCustomerPriceCustom(customRaw) + if (!custom) return row + return { + ...row, + has_custom: true, + custom, + custom_excess_performance_groups: custom.excess_performance_groups ?? [], + custom_benefit_groups: custom.benefit_groups ?? [], + special_amount: custom.special_amount, } } @@ -583,38 +761,44 @@ export function createEmptyCustomerPriceCustomForm() { excess_profit_amount: 0, habitual_bargain_ratio: 0, benefit_buckets: groupsToBenefitBuckets([]), + special_amount: 0, } } /** 列表行 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 customParsed = parseCustomerPriceCustomRaw(src.custom) + const custom = (src.custom != null && typeof src.custom === 'object') + ? src.custom + : customParsed + if (!custom && !src.has_custom) return createEmptyCustomerPriceCustomForm() + const customSrc = custom || {} const excess_performance_buckets = groupsToPerformanceBuckets( - Array.isArray(custom.excess_performance_groups) && custom.excess_performance_groups.length - ? custom.excess_performance_groups - : parseExcessPerformanceGroups(custom), + Array.isArray(customSrc.excess_performance_groups) && customSrc.excess_performance_groups.length + ? customSrc.excess_performance_groups + : parseExcessPerformanceGroups(customSrc), ) const benefit_buckets = groupsToBenefitBuckets( - Array.isArray(custom.benefit_groups) && custom.benefit_groups.length - ? custom.benefit_groups - : parseBenefitGroups(custom), + Array.isArray(customSrc.benefit_groups) && customSrc.benefit_groups.length + ? customSrc.benefit_groups + : parseBenefitGroups(customSrc), ) 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), + sales_policy_type: customSrc.sales_policy_type == null ? 0 : toPriceNum(customSrc.sales_policy_type), + is_support_acceptance: customSrc.is_support_acceptance === true + || customSrc.is_support_acceptance === 1 + || customSrc.is_support_acceptance === '1', + rebate_delay_base_months: toPriceNum(customSrc.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), + excess_profit_rate: toPriceNum(customSrc.excess_profit_rate), + excess_profit_amount: toPriceNum(customSrc.excess_profit_amount), + habitual_bargain_ratio: toPriceNum(customSrc.habitual_bargain_ratio), benefit_buckets, + special_amount: pickCustomerPriceSpecialAmount(src, customSrc), } } @@ -626,11 +810,12 @@ export function buildCustomerPriceCustomWriteBody(form, { customer_price_code } 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_performance: serializeExcessPerformanceBucketsAsCustomArray(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), + benefit: serializeBenefitBucketsAsCustomArray(form?.benefit_buckets), + special_amount: toPriceNum(form?.special_amount), } } @@ -694,8 +879,20 @@ export function normalizeCustomerPriceRow(raw) { 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 + const has_custom = src.custom != null && typeof src.custom !== 'undefined' + && (parseCustomerPriceCustomRaw(src.custom) != null + || (typeof src.custom === 'object' && src.custom !== null) + || src.has_custom === true + || src.has_custom === 1 + || src.has_custom === '1') + const customRaw = parseCustomerPriceCustomRaw(src.custom) + || (typeof src.custom === 'object' && src.custom !== null ? src.custom : null) + const custom = customRaw + ? normalizeCustomerPriceCustom({ + ...customRaw, + special_amount: pickCustomerPriceSpecialAmount(src, customRaw), + }) + : null return { ...src, customer_price_code, @@ -718,6 +915,7 @@ export function normalizeCustomerPriceRow(raw) { habitual_bargain_ratio: toPriceNum(src.habitual_bargain_ratio), goods_categorys_list, goods_categorys_text, + special_amount: pickCustomerPriceSpecialAmount(src, customRaw, custom), goods_name: pickGoodsNameFromListedRow(src) || String(src.goods_name ?? src.goods?.name ?? src.name ?? '').trim(), company_name: String( diff --git a/src/api/fixedAssetsCost.js b/src/api/fixedAssetsCost.js new file mode 100644 index 0000000..2536bcc --- /dev/null +++ b/src/api/fixedAssetsCost.js @@ -0,0 +1,4 @@ +export { + fixedAssetsShareListed, + pickApiErrorMessage, +} from '@/api/costShareListed' diff --git a/src/api/lowValueConsumablesCost.js b/src/api/lowValueConsumablesCost.js new file mode 100644 index 0000000..d75613d --- /dev/null +++ b/src/api/lowValueConsumablesCost.js @@ -0,0 +1,4 @@ +export { + lowValueConsumablesShareListed, + pickApiErrorMessage, +} from '@/api/costShareListed' diff --git a/src/api/pricingTable.js b/src/api/pricingTable.js index a326e09..3f6f500 100644 --- a/src/api/pricingTable.js +++ b/src/api/pricingTable.js @@ -7,6 +7,7 @@ function str(v) { /** 默认内销(暂无出口场景) */ export const PRICING_TRADE_DOMESTIC = 'DOMESTIC' +export const PRICING_TRADE_EXPORT = 'EXPORT' /** 成本围栏 · 纳税身份 Tab(key 即接口 taxpayer_type 枚举) */ export const PRICING_TAXPAYER_TABS = [ @@ -140,3 +141,64 @@ export async function pricingTableQuery(body = {}) { export function pickPricingTableError(err, fallback = '加载定价决策表失败') { return String(err?.data?.message || err?.message || fallback).trim() || fallback } + +function unwrapPricingCalculatePayload(res) { + const envelope = res?.data ?? res + if (envelope && typeof envelope === 'object' && !Array.isArray(envelope)) { + if (envelope.debug != null || envelope.data != null) return envelope.data ?? envelope + } + return envelope +} + +/** 算价响应 → debug 文本(供弹窗展示) */ +export function formatPricingCalculateDebug(res) { + const payload = unwrapPricingCalculatePayload(res) + const debug = payload?.debug ?? res?.data?.debug ?? res?.debug + if (debug == null || debug === '') { + const fallback = payload && typeof payload === 'object' ? payload : (res?.data ?? res) + try { + return `(接口未返回 debug)\n\n${JSON.stringify(fallback, null, 2)}` + } catch { + return '(接口未返回 debug)' + } + } + if (typeof debug === 'string') return debug + try { + return JSON.stringify(debug, null, 2) + } catch { + return String(debug) + } +} + +/** @deprecated 请用 formatPricingCalculateDebug */ +export const pickPricingCalculateDebug = formatPricingCalculateDebug + +/** + * POST /api/pricing/calculate + * 按商品(可选客户价格本 / 供货目录)批量展开维度并计算最优含税售价 + */ +export async function pricingCalculate(body = {}) { + const taxpayer_type = str(body.taxpayer_type).toUpperCase() || PRICING_TAXPAYER_DEFAULT + const trade_type = str(body.trade_type) || PRICING_TRADE_DOMESTIC + const goods_code = str(body.goods_code) + if (!goods_code) throw new Error('缺少商品编码') + if (!PRICING_TAXPAYER_KEYS.has(taxpayer_type)) { + throw new Error(`纳税人身份无效:${taxpayer_type}`) + } + + const payload = { + taxpayer_type, + trade_type, + goods_code, + } + const supply_collection_code = str(body.supply_collection_code) + const customer_price_code = str(body.customer_price_code) + if (supply_collection_code) payload.supply_collection_code = supply_collection_code + if (customer_price_code) payload.customer_price_code = customer_price_code + + return request.post('/api/pricing/calculate', payload) +} + +export function pickPricingCalculateError(err, fallback = '算价失败') { + return String(err?.data?.message || err?.message || fallback).trim() || fallback +} diff --git a/src/api/supplyCollectionGoods.js b/src/api/supplyCollectionGoods.js index e9997d7..363df6b 100755 --- a/src/api/supplyCollectionGoods.js +++ b/src/api/supplyCollectionGoods.js @@ -296,7 +296,7 @@ export function extraFeeLogActionText(log) { const op = extraFeeLogOperator(log) const parts = [] if (clock) parts.push(clock) - parts.push(`${tax} 追加 ${amt}`) + parts.push(`${tax} 修改为 ${amt}`) if (op) parts.push(op) return parts.join(' · ') } @@ -377,9 +377,53 @@ export function mergeExtraFeesPatch(current, patch) { } /** - * @param {Record} addDraft + * 对比额外费用字段是否变化(空与 0 视为等价) + * @param {unknown} a + * @param {unknown} b + */ +function extraFeeAmountEqual(a, b) { + const sa = String(a ?? '').trim() + const sb = String(b ?? '').trim() + if (sa === '' && sb === '') return true + const na = sa === '' ? 0 : Number(sa) + const nb = sb === '' ? 0 : Number(sb) + if (Number.isNaN(na) || Number.isNaN(nb)) return sa === sb + return na === nb +} + +/** + * 组装 extra_fees/set 请求体:amount 为修改后的目标值(非增量),仅包含相对 baseline 有变化的票种 + * @param {Record} editDraft + * @param {Record} baseline * @param {string} [remark] */ +export function buildExtraFeesModifyList(editDraft, baseline, remark = '') { + const edit = editDraft && typeof editDraft === 'object' && !Array.isArray(editDraft) + ? editDraft + : {} + const base = baseline && typeof baseline === 'object' && !Array.isArray(baseline) + ? baseline + : {} + const rm = String(remark || '').trim() + const list = [] + for (const f of COLLECTION_GOODS_EXTRA_FEE_FIELDS) { + const nextStr = String(edit[f.key] ?? '').trim() + const prevStr = String(base[f.key] ?? '').trim() + if (extraFeeAmountEqual(nextStr, prevStr)) continue + let amount = 0 + if (nextStr !== '') { + const n = Number(nextStr) + if (Number.isNaN(n)) continue + amount = n + } + const item = { bill_tax: f.bill_tax, amount } + if (rm) item.remark = rm + list.push(item) + } + return list +} + +/** @deprecated 累加模式已废弃,使用 buildExtraFeesModifyList */ export function buildExtraFeesAddList(addDraft, remark = '') { const src = addDraft && typeof addDraft === 'object' && !Array.isArray(addDraft) ? addDraft @@ -432,7 +476,7 @@ function parseExtraFeesLogPayload(data) { } /** @param {unknown} data */ -export function extractExtraFeesFromAddResponse(data) { +export function extractExtraFeesFromSetResponse(data) { const coerced = coerceExtraFeesSource(data) if (!coerced || typeof coerced !== 'object') return null const row = coerced @@ -454,11 +498,12 @@ export function extractExtraFeesFromAddResponse(data) { } /** - * POST /api/supply/collection/goods/extra_fees/add + * POST /api/supply/collection/goods/extra_fees/set + * amount 为修改后的目标值;后端落库并记修改日志,返回更新后的 extra_fees * @param {{ supply_collection_code: string, goods_code: string, list: object[] }} body */ -export async function supplyCollectionGoodsExtraFeesAdd(body) { - const res = await request.post('/api/supply/collection/goods/extra_fees/add', body) +export async function supplyCollectionGoodsExtraFeesSet(body) { + const res = await request.post('/api/supply/collection/goods/extra_fees/set', body) return unwrapResponseData(res) } diff --git a/src/api/warehouseRdcDeploy.js b/src/api/warehouseRdcDeploy.js index bcef1ff..e154bfd 100755 --- a/src/api/warehouseRdcDeploy.js +++ b/src/api/warehouseRdcDeploy.js @@ -15,10 +15,11 @@ function unwrapListed(res) { return [] } -/** 接口 form 侧 RDC 编码(读响应兼容旧字段) */ -export function pickFormWarehouseCode(raw) { +/** 所在仓 RDC 编码(读响应兼容 form / 旧字段) */ +export function pickSelfWarehouseCode(raw) { return String( - raw?.form_warehouse_code + raw?.self_warehouse_code + ?? raw?.form_warehouse_code ?? raw?.source_warehouse_rdc_code ?? raw?.warehouse_rdc_code ?? raw?.rdc_code @@ -26,10 +27,11 @@ export function pickFormWarehouseCode(raw) { ).trim() } -/** 接口 to 侧 RDC 编码(读响应兼容旧字段;勿随意丢弃仅含 warehouse_code 的行) */ -export function pickToWarehouseCode(raw) { +/** 源采购仓 RDC 编码(读响应兼容 to / 旧字段) */ +export function pickSourceWarehouseCode(raw) { return String( - raw?.to_warehouse_code + raw?.source_warehouse_code + ?? raw?.to_warehouse_code ?? raw?.deploy_warehouse_rdc_code ?? raw?.warehouse_rdc_code ?? raw?.rdc_code @@ -38,8 +40,11 @@ export function pickToWarehouseCode(raw) { ).trim() } -export function pickFormWarehouseCodes(body) { - const arr = body?.form_warehouse_codes ?? body?.source_warehouse_rdc_codes ?? [] +export function pickSelfWarehouseCodes(body) { + const arr = body?.self_warehouse_codes + ?? body?.form_warehouse_codes + ?? body?.source_warehouse_rdc_codes + ?? [] return [...new Set( (Array.isArray(arr) ? arr : []) .map((c) => String(c ?? '').trim()) @@ -47,35 +52,47 @@ export function pickFormWarehouseCodes(body) { )] } +/** @deprecated 使用 pickSelfWarehouseCode */ +export const pickFormWarehouseCode = pickSelfWarehouseCode + +/** @deprecated 使用 pickSourceWarehouseCode */ +export const pickToWarehouseCode = pickSourceWarehouseCode + +/** @deprecated 使用 pickSelfWarehouseCodes */ +export const pickFormWarehouseCodes = pickSelfWarehouseCodes + export function normalizeRdcDeployRow(raw) { if (!raw || typeof raw !== 'object') return null const deploy_warehouse_code = String( - raw.deploy_warehouse_code ?? raw.warehouse_code ?? pickToWarehouseCode(raw) ?? '', + raw.deploy_warehouse_code ?? raw.warehouse_code ?? pickSourceWarehouseCode(raw) ?? '', ).trim() - const to_warehouse_code = pickToWarehouseCode(raw) || deploy_warehouse_code - if (!to_warehouse_code && !deploy_warehouse_code) return null + const source_warehouse_code = pickSourceWarehouseCode(raw) || deploy_warehouse_code + if (!source_warehouse_code && !deploy_warehouse_code) return null const deploy_warehouse_name = String( raw.deploy_warehouse_name ?? raw.to_warehouse_name + ?? raw.source_warehouse_name ?? raw.warehouse_name ?? raw.name ?? '', ).trim() const deploy_rdc_name = String( raw.to_warehouse_name + ?? raw.source_warehouse_name ?? raw.deploy_warehouse_rdc_name ?? raw.deploy_rdc_name ?? raw.rdc_name ?? raw.warehouse_rdc_name ?? '', ).trim() - const displayCode = to_warehouse_code || deploy_warehouse_code + const displayCode = source_warehouse_code || deploy_warehouse_code const displayName = deploy_rdc_name || deploy_warehouse_name const label = displayName && displayCode && displayName !== displayCode ? `${displayName}(${displayCode})` : (displayName || displayCode) return { deploy_warehouse_code: deploy_warehouse_code || displayCode, + source_warehouse_code: displayCode, to_warehouse_code: displayCode, deploy_warehouse_name: deploy_warehouse_name || displayName, deploy_rdc_name: deploy_rdc_name || displayName, @@ -84,7 +101,7 @@ export function normalizeRdcDeployRow(raw) { } } -/** 可调配仓列表展示:优先 label / to 侧名称+编码 */ +/** 源采购仓列表展示:优先 label / 名称+编码 */ export function formatRdcDeployRowLabel(row) { if (!row || typeof row !== 'object') return '—' const label = String(row.label ?? '').trim() @@ -92,15 +109,19 @@ export function formatRdcDeployRowLabel(row) { const name = String( row.deploy_rdc_name ?? row.to_warehouse_name + ?? row.source_warehouse_name ?? row.row?.to_warehouse_name + ?? row.row?.source_warehouse_name ?? row.deploy_warehouse_name ?? row.warehouse_name ?? '', ).trim() const code = String( - row.to_warehouse_code + row.source_warehouse_code + ?? row.to_warehouse_code ?? row.deploy_warehouse_code ?? row.warehouse_code + ?? row.row?.source_warehouse_code ?? row.row?.to_warehouse_code ?? '', ).trim() @@ -112,7 +133,7 @@ export function buildRdcDeploySourceCode(goodsRow, warehouseCode = '') { return pickRdcCodeFromGoodsRow(goodsRow, warehouseCode) } -/** deploy 按物料定位:warehouse_code + goods_code */ +/** deploy/set 物料定位:warehouse_code + goods_code */ export function buildRdcDeployGoodsLocateItem(raw, warehouseCode = '') { const warehouse_code = String( raw?.warehouse_code ?? buildRdcDeploySourceCode(raw, warehouseCode) ?? warehouseCode ?? '', @@ -124,23 +145,23 @@ export function buildRdcDeployGoodsLocateItem(raw, warehouseCode = '') { /** deploy 查询 / link / unlink 共用查询体 */ export function buildRdcDeployQueryBody(goodsRow, warehouseCode = '') { - const form_warehouse_code = buildRdcDeploySourceCode(goodsRow, warehouseCode) + const self_warehouse_code = buildRdcDeploySourceCode(goodsRow, warehouseCode) const goods_code = String(goodsRow?.goods_code ?? '').trim() - if (!form_warehouse_code || !goods_code) return null - return { form_warehouse_code, goods_code } + if (!self_warehouse_code || !goods_code) return null + return { self_warehouse_code, goods_code } } function normalizeDeployToListItem(raw) { - const to_warehouse_code = pickToWarehouseCode(raw) - if (!to_warehouse_code) return null - return { to_warehouse_code } + const source_warehouse_code = pickSourceWarehouseCode(raw) + if (!source_warehouse_code) return null + return { source_warehouse_code } } export function buildRdcDeployLinkBody(goodsRow, warehouseCode, deployTarget) { const base = buildRdcDeployQueryBody(goodsRow, warehouseCode) - const to_warehouse_code = pickToWarehouseCode(deployTarget) - if (!base || !to_warehouse_code) return null - return { ...base, to_warehouse_code } + const source_warehouse_code = pickSourceWarehouseCode(deployTarget) + if (!base || !source_warehouse_code) return null + return { ...base, source_warehouse_code } } export function buildRdcDeployUnlinkBody(goodsRow, warehouseCode, deployTarget) { @@ -157,22 +178,22 @@ export function buildRdcDeploySetBody(goodsRow, warehouseCode, deployTargets) { return { list: [material], to_list } } -/** POST /api/warehouse/rdc/deploy/candidates — 检查可调配仓候选 */ +/** POST /api/warehouse/rdc/deploy/candidates — 源采购仓候选项 */ export function warehouseRdcDeployCandidates(body) { - const form_warehouse_codes = pickFormWarehouseCodes(body) - if (!form_warehouse_codes.length) { - return Promise.reject(new Error('deploy/candidates 缺少 form_warehouse_codes')) + const self_warehouse_codes = pickSelfWarehouseCodes(body) + if (!self_warehouse_codes.length) { + return Promise.reject(new Error('deploy/candidates 缺少 self_warehouse_codes')) } - const payload = { form_warehouse_codes } + const payload = { self_warehouse_codes } return request.post('/api/warehouse/rdc/deploy/candidates', payload).then((res) => res) } -/** POST /api/warehouse/rdc/deploy — 可调配仓列表 */ +/** POST /api/warehouse/rdc/deploy — 源采购仓列表 */ export async function warehouseRdcDeployListed(body) { - const form_warehouse_code = pickFormWarehouseCode(body) + const self_warehouse_code = pickSelfWarehouseCode(body) const goods_code = String(body?.goods_code ?? '').trim() - if (!form_warehouse_code || !goods_code) return [] - const payload = { form_warehouse_code, goods_code } + if (!self_warehouse_code || !goods_code) return [] + const payload = { self_warehouse_code, goods_code } const res = await request.post('/api/warehouse/rdc/deploy', payload) return unwrapListed(res).map(normalizeRdcDeployRow).filter(Boolean) } @@ -181,12 +202,12 @@ export function rowToRdcDeploySetItem(row) { return normalizeDeployToListItem(row) } -/** 合并已有可调配仓 + 新勾选(deploy/set 为按物料整表替换 to_list) */ +/** 合并已有源采购仓 + 新勾选(deploy/set 为按物料整表替换 to_list) */ export function mergeRdcDeploySetList(existingRows, addedList) { const map = new Map() const push = (raw) => { const item = normalizeDeployToListItem(raw) - if (item) map.set(item.to_warehouse_code, item) + if (item) map.set(item.source_warehouse_code, item) } for (const row of existingRows || []) push(row) for (const item of addedList || []) push(item) @@ -197,13 +218,13 @@ export function mergeRdcDeploySetList(existingRows, addedList) { export function subtractRdcDeploySetList(existingRows, removeRows) { const removeKeys = new Set( (removeRows || []) - .map((row) => pickToWarehouseCode(row)) + .map((row) => pickSourceWarehouseCode(row)) .filter(Boolean), ) return (existingRows || []) .map(normalizeDeployToListItem) .filter(Boolean) - .filter((item) => !removeKeys.has(item.to_warehouse_code)) + .filter((item) => !removeKeys.has(item.source_warehouse_code)) } /** @@ -230,7 +251,7 @@ export async function buildRdcDeploySetPayloadForMaterial( } /** - * 批量关联可调配仓:按物料分别 deploy/set(避免不同物料互相覆盖)。 + * 批量关联源采购仓:按物料分别 deploy/set(避免不同物料互相覆盖)。 */ export async function persistRdcDeploySetForRows(rows, anchorToDeployMap, warehouseCode, pickAnchorKey) { const pickKey = typeof pickAnchorKey === 'function' ? pickAnchorKey : () => '' @@ -263,29 +284,29 @@ export function warehouseRdcDeploySet(body) { } function serializeDeployMutateBody(body) { - const form_warehouse_code = pickFormWarehouseCode(body) - const to_warehouse_code = pickToWarehouseCode(body) + const self_warehouse_code = pickSelfWarehouseCode(body) + const source_warehouse_code = pickSourceWarehouseCode(body) const goods_code = String(body?.goods_code ?? '').trim() - if (!form_warehouse_code || !to_warehouse_code || !goods_code) { + if (!self_warehouse_code || !source_warehouse_code || !goods_code) { return null } - return { form_warehouse_code, to_warehouse_code, goods_code } + return { self_warehouse_code, source_warehouse_code, goods_code } } -/** POST /api/warehouse/rdc/deploy/link — 关联可调配仓 */ +/** POST /api/warehouse/rdc/deploy/link — 新增源采购仓关联 */ export function warehouseRdcDeployLink(body) { const payload = serializeDeployMutateBody(body) if (!payload) { - return Promise.reject(new Error('deploy/link 缺少 form_warehouse_code / to_warehouse_code / goods_code')) + return Promise.reject(new Error('deploy/link 缺少 self_warehouse_code / source_warehouse_code / goods_code')) } return request.post('/api/warehouse/rdc/deploy/link', payload).then((res) => res) } -/** POST /api/warehouse/rdc/deploy/unlink — 取关可调配仓 */ +/** POST /api/warehouse/rdc/deploy/unlink — 移除源采购仓关联 */ export function warehouseRdcDeployUnlink(body) { const payload = serializeDeployMutateBody(body) if (!payload) { - return Promise.reject(new Error('deploy/unlink 缺少 form_warehouse_code / to_warehouse_code / goods_code')) + return Promise.reject(new Error('deploy/unlink 缺少 self_warehouse_code / source_warehouse_code / goods_code')) } return request.post('/api/warehouse/rdc/deploy/unlink', payload).then((res) => res) } diff --git a/src/components/warehouse/WopsPageHeader.vue b/src/components/warehouse/WopsPageHeader.vue new file mode 100644 index 0000000..b9a9e40 --- /dev/null +++ b/src/components/warehouse/WopsPageHeader.vue @@ -0,0 +1,53 @@ + + + diff --git a/src/config/buildId.json b/src/config/buildId.json index cadd8b8..1ade934 100755 --- a/src/config/buildId.json +++ b/src/config/buildId.json @@ -1,4 +1,4 @@ { - "buildId": "20260707001", - "generatedAt": "2026-07-06T16:11:57.042Z" + "buildId": "20260707009", + "generatedAt": "2026-07-07T14:48:25.301Z" } \ No newline at end of file diff --git a/src/data/appChangelog.js b/src/data/appChangelog.js index 574766f..6150a4b 100755 --- a/src/data/appChangelog.js +++ b/src/data/appChangelog.js @@ -42,11 +42,111 @@ * @typedef {{ date: string, items: ChangelogItem[] }} ChangelogDay */ -/** 应用版本(如 0.7.029 → 侧栏 v0.7,弹窗 v0.7.029) */ -export const APP_VERSION = '0.7.029' +/** 应用版本(如 0.7.032 → 侧栏 v0.7,弹窗 v0.7.032) */ +export const APP_VERSION = '0.7.032' /** @type {ChangelogDay[]} */ export const APP_CHANGELOG = [ + { + date: '2026-07-07', + items: [ + { + icon: 'mdi:clipboard-list-outline', + kind: 'change', + text: '「统筹 → 录入商品 → 额外费用」弹窗布局收敛:左侧收窄为票种/当前值表格,备注移至表格右侧并撑满同高区域;右侧「修改日志」与左侧底边对齐,仅日志列表区内滚动,不再撑高整窗。', + }, + { + icon: 'mdi:clipboard-list-outline', + kind: 'fix', + text: '「录入商品 → 额外费用」备注框:字数统计改为底部独立行,不与正文重叠;聚焦描边改外层容器绘制,出现滚动条时四周边框(含右侧蓝框)正常显示。', + }, + { + icon: 'mdi:notebook-outline', + kind: 'add', + text: '「客户管理 → 客户价格本」顶栏新增「批量计算」,对接 `POST /api/pricing/calculate`(按 `customer_price_code`);移除行内「计算」,算价 debug 弹窗宽 1200px、仅弹窗内滚动。', + }, + { + icon: 'mdi:notebook-outline', + kind: 'change', + text: '「客户价格本」列表:状态列收窄为「启用/停用」;超额绩效/好处费列宽微调;操作列特殊定价/编辑/删除三按钮均分对齐,表头与状态/特价列底对齐居中。', + }, + { + icon: 'mdi:warehouse', + kind: 'change', + text: '「仓库管理 → 源采购 RDC」deploy 五接口请求字段对齐后端:`form_warehouse_code(s)` → `self_warehouse_code(s)`,目标仓 `to_warehouse_code` → `source_warehouse_code`(`set` 的 `to_list[]` 同步);读响应仍兼容旧字段名。', + }, + { + icon: 'mdi:warehouse', + kind: 'fix', + text: '「源采购 RDC」移除关联改调 `POST /api/warehouse/rdc/deploy/unlink`,请求体补齐 `self_warehouse_code` + `goods_code` + `source_warehouse_code`;不再用 `deploy/set` 全量替换实现删除。', + }, + { + icon: 'mdi:clipboard-list-outline', + kind: 'change', + text: '「统筹 → 录入商品 → 额外费用」改为当前值直接修改保存;接口由 `extra_fees/add` 更名为 `extra_fees/set`(`amount` 为目标值);去掉「本次追加」列,右侧日志改称「修改日志」。', + }, + { + icon: 'mdi:warehouse', + kind: 'change', + text: '「仓库管理 → 成本分摊预估」五页(固定资产/低值易耗/采购·销售·仓储用人成本)去掉「重新跑批」按钮;进页与重置仍先跑批,「查询」与翻页仅拉 listed。', + }, + { + icon: 'mdi:warehouse', + kind: 'fix', + text: '「仓库管理」运维页与分摊页顶栏、筛选区布局统一:`WopsPageHeader` 固定高度(含视图切换/票据 Tab/操作按钮各槽位),无内容时不占位、左对齐;筛选区统一为 `.wops-page-filters` 灰底盒子与最小行高。业务页(如调货)与对应分摊页切换时列表起点不再上下跳动。', + }, + { + icon: 'mdi:warehouse', + kind: 'add', + text: '「仓库管理 → 固定资产分摊预估」(`/warehouse/fixed-assets-share`):对接 `POST /api/cost/fixed_assets/amortize` + `share/listed`;筛选 `goods_code`、`warehouse_code`;进页/查询先跑批再拉列表,翻页仅查 listed;列按接口返回 key 动态展示。', + }, + { + icon: 'mdi:warehouse', + kind: 'add', + text: '「仓库管理 → 低值易耗品分摊预估」(`/warehouse/low-value-consumables-share`):对接 `POST /api/cost/low_value_consumables/amortize` + `share/listed`;筛选 `goods_code`、`warehouse_code`;跑批与列表交互同固定资产分摊预估。', + }, + { + icon: 'mdi:warehouse', + kind: 'add', + text: '「仓库管理 → 采购用人成本分摊预估」(`/warehouse/purchasing-labor-share`):对接 `POST /api/cost/purchasing_labor/amortize` + `share/listed`;按 `goods_code` 查询;统计周期内按采购订单行数占比分摊采购部门工资。', + }, + { + icon: 'mdi:warehouse', + kind: 'add', + text: '「仓库管理 → 销售用人成本分摊预估」(`/warehouse/sales-doc-labor-share`):对接 `POST /api/cost/sales_doc_labor/amortize` + `share/listed`;按 `goods_code` 查询;统计周期内按发货单行数占比分摊销售部门工资。', + }, + { + icon: 'mdi:warehouse', + kind: 'add', + text: '「仓库管理 → 仓储用人成本分摊预估」(`/warehouse/warehouse-labor-share`):对接 `POST /api/cost/warehouse_labor/amortize` + `share/listed`;筛选 `goods_code`、`warehouse_code`;统计周期内按仓库加权难度系数分摊仓储人员成本。', + }, + { + icon: 'mdi:warehouse', + kind: 'fix', + text: '「仓库管理」运维/分摊列表「暂无数据」「暂无费用票据」等空态字号接入 `--wh-fs-delta`(1920 基准,2560 +2px,3840 +4px);主内容区统一定义变量,`.wops-empty` 与费用票据空提示改为动态字号。', + }, + { + icon: 'mdi:notebook-outline', + kind: 'fix', + text: '「客户管理 → 客户价格本」列表超额绩效/好处费多组展示:有几条就显示「N种」+ 悬停明细(首条摘要不变),不再因各组数值相同而合并为单行。', + }, + { + icon: 'mdi:notebook-outline', + kind: 'change', + text: '「客户价格本」`excess_performance` / `benefit` 兼容数组 `[{ bill_label, bill_tax, rate, fees }]` 与对象分桶;Tab / 列表悬停优先 `bill_label`(含税率)展示。', + }, + { + icon: 'mdi:notebook-outline', + kind: 'change', + text: '「客户价格本 → 特殊定价 → 自定义」`modify_custom` 超额绩效/好处费改数组提交;底部「特殊售价」可编辑并保存 `special_amount`;保存后 listed 未回传时用快照合并列表展示。', + }, + { + icon: 'mdi:notebook-outline', + kind: 'change', + text: '「客户价格本」批量编辑弹窗:是否启用/支持承兑/是否特价三开关独立一行;编辑/批量票据 Tab 票种名 + 橙色税率;定价参数数字框收窄、标签不换行。', + }, + ], + }, { date: '2026-07-06', items: [ diff --git a/src/layouts/index.vue b/src/layouts/index.vue index 9cf2f55..2fc39e5 100755 --- a/src/layouts/index.vue +++ b/src/layouts/index.vue @@ -1825,6 +1825,7 @@ const handleModifyPassword = async () => { flex-direction: column; overflow-y: auto; /* 允许垂直滚动 */ overflow-x: hidden; /* 隐藏横向滚动条 */ + --wh-fs-delta: clamp(0px, calc((100vw - 1920px) * 0.003125), 4px); } /* 懒加载路由 chunk 未就绪时,避免主区域完全空白 */ diff --git a/src/router/index.js b/src/router/index.js index ff8b65c..d023d85 100755 --- a/src/router/index.js +++ b/src/router/index.js @@ -415,6 +415,16 @@ export const asyncRoutes = [ powerKey: 'warehouse', }, }, + { + path: 'deploy-share', + name: 'WarehouseDeployShare', + component: () => import('@/views/base/DeployShareListed.vue'), + meta: { + title: '调货分摊', + icon: 'mdi:swap-horizontal', + powerKey: 'warehouse', + }, + }, { path: 'alloc', name: 'WarehouseAlloc', @@ -425,6 +435,16 @@ export const asyncRoutes = [ powerKey: 'warehouse', }, }, + { + path: 'alloc-share', + name: 'WarehouseAllocShare', + component: () => import('@/views/base/AllocShareListed.vue'), + meta: { + title: '配货分摊', + icon: 'mdi:truck-delivery-outline', + powerKey: 'warehouse', + }, + }, { path: 'freight', name: 'WarehouseFreight', @@ -435,6 +455,16 @@ export const asyncRoutes = [ powerKey: 'warehouse', }, }, + { + path: 'freight-share', + name: 'WarehouseFreightShare', + component: () => import('@/views/base/FreightShareListed.vue'), + meta: { + title: '运费分摊', + icon: 'mdi:truck-delivery-outline', + powerKey: 'warehouse', + }, + }, { path: 'waste', name: 'WarehouseWaste', @@ -445,6 +475,16 @@ export const asyncRoutes = [ powerKey: 'warehouse', }, }, + { + path: 'waste-share', + name: 'WarehouseWasteShare', + component: () => import('@/views/base/WasteShareListed.vue'), + meta: { + title: '损耗分摊', + icon: 'mdi:package-variant-closed-minus', + powerKey: 'warehouse', + }, + }, { path: 'purchase', name: 'WarehousePurchase', @@ -455,6 +495,16 @@ export const asyncRoutes = [ powerKey: 'warehouse', }, }, + { + path: 'purchasing-labor-share', + name: 'WarehousePurchasingLaborShare', + component: () => import('@/views/base/PurchasingLaborShareListed.vue'), + meta: { + title: '采购用人成本分摊', + icon: 'mdi:account-cash-outline', + powerKey: 'warehouse', + }, + }, { path: 'pr-cost', name: 'WarehousePrCost', @@ -465,53 +515,53 @@ export const asyncRoutes = [ powerKey: 'warehouse', }, }, - { - path: 'freight-share', - name: 'WarehouseFreightShare', - component: () => import('@/views/base/FreightShareListed.vue'), - meta: { - title: '运费分摊预估', - icon: 'mdi:truck-delivery-outline', - powerKey: 'warehouse', - }, - }, { path: 'pr-cost-share', name: 'WarehousePrCostShare', component: () => import('@/views/base/PrCostShareListed.vue'), meta: { - title: '公关分摊预估', + title: '公关分摊', icon: 'mdi:currency-usd', powerKey: 'warehouse', }, }, { - path: 'alloc-share', - name: 'WarehouseAllocShare', - component: () => import('@/views/base/AllocShareListed.vue'), + path: 'sales-doc-labor-share', + name: 'WarehouseSalesDocLaborShare', + component: () => import('@/views/base/SalesDocLaborShareListed.vue'), meta: { - title: '配货分摊预估', - icon: 'mdi:truck-delivery-outline', + title: '销售用人成本分摊', + icon: 'mdi:file-document-edit-outline', powerKey: 'warehouse', }, }, { - path: 'deploy-share', - name: 'WarehouseDeployShare', - component: () => import('@/views/base/DeployShareListed.vue'), + path: 'warehouse-labor-share', + name: 'WarehouseWarehouseLaborShare', + component: () => import('@/views/base/WarehouseLaborShareListed.vue'), meta: { - title: '调货分摊预估', - icon: 'mdi:swap-horizontal', + title: '仓储用人成本分摊', + icon: 'mdi:account-hard-hat-outline', powerKey: 'warehouse', }, }, { - path: 'waste-share', - name: 'WarehouseWasteShare', - component: () => import('@/views/base/WasteShareListed.vue'), + path: 'fixed-assets-share', + name: 'WarehouseFixedAssetsShare', + component: () => import('@/views/base/FixedAssetsShareListed.vue'), meta: { - title: '损耗分摊预估', - icon: 'mdi:package-variant-closed-minus', + title: '固定资产分摊', + icon: 'mdi:office-building-cog-outline', + powerKey: 'warehouse', + }, + }, + { + path: 'low-value-consumables-share', + name: 'WarehouseLowValueConsumablesShare', + component: () => import('@/views/base/LowValueConsumablesShareListed.vue'), + meta: { + title: '低值易耗品分摊', + icon: 'mdi:basket-outline', powerKey: 'warehouse', }, }, @@ -1101,7 +1151,21 @@ router.beforeEach(async (to, from, next) => { } if (!bootstrapStore.ready) { - const redirect = to.fullPath && to.fullPath !== '/bootstrap' ? to.fullPath : undefined + const redirect = + to.fullPath && to.fullPath !== '/bootstrap' && to.fullPath !== '/login' + ? to.fullPath + : undefined + + // 未登录:在登录页完成初始化(保留左上天气与日期) + if (!token) { + if (to.path === '/login') return next() + return next({ + path: '/login', + query: redirect ? { redirect } : {}, + }) + } + + if (to.path === '/bootstrap') return next() return next({ path: '/bootstrap', query: redirect ? { redirect } : {}, diff --git a/src/styles/warehouseOpsFlexTable.css b/src/styles/warehouseOpsFlexTable.css index 7aaf093..281f4b0 100644 --- a/src/styles/warehouseOpsFlexTable.css +++ b/src/styles/warehouseOpsFlexTable.css @@ -85,6 +85,10 @@ .wops-flex-table.is-cols-share-waste { --wops-cols: 14fr 11fr 5fr 5fr 4fr 5fr 5fr 5fr 8fr; } +/* 分摊结果 — 列数由接口字段动态决定(inline --wops-cols) */ +.wops-flex-table.is-cols-share-dynamic { + --wops-cols: repeat(4, minmax(96px, 1fr)); +} /* 公关费用(10 列):费用编码略窄,集团 / 组织各加半份 */ .wops-flex-table.is-cols-pr-cost { --wops-cols: 7fr 10.5fr 10.5fr 7fr 8fr 8fr 6fr 9fr 6fr 8fr; @@ -105,6 +109,27 @@ flex-direction: column; } +/* 表头固定在表格顶部;loading 仅覆盖下方数据区 */ +.wops-flex-table > .wops-flex-head { + flex-shrink: 0; + position: static; +} + +.wops-flex-body { + flex: 1; + min-height: 0; + min-width: 0; + display: flex; + flex-direction: column; + overflow: hidden; + position: relative; +} + +.wops-flex-body > .wops-flex-scroll { + flex: 1; + min-height: 0; +} + .wops-flex-row { display: grid; grid-template-columns: var(--wops-cols); @@ -319,6 +344,7 @@ padding: 12px 16px; color: #909399; text-align: center; + font-size: calc(12px + var(--wh-fs-delta, 0px)); } .wops-op-link { @@ -339,3 +365,213 @@ .wops-op-link--danger:hover { color: #f78989; } + +/* ── 仓库运维页顶栏:title → 视图切换 → 条数 → 票据 Tab(左对齐);操作按钮靠右 ── */ +.wops-page-header { + --wops-header-h: calc(36px + var(--wh-fs-delta, 0px)); + flex-shrink: 0; + display: flex; + align-items: center; + gap: 12px 16px; + margin-bottom: 12px; + height: var(--wops-header-h); + min-height: var(--wops-header-h); + max-height: var(--wops-header-h); + box-sizing: border-box; +} + +.wops-page-header-row { + flex: 1; + min-width: 0; + height: var(--wops-header-h); + display: flex; + align-items: center; + flex-wrap: nowrap; + gap: 10px 12px; + box-sizing: border-box; +} + +.wops-page-title { + margin: 0; + font-size: calc(16px + var(--wh-fs-delta, 0px)); + font-weight: 600; + color: #303133; + flex-shrink: 0; + line-height: 1.2; +} + +.wops-page-total { + flex-shrink: 0; + min-width: 56px; + font-size: 12px; + color: #909399; + white-space: nowrap; + line-height: 1.2; +} + +.wops-page-header-toggle { + flex: 0 0 auto; + display: flex; + align-items: center; + height: var(--wops-header-h); + max-height: var(--wops-header-h); + overflow: hidden; +} + +.wops-page-header-toggle.is-empty { + display: none; +} + +.wops-page-header-bills { + flex: 1 1 auto; + min-width: 0; + height: var(--wops-header-h); + max-height: var(--wops-header-h); + display: flex; + align-items: center; + flex-wrap: nowrap; + gap: 6px; + overflow-x: auto; + overflow-y: hidden; + padding: 0; + box-sizing: border-box; + scrollbar-width: thin; + scrollbar-color: #e4e7ed transparent; +} + +.wops-page-header-bills.is-empty { + display: none; +} + +.wops-page-header-bills::-webkit-scrollbar { height: 4px; } +.wops-page-header-bills::-webkit-scrollbar-thumb { background: #e4e7ed; border-radius: 2px; } + +.wops-page-header-actions { + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: flex-end; + height: var(--wops-header-h); + max-height: var(--wops-header-h); + overflow: hidden; +} + +.wops-page-header-actions.is-empty { + display: none; +} + +/* ── 仓库运维页筛选区(统一灰底盒子 + 最小行高,左对齐) ── */ +.wops-page-filters { + flex-shrink: 0; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin-bottom: 12px; + padding: 10px 12px; + background: #f8fafc; + border: 1px solid #ebeef5; + border-radius: 8px; + box-sizing: border-box; + min-height: calc(46px + var(--wh-fs-delta, 0px)); +} + +.wops-page-filters > * { + flex: 0 0 auto; +} + +.wops-page-filters .wops-filter-field { + width: 140px; +} + +.wops-page-filters .wops-filter-field--goods { + width: 160px; +} + +.wops-page-filters .wops-filter-field--wide { + width: 180px; +} + +.wops-page-filters .el-date-editor.wops-filter-date { + width: 150px !important; + flex: 0 0 auto; +} + +.wops-page-filters .el-date-editor.wops-filter-date--sm { + width: 132px !important; + flex: 0 0 auto; +} + +.wops-bill-tab { + flex-shrink: 0; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + border: 1px solid #e4e7ed; + border-radius: 6px; + background: #f5f7fa; + color: #606266; + font-size: calc(12px + var(--wh-fs-delta, 0px)); + cursor: pointer; + transition: background .15s, border-color .15s, color .15s; + max-width: 180px; +} + +.wops-bill-tab:hover { + border-color: #c6d8f5; + background: #ecf5ff; + color: #409eff; +} + +.wops-bill-tab.is-active { + border-color: #409eff; + background: #ecf5ff; + color: #409eff; + font-weight: 600; +} + +.wops-bill-tab-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.wops-bill-tab-tax { + flex-shrink: 0; + font-size: calc(11px + var(--wh-fs-delta, 0px)); + color: #e6a23c; + background: #fdf6ec; + border-radius: 3px; + padding: 0 5px; + line-height: 17px; +} + +.wops-bill-tab.is-active .wops-bill-tab-tax { + background: #fff; +} + +.wops-bill-tab--skeleton { + width: 72px; + min-height: calc(28px + var(--wh-fs-delta, 0px)); + padding: 0; + border-color: #ebeef5; + background: #f2f4f7; + cursor: default; + pointer-events: none; + animation: wops-bill-tab-skeleton-pulse 1.2s ease-in-out infinite; +} + +@keyframes wops-bill-tab-skeleton-pulse { + 0%, 100% { opacity: 0.55; } + 50% { opacity: 0.9; } +} + +.wops-bill-tabs-empty { + flex-shrink: 0; + font-size: calc(12px + var(--wh-fs-delta, 0px)); + color: #c0c4cc; + white-space: nowrap; + padding: 0 8px; +} diff --git a/src/utils/costShareListedDisplay.js b/src/utils/costShareListedDisplay.js new file mode 100644 index 0000000..0267409 --- /dev/null +++ b/src/utils/costShareListedDisplay.js @@ -0,0 +1,114 @@ +import { formatOpsLogTime } from '@/utils/warehouseOpsDisplay' + +export const COST_SHARE_COLUMN_LABELS = { + goods_code: '物料编码', + goods_name: '物料名称', + goods_display_name: '物料名称', + warehouse_code: '仓库编码', + warehouse_name: '仓库名称', + rdc_warehouse_code: 'RDC仓库编码', + rdc_warehouse_name: 'RDC仓库名称', + share_total_amount: '分摊总额', + unit_amount: '单件分摊', + ratio: '占比', + bill_tax: '税率', + share_time: '分摊时间', + ctime: '创建时间', +} + +export const COST_SHARE_COLUMN_ORDER = [ + 'goods_code', + 'goods_name', + 'goods_display_name', + 'warehouse_code', + 'warehouse_name', + 'rdc_warehouse_code', + 'rdc_warehouse_name', +] + +const SKIP_KEYS = new Set([ + 'goods_categorys', + 'goods_categorys_list', + 'category_path', +]) + +const TIME_KEYS = new Set(['share_time', 'ctime', 'create_time', 'update_time', 'amortize_time']) +const NUM_KEYS = new Set([ + 'ratio', + 'bill_tax', + 'share_total_amount', + 'unit_amount', + 'purchase_count', + 'total_count', + 'amount', + 'total_amount', +]) + +export function deriveCostShareColumns(list) { + const keys = new Set() + for (const row of list || []) { + if (!row || typeof row !== 'object') continue + for (const key of Object.keys(row)) { + if (SKIP_KEYS.has(key)) continue + const val = row[key] + if (val != null && typeof val === 'object') continue + keys.add(key) + } + } + const ordered = [] + for (const key of COST_SHARE_COLUMN_ORDER) { + if (keys.has(key)) { + ordered.push({ key, label: COST_SHARE_COLUMN_LABELS[key] || key }) + keys.delete(key) + } + } + const rest = [...keys].sort((a, b) => a.localeCompare(b, 'zh-CN')) + for (const key of rest) { + ordered.push({ key, label: COST_SHARE_COLUMN_LABELS[key] || key }) + } + return ordered +} + +export function costShareColHeadClass(key) { + if (NUM_KEYS.has(key) || key.endsWith('_amount') || key.endsWith('_count')) return 'wops-col--num' + if (TIME_KEYS.has(key) || key.endsWith('_time')) return 'wops-col--time' + if (key === 'goods_code' || key === 'goods_name' || key === 'goods_display_name') return 'wops-col--goods' + if (key.includes('warehouse') || key.includes('company') || key.includes('bloc')) return 'wops-col--wh' + return 'wops-col--plain' +} + +function fmtNum(v, digits = 2) { + if (v == null || v === '') return '—' + const n = Number(v) + return Number.isFinite(n) ? n.toFixed(digits) : String(v) +} + +export function formatCostShareCell(row, key) { + const v = row?.[key] + if (v == null || v === '') return '—' + if (TIME_KEYS.has(key) || key.endsWith('_time')) return formatOpsLogTime(v) || '—' + if (key === 'bill_tax') { + const n = Number(v) + return Number.isFinite(n) ? `${n}%` : String(v) + } + if (NUM_KEYS.has(key) || key.endsWith('_amount') || key.endsWith('_count') || key.endsWith('_ratio')) { + const digits = key.includes('ratio') ? 4 : 2 + return fmtNum(v, digits) + } + if (typeof v === 'boolean') return v ? '是' : '否' + return String(v) +} + +export function costShareCellTitle(row, key) { + const text = formatCostShareCell(row, key) + return text === '—' ? '' : text +} + +export function costShareRowKey(row, idx) { + return [ + row?.goods_code ?? '', + row?.warehouse_code ?? row?.rdc_warehouse_code ?? '', + row?.share_time ?? row?.ctime ?? '', + idx, + ].join(':') +} diff --git a/src/utils/whRdcDeployCandidatesTree.js b/src/utils/whRdcDeployCandidatesTree.js index cbc756f..260a38f 100755 --- a/src/utils/whRdcDeployCandidatesTree.js +++ b/src/utils/whRdcDeployCandidatesTree.js @@ -344,7 +344,7 @@ export function groupDeployLinkRequests(rows, anchorToDeployMap, warehouseCode) const pick = anchorKey ? anchorToDeployMap?.[anchorKey] : null const body = buildRdcDeployLinkBody(row, warehouseCode, pick) if (!body) continue - const dedupe = `${body.form_warehouse_code}::${body.to_warehouse_code}` + const dedupe = `${body.self_warehouse_code}::${body.goods_code}::${body.source_warehouse_code}` if (seen.has(dedupe)) continue seen.add(dedupe) payloads.push(body) @@ -370,7 +370,7 @@ export function groupDeploySetRequests(rows, anchorToDeployMap, warehouseCode) { }) } return [...bySource.entries()].map(([sourceCode, deployMap]) => ({ - form_warehouse_codes: [sourceCode], + self_warehouse_codes: [sourceCode], list: [...deployMap.values()], })) } diff --git a/src/views/base/AllocShareListed.vue b/src/views/base/AllocShareListed.vue index 5d31d1a..c3bab27 100644 --- a/src/views/base/AllocShareListed.vue +++ b/src/views/base/AllocShareListed.vue @@ -1,37 +1,36 @@