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 (
+
+ }
+ loading={submitting}
+ disabled={submitting || !canSave}
+ onClick={() => onSave()}
+ data-testid="btn-save"
+ >
+ {TEXT_SAVE}
+
+ }
+ onClick={() => onCancel()}
+ data-testid="btn-cancel"
+ >
+ {TEXT_CANCEL}
+
+ }
+ onClick={() => onNew()}
+ data-testid="btn-new"
+ >
+ {TEXT_NEW}
+
+
+ {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();
+ });
+});