diff --git a/admin-frontend-vue/src/pages/shop/shop-key-api.ts b/admin-frontend-vue/src/pages/shop/shop-key-api.ts index 34887f54..ef4e3019 100644 --- a/admin-frontend-vue/src/pages/shop/shop-key-api.ts +++ b/admin-frontend-vue/src/pages/shop/shop-key-api.ts @@ -50,3 +50,8 @@ export async function updateShopKey(id: number, form: ShopKeyFormValues): Promis } return item } + +/** 删除店铺密钥:DELETE /api/admin/shop-keys/{id};成功后由调用方按目标页刷新。 */ +export async function deleteShopKey(id: number): Promise { + await http.delete(`${SHOP_KEYS_ENDPOINT}/${id}`) +} diff --git a/admin-frontend-vue/src/pages/shop/shop-key-delete.ts b/admin-frontend-vue/src/pages/shop/shop-key-delete.ts new file mode 100644 index 00000000..09c9fa96 --- /dev/null +++ b/admin-frontend-vue/src/pages/shop/shop-key-delete.ts @@ -0,0 +1,39 @@ +/** 店铺密钥删除确认与刷新(任务 87):删除前确认文案与删除后目标页计算,纯逻辑。 */ +import { SHOP_PAGE_DEFAULT_SIZE } from './shop-dto.ts' +import type { ShopKeyItem } from './shop-dto.ts' + +function nameOrFallback(item: ShopKeyItem | null | undefined): string { + if (!item || typeof item !== 'object') return '' + if (typeof item.ziniaoAccountName === 'string' && item.ziniaoAccountName.trim()) { + return item.ziniaoAccountName.trim() + } + if (typeof item.id === 'number' && Number.isFinite(item.id) && item.id >= 1) { + return `#${Math.floor(item.id)}` + } + return '' +} + +/** 确认文案用到的可读名称(账号名优先,空则回退 #id)。 */ +export function shopKeyDeleteTargetName(item: ShopKeyItem | null | undefined): string { + return nameOrFallback(item) +} + +/** 删除确认文案;空记录返回 null(不弹可误删的确认)。 */ +export function shopKeyDeleteConfirmText(item: ShopKeyItem | null | undefined): string | null { + const name = nameOrFallback(item) + return name ? `确定删除店铺密钥“${name}”吗?` : null +} + +function pageSizeOr(size: unknown, fallback: number): number { + return typeof size === 'number' && Number.isFinite(size) && size >= 1 ? Math.floor(size) : fallback +} + +/** 删除一行后应请求的目标页:若被删的是末页最后一条则回退,否则保持原页。 */ +export function nextShopKeyPageAfterDelete(page: unknown, pageSize: unknown, totalBefore: unknown): number { + const size = pageSizeOr(pageSize, SHOP_PAGE_DEFAULT_SIZE) + const before = typeof totalBefore === 'number' && Number.isFinite(totalBefore) ? Math.max(Math.floor(totalBefore), 0) : 0 + const after = Math.max(before - 1, 0) + const pagesAfter = after === 0 ? 1 : Math.max(1, Math.ceil(after / size)) + const requested = typeof page === 'number' && Number.isFinite(page) ? Math.floor(page) : 1 + return Math.max(1, Math.min(requested, pagesAfter)) +} diff --git a/admin-frontend-vue/tests/task-87.test.ts b/admin-frontend-vue/tests/task-87.test.ts new file mode 100644 index 00000000..85b8eb19 --- /dev/null +++ b/admin-frontend-vue/tests/task-87.test.ts @@ -0,0 +1,66 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + nextShopKeyPageAfterDelete, + shopKeyDeleteConfirmText, + shopKeyDeleteTargetName, +} from '../src/pages/shop/shop-key-delete.ts' +import type { ShopKeyItem } from '../src/pages/shop/shop-dto.ts' + +test('test_task_087_shop_key_delete_normal_primary_path', () => { + // 正常主路径:删除确认文案以紫鸟账号名落文案,随成功删除刷新到有效页。 + const item = { id: 7, ziniaoAccountName: 'blue_agent' } as ShopKeyItem + assert.equal(shopKeyDeleteTargetName(item), 'blue_agent') + assert.equal(shopKeyDeleteConfirmText(item), '确定删除店铺密钥“blue_agent”吗?') + assert.equal(nextShopKeyPageAfterDelete(2, 15, 31), 2, '中间页删除后仍在原页') +}) + +test('test_task_087_shop_key_delete_normal_variant_input', () => { + // 正常变体:账号名为空时用 id 兜底;末页仅剩一条删除后回退一页。 + const item = { id: 12, ziniaoAccountName: '' } as ShopKeyItem + assert.equal(shopKeyDeleteTargetName(item), '#12') + assert.equal(shopKeyDeleteConfirmText(item), '确定删除店铺密钥“#12”吗?') + assert.equal(nextShopKeyPageAfterDelete(2, 15, 16), 1, '第 2 页最后一条删除后回到第 1 页') +}) + +test('test_task_087_shop_key_delete_repeated_is_idempotent', () => { + // 正常重复:确认文案/翻页计算稳定、不改入参。 + const item = { id: 1, ziniaoAccountName: 'A' } as ShopKeyItem + assert.equal(shopKeyDeleteConfirmText(item), shopKeyDeleteConfirmText(item)) + const before = { page: 5, pageSize: 15, totalBefore: 61 } + assert.equal(nextShopKeyPageAfterDelete(before.page, before.pageSize, before.totalBefore), nextShopKeyPageAfterDelete(before.page, before.pageSize, before.totalBefore)) + assert.deepEqual(before, { page: 5, pageSize: 15, totalBefore: 61 }) +}) + +test('test_task_087_shop_key_delete_boundary_empty_input', () => { + // 边界空值:删除后总数恰为空时回到第 1 页。 + assert.equal(nextShopKeyPageAfterDelete(1, 15, 1), 1) + assert.equal(nextShopKeyPageAfterDelete(2, 15, 0), 1, '空列表请求页钳到 1') +}) + +test('test_task_087_shop_key_delete_boundary_single_item', () => { + // 边界单元素:首页单条删除后仍为第 1 页。 + assert.equal(nextShopKeyPageAfterDelete(1, 15, 1), 1) +}) + +test('test_task_087_shop_key_delete_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:页大小无效时按默认页大小参与末页判断。 + assert.equal(nextShopKeyPageAfterDelete(2, 0, 16), 1, '页大小非正回默认后按末页钳制') +}) + +test('test_task_087_shop_key_delete_invalid_input_rejected', () => { + // 异常输入:空记录不产出可误删的确认文案。 + assert.equal(shopKeyDeleteTargetName(null as unknown as ShopKeyItem), '') + assert.equal(shopKeyDeleteConfirmText(null as unknown as ShopKeyItem), null) +}) + +test('test_task_087_shop_key_delete_dependency_failure_returns_actionable_message', () => { + // 依赖失败/删除走 adapter:删除模型纯逻辑;adapter DELETE /api/admin/shop-keys/{id}。 + const model = readSource('src/pages/shop/shop-key-delete.ts') + assert.equal(/axios|http\.|vue/.test(model), false, '删除模型保持纯逻辑') + const api = readSource('src/pages/shop/shop-key-api.ts') + assert.match(api, /deleteShopKey/) + assert.match(api, /http\.delete/) + assert.match(api, /\/api\/admin\/shop-keys/) +})