Commit bab4219332697e98f3a707c7ff206a5c0d084305

Authored by zichun
1 parent 16e80d5f

feat(fe-shell): 主页落地页区域组合 REQ-USR-003

frontend/src/layouts/AppLayout/AppFooter.module.css 0 → 100644
  1 +/* REQ-USR-003: 页脚 scoped 样式(语义色用 var(--color-*))。 */
  2 +.foot {
  3 + background: var(--color-bg-base);
  4 + border-top: 1px solid var(--color-border);
  5 + padding: 10px 14px;
  6 + text-align: center;
  7 + color: var(--color-text-secondary);
  8 + font-size: 12px;
  9 +}
  10 +.pipe {
  11 + margin: 0 8px;
  12 + color: var(--color-border);
  13 +}
  14 +.police {
  15 + display: inline-flex;
  16 + align-items: center;
  17 + gap: 4px;
  18 + margin-left: 6px;
  19 +}
frontend/src/layouts/AppLayout/AppFooter.tsx 0 → 100644
  1 +// REQ-USR-003: 页脚(复刻原型 footer.foot 版权/经营范围/备案号)。
  2 +import styles from './AppFooter.module.css';
  3 +
  4 +export default function AppFooter() {
  5 + return (
  6 + <footer className={styles.foot}>
  7 + <span aria-hidden="true">🛠</span> ©Copyright Antler Software
  8 + <span className={styles.pipe}>|</span> 印刷智慧工厂
  9 + <span className={styles.pipe}>|</span> 印刷MES
  10 + <span className={styles.pipe}>|</span> 印刷ERP
  11 + <span className={styles.pipe}>|</span> 印刷电商平台
  12 + <span className={styles.pipe}>|</span> 文件智能处理
  13 + <span className={styles.pipe}>|</span> 印前自动化
  14 + <span className={styles.pipe}>|</span> 400-880-6237
  15 + <span className={styles.police}>
  16 + <svg width="14" height="14" viewBox="0 0 24 24" fill="var(--color-primary)" aria-hidden="true">
  17 + <path d="M12 2l9 4v6c0 5-4 9-9 10-5-1-9-5-9-10V6z" />
  18 + </svg>
  19 + 沪ICP备14034791号-1
  20 + </span>
  21 + </footer>
  22 + );
  23 +}
frontend/src/pages/home/HomePage/CommonOps.tsx 0 → 100644
  1 +// REQ-USR-003: 常用操作卡(用户列表 → 路由;系统功能模块设置 → 占位,复刻原型 .common-ops)。
  2 +import { App as AntdApp } from 'antd';
  3 +import styles from './HomePage.module.css';
  4 +
  5 +interface CommonOpsProps {
  6 + onOpenUserList: () => void;
  7 +}
  8 +
  9 +export default function CommonOps({ onOpenUserList }: CommonOpsProps) {
  10 + const { message } = AntdApp.useApp();
  11 +
  12 + return (
  13 + <div className={`${styles.panel} ${styles.commonOps}`}>
  14 + <div className={styles.commonOpsTitle}>常用操作</div>
  15 + <button type="button" className={styles.commonOpsLink} onClick={onOpenUserList}>
  16 + 用户列表
  17 + </button>
  18 + <button
  19 + type="button"
  20 + className={styles.commonOpsLink}
  21 + onClick={() => message.info('功能开发中')}
  22 + >
  23 + 系统功能模块设置
  24 + </button>
  25 + </div>
  26 + );
  27 +}
