From 39f43469c2bcdf2707f40d2a84be2f0700ce86ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 20:57:42 +0800 Subject: [PATCH] =?UTF-8?q?task-245(admin.html=E8=A7=82=E6=84=9F=E5=AF=B9?= =?UTF-8?q?=E9=BD=90):=20=E4=BA=8C=E6=AC=A1=E7=A1=AE=E8=AE=A4=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=E7=BB=84=E4=BB=B6=EF=BC=88=E6=96=87=E6=A1=88=E8=A7=84?= =?UTF-8?q?=E5=88=99=E5=AF=B9=E9=BD=90=20admin-interactions=20+=20?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E9=94=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/AdminConfirm.vue | 16 ++++ .../src/components/admin-confirm-model.ts | 68 +++++++++++++++ .../src/components/admin-confirm.ts | 28 ++++++ admin-frontend-vue/tests/task-245.test.ts | 86 +++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 admin-frontend-vue/src/components/AdminConfirm.vue create mode 100644 admin-frontend-vue/src/components/admin-confirm-model.ts create mode 100644 admin-frontend-vue/src/components/admin-confirm.ts create mode 100644 admin-frontend-vue/tests/task-245.test.ts diff --git a/admin-frontend-vue/src/components/AdminConfirm.vue b/admin-frontend-vue/src/components/AdminConfirm.vue new file mode 100644 index 00000000..f7597bc8 --- /dev/null +++ b/admin-frontend-vue/src/components/AdminConfirm.vue @@ -0,0 +1,16 @@ + diff --git a/admin-frontend-vue/src/components/admin-confirm-model.ts b/admin-frontend-vue/src/components/admin-confirm-model.ts new file mode 100644 index 00000000..cf71fa4d --- /dev/null +++ b/admin-frontend-vue/src/components/admin-confirm-model.ts @@ -0,0 +1,68 @@ +/** + * 二次确认模型(module 13 task 245):文案/按钮键规则对齐 admin-interactions.js —— + * 优先显式 message,否则由行名拼接「确认删除“X”吗?此操作可能影响已有数据。」; + * danger(删除/清空/删除导入类)默认标题「删除前确认」、确认键「确认删除」。 + * 纯逻辑、无 UI 依赖,便于单测;打开入口见 admin-confirm.ts / AdminConfirm.vue。 + */ +export const ADMIN_CONFIRM = { + dangerTitle: '删除前确认', + defaultTitle: '请确认操作', + confirmDeleteOk: '确认删除', + confirmOk: '确认操作', + cancel: '取消', + deleteMessageSuffix: '此操作可能影响已有数据。', +} as const + +export interface AdminConfirmPlan { + danger: boolean + title: string + message: string + confirmText: string + cancelText: string + type: 'warning' | 'info' +} + +/** 由目标名称生成删除确认文案;无名称时退化为「该项」。 */ +export function confirmMessageFor(targetName?: string): string { + const name = typeof targetName === 'string' && targetName.trim() ? `“${targetName.trim()}”` : '该项' + return `确认删除${name}吗?${ADMIN_CONFIRM.deleteMessageSuffix}` +} + +export interface AdminConfirmOptions { + danger?: boolean + targetName?: string + message?: string + title?: string + confirmText?: string +} + +/** 归一出二次确认计划:显式字段优先,否则按 danger/目标名落默认值。 */ +export function createConfirmPlan(opts: AdminConfirmOptions): AdminConfirmPlan { + const danger = opts.danger !== false + const message = opts.message && opts.message.trim() ? opts.message.trim() : confirmMessageFor(opts.targetName) + return { + danger, + title: opts.title ?? (danger ? ADMIN_CONFIRM.dangerTitle : ADMIN_CONFIRM.defaultTitle), + message, + confirmText: opts.confirmText ?? (danger ? ADMIN_CONFIRM.confirmDeleteOk : ADMIN_CONFIRM.confirmOk), + cancelText: ADMIN_CONFIRM.cancel, + type: danger ? 'warning' : 'info', + } +} + +// 并发防护:同一时刻只允许一个确认框在途(拒绝重复弹出)。 +let confirmLocked = false + +export function acquireConfirmLock(): boolean { + if (confirmLocked) return false + confirmLocked = true + return true +} + +export function releaseConfirmLock(): void { + confirmLocked = false +} + +export function isConfirmLocked(): boolean { + return confirmLocked +} diff --git a/admin-frontend-vue/src/components/admin-confirm.ts b/admin-frontend-vue/src/components/admin-confirm.ts new file mode 100644 index 00000000..0d5b678b --- /dev/null +++ b/admin-frontend-vue/src/components/admin-confirm.ts @@ -0,0 +1,28 @@ +/** + * 二次确认打开入口(module 13 task 245):把模型计划交给 ElMessageBox,并对 + * 确认/取消统一归约为 boolean;并发锁拒绝重复弹出。 + */ +import { ElMessageBox } from 'element-plus' +import { + acquireConfirmLock, + createConfirmPlan, + releaseConfirmLock, + type AdminConfirmOptions, +} from './admin-confirm-model' + +export async function openAdminConfirm(options: AdminConfirmOptions = {}): Promise { + const plan = createConfirmPlan(options) + if (!acquireConfirmLock()) return false + try { + return await ElMessageBox.confirm(plan.message, plan.title, { + confirmButtonText: plan.confirmText, + cancelButtonText: plan.cancelText, + type: plan.type, + }).then( + () => true, + () => false, + ) + } finally { + releaseConfirmLock() + } +} diff --git a/admin-frontend-vue/tests/task-245.test.ts b/admin-frontend-vue/tests/task-245.test.ts new file mode 100644 index 00000000..fdf529a2 --- /dev/null +++ b/admin-frontend-vue/tests/task-245.test.ts @@ -0,0 +1,86 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + ADMIN_CONFIRM, + acquireConfirmLock, + confirmMessageFor, + createConfirmPlan, + isConfirmLocked, + releaseConfirmLock, +} from '../src/components/admin-confirm-model.ts' + +// module 13 task 245:二次确认共享组件 —— 默认文案规则对齐 admin-interactions +// (优先显式 message;否则「确认删除“X”吗?此操作可能影响已有数据。」;删除类 danger 默认「删除前确认/确认删除」)。 + +test('test_task_245_confirm_model_normal_primary_path', () => { + // 正常主路径:danger 默认删除确认计划。 + const plan = createConfirmPlan({ danger: true, targetName: '店铺A' }) + assert.equal(plan.title, ADMIN_CONFIRM.dangerTitle) + assert.equal(plan.confirmText, ADMIN_CONFIRM.confirmDeleteOk) + assert.equal(plan.cancelText, ADMIN_CONFIRM.cancel) + assert.equal(plan.message, '确认删除“店铺A”吗?此操作可能影响已有数据。') +}) + +test('test_task_245_confirm_model_normal_variant_input', () => { + // 正常变体:显式 message 优先于行名拼接;非危险操作使用默认标题与「确认操作」。 + const explicit = createConfirmPlan({ danger: true, targetName: 'X', message: '确定清空该次导入吗?' }) + assert.equal(explicit.message, '确定清空该次导入吗?') + const soft = createConfirmPlan({ danger: false }) + assert.equal(soft.title, ADMIN_CONFIRM.defaultTitle) + assert.equal(soft.confirmText, ADMIN_CONFIRM.confirmOk) + assert.notEqual(soft.type, 'warning') +}) + +test('test_task_245_confirm_model_normal_repeated_operation_is_idempotent', () => { + // 正常重复:同一入参产出同一计划(无状态污染)。 + const a = createConfirmPlan({ danger: true, targetName: '店铺B' }) + const b = createConfirmPlan({ danger: true, targetName: '店铺B' }) + assert.deepEqual(a, b) +}) + +test('test_task_245_confirm_model_boundary_empty_input', () => { + // 边界空:无名称时用「该项」默认文案;空串/空白名称被修剪。 + const noName = createConfirmPlan({}) + assert.match(noName.message, /确认删除该项吗?/) + const blank = createConfirmPlan({ targetName: ' ' }) + assert.match(blank.message, /该项/) +}) + +test('test_task_245_confirm_model_boundary_single_item', () => { + // 边界单元素:confirmMessageFor 修剪首尾空白,保留内部名称。 + assert.equal(confirmMessageFor(' 用户1 '), '确认删除“用户1”吗?此操作可能影响已有数据。') + assert.equal(confirmMessageFor(undefined), '确认删除该项吗?此操作可能影响已有数据。') +}) + +test('test_task_245_confirm_model_boundary_limit_or_missing_field', () => { + // 边界上限:danger=false 但给 dangerTitle/删除文案仍能被显式覆盖。 + const plan = createConfirmPlan({ danger: false, title: '请确认停用', confirmText: '确认停用' }) + assert.equal(plan.title, '请确认停用') + assert.equal(plan.confirmText, '确认停用') +}) + +test('test_task_245_confirm_model_invalid_input_rejected', () => { + // 异常:并发多开被拒绝(acquire 返回 false),释放后可再开。 + assert.equal(isConfirmLocked(), false) + assert.equal(acquireConfirmLock(), true) + assert.equal(acquireConfirmLock(), false) + assert.equal(isConfirmLocked(), true) + releaseConfirmLock() + assert.equal(isConfirmLocked(), false) + assert.equal(acquireConfirmLock(), true) + releaseConfirmLock() +}) + +test('test_task_245_confirm_component_dependency_failure_returns_actionable_message', () => { + // 依赖失败:组件/调用入口真实接入 ElMessageBox,按钮键来自模型。 + const imperative = readSource('src/components/admin-confirm.ts') + assert.match(imperative, /ElMessageBox\.confirm/, '打开入口需调用 ElMessageBox.confirm') + assert.match(imperative, /confirmButtonText/) + assert.match(imperative, /cancelButtonText/) + assert.match(imperative, /createConfirmPlan/) + const component = readSource('src/components/AdminConfirm.vue') + assert.match(component, /openAdminConfirm/, '组件需暴露 open 打开入口') + const model = readSource('src/components/admin-confirm-model.ts') + assert.match(model, /此操作可能影响已有数据。/) +})