task-74(ASIN 数据中心): 实现查询 ASIN 详情抽屉

抽屉副标题/5 国家草稿与变更差异(空值→删除)纯逻辑, 保存走 PUT/DELETE
/api/admin/query-asins/{id}/countries/{country}, 9 个契约测试。
This commit is contained in:
2026-09-05 16:09:31 +08:00
parent a9b4a56986
commit ad6658e490
3 changed files with 200 additions and 0 deletions
@@ -0,0 +1,18 @@
/** 查询 ASIN 详情抽屉保存适配(任务 74):PUT/DELETE /api/admin/query-asins/{id}/countries/{country}。 */
import { http } from '@/api/http'
export const QUERY_ASIN_DETAIL_ENDPOINT = '/api/admin/query-asins'
function countryUrl(id: number, country: string): string {
return `${QUERY_ASIN_DETAIL_ENDPOINT}/${id}/countries/${country}`
}
/** 保存某国家 ASINbody { asin });返回后由列表刷新回读最新行。 */
export async function updateQueryAsinCountryAsin(id: number, country: string, asin: string): Promise<void> {
await http.put<unknown>(countryUrl(id, country), { asin })
}
/** 删除某国家 ASIN(清空该站点 ASIN)。 */
export async function deleteQueryAsinCountryAsin(id: number, country: string): Promise<void> {
await http.delete<unknown>(countryUrl(id, country))
}
@@ -0,0 +1,76 @@
/** 查询 ASIN 详情抽屉模型(任务 74):副标题、5 国家草稿与变更差异(空→删除),纯逻辑。 */
import { QUERY_ASIN_COUNTRIES, asinForCountry, type QueryAsinCountry, type QueryAsinItem } from './query-asin-model.ts'
export const QUERY_ASIN_COUNTRY_LABELS: Record<QueryAsinCountry, string> = {
DE: '德国',
UK: '英国',
FR: '法国',
IT: '意大利',
ES: '西班牙',
}
/** 国家码 → 中文标签;不属白名单原样返回。 */
export function queryAsinCountryLabel(country: string): string {
return QUERY_ASIN_COUNTRY_LABELS[country as QueryAsinCountry] || country
}
/** 抽屉副标题:`分组 / 店铺名`;无分组时仅店铺名。 */
export function queryAsinDetailSubtitle(item: Pick<QueryAsinItem, 'groupId' | 'groupName' | 'shopName'>): string {
const group = (item.groupName || '').trim()
const shop = (item.shopName || '').trim()
return group ? `${group} / ${shop}` : shop
}
/** 抽屉单国家输入草稿(编辑前的初值)。 */
export interface QueryAsinCountryDraft {
code: string
value: string
}
/** 由列表行生成 5 国家草稿(当前值 trimmed,保持支持国家序)。 */
export function queryAsinCountryDraftsOf(item: QueryAsinItem): QueryAsinCountryDraft[] {
return QUERY_ASIN_COUNTRIES.map((code) => ({
code,
value: asinForCountry(item, code).trim(),
}))
}
export type QueryAsinCountryChangeKind = 'update' | 'delete'
export interface QueryAsinCountryChange {
code: string
label: string
kind: QueryAsinCountryChangeKind
asin?: string
}
/**
* 与当前行比较草稿产出变更:
* - 归一(trim + 大写)后与当前相同 → 跳过;
* - 草稿留空(当前有值) → delete;
* - 新值非空 → update。
* 未知国家码或缺失草稿不产生变更,避免误删;结果按支持国家序稳定输出。
*/
export function diffQueryAsinCountryChanges(
item: QueryAsinItem,
drafts: QueryAsinCountryDraft[],
): QueryAsinCountryChange[] {
const byCode = new Map<string, string>()
for (const draft of drafts || []) {
if (!draft || typeof draft.code !== 'string') continue
byCode.set(draft.code.trim().toUpperCase(), typeof draft.value === 'string' ? draft.value : '')
}
const changes: QueryAsinCountryChange[] = []
for (const code of QUERY_ASIN_COUNTRIES) {
if (!byCode.has(code)) continue
const next = (byCode.get(code) || '').trim().toUpperCase()
const current = asinForCountry(item, code).trim().toUpperCase()
if (next === current) continue
if (!next) {
changes.push({ code, label: queryAsinCountryLabel(code), kind: 'delete' })
} else {
changes.push({ code, label: queryAsinCountryLabel(code), kind: 'update', asin: next })
}
}
return changes
}
+106
View File
@@ -0,0 +1,106 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readSource } from './helpers.ts'
import {
QUERY_ASIN_COUNTRY_LABELS,
queryAsinCountryLabel,
queryAsinDetailSubtitle,
queryAsinCountryDraftsOf,
diffQueryAsinCountryChanges,
type QueryAsinCountryChange,
} from '../src/pages/asin/query-asin-detail-model.ts'
import { toQueryAsinItem } from '../src/pages/asin/query-asin-model.ts'
test('test_task_074_query_asin_detail_subtitle_normal_primary_path', () => {
// 正常主路径:抽屉副标题 = 分组 / 店铺名。
const item = toQueryAsinItem({ id: 1, groupName: '华东组', shopName: 'BlueWave' })!
assert.equal(queryAsinDetailSubtitle(item), '华东组 / BlueWave')
})
test('test_task_074_query_asin_detail_subtitle_normal_variant_input', () => {
// 正常变体:无分组时仅店铺名;5 国家标签齐全。
const item = toQueryAsinItem({ id: 2, shopName: 'RedSun' })!
assert.equal(queryAsinDetailSubtitle(item), 'RedSun')
assert.deepEqual(QUERY_ASIN_COUNTRY_LABELS, {
DE: '德国', UK: '英国', FR: '法国', IT: '意大利', ES: '西班牙',
})
assert.equal(queryAsinCountryLabel('DE'), '德国')
assert.equal(queryAsinCountryLabel('FR'), '法国')
})
test('test_task_074_query_asin_detail_drafts_normal_primary', () => {
// 正常主路径:由行生成 5 国家草稿(trimmed 当前值)。
const item = toQueryAsinItem({ id: 3, shopName: 'S', asinDe: ' B0DE1 ', asinEs: 'B0ES1' })!
const drafts = queryAsinCountryDraftsOf(item)
assert.equal(drafts.length, 5)
assert.equal(drafts[0].code, 'DE')
assert.equal(drafts[0].value, 'B0DE1')
assert.equal(drafts[4].value, 'B0ES1')
assert.equal(drafts[1].value, '')
})
test('test_task_074_query_asin_detail_diff_normal_no_change', () => {
// 正常重复:值未变(大小写/空白归一后相等) → 无任何变更。
const item = toQueryAsinItem({ id: 4, shopName: 'S', asinDe: 'B0DE1' })!
const changes = diffQueryAsinCountryChanges(item, [
{ code: 'DE', value: ' b0de1 ' },
{ code: 'UK', value: '' },
])
assert.deepEqual(changes, [])
})
test('test_task_074_query_asin_detail_diff_boundary_empty_means_delete', () => {
// 边界空值:当前有值但草稿留空 → 删除该国家。
const item = toQueryAsinItem({ id: 5, shopName: 'S', asinUk: 'B0UK1', asinDe: 'B0DE1' })!
const changes = diffQueryAsinCountryChanges(item, [
{ code: 'UK', value: '' },
{ code: 'DE', value: 'B0DE1' },
])
assert.equal(changes.length, 1)
assert.equal(changes[0].kind, 'delete')
assert.equal(changes[0].code, 'UK')
})
test('test_task_074_query_asin_detail_diff_normal_update', () => {
// 正常主路径:值不同且非空 → update,ASIN 大写归一。
const item = toQueryAsinItem({ id: 6, shopName: 'S', asinFr: 'B0OLD' })!
const changes = diffQueryAsinCountryChanges(item, [{ code: 'FR', value: ' b0new1 ' }])
assert.equal(changes.length, 1)
assert.equal(changes[0].kind, 'update')
assert.equal(changes[0].asin, 'B0NEW1')
})
test('test_task_074_query_asin_detail_diff_invalid_input_rejected', () => {
// 异常输入:草稿含未知国家码被忽略;缺失国家草稿不产生变更(不会误删)。
const item = toQueryAsinItem({ id: 7, shopName: 'S', asinDe: 'B0DE1' })!
const changes = diffQueryAsinCountryChanges(item, [
{ code: 'XX', value: 'abc' },
{ code: 'DE', value: 'B0DE1' },
])
assert.deepEqual(changes, [])
})
test('test_task_074_query_asin_detail_diff_boundary_multi', () => {
// 边界/多变更:同批 update + delete 混合且顺序稳定(按支持国家序)。
const item = toQueryAsinItem({ id: 8, shopName: 'S', asinDe: 'D', asinIt: 'I', asinEs: 'E' })!
const changes: QueryAsinCountryChange[] = diffQueryAsinCountryChanges(item, [
{ code: 'IT', value: '' },
{ code: 'DE', value: 'NEWD' },
{ code: 'ES', value: 'E' },
])
assert.equal(changes.length, 2)
assert.deepEqual(changes.map((c) => `${c.kind}:${c.code}`), ['update:DE', 'delete:IT'])
})
test('test_task_074_query_asin_detail_dependency_failure_returns_actionable_message', () => {
// 依赖失败/保存走 adapterPUT/DELETE /query-asins/{id}/countries/{country},页面不内联。
const api = readSource('src/pages/asin/query-asin-detail-api.ts')
assert.match(api, /query-asins/)
assert.match(api, /countries/)
assert.match(api, /http\.put/)
assert.match(api, /http\.delete/)
assert.match(api, /updateQueryAsinCountryAsin|saveQueryAsinCountry/)
assert.match(api, /deleteQueryAsinCountryAsin/)
const mod = readSource('src/pages/asin/query-asin-detail-model.ts')
assert.equal(/axios|http\./.test(mod), false, '详情抽屉模型保持纯逻辑')
})