frontend/src/pages/home/HomePage/HomePage.tsx 0 → 100644
  1 +// REQ-USR-003: 主页落地页根(复刻原型 #screen-main:KPI 头条 + 角色树 + KPI 网格 + 常用操作 + 页脚)。
  2 +import { useNavigate } from 'react-router-dom';
  3 +import KpiHeadBar from './KpiHeadBar';
  4 +import RoleProcessTree from './RoleProcessTree';
  5 +import KpiBoard from './KpiBoard';
  6 +import CommonOps from './CommonOps';
  7 +import AppFooter from '../../../layouts/AppLayout/AppFooter';
  8 +import { KPI_STATS, KPI_ROWS, ROLE_GROUPS, PROCESS_GROUPS } from './dashboardData';
  9 +import styles from './HomePage.module.css';
  10 +
  11 +export default function HomePage() {
  12 + const navigate = useNavigate();
  13 +
  14 + return (
  15 + <>
  16 + <div className={styles.home}>
  17 + <div className={styles.mainCol}>
  18 + <KpiHeadBar stats={KPI_STATS} />
  19 + <div className={styles.threeCol}>
  20 + <RoleProcessTree roleGroups={ROLE_GROUPS} processGroups={PROCESS_GROUPS} />
  21 + <div className={styles.center}>
  22 + <div className={styles.panel} style={{ overflow: 'auto' }}>
  23 + <KpiBoard rows={KPI_ROWS} />
  24 + </div>
  25 + </div>
  26 + </div>
  27 + </div>
  28 + <CommonOps onOpenUserList={() => navigate('/usr/users')} />
  29 + </div>
  30 + <AppFooter />
  31 + </>
  32 + );
  33 +}
frontend/src/pages/home/HomePage/KpiHeadBar.tsx 0 → 100644
  1 +// REQ-USR-003: KPI 头条(标题 + 今日未处理/未清总数统计 + AI 占位按钮,复刻原型 .kpi-head)。
  2 +import { Button } from 'antd';
  3 +import { ThunderboltOutlined } from '@ant-design/icons';
  4 +import type { KPI_STATS } from './dashboardData';
  5 +import styles from './HomePage.module.css';
  6 +
  7 +interface KpiHeadBarProps {
  8 + stats: typeof KPI_STATS;
  9 +}
  10 +
  11 +export default function KpiHeadBar({ stats }: KpiHeadBarProps) {
  12 + return (
  13 + <div className={`${styles.panel} ${styles.kpiHead}`}>
  14 + <span className={styles.kpiHeadTitle}>KPI监控</span>
  15 + <span className={styles.kpiStat}>
  16 + <span>今日未处理:</span>
  17 + <b>{stats.todayPending}</b>
  18 + </span>
  19 + <span className={styles.kpiSep}>|</span>
  20 + <span className={`${styles.kpiStat} ${styles.kpiStatBlue}`}>
  21 + <span>未清总数:</span>
  22 + <b>{stats.openTotal}</b>
  23 + </span>
  24 + <Button type="primary" className={styles.aiBtn} icon={<ThunderboltOutlined />}>
  25 + 小ai同学,请帮我安排今日工作
  26 + </Button>
  27 + </div>
  28 + );
  29 +}
frontend/src/pages/home/HomePage/RoleProcessTree.tsx 0 → 100644
  1 +// REQ-USR-003: 左侧角色/流程树(按角色/按流程分组 + 计数;点击高亮,不取数,BR11)。
  2 +import { useState } from 'react';
  3 +import type { GroupItem } from './dashboardData';
  4 +import styles from './HomePage.module.css';
  5 +
  6 +interface RoleProcessTreeProps {
  7 + roleGroups: GroupItem[];
  8 + processGroups: GroupItem[];
  9 + onSelect?: (label: string) => void;
  10 +}
  11 +
  12 +export default function RoleProcessTree({
  13 + roleGroups,
  14 + processGroups,
  15 + onSelect,
  16 +}: RoleProcessTreeProps) {
  17 + // 本地高亮态:默认高亮「所有部门」(复刻原型首项 active)
  18 + const [activeLabel, setActiveLabel] = useState<string>('所有部门');
  19 +
  20 + const handleClick = (label: string) => {
  21 + setActiveLabel(label);
  22 + onSelect?.(label);
  23 + };
  24 +
  25 + const renderItem = (g: GroupItem, keyPrefix: string) => (
  26 + <button
  27 + type="button"
  28 + key={`${keyPrefix}-${g.label}`}
  29 + className={`${styles.treeItem} ${activeLabel === g.label ? styles.treeItemActive : ''}`}
  30 + aria-pressed={activeLabel === g.label}
  31 + onClick={() => handleClick(g.label)}
  32 + >
  33 + {g.label} ({g.count})
  34 + </button>
  35 + );
  36 +
  37 + return (
  38 + <div className={`${styles.leftNav} ${styles.tree}`} data-testid="role-tree">
  39 + <div className={styles.treeGroup}>按角色</div>
  40 + {roleGroups.map((g) => renderItem(g, 'role'))}
  41 + <div className={styles.treeGroup}>按流程</div>
  42 + {processGroups.map((g) => renderItem(g, 'proc'))}
  43 + </div>
  44 + );
  45 +}
