task-77(ASIN 数据中心): 实现最低价 ASIN 详情编辑抽屉
抽屉副标题/5 国家草稿(ASIN+最低价)与变更差异(清空 ASIN→删除, 仅清价→
update)纯逻辑, 保存走 PUT/DELETE /api/admin/skip-price-asins/{id}/countries/{country},
10 个契约测试。
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
/** 最低价 ASIN 详情编辑抽屉保存适配(任务 77):PUT/DELETE /api/admin/skip-price-asins/{id}/countries/{country}。 */
|
||||
import { http } from '@/api/http'
|
||||
|
||||
export const SKIP_PRICE_DETAIL_ENDPOINT = '/api/admin/skip-price-asins'
|
||||
|
||||
function countryUrl(id: number, country: string): string {
|
||||
return `${SKIP_PRICE_DETAIL_ENDPOINT}/${id}/countries/${country}`
|
||||
}
|
||||
|
||||
/** 保存某国家 ASIN 与最低价(body { asin, minimumPrice });返回后由列表刷新回读最新行。 */
|
||||
export async function updateSkipPriceCountry(
|
||||
id: number,
|
||||
country: string,
|
||||
asin: string,
|
||||
minimumPrice: number | null,
|
||||
): Promise<void> {
|
||||
await http.put<unknown>(countryUrl(id, country), { asin, minimumPrice })
|
||||
}
|
||||
|
||||
/** 删除某国家最低价 ASIN(清空该站点)。 */
|
||||
export async function deleteSkipPriceCountry(id: number, country: string): Promise<void> {
|
||||
await http.delete<unknown>(countryUrl(id, country))
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/** 最低价 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
|
||||
}
|
||||
Reference in New Issue
Block a user