From bdfcfae299dbdec30d80ffeaac57e3e77c1c6d3d Mon Sep 17 00:00:00 2001 From: zichun Date: Mon, 1 Jun 2026 18:15:56 +0800 Subject: [PATCH] feat(usr): 用户单据工具栏 UserDetailToolbar REQ-USR-001 REQ-USR-002 --- frontend/src/pages/usr/UserDetail/UserDetailToolbar.tsx | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ frontend/tests/unit/UserDetailToolbar.test.tsx | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 0 deletions(-) create mode 100644 frontend/src/pages/usr/UserDetail/UserDetailToolbar.tsx create mode 100644 frontend/tests/unit/UserDetailToolbar.test.tsx diff --git a/frontend/src/pages/usr/UserDetail/UserDetailToolbar.tsx b/frontend/src/pages/usr/UserDetail/UserDetailToolbar.tsx new file mode 100644 index 0000000..9b4ed18 --- /dev/null +++ b/frontend/src/pages/usr/UserDetail/UserDetailToolbar.tsx @@ -0,0 +1,91 @@ +// REQ-USR-001 / REQ-USR-002: 用户单据工具栏(保存/取消/新增 + 占位按钮 + 齿轮,BR12/BR13/BR14/BR15/D8/D10) +import { Button, App as AntdApp } from 'antd'; +import { + SaveOutlined, + CloseCircleOutlined, + PlusCircleOutlined, + SettingOutlined, +} from '@ant-design/icons'; +import { + TEXT_SAVE, + TEXT_CANCEL, + TEXT_NEW, + TEXT_DELETE, + TEXT_VOID, + TEXT_RESET_PWD, + TEXT_UNVOID, + TEXT_FUNC, + MSG_FUNC_PLACEHOLDER, +} from './constants'; +import styles from './UserDetail.module.css'; + +export interface UserDetailToolbarProps { + mode: 'create' | 'edit'; + submitting: boolean; + canSave: boolean; + onSave(): void; + onCancel(): void; + onNew(): void; +} + +const PLACEHOLDER_BUTTONS = [TEXT_DELETE, TEXT_VOID, TEXT_RESET_PWD, TEXT_UNVOID, TEXT_FUNC]; + +export default function UserDetailToolbar({ + submitting, + canSave, + onSave, + onCancel, + onNew, +}: UserDetailToolbarProps) { + const { message } = AntdApp.useApp(); + const placeholder = () => message.info(MSG_FUNC_PLACEHOLDER); + + return ( +
+ + + + + {PLACEHOLDER_BUTTONS.map((label) => ( + + ))} + + + + + + +
+ ); +} diff --git a/frontend/tests/unit/UserDetailToolbar.test.tsx b/frontend/tests/unit/UserDetailToolbar.test.tsx new file mode 100644 index 0000000..dbfa37e --- /dev/null +++ b/frontend/tests/unit/UserDetailToolbar.test.tsx @@ -0,0 +1,90 @@ +// REQ-USR-001 / REQ-USR-002: UserDetailToolbar 工具栏单测(保存/取消/新增 + 提交中禁用 + 占位按钮,BR12/BR13/BR14/BR15/D8) +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +const messageSpy = { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() }; +vi.mock('antd', async () => { + const actual = await vi.importActual('antd'); + return { + ...actual, + App: Object.assign(actual.App, { useApp: () => ({ message: messageSpy }) }), + }; +}); + +import { renderShell } from './renderShell'; +import UserDetailToolbar from '../../src/pages/usr/UserDetail/UserDetailToolbar'; + +function setup(over: { mode?: 'create' | 'edit'; submitting?: boolean; canSave?: boolean } = {}) { + const onSave = vi.fn(); + const onCancel = vi.fn(); + const onNew = vi.fn(); + renderShell( + , + { preloadedAuth: { token: 't', user: { id: 1, sUserName: 'a', sUserType: 'x', sLanguage: '中文' } } }, + ); + return { onSave, onCancel, onNew }; +} + +describe('UserDetailToolbar', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders 保存/取消/新增 + placeholder buttons + gear', () => { + setup(); + expect(screen.getByTestId('btn-save')).toBeInTheDocument(); + expect(screen.getByTestId('btn-cancel')).toBeInTheDocument(); + expect(screen.getByTestId('btn-new')).toBeInTheDocument(); + expect(screen.getByText('删除')).toBeInTheDocument(); + expect(screen.getByText('作废')).toBeInTheDocument(); + expect(screen.getByText('重置密码')).toBeInTheDocument(); + expect(screen.getByText('取消作废')).toBeInTheDocument(); + expect(screen.getByText('功能')).toBeInTheDocument(); + expect(screen.getByTestId('btn-gear')).toBeInTheDocument(); + }); + + it('click 保存 calls onSave / 取消 calls onCancel / 新增 calls onNew', async () => { + const user = userEvent.setup(); + const { onSave, onCancel, onNew } = setup(); + await user.click(screen.getByTestId('btn-save')); + expect(onSave).toHaveBeenCalledTimes(1); + await user.click(screen.getByTestId('btn-cancel')); + expect(onCancel).toHaveBeenCalledTimes(1); + await user.click(screen.getByTestId('btn-new')); + expect(onNew).toHaveBeenCalledTimes(1); + }); + + it('submitting disables 保存 and shows loading', async () => { + const user = userEvent.setup(); + const { onSave } = setup({ submitting: true }); + const btn = screen.getByTestId('btn-save'); + expect(btn).toBeDisabled(); + await user.click(btn); + expect(onSave).not.toHaveBeenCalled(); + }); + + it('canSave=false disables 保存', () => { + setup({ canSave: false }); + expect(screen.getByTestId('btn-save')).toBeDisabled(); + }); + + it('placeholder buttons show 功能开发中 (no business callback)', async () => { + const user = userEvent.setup(); + const { onSave, onCancel, onNew } = setup(); + await user.click(screen.getByText('删除')); + expect(messageSpy.info).toHaveBeenCalledWith('功能开发中'); + await user.click(screen.getByTestId('btn-gear')); + expect(messageSpy.info).toHaveBeenCalledWith('功能开发中'); + expect(onSave).not.toHaveBeenCalled(); + expect(onCancel).not.toHaveBeenCalled(); + expect(onNew).not.toHaveBeenCalled(); + }); +}); -- libgit2 0.22.2