Commit bdfcfae299dbdec30d80ffeaac57e3e77c1c6d3d

Authored by zichun
1 parent 06252b2c

feat(usr): 用户单据工具栏 UserDetailToolbar REQ-USR-001 REQ-USR-002

frontend/src/pages/usr/UserDetail/UserDetailToolbar.tsx 0 → 100644
  1 +// REQ-USR-001 / REQ-USR-002: 用户单据工具栏(保存/取消/新增 + 占位按钮 + 齿轮,BR12/BR13/BR14/BR15/D8/D10)
  2 +import { Button, App as AntdApp } from 'antd';
  3 +import {
  4 + SaveOutlined,
  5 + CloseCircleOutlined,
  6 + PlusCircleOutlined,
  7 + SettingOutlined,
  8 +} from '@ant-design/icons';
  9 +import {
  10 + TEXT_SAVE,
  11 + TEXT_CANCEL,
  12 + TEXT_NEW,
  13 + TEXT_DELETE,
  14 + TEXT_VOID,
  15 + TEXT_RESET_PWD,
  16 + TEXT_UNVOID,
  17 + TEXT_FUNC,
  18 + MSG_FUNC_PLACEHOLDER,
  19 +} from './constants';
  20 +import styles from './UserDetail.module.css';
  21 +
  22 +export interface UserDetailToolbarProps {
  23 + mode: 'create' | 'edit';
  24 + submitting: boolean;
  25 + canSave: boolean;
  26 + onSave(): void;
  27 + onCancel(): void;
  28 + onNew(): void;
  29 +}
  30 +
  31 +const PLACEHOLDER_BUTTONS = [TEXT_DELETE, TEXT_VOID, TEXT_RESET_PWD, TEXT_UNVOID, TEXT_FUNC];
  32 +
  33 +export default function UserDetailToolbar({
  34 + submitting,
  35 + canSave,
  36 + onSave,
  37 + onCancel,
  38 + onNew,
  39 +}: UserDetailToolbarProps) {
  40 + const { message } = AntdApp.useApp();
  41 + const placeholder = () => message.info(MSG_FUNC_PLACEHOLDER);
  42 +
  43 + return (
  44 + <div className={styles.toolbar}>
  45 + <Button
  46 + type="primary"
  47 + icon={<SaveOutlined />}
  48 + loading={submitting}
  49 + disabled={submitting || !canSave}
  50 + onClick={() => onSave()}
  51 + data-testid="btn-save"
  52 + >
  53 + {TEXT_SAVE}
  54 + </Button>
  55 + <Button
  56 + type="text"
  57 + icon={<CloseCircleOutlined />}
  58 + onClick={() => onCancel()}
  59 + data-testid="btn-cancel"
  60 + >
  61 + {TEXT_CANCEL}
  62 + </Button>
  63 + <Button
  64 + type="text"
  65 + icon={<PlusCircleOutlined />}
  66 + onClick={() => onNew()}
  67 + data-testid="btn-new"
  68 + >
  69 + {TEXT_NEW}
  70 + </Button>
  71 +
  72 + {PLACEHOLDER_BUTTONS.map((label) => (
  73 + <Button key={label} type="text" onClick={placeholder} data-testid={'btn-ph-' + label}>
  74 + {label}
  75 + </Button>
  76 + ))}
  77 +
  78 + <span className={styles.toolbarSpacer} />
  79 +
  80 + <span
  81 + className={styles.gear}
  82 + data-testid="btn-gear"
  83 + role="presentation"
  84 + aria-label="设置"
  85 + onClick={placeholder}
  86 + >
  87 + <SettingOutlined />
  88 + </span>
  89 + </div>
  90 + );
  91 +}
