task-245(admin.html观感对齐): 二次确认共享组件(文案规则对齐 admin-interactions + 并发锁)
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { openAdminConfirm } from './admin-confirm'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AdminConfirm 共享确认组件(module 13 task 245):renderless 单例承载。
|
||||||
|
* 使用:<AdminConfirm ref="confirmRef" />,需确认时 await confirmRef.open({...})。
|
||||||
|
*/
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'AdminConfirm',
|
||||||
|
setup(_props, { expose }) {
|
||||||
|
expose({ open: openAdminConfirm })
|
||||||
|
return () => null
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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<boolean> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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, /此操作可能影响已有数据。/)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user