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, /此操作可能影响已有数据。/)
+})