import request from '@/utils/request' import { collectWarehouseCodesFromRow } from '@/api/supplyCoordCollection' import { syncPolicyFieldsToApi } from '@/utils/supplyCollectionGoodsCalc' import { loadGoodsInScenes, sceneValuesObjectFromRaw } from '@/utils/rfSceneGoodsFilter' import { pickGoodsCategoryEntry, pickGoodsDisplayName, pickGoodsNameFromListedRow, resolveGoodsCategoryCode, } from '@/utils/goodsCategorys' function unwrapResponseData(res) { if (res == null) return res if (typeof res === 'object' && 'data' in res && res.data !== undefined) return res.data return res } function unwrapListedRows(data) { if (Array.isArray(data)) return data if (!data || typeof data !== 'object') return [] const row = data const list = row.list || row.rows || row.goods || row.products || row.records || row.items || row.goods_list || row.data if (Array.isArray(list)) return list const nested = row.data if (nested && typeof nested === 'object' && !Array.isArray(nested)) { return unwrapListedRows(nested) } return [] } function parseCollectionGoodsSearchPayload(data) { if (!data) return { list: [], total: 0, authorizedCount: null } if (Array.isArray(data)) { return { list: data, total: data.length, authorizedCount: null } } const list = unwrapListedRows(data) const raw = data?.total ?? data?.count ?? data?.total_count const n = raw != null && raw !== '' ? Number(raw) : NaN const total = Number.isFinite(n) && n >= 0 ? n : list.length const authRaw = data?.authorized_count ?? data?.auth_count ?? data?.authorized_total ?? data?.purchasable_count const authN = authRaw != null && authRaw !== '' ? Number(authRaw) : NaN const authorizedCount = Number.isFinite(authN) && authN >= 0 ? authN : null return { list, total, authorizedCount } } function normalizeCollectionSearchScenes(sceneRows) { return (Array.isArray(sceneRows) ? sceneRows : []) .map((row) => { const scene_code = String(row?.scene_code || '').trim() if (!scene_code) return null const scene_values = sceneValuesObjectFromRaw(row?.scene_values) return Object.keys(scene_values).length > 0 ? { scene_code, scene_values } : { scene_code } }) .filter(Boolean) } /** * POST /api/supply/collection/goods/search — 统筹集合录入商品·左侧物料池 * @param {{ * supply_collection_code: string, * page?: number, * limit?: number, * keywords?: string, * scenes?: { scene_code: string, scene_values?: Record }[], * }} body */ export async function searchCollectionGoods(body) { const supply_collection_code = String(body?.supply_collection_code || '').trim() if (!supply_collection_code) return { list: [], total: 0, authorizedCount: null } const page = Math.max(1, Number(body?.page) || 1) const limit = Math.max(1, Number(body?.limit) || 20) const kw = String(body?.keywords ?? body?.keyworkds ?? '').trim() const scenes = normalizeCollectionSearchScenes(body?.scenes) const res = await request.post('/api/supply/collection/goods/search', { supply_collection_code, page, limit, keyworkds: kw, keywords: kw, scenes, }) return parseCollectionGoodsSearchPayload(unwrapResponseData(res)) } /** * POST /api/supply/collection/in_scenes — 统筹集合·本集合内按场景筛查 * @param {{ * supply_collection_code: string, * page?: number, * limit?: number, * keywords?: string, * scenes?: { scene_code: string, scene_values?: Record }[], * }} body */ export async function searchCollectionInScenes(body) { const supply_collection_code = String(body?.supply_collection_code || '').trim() if (!supply_collection_code) return { list: [], total: 0, authorizedCount: null } const page = Math.max(1, Number(body?.page) || 1) const limit = Math.max(1, Number(body?.limit) || 20) const kw = String(body?.keywords ?? body?.keyworkds ?? '').trim() const scenes = normalizeCollectionSearchScenes(body?.scenes) const res = await request.post('/api/supply/collection/in_scenes', { supply_collection_code, page, limit, keyworkds: kw, keywords: kw, scenes, }) return parseCollectionGoodsSearchPayload(unwrapResponseData(res)) } const COLLECTION_IN_SCENES_MAX_PAGES = 50 /** 翻页拉齐本集合内场景筛查命中的 goods_code(供右侧本地表格过滤) */ export async function fetchCollectionInScenesCodes(body, options = {}) { const limit = Math.max(1, Number(body?.limit) || 50) const maxPages = Math.max(1, Number(options?.maxPages) || COLLECTION_IN_SCENES_MAX_PAGES) const codes = new Set() let page = 1 let total = Infinity while (page <= maxPages && codes.size < total) { const { list, total: pageTotal } = await searchCollectionInScenes({ ...body, page, limit }) if (Number.isFinite(pageTotal) && pageTotal >= 0) total = pageTotal if (!list.length) break for (const row of list) { const code = String(row?.goods_code || row?.code || '').trim() if (code) codes.add(code) } if (list.length < limit) break page += 1 } return { codes, total: Number.isFinite(total) && total >= 0 ? total : codes.size, } } /** * listed / update 政策字段(与 goods/update 一致) * - 销售:sales_type, sales_mode, sales_rate(%), sales_amount(¥) * - 返利:rebate_type, rebate_mode, rebate_rate(%), rebate_amount(¥) * - 手工基准:rebate_manual_base(仅 rebate_type=3) */ export const COLLECTION_GOODS_POLICY_FIELD_KEYS = [ 'sales_type', 'sales_mode', 'sales_rate', 'sales_amount', 'rebate_type', 'rebate_mode', 'rebate_rate', 'rebate_amount', 'rebate_manual_base', ] export const COLLECTION_GOODS_REBATE_MANUAL_BASE_KEY = 'rebate_manual_base' /** 额外费用 · 单位商品各票种成本(统筹录入弹窗) */ export const COLLECTION_GOODS_EXTRA_FEE_FIELDS = [ { key: 'extra_cost_vat_13', label: '13%专票成本', group: 'special', bill_tax: '13' }, { key: 'extra_cost_vat_9', label: '9%专票成本', group: 'special', bill_tax: '9' }, { key: 'extra_cost_vat_6', label: '6%专票成本', group: 'special', bill_tax: '6' }, { key: 'extra_cost_vat_5', label: '5%专票成本', group: 'special', bill_tax: '5' }, { key: 'extra_cost_vat_3', label: '3%专票成本', group: 'special', bill_tax: '3' }, { key: 'extra_cost_general', label: '普票成本', group: 'other', bill_tax: 'general' }, { key: 'extra_cost_no_invoice', label: '无票成本', group: 'other', bill_tax: 'no_invoice' }, ] const EXTRA_FEE_FIELD_BY_BILL_TAX = Object.fromEntries( COLLECTION_GOODS_EXTRA_FEE_FIELDS.map((f) => [f.bill_tax, f]), ) /** @param {unknown} raw */ export function normalizeBillTaxKey(raw) { const t = String(raw ?? '').trim() if (!t) return '' if (EXTRA_FEE_FIELD_BY_BILL_TAX[t]) return t if (t === '普票' || t === 'general_ticket') return 'general' if (t === '无票' || t === 'no_ticket') return 'no_invoice' const n = Number(t) if (Number.isFinite(n)) { const intKey = String(Math.trunc(n)) if (EXTRA_FEE_FIELD_BY_BILL_TAX[intKey]) return intKey } return t } /** @param {unknown} raw */ function coerceExtraFeesSource(raw) { if (raw == null) return null if (typeof raw === 'string') { const s = raw.trim() if (!s) return null try { return JSON.parse(s) } catch { return null } } return raw } /** @param {Record} src @param {{ key: string, bill_tax: string }} field */ function readExtraFeeValue(src, field) { if (!src || typeof src !== 'object') return null const keys = [field.key, field.bill_tax] for (const k of keys) { const v = src[k] if (v != null && v !== '') return v } const n = Number(field.bill_tax) if (Number.isFinite(n)) { const numVal = src[n] ?? src[String(n)] if (numVal != null && numVal !== '') return numVal } return null } /** @param {unknown} extraFees */ export function hasAnyExtraFeeValue(extraFees) { if (!extraFees || typeof extraFees !== 'object') return false return COLLECTION_GOODS_EXTRA_FEE_FIELDS.some((f) => { const v = String(/** @type {Record} */ (extraFees)[f.key] ?? '').trim() return v !== '' && v !== '—' }) } /** @param {unknown} billTax */ export function extraFeeBillTaxLabel(billTax) { const t = normalizeBillTaxKey(billTax) if (!t) return '—' const field = EXTRA_FEE_FIELD_BY_BILL_TAX[t] if (field) return field.label.replace(/成本$/, '') if (/^\d+(\.\d+)?$/.test(t)) return `${t}%专票` return t } /** @param {unknown} value */ export function formatExtraFeeLogAmount(value) { if (value == null || value === '') return '+¥—' const n = Number(value) if (!Number.isNaN(n)) return `+¥${n.toFixed(2)}` const s = String(value).trim() return s ? `+¥${s}` : '+¥—' } /** @param {unknown} raw */ export function formatExtraFeeLogTime(raw) { const s = String(raw ?? '').trim() if (!s) return '' const d = new Date(s) if (!Number.isNaN(d.getTime())) { const pad = (n) => String(n).padStart(2, '0') return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` } return s.replace('T', ' ').replace(/\.\d+$/, '') } /** @param {unknown} raw */ export function formatExtraFeeLogDate(raw) { const s = formatExtraFeeLogTime(raw) if (!s) return '' return s.split(' ')[0] || '' } /** @param {unknown} raw */ export function formatExtraFeeLogClock(raw) { const s = formatExtraFeeLogTime(raw) if (!s || !s.includes(' ')) return '' return s.split(' ')[1] || '' } /** @param {{ bill_tax?: string, add_amount?: unknown, time?: string, operator?: string, operator_code?: string }} log */ export function extraFeeLogActionText(log) { const clock = formatExtraFeeLogClock(log?.time) const tax = extraFeeBillTaxLabel(log?.bill_tax) const amt = formatExtraFeeLogAmount(log?.add_amount) const op = extraFeeLogOperator(log) const parts = [] if (clock) parts.push(clock) parts.push(`${tax} 追加 ${amt}`) if (op) parts.push(op) return parts.join(' · ') } /** @param {{ operator?: string, operator_code?: string }} log */ export function extraFeeLogOperator(log) { const name = String(log?.operator || '').trim() const code = String(log?.operator_code || '').trim() return name || code || '' } /** @param {Record} row */ function pickExtraFeeEntryTotal(row) { const v = row.after_amount ?? row.amount ?? row.total ?? row.value if (v == null || v === '') return null return v } /** * 日志条目 → 各票种累计(同 bill_tax 取 ctime 最新一条的 after_amount/amount) * @param {unknown[]} entries */ export function extraFeesTotalsFromLatestLogEntries(entries) { const out = emptyExtraFees() const latestByTax = new Map() for (const row of entries || []) { if (!row || typeof row !== 'object') continue const tax = normalizeBillTaxKey(row.bill_tax) const field = EXTRA_FEE_FIELD_BY_BILL_TAX[tax] if (!field) continue const total = pickExtraFeeEntryTotal(row) if (total == null) continue const time = String(row.time ?? row.ctime ?? '').trim() const prev = latestByTax.get(tax) if (!prev || time >= prev.time) { latestByTax.set(tax, { time, total }) } } for (const [tax, { total }] of latestByTax) { const field = EXTRA_FEE_FIELD_BY_BILL_TAX[tax] if (field) out[field.key] = toDisplayNum(total) } return out } /** * 日志条目 after_amount → 各票种累计(同批同 bill_tax 取最后一条) * @param {unknown[]} entries */ export function extraFeesTotalsFromLogEntries(entries) { const out = emptyExtraFees() const byTax = new Map() for (const row of entries || []) { if (!row || typeof row !== 'object') continue const tax = normalizeBillTaxKey(row.bill_tax) const field = EXTRA_FEE_FIELD_BY_BILL_TAX[tax] if (!field) continue const total = pickExtraFeeEntryTotal(row) if (total == null) continue byTax.set(tax, total) } for (const [tax, val] of byTax) { const field = EXTRA_FEE_FIELD_BY_BILL_TAX[tax] if (field) out[field.key] = toDisplayNum(val) } return out } /** @param {unknown} current @param {unknown} patch */ export function mergeExtraFeesPatch(current, patch) { const out = normalizeExtraFees(current) const p = normalizeExtraFees(patch) for (const f of COLLECTION_GOODS_EXTRA_FEE_FIELDS) { const v = String(p[f.key] ?? '').trim() if (v !== '') out[f.key] = v } return out } /** * @param {Record} addDraft * @param {string} [remark] */ export function buildExtraFeesAddList(addDraft, remark = '') { const src = addDraft && typeof addDraft === 'object' && !Array.isArray(addDraft) ? addDraft : {} const rm = String(remark || '').trim() const list = [] for (const f of COLLECTION_GOODS_EXTRA_FEE_FIELDS) { const str = String(src[f.key] ?? '').trim() if (str === '') continue const n = Number(str) if (Number.isNaN(n)) continue const item = { bill_tax: f.bill_tax, amount: n } if (rm) item.remark = rm list.push(item) } return list } function normalizeExtraFeeLogRow(row) { if (!row || typeof row !== 'object') return null const addAmount = row.add_amount return { id: row.id, operator: String(row.cname || '').trim(), operator_code: String(row.cuser || '').trim(), time: String(row.ctime || '').trim(), bill_tax: normalizeBillTaxKey(row.bill_tax), add_amount: addAmount, before_amount: row.before_amount, after_amount: row.after_amount, remark: String(row.remark || '').trim(), } } function parseExtraFeesLogPayload(data) { if (!data) return { list: [], total: 0 } if (Array.isArray(data)) { const rows = data.map(normalizeExtraFeeLogRow).filter(Boolean) return { list: rows, total: rows.length } } let list = unwrapListedRows(data) if (!list.length && Array.isArray(data.logs)) list = data.logs if (!list.length && Array.isArray(data.log_list)) list = data.log_list if (!list.length && Array.isArray(data.records)) list = data.records const rows = list.map(normalizeExtraFeeLogRow).filter(Boolean) const raw = data?.total ?? data?.count ?? data?.total_count const n = raw != null && raw !== '' ? Number(raw) : NaN const total = Number.isFinite(n) && n >= 0 ? n : rows.length return { list: rows, total } } /** @param {unknown} data */ export function extractExtraFeesFromAddResponse(data) { const coerced = coerceExtraFeesSource(data) if (!coerced || typeof coerced !== 'object') return null const row = coerced if (row.extra_fees != null) { const normalized = normalizeExtraFees(row.extra_fees) if (hasAnyExtraFeeValue(normalized)) return normalized } if (Array.isArray(row.list) && row.list.length) { const fromLogs = extraFeesTotalsFromLatestLogEntries(row.list) if (hasAnyExtraFeeValue(fromLogs)) return fromLogs } const direct = normalizeExtraFees(row) if (hasAnyExtraFeeValue(direct)) return direct return null } /** * POST /api/supply/collection/goods/extra_fees/add * @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) return unwrapResponseData(res) } /** * POST /api/supply/collection/goods/extra_fees/log * @param {string} supplyCollectionCode * @param {string} goodsCode * @param {{ page?: number, limit?: number }} [options] */ export async function fetchCollectionGoodsExtraFeesLog( supplyCollectionCode, goodsCode, options = {}, ) { const supply_collection_code = String(supplyCollectionCode || '').trim() const goods_code = String(goodsCode || '').trim() const page = Math.max(1, Number(options.page) || 1) const limit = Math.min(50, Math.max(1, Number(options.limit) || 20)) const res = await request.post('/api/supply/collection/goods/extra_fees/log', { supply_collection_code, goods_code, page, limit, }) return parseExtraFeesLogPayload(unwrapResponseData(res)) } export function emptyExtraFees() { return Object.fromEntries( COLLECTION_GOODS_EXTRA_FEE_FIELDS.map((f) => [f.key, '']), ) } /** @deprecated 使用 emptyExtraFees */ export const emptyExtraFeeCosts = emptyExtraFees export function normalizeExtraFees(raw) { const coerced = coerceExtraFeesSource(raw) if (!coerced) return emptyExtraFees() if (Array.isArray(coerced)) { return extraFeesTotalsFromLatestLogEntries(coerced) } const src = typeof coerced === 'object' ? coerced : {} const out = emptyExtraFees() for (const f of COLLECTION_GOODS_EXTRA_FEE_FIELDS) { const v = readExtraFeeValue(src, f) if (v != null && v !== '') out[f.key] = toDisplayNum(v) } return out } export function buildExtraFeesPayload(extraFees) { const src = extraFees && typeof extraFees === 'object' && !Array.isArray(extraFees) ? extraFees : {} const out = {} for (const f of COLLECTION_GOODS_EXTRA_FEE_FIELDS) { const str = String(src[f.key] ?? '').trim() if (str === '') continue const n = Number(str) if (!Number.isNaN(n)) out[f.key] = n } return out } export function hydrateExtraFeesOnRow(row, source) { const r = source && typeof source === 'object' ? source : {} row.extra_fees = normalizeExtraFees(r.extra_fees ?? r.raw?.extra_fees) } /** @deprecated 使用 hydrateExtraFeesOnRow */ export function hydrateExtraFeeCostsFromApi(target, source) { hydrateExtraFeesOnRow(target, source) } /** 保底返利 rebate_type(与 demo 下拉一致) */ export const SUPPLY_COLLECTION_REBATE_TYPE_OPTIONS = [ { value: 0, label: '不设置' }, { value: 1, label: '基于网价' }, { value: 2, label: '基于付款单价' }, { value: 3, label: '手工录入基准值' }, { value: 4, label: '固定金额返利' }, ] /** @deprecated 使用 @/utils/supplyCollectionGoodsCalc COLLECTION_SALES_HAS_OPTIONS */ export { COLLECTION_SALES_HAS_OPTIONS as SUPPLY_COLLECTION_SALES_YES_NO_OPTIONS } from '@/utils/supplyCollectionGoodsCalc' function toDisplayNum(v) { if (v == null || v === '') return '' const n = Number(v) return Number.isNaN(n) ? String(v) : String(v) } function strNum(v) { if (v == null || v === '') return '' return String(v).trim() } function parseIsAuthorized(v) { if (v === true || v === 1 || v === '1') return true if (v === false || v === 0 || v === '0') return false return null } function parseStableSupply(v) { return v === true || v === 1 || v === '1' } /** @param {unknown} row */ export function normalizeCollectionGoodsRow(row) { if (!row || typeof row !== 'object') return null const embedded = row.raw && typeof row.raw === 'object' ? row.raw : null const r = embedded ? { ...embedded, ...row, goods_categorys: row.goods_categorys ?? embedded.goods_categorys, goods: row.goods ?? embedded.goods, } : row const nested = r.goods && typeof r.goods === 'object' ? r.goods : null const goods_code = String( r.goods_code || r.goodsCode || r.product_code || r.material_code || r.sku || r.item_code || r.code || nested?.goods_code || nested?.code || '', ).trim() if (!goods_code) return null const category_code = resolveGoodsCategoryCode(r) || String( r.category_code || r.classify_code || r.categoryCode || nested?.category_code || nested?.classify_code || '', ).trim() const catEntry = pickGoodsCategoryEntry(r, category_code) const category_name = String( catEntry?.category_name || r.category_name || r.classify_name || '', ).trim() const name = pickGoodsDisplayName(r, category_code, { allowTopLevelName: true }) || String( r.name || r.goods_name || r.goodsName || r.product_name || r.material_name || nested?.goods_name || nested?.name || goods_code, ).trim() return { goods_code, name, category_code, category_name, brand: String(r.brand || r.brand_name || '').trim(), model: String(r.model || r.spec || r.specification || '').trim(), scene_code: String(r.scene_code || '').trim(), web_price: toDisplayNum(r.net_price ?? r.web_price ?? r.price ?? ''), warehouse_codes: collectWarehouseCodesFromRow(r), rebate_type: r.rebate_type ?? '', rebate_mode: r.rebate_mode ?? '', rebate_rate: toDisplayNum(r.rebate_rate ?? ''), rebate_amount: toDisplayNum(r.rebate_amount ?? ''), sales_type: r.sales_type ?? '', sales_mode: r.sales_mode ?? '', sales_rate: toDisplayNum(r.sales_rate ?? ''), sales_amount: toDisplayNum(r.sales_amount ?? ''), rebate_manual_base: toDisplayNum(r.rebate_manual_base ?? r.manual_base ?? ''), is_authorized: parseIsAuthorized(r.is_authorized), is_stable: parseStableSupply(r.is_stable ?? r.is_stable_supply ?? r.stable_supply), extra_fees: normalizeExtraFees(r.extra_fees), business_type_code: String(r.business_type_code || '').trim(), business_type_name: String(r.business_type_name || '').trim(), bills: Array.isArray(r.bills) ? r.bills : [], raw: row, } } const COLLECTION_GOODS_LISTED_PAGE_SIZE = 100 const COLLECTION_GOODS_LISTED_MAX_PAGES = 200 function normalizeCollectionGoodsListedPayload(data) { const { list, total } = parseCollectionGoodsSearchPayload(data) const rows = [] const seen = new Set() for (const row of list) { const g = normalizeCollectionGoodsRow(row) if (!g || seen.has(g.goods_code)) continue seen.add(g.goods_code) rows.push(g) } return { rows, total } } /** 从统筹商品 bills[] 取首选进项税率(默认 bills[0]) */ export function pickCollectionGoodsPreferredTax(row) { const normalized = normalizeCollectionGoodsRow(row) || (row && typeof row === 'object' ? row : null) if (!normalized) return null const raw = normalized.raw || row const bills = Array.isArray(normalized.bills) ? normalized.bills : (Array.isArray(raw?.bills) ? raw.bills : []) const first = bills[0] if (!first) return null const v = first.tax ?? first.rate if (v == null || v === '') return null const n = Number(v) return Number.isFinite(n) ? n : null } /** 选品弹窗:从集合商品行提取 goods_code / name / payment_price / 进项税率 */ export function pickCollectionGoodsPickFields(row) { const normalized = normalizeCollectionGoodsRow(row) || (row && typeof row === 'object' ? row : null) if (!normalized) return null const sourceRow = normalized.raw || row const goods_code = String(normalized.goods_code || '').trim() if (!goods_code) return null const payRaw = sourceRow?.payment_price ?? row?.payment_price ?? '' const payment_price = payRaw !== '' && payRaw != null ? Number(payRaw) : null const bill_tax = pickCollectionGoodsPreferredTax(sourceRow) const nameCandidates = [ String(row?.name || '').trim(), String(normalized.name || '').trim(), pickGoodsNameFromListedRow(sourceRow), pickGoodsNameFromListedRow(row), ].filter(Boolean) const name = nameCandidates.find((n) => n !== goods_code) || nameCandidates[0] || goods_code return { goods_code, name, payment_price: Number.isFinite(payment_price) ? payment_price : null, bill_tax, raw: normalized, } } /** 按税率在 type=4 费用票据列表中匹配 bill_code;无匹配则返回 fallback */ export function matchExpenseBillCodeByTax(tax, expenseBills, fallbackCode = '') { if (!Number.isFinite(tax)) return String(fallbackCode || '').trim() const list = Array.isArray(expenseBills) ? expenseBills : [] const hit = list.find((b) => { const v = b?.tax ?? b?.rate if (v == null || v === '') return false const n = Number(v) return Number.isFinite(n) && n === tax }) const code = String(hit?.bill_code ?? '').trim() return code || String(fallbackCode || '').trim() } /** * 统筹集合商品分页(无关键词走 listed,有关键词走 search) * @param {{ supply_collection_code: string, page?: number, limit?: number, keywords?: string }} body */ export async function listCollectionGoodsPage(body = {}) { const supply_collection_code = String(body?.supply_collection_code || '').trim() if (!supply_collection_code) return { list: [], total: 0 } const page = Math.max(1, Number(body?.page) || 1) const limit = Math.max(1, Number(body?.limit) || 20) const kw = String(body?.keywords ?? body?.keyworkds ?? '').trim() if (kw) { const res = await searchCollectionGoods({ supply_collection_code, keywords: kw, page, limit, }) const list = (res.list || []).map(normalizeCollectionGoodsRow).filter(Boolean) return { list, total: res.total || list.length } } const res = await request.post('/api/supply/collection/goods/listed', { supply_collection_code, scene_code: '', page, limit, }) const { rows, total } = normalizeCollectionGoodsListedPayload(unwrapResponseData(res)) return { list: rows, total } } /** * POST /api/supply/collection/goods/listed * 接口已分页:supply_collection_code + scene_code(空=全部场景)+ page + limit * @param {string} supplyCollectionCode * @param {{ silent?: boolean, scene_code?: string, pageSize?: number }} [options] */ export async function fetchCollectionGoods(supplyCollectionCode, options = {}) { const { silent = false, scene_code = '', pageSize = COLLECTION_GOODS_LISTED_PAGE_SIZE, } = options const supply_collection_code = String(supplyCollectionCode || '').trim() if (!supply_collection_code) return [] const sceneCode = String(scene_code || '').trim() const limit = Math.max(1, Number(pageSize) || COLLECTION_GOODS_LISTED_PAGE_SIZE) try { const all = [] const seen = new Set() let page = 1 let total = Infinity while (page <= COLLECTION_GOODS_LISTED_MAX_PAGES && all.length < total) { const res = await request.post('/api/supply/collection/goods/listed', { supply_collection_code, scene_code: sceneCode, page, limit, }) const d = unwrapResponseData(res) const { rows, total: pageTotal } = normalizeCollectionGoodsListedPayload(d) if (Number.isFinite(pageTotal) && pageTotal >= 0) total = pageTotal if (!rows.length) break for (const g of rows) { if (seen.has(g.goods_code)) continue seen.add(g.goods_code) all.push(g) } if (rows.length < limit) break page += 1 } return all } catch (e) { if (silent) return [] throw e } } /** POST /api/supply/collection/goods/add */ export function supplyCollectionGoodsAdd(body) { return request.post('/api/supply/collection/goods/add', body) } /** POST /api/supply/collection/goods/remove */ export function supplyCollectionGoodsRemove(body) { return request.post('/api/supply/collection/goods/remove', body) } /** POST /api/supply/collection/goods/batch_add */ export function supplyCollectionGoodsBatchAdd(body) { return request.post('/api/supply/collection/goods/batch_add', body) } /** POST /api/supply/collection/goods/update */ export function supplyCollectionGoodsUpdate(body) { return request.post('/api/supply/collection/goods/update', body) } /** POST /api/supply/collection/copy_goods — 复制物料到其他统筹集合 */ export const COPY_COLLECTION_GOODS_FIELD_KEYS = [ 'net_price', 'warehouses', 'rebate_type', 'rebate_mode', 'rebate_rate', 'rebate_amount', 'sales_type', 'sales_mode', 'sales_rate', 'sales_amount', 'rebate_manual_base', ] export const COPY_COLLECTION_GOODS_FIELD_LABELS = { net_price: '网价', warehouses: '直达仓', rebate_type: '基础返利类型', rebate_mode: '基础返利方式', rebate_rate: '基础返利比例', rebate_amount: '基础返利金额', sales_type: '销售折让类型', sales_mode: '销售折让方式', sales_rate: '销售折让比例', sales_amount: '销售折让金额', rebate_manual_base: '返利手工基准', } /** 复制弹窗表头短标签 */ export const COPY_COLLECTION_GOODS_FIELD_SHORT_LABELS = { net_price: '网价', warehouses: '直达仓', rebate_type: '返利类型', rebate_mode: '返利方式', rebate_rate: '返利比例', rebate_amount: '返利金额', sales_type: '折让类型', sales_mode: '折让方式', sales_rate: '折让比例', sales_amount: '折让金额', rebate_manual_base: '手工基准', } /** * @param {{ * source_supply_collection_code: string, * target_supply_collection_codes: string[], * goods_list: { goods_code: string, category_code?: string }[], * fields?: string[], * }} body fields 为空表示复制全部可支持字段 */ export function buildCollectionCopyGoodsBody(body = {}) { const source_supply_collection_code = String( body.source_supply_collection_code || '', ).trim() const target_supply_collection_codes = [ ...new Set( (Array.isArray(body.target_supply_collection_codes) ? body.target_supply_collection_codes : []) .map((c) => String(c ?? '').trim()) .filter(Boolean), ), ] const goods_list = (Array.isArray(body.goods_list) ? body.goods_list : []) .map((row) => ({ goods_code: String(row?.goods_code ?? '').trim(), category_code: String(row?.category_code ?? '').trim(), })) .filter((row) => row.goods_code) const fields = (Array.isArray(body.fields) ? body.fields : []) .map((k) => String(k ?? '').trim()) .filter((k) => COPY_COLLECTION_GOODS_FIELD_KEYS.includes(k)) const payload = { source_supply_collection_code, target_supply_collection_codes, goods_list, fields, } if (!source_supply_collection_code) { throw new Error('缺少 source_supply_collection_code') } if (!target_supply_collection_codes.length) { throw new Error('请选择目标统筹集合') } if (!goods_list.length) { throw new Error('请选择要复制的商品') } return payload } /** POST /api/supply/collection/copy_goods */ export function supplyCollectionCopyGoods(body) { return request.post('/api/supply/collection/copy_goods', buildCollectionCopyGoodsBody(body)) } /** * 按各商品 fields 分组,便于一次请求只带同一套 fields(接口为整批 goods 共用 fields) * @param {{ goods_code: string, category_code?: string }[]} goodsList * @param {Record} fieldMap goods_code → 勾选的字段 */ export function groupCollectionCopyGoodsByFields(goodsList, fieldMap = {}) { const groups = new Map() for (const row of goodsList || []) { const goods_code = String(row?.goods_code ?? '').trim() if (!goods_code) continue const fields = (Array.isArray(fieldMap[goods_code]) ? fieldMap[goods_code] : []) .map((k) => String(k ?? '').trim()) .filter((k) => COPY_COLLECTION_GOODS_FIELD_KEYS.includes(k)) const sig = fields.length ? fields.slice().sort().join('|') : '__all__' if (!groups.has(sig)) { groups.set(sig, { fields: fields.length ? fields : [], goods_list: [], }) } groups.get(sig).goods_list.push({ goods_code, category_code: String(row?.category_code ?? '').trim(), }) } return [...groups.values()] } export function buildCollectionGoodsUpdateBody(supplyCollectionCode, row) { const supply_collection_code = String(supplyCollectionCode || '').trim() const goods_code = String(row?.goods_code || '').trim() const category_code = String(row?.category_code || '').trim() const r = { ...row } syncPolicyFieldsToApi(r) const body = { supply_collection_code, category_code, goods_code } const priceStr = String(r.web_price ?? '').trim() if (priceStr !== '') { const n = Number(priceStr) if (!Number.isNaN(n)) body.net_price = n } const wh = Array.isArray(r.warehouse_codes) ? r.warehouse_codes.map((c) => String(c || '').trim()).filter(Boolean) : [] body.warehouses = wh const putNum = (key, val) => { if (val == null || val === '') return const n = Number(val) if (!Number.isNaN(n)) body[key] = n } for (const key of COLLECTION_GOODS_POLICY_FIELD_KEYS) { if (key === COLLECTION_GOODS_REBATE_MANUAL_BASE_KEY) continue putNum(key, r[key]) } const rt = Number(r.rebate_type) || 0 if (rt === 3) { const manualBase = strNum(r.rebate_manual_base ?? r.gr_manual_base) if (manualBase !== '') { const m = Number(manualBase) if (!Number.isNaN(m)) body[COLLECTION_GOODS_REBATE_MANUAL_BASE_KEY] = m } } body.extra_fees = buildExtraFeesPayload(r.extra_fees) body.is_stable = r.is_stable === true return body } /** listed/update 行是否带回 warehouses 或 warehouse_codes 字段(区分「未配置」与「已存空」) */ export function goodsRowHasWarehouseSnapshot(row) { const r = row && typeof row === 'object' ? row : {} return Array.isArray(r.warehouses) || Array.isArray(r.warehouse_codes) } /** * @param {string} supplyCollectionCode * @param {ReturnType[]} nextItems */ export async function persistCollectionGoods(supplyCollectionCode, nextItems) { const supply_collection_code = String(supplyCollectionCode || '').trim() if (!supply_collection_code) throw new Error('缺少统筹集合编码') const makeCompositeKey = (g) => { const cc = String(g?.category_code || '').trim() const gc = String(g?.goods_code || '').trim() return cc ? `${cc}:${gc}` : gc } const existing = await fetchCollectionGoods(supply_collection_code, { silent: true }) const existingMap = new Map(existing.map((g) => [makeCompositeKey(g), g])) const nextMap = new Map() for (const g of nextItems || []) { if (!g?.goods_code) continue nextMap.set(makeCompositeKey(g), g) } const removeKeys = [...existingMap.keys()].filter((k) => !nextMap.has(k)) const addKeys = [...nextMap.keys()].filter((k) => !existingMap.has(k)) for (const key of removeKeys) { const row = existingMap.get(key) await supplyCollectionGoodsRemove({ supply_collection_code, goods_code: row?.goods_code || '', category_code: row?.category_code || '', }) } if (addKeys.length) { await supplyCollectionGoodsBatchAdd({ supply_collection_code, list: addKeys.map((key) => { const row = nextMap.get(key) return { goods_code: row?.goods_code || '', category_code: row?.category_code || '', } }), }) } } /** * 按统筹集合已关联场景拉物料池(逐场景 goods/in_scenes,无筛查时不传 scene_values) * @param {string[]} sceneCodes */ export async function loadGoodsPoolByScenes(sceneCodes) { const codes = [...new Set(sceneCodes.map((c) => String(c || '').trim()).filter(Boolean))] if (!codes.length) return [] const map = new Map() const results = await Promise.allSettled( codes.map(async (scene_code) => { const rows = await loadGoodsInScenes(scene_code, []) return rows.map((row) => { const g = normalizeCollectionGoodsRow(row) return g ? { ...g, scene_code } : null }).filter(Boolean) }), ) for (const r of results) { if (r.status !== 'fulfilled') continue for (const g of r.value || []) { if (!g.goods_code || map.has(g.goods_code)) continue map.set(g.goods_code, g) } } return [...map.values()] }