frontend/tests/unit/HomePage.test.tsx 0 → 100644
  1 +// REQ-USR-003: HomePage 落地页区域组合(KPI 头条 / 角色树 / 常用操作 / 页脚,BR8/BR11)
  2 +import { describe, it, expect } from 'vitest';
  3 +import { screen, within } from '@testing-library/react';
  4 +import userEvent from '@testing-library/user-event';
  5 +import { Routes, Route, useLocation } from 'react-router-dom';
  6 +import { renderShell } from './renderShell';
  7 +import HomePage from '../../src/pages/home/HomePage/HomePage';
  8 +
  9 +function LocationProbe() {
  10 + const loc = useLocation();
  11 + return <div data-testid="loc">{loc.pathname}</div>;
  12 +}
  13 +
  14 +function renderHome() {
  15 + return renderShell(
  16 + <>
  17 + <LocationProbe />
  18 + <Routes>
  19 + <Route path="/" element={<HomePage />} />
  20 + <Route path="/usr/users" element={<div data-testid="users-sentinel">users</div>} />
  21 + </Routes>
  22 + </>,
  23 + {
  24 + initialEntries: ['/'],
  25 + preloadedAuth: {
  26 + token: 't',
  27 + user: { id: 1, sUserName: '朱子纯', sUserType: '超级管理员', sLanguage: '中文' },
  28 + },
  29 + },
  30 + );
  31 +}
  32 +
  33 +describe('HomePage', () => {
  34 + it('renders KPI head with title and stats', () => {
  35 + renderHome();
  36 + expect(screen.getByText('KPI监控')).toBeInTheDocument();
  37 + expect(screen.getByText('今日未处理:')).toBeInTheDocument();
  38 + expect(screen.getByText('37428')).toBeInTheDocument();
  39 + expect(screen.getByText('未清总数:')).toBeInTheDocument();
  40 + expect(screen.getByText('56433')).toBeInTheDocument();
  41 + expect(screen.getByText('小ai同学,请帮我安排今日工作')).toBeInTheDocument();
  42 + });
  43 +
  44 + it('renders role/process tree groups', () => {
  45 + renderHome();
  46 + const tree = within(screen.getByTestId('role-tree'));
  47 + expect(tree.getByText('按角色')).toBeInTheDocument();
  48 + expect(tree.getByText('按流程')).toBeInTheDocument();
  49 + expect(tree.getByText(/所有部门/)).toBeInTheDocument();
  50 + expect(tree.getByText(/客服部/)).toBeInTheDocument();
  51 + });
  52 +
  53 + it('tree item click highlights without navigation', async () => {
  54 + renderHome();
  55 + const tree = within(screen.getByTestId('role-tree'));
  56 + const item = tree.getByRole('button', { name: /核价人员/ });
  57 + await userEvent.click(item);
  58 + // 高亮(aria-pressed),不触发路由跳转
  59 + expect(item).toHaveAttribute('aria-pressed', 'true');
  60 + expect(screen.getByTestId('loc').textContent).toBe('/');
  61 + });
  62 +
  63 + it('common ops user-list click navigates to /usr/users', async () => {
  64 + renderHome();
  65 + // 常用操作卡内「用户列表」
  66 + const opsUserList = screen.getByRole('button', { name: '用户列表' });
  67 + await userEvent.click(opsUserList);
  68 + expect(screen.getByTestId('loc').textContent).toBe('/usr/users');
  69 + });
  70 +
  71 + it('renders footer copyright text', () => {
  72 + renderHome();
  73 + expect(screen.getByText(/©Copyright Antler Software/)).toBeInTheDocument();
  74 + expect(screen.getByText(/沪ICP备14034791号-1/)).toBeInTheDocument();
  75 + });
  76 +});