From 8b0bf3d5635ea382df45ea709d93a3cf7260ce03 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:58:33 +0800 Subject: [PATCH] =?UTF-8?q?task-246(admin.html=E8=A7=82=E6=84=9F=E5=AF=B9?= =?UTF-8?q?=E9=BD=90):=20=E5=8F=8D=E9=A6=88=E8=AF=AD=E4=B9=89=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=EF=BC=88success/error/warning=20+=20busy=20=E9=98=B2?= =?UTF-8?q?=E9=87=8D=E5=85=A5=20+=20=E5=BF=99=E7=A2=8C=E6=96=87=E6=A1=88?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/admin-feedback-ui.ts | 28 +++++++ .../src/components/admin-feedback.ts | 60 ++++++++++++++ admin-frontend-vue/tests/task-246.test.ts | 79 +++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 admin-frontend-vue/src/components/admin-feedback-ui.ts create mode 100644 admin-frontend-vue/src/components/admin-feedback.ts create mode 100644 admin-frontend-vue/tests/task-246.test.ts diff --git a/admin-frontend-vue/src/components/admin-feedback-ui.ts b/admin-frontend-vue/src/components/admin-feedback-ui.ts new file mode 100644 index 00000000..21f61b9a --- /dev/null +++ b/admin-frontend-vue/src/components/admin-feedback-ui.ts @@ -0,0 +1,28 @@ +/** + * 反馈 UI 接线(module 13 task 246):把模型语义接到 ElMessage 与 busy 锁上。 + * 页面统一用 showAdminFeedback / runBusy,避免各页 ElMessage 直接散打文案。 + */ +import { ElMessage } from 'element-plus' +import { + endBusy, + normalizeFeedback, + tryBeginBusy, + type AdminFeedbackKind, +} from './admin-feedback' + +export function showAdminFeedback(message?: string, kind?: AdminFeedbackKind): void { + const { kind: k, message: m } = normalizeFeedback(message, kind) + if (k === 'success') ElMessage.success(m) + else if (k === 'error') ElMessage.error(m) + else ElMessage.warning(m) +} + +/** 以 busy 锁包裹一个提交动作;已在途则直接返回 undefined,防止重复提交。 */ +export async function runBusy(task: () => Promise): Promise { + if (!tryBeginBusy()) return undefined + try { + return await task() + } finally { + endBusy() + } +} diff --git a/admin-frontend-vue/src/components/admin-feedback.ts b/admin-frontend-vue/src/components/admin-feedback.ts new file mode 100644 index 00000000..8380a797 --- /dev/null +++ b/admin-frontend-vue/src/components/admin-feedback.ts @@ -0,0 +1,60 @@ +/** + * 反馈语义模型(module 13 task 246):成功/错误/警告统一文案与 busy 防重入。 + * 纯逻辑、不依赖 Element;UI 接线见 admin-feedback-ui.ts。 + */ +export type AdminFeedbackKind = 'success' | 'error' | 'warning' + +export const FEEDBACK_TEXT = { + successDefault: '操作成功', + errorDefault: '操作失败,请稍后重试', + warningDefault: '请确认后再继续', + busySuffix: '中…', +} as const + +export interface NormalizedFeedback { + kind: AdminFeedbackKind + message: string +} + +const KIND_DEFAULT: Record = { + success: FEEDBACK_TEXT.successDefault, + error: FEEDBACK_TEXT.errorDefault, + warning: FEEDBACK_TEXT.warningDefault, +} + +/** 归一反饋消息:显式 message 保留并去首尾空白,空值按 kind 落默认文案。 */ +export function normalizeFeedback(message?: string, kind?: AdminFeedbackKind): NormalizedFeedback { + const k: AdminFeedbackKind = kind ?? 'warning' + const trimmed = typeof message === 'string' ? message.trim() : '' + return { kind: k, message: trimmed ? trimmed : KIND_DEFAULT[k] } +} + +/** 忙碌按钮文案:「保存」→「保存中…」;已以「中」结尾则不重复补。 */ +export function busyLabel(verb: string): string { + const v = (verb || '处理').trim() + return v.endsWith('中') ? `${v}…` : `${v}${FEEDBACK_TEXT.busySuffix}` +} + +/** 把 Error/字符串/未知错误转成可行动错误文案;空值回默认错误。 */ +export function actionableErrorText(error: unknown): string { + if (error instanceof Error && error.message && error.message.trim()) return error.message.trim() + if (typeof error === 'string' && error.trim()) return error.trim() + return FEEDBACK_TEXT.errorDefault +} + +// busy 单例锁:表单提交/导出等防重复进入。 +let busy = false + +export function tryBeginBusy(): boolean { + if (busy) return false + busy = true + return true +} + +export function endBusy(): void { + busy = false +} + +export function isBusy(): boolean { + return busy +} diff --git a/admin-frontend-vue/tests/task-246.test.ts b/admin-frontend-vue/tests/task-246.test.ts new file mode 100644 index 00000000..25c889f3 --- /dev/null +++ b/admin-frontend-vue/tests/task-246.test.ts @@ -0,0 +1,79 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + actionableErrorText, + busyLabel, + endBusy, + isBusy, + normalizeFeedback, + tryBeginBusy, +} from '../src/components/admin-feedback.ts' + +// module 13 task 246:反馈语义统一 —— 成功/错误/警告通道、is-busy 防重入、忙碌文案「保存中…」、 +// 网络失败给可行动文案。 + +test('test_task_246_feedback_normal_primary_path', () => { + // 正常主路径:成功/错误/警告各有默认文案与语义 kind。 + const ok = normalizeFeedback(undefined, 'success') + assert.equal(ok.kind, 'success') + assert.equal(ok.message, '操作成功') + const err = normalizeFeedback(undefined, 'error') + assert.equal(err.kind, 'error') + assert.equal(err.message, '操作失败,请稍后重试') + const warn = normalizeFeedback(undefined, 'warning') + assert.equal(warn.kind, 'warning') +}) + +test('test_task_246_feedback_normal_variant_input', () => { + // 正常变体:显式 message 保留并去首尾空白。 + assert.equal(normalizeFeedback(' 已保存 ', 'success').message, '已保存') +}) + +test('test_task_246_feedback_normal_repeated_operation_is_idempotent', () => { + // 正常重复:无状态污染。 + const a = normalizeFeedback('A', 'error') + const b = normalizeFeedback('A', 'error') + assert.deepEqual(a, b) +}) + +test('test_task_246_feedback_boundary_empty_input', () => { + // 边界空:未指定 kind 时空消息落 warning;空消息回退默认。 + const none = normalizeFeedback() + assert.equal(none.kind, 'warning') + assert.equal(none.message, '请确认后再继续') +}) + +test('test_task_246_feedback_boundary_single_item', () => { + // 边界单元素:忙碌文案补「中…」;以「中」结尾则不重复。 + assert.equal(busyLabel('保存'), '保存中…') + assert.equal(busyLabel('导出'), '导出中…') + assert.equal(busyLabel('保存中'), '保存中…') +}) + +test('test_task_246_feedback_boundary_limit_or_missing_field', () => { + // 边界上限:busy 锁单例防重入。 + assert.equal(isBusy(), false) + assert.equal(tryBeginBusy(), true) + assert.equal(tryBeginBusy(), false) + assert.equal(isBusy(), true) + endBusy() + assert.equal(isBusy(), false) +}) + +test('test_task_246_feedback_invalid_input_rejected', () => { + // 异常:Error/字符串/未知错误都转成可行动文案,空值回默认错误。 + assert.equal(actionableErrorText(new Error('接口超时')), '接口超时') + assert.equal(actionableErrorText('网络不可用'), '网络不可用') + assert.equal(actionableErrorText(null), '操作失败,请稍后重试') +}) + +test('test_task_246_feedback_dependency_failure_returns_actionable_message', () => { + // 依赖失败:纯模型不依赖 Element;UI 层接入 ElMessage 且给足 success/error/warning。 + const pure = readSource('src/components/admin-feedback.ts') + assert.equal(/element-plus|ElMessage/.test(pure), false, '纯模型不得引入 Element') + const ui = readSource('src/components/admin-feedback-ui.ts') + assert.match(ui, /ElMessage/) + assert.match(ui, /showAdminFeedback/) + assert.match(ui, /runBusy/) +})