frontend/tests/unit/UserDetailToolbar.test.tsx 0 → 100644
  1 +// REQ-USR-001 / REQ-USR-002: UserDetailToolbar 工具栏单测(保存/取消/新增 + 提交中禁用 + 占位按钮,BR12/BR13/BR14/BR15/D8)
  2 +import { describe, it, expect, vi, beforeEach } from 'vitest';
  3 +import { screen } from '@testing-library/react';
  4 +import userEvent from '@testing-library/user-event';
  5 +
  6 +const messageSpy = { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() };
  7 +vi.mock('antd', async () => {
  8 + const actual = await vi.importActual<typeof import('antd')>('antd');
  9 + return {
  10 + ...actual,
  11 + App: Object.assign(actual.App, { useApp: () => ({ message: messageSpy }) }),
  12 + };
  13 +});
  14 +
  15 +import { renderShell } from './renderShell';
  16 +import UserDetailToolbar from '../../src/pages/usr/UserDetail/UserDetailToolbar';
  17 +
  18 +function setup(over: { mode?: 'create' | 'edit'; submitting?: boolean; canSave?: boolean } = {}) {
  19 + const onSave = vi.fn();
  20 + const onCancel = vi.fn();
  21 + const onNew = vi.fn();
  22 + renderShell(
  23 + <UserDetailToolbar
  24 + mode={over.mode ?? 'create'}
  25 + submitting={over.submitting ?? false}
  26 + canSave={over.canSave ?? true}
  27 + onSave={onSave}
  28 + onCancel={onCancel}
  29 + onNew={onNew}
  30 + />,
  31 + { preloadedAuth: { token: 't', user: { id: 1, sUserName: 'a', sUserType: 'x', sLanguage: '中文' } } },
  32 + );
  33 + return { onSave, onCancel, onNew };
  34 +}
  35 +
  36 +describe('UserDetailToolbar', () => {
  37 + beforeEach(() => {
  38 + vi.clearAllMocks();
  39 + });
  40 +
  41 + it('renders 保存/取消/新增 + placeholder buttons + gear', () => {
  42 + setup();
  43 + expect(screen.getByTestId('btn-save')).toBeInTheDocument();
  44 + expect(screen.getByTestId('btn-cancel')).toBeInTheDocument();
  45 + expect(screen.getByTestId('btn-new')).toBeInTheDocument();
  46 + expect(screen.getByText('删除')).toBeInTheDocument();
  47 + expect(screen.getByText('作废')).toBeInTheDocument();
  48 + expect(screen.getByText('重置密码')).toBeInTheDocument();
  49 + expect(screen.getByText('取消作废')).toBeInTheDocument();
  50 + expect(screen.getByText('功能')).toBeInTheDocument();
  51 + expect(screen.getByTestId('btn-gear')).toBeInTheDocument();
  52 + });
  53 +
  54 + it('click 保存 calls onSave / 取消 calls onCancel / 新增 calls onNew', async () => {
  55 + const user = userEvent.setup();
  56 + const { onSave, onCancel, onNew } = setup();
  57 + await user.click(screen.getByTestId('btn-save'));
  58 + expect(onSave).toHaveBeenCalledTimes(1);
  59 + await user.click(screen.getByTestId('btn-cancel'));
  60 + expect(onCancel).toHaveBeenCalledTimes(1);
  61 + await user.click(screen.getByTestId('btn-new'));
  62 + expect(onNew).toHaveBeenCalledTimes(1);
  63 + });
  64 +
  65 + it('submitting disables 保存 and shows loading', async () => {
  66 + const user = userEvent.setup();
  67 + const { onSave } = setup({ submitting: true });
  68 + const btn = screen.getByTestId('btn-save');
  69 + expect(btn).toBeDisabled();
  70 + await user.click(btn);
  71 + expect(onSave).not.toHaveBeenCalled();
  72 + });
  73 +
  74 + it('canSave=false disables 保存', () => {
  75 + setup({ canSave: false });
  76 + expect(screen.getByTestId('btn-save')).toBeDisabled();
  77 + });
  78 +
  79 + it('placeholder buttons show 功能开发中 (no business callback)', async () => {
  80 + const user = userEvent.setup();
  81 + const { onSave, onCancel, onNew } = setup();
  82 + await user.click(screen.getByText('删除'));
  83 + expect(messageSpy.info).toHaveBeenCalledWith('功能开发中');
  84 + await user.click(screen.getByTestId('btn-gear'));
  85 + expect(messageSpy.info).toHaveBeenCalledWith('功能开发中');
  86 + expect(onSave).not.toHaveBeenCalled();
  87 + expect(onCancel).not.toHaveBeenCalled();
  88 + expect(onNew).not.toHaveBeenCalled();
  89 + });
  90 +});