/** 最低价 ASIN 详情编辑抽屉模型(任务 77):副标题、5 国家草稿(ASIN+最低价)与变更差异,纯逻辑。 */ import { ASIN_COUNTRY_CODES, asinCountryLabel } from './asin-country.ts' import { skipPriceAsin, skipPriceMinimum, type SkipPriceItem } from './skip-price-model.ts' /** 抽屉副标题:`分组 / 店铺名`;无分组时仅店铺名。 */ export function skipPriceDetailSubtitle(item: Pick): string { const group = (item.groupName || '').trim() const shop = (item.shopName || '').trim() return group ? `${group} / ${shop}` : shop } /** 抽屉单国家草稿(编辑前初值)。 */ export interface SkipPriceCountryDraft { code: string label: string asin: string /** 最低价输入原文;空串表示清除。 */ price: string } /** 由列表行生成 5 国家草稿,保持支持国家序。 */ export function skipPriceCountryDraftsOf(item: SkipPriceItem): SkipPriceCountryDraft[] { return ASIN_COUNTRY_CODES.map((code) => { const price = skipPriceMinimum(item, code) return { code, label: asinCountryLabel(code), asin: skipPriceAsin(item, code), price: price === null ? '' : String(price), } }) } /** 非负可解析价格 → number;空/非法/负数返回 null。 */ export function normalizeSkipPriceCellPrice(value: string): number | null { const trimmed = (value || '').trim() if (!trimmed) return null if (!/^\d+(\.\d+)?$/.test(trimmed)) return null const num = Number(trimmed) return Number.isFinite(num) && num >= 0 ? num : null } function priceEquals(a: number | null, b: number | null): boolean { if (a === null || b === null) return a === b return a === b } export type SkipPriceCountryChangeKind = 'update' | 'delete' export interface SkipPriceCountryChange { code: string label: string kind: SkipPriceCountryChangeKind asin?: string minimumPrice?: number | null } /** * 与当前行比较草稿产出变更: * - ASIN 与最低价均未变(归一后相等) → 跳过; * - ASIN 留空(当前有值) → delete; * - 否则 update(ASIN 大写 + 最低价随行,可为空以清价)。 * 未知国家码或缺失草稿不产生变更,避免误删;结果按支持国家序稳定输出。 */ export function diffSkipPriceCountryChanges( item: SkipPriceItem, drafts: SkipPriceCountryDraft[], ): SkipPriceCountryChange[] { const byCode = new Map() for (const draft of drafts || []) { if (!draft || typeof draft.code !== 'string') continue byCode.set(draft.code.trim().toUpperCase(), draft) } const changes: SkipPriceCountryChange[] = [] for (const code of ASIN_COUNTRY_CODES) { const draft = byCode.get(code) if (!draft) continue const nextAsin = (draft.asin || '').trim().toUpperCase() const nextPrice = normalizeSkipPriceCellPrice(draft.price) const currentAsin = skipPriceAsin(item, code).trim().toUpperCase() const currentPrice = skipPriceMinimum(item, code) if (nextAsin === currentAsin && priceEquals(currentPrice, nextPrice)) continue if (!nextAsin) { changes.push({ code, label: asinCountryLabel(code), kind: 'delete' }) } else { changes.push({ code, label: asinCountryLabel(code), kind: 'update', asin: nextAsin, minimumPrice: nextPrice }) } } return changes }