Files
crawler-plugin/admin-frontend-vue/src/pages/asin/skip-price-detail-model.ts
T
huangzd1997 c1d6ce6d20 task-77(ASIN 数据中心): 实现最低价 ASIN 详情编辑抽屉
抽屉副标题/5 国家草稿(ASIN+最低价)与变更差异(清空 ASIN→删除, 仅清价→
update)纯逻辑, 保存走 PUT/DELETE /api/admin/skip-price-asins/{id}/countries/{country},
10 个契约测试。
2026-09-05 16:14:22 +08:00

91 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 最低价 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<SkipPriceItem, 'groupName' | 'shopName'>): 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<string, SkipPriceCountryDraft>()
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
}