Commit bab4219332697e98f3a707c7ff206a5c0d084305
1 parent
16e80d5f
feat(fe-shell): 主页落地页区域组合 REQ-USR-003
Showing
7 changed files
with
252 additions
and
0 deletions
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 | +}); |