diff --git a/frontend/src/layouts/AppLayout/AppFooter.module.css b/frontend/src/layouts/AppLayout/AppFooter.module.css new file mode 100644 index 0000000..d1c4650 --- /dev/null +++ b/frontend/src/layouts/AppLayout/AppFooter.module.css @@ -0,0 +1,19 @@ +/* REQ-USR-003: 页脚 scoped 样式(语义色用 var(--color-*))。 */ +.foot { + background: var(--color-bg-base); + border-top: 1px solid var(--color-border); + padding: 10px 14px; + text-align: center; + color: var(--color-text-secondary); + font-size: 12px; +} +.pipe { + margin: 0 8px; + color: var(--color-border); +} +.police { + display: inline-flex; + align-items: center; + gap: 4px; + margin-left: 6px; +} diff --git a/frontend/src/layouts/AppLayout/AppFooter.tsx b/frontend/src/layouts/AppLayout/AppFooter.tsx new file mode 100644 index 0000000..840f025 --- /dev/null +++ b/frontend/src/layouts/AppLayout/AppFooter.tsx @@ -0,0 +1,23 @@ +// REQ-USR-003: 页脚(复刻原型 footer.foot 版权/经营范围/备案号)。 +import styles from './AppFooter.module.css'; + +export default function AppFooter() { + return ( + + ); +} diff --git a/frontend/src/pages/home/HomePage/CommonOps.tsx b/frontend/src/pages/home/HomePage/CommonOps.tsx new file mode 100644 index 0000000..2b40796 --- /dev/null +++ b/frontend/src/pages/home/HomePage/CommonOps.tsx @@ -0,0 +1,27 @@ +// REQ-USR-003: 常用操作卡(用户列表 → 路由;系统功能模块设置 → 占位,复刻原型 .common-ops)。 +import { App as AntdApp } from 'antd'; +import styles from './HomePage.module.css'; + +interface CommonOpsProps { + onOpenUserList: () => void; +} + +export default function CommonOps({ onOpenUserList }: CommonOpsProps) { + const { message } = AntdApp.useApp(); + + return ( +
+
常用操作
+ + +
+ ); +} diff --git a/frontend/src/pages/home/HomePage/HomePage.tsx b/frontend/src/pages/home/HomePage/HomePage.tsx new file mode 100644 index 0000000..d4387af --- /dev/null +++ b/frontend/src/pages/home/HomePage/HomePage.tsx @@ -0,0 +1,33 @@ +// REQ-USR-003: 主页落地页根(复刻原型 #screen-main:KPI 头条 + 角色树 + KPI 网格 + 常用操作 + 页脚)。 +import { useNavigate } from 'react-router-dom'; +import KpiHeadBar from './KpiHeadBar'; +import RoleProcessTree from './RoleProcessTree'; +import KpiBoard from './KpiBoard'; +import CommonOps from './CommonOps'; +import AppFooter from '../../../layouts/AppLayout/AppFooter'; +import { KPI_STATS, KPI_ROWS, ROLE_GROUPS, PROCESS_GROUPS } from './dashboardData'; +import styles from './HomePage.module.css'; + +export default function HomePage() { + const navigate = useNavigate(); + + return ( + <> +
+
+ +
+ +
+
+ +
+
+
+
+ navigate('/usr/users')} /> +
+ + + ); +} diff --git a/frontend/src/pages/home/HomePage/KpiHeadBar.tsx b/frontend/src/pages/home/HomePage/KpiHeadBar.tsx new file mode 100644 index 0000000..e6217db --- /dev/null +++ b/frontend/src/pages/home/HomePage/KpiHeadBar.tsx @@ -0,0 +1,29 @@ +// REQ-USR-003: KPI 头条(标题 + 今日未处理/未清总数统计 + AI 占位按钮,复刻原型 .kpi-head)。 +import { Button } from 'antd'; +import { ThunderboltOutlined } from '@ant-design/icons'; +import type { KPI_STATS } from './dashboardData'; +import styles from './HomePage.module.css'; + +interface KpiHeadBarProps { + stats: typeof KPI_STATS; +} + +export default function KpiHeadBar({ stats }: KpiHeadBarProps) { + return ( +
+ KPI监控 + + 今日未处理: + {stats.todayPending} + + | + + 未清总数: + {stats.openTotal} + + +
+ ); +} diff --git a/frontend/src/pages/home/HomePage/RoleProcessTree.tsx b/frontend/src/pages/home/HomePage/RoleProcessTree.tsx new file mode 100644 index 0000000..8cc578b --- /dev/null +++ b/frontend/src/pages/home/HomePage/RoleProcessTree.tsx @@ -0,0 +1,45 @@ +// REQ-USR-003: 左侧角色/流程树(按角色/按流程分组 + 计数;点击高亮,不取数,BR11)。 +import { useState } from 'react'; +import type { GroupItem } from './dashboardData'; +import styles from './HomePage.module.css'; + +interface RoleProcessTreeProps { + roleGroups: GroupItem[]; + processGroups: GroupItem[]; + onSelect?: (label: string) => void; +} + +export default function RoleProcessTree({ + roleGroups, + processGroups, + onSelect, +}: RoleProcessTreeProps) { + // 本地高亮态:默认高亮「所有部门」(复刻原型首项 active) + const [activeLabel, setActiveLabel] = useState('所有部门'); + + const handleClick = (label: string) => { + setActiveLabel(label); + onSelect?.(label); + }; + + const renderItem = (g: GroupItem, keyPrefix: string) => ( + + ); + + return ( +
+
按角色
+ {roleGroups.map((g) => renderItem(g, 'role'))} +
按流程
+ {processGroups.map((g) => renderItem(g, 'proc'))} +
+ ); +} diff --git a/frontend/tests/unit/HomePage.test.tsx b/frontend/tests/unit/HomePage.test.tsx new file mode 100644 index 0000000..e2d35c5 --- /dev/null +++ b/frontend/tests/unit/HomePage.test.tsx @@ -0,0 +1,76 @@ +// REQ-USR-003: HomePage 落地页区域组合(KPI 头条 / 角色树 / 常用操作 / 页脚,BR8/BR11) +import { describe, it, expect } from 'vitest'; +import { screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Routes, Route, useLocation } from 'react-router-dom'; +import { renderShell } from './renderShell'; +import HomePage from '../../src/pages/home/HomePage/HomePage'; + +function LocationProbe() { + const loc = useLocation(); + return
{loc.pathname}
; +} + +function renderHome() { + return renderShell( + <> + + + } /> + users} /> + + , + { + initialEntries: ['/'], + preloadedAuth: { + token: 't', + user: { id: 1, sUserName: '朱子纯', sUserType: '超级管理员', sLanguage: '中文' }, + }, + }, + ); +} + +describe('HomePage', () => { + it('renders KPI head with title and stats', () => { + renderHome(); + expect(screen.getByText('KPI监控')).toBeInTheDocument(); + expect(screen.getByText('今日未处理:')).toBeInTheDocument(); + expect(screen.getByText('37428')).toBeInTheDocument(); + expect(screen.getByText('未清总数:')).toBeInTheDocument(); + expect(screen.getByText('56433')).toBeInTheDocument(); + expect(screen.getByText('小ai同学,请帮我安排今日工作')).toBeInTheDocument(); + }); + + it('renders role/process tree groups', () => { + renderHome(); + const tree = within(screen.getByTestId('role-tree')); + expect(tree.getByText('按角色')).toBeInTheDocument(); + expect(tree.getByText('按流程')).toBeInTheDocument(); + expect(tree.getByText(/所有部门/)).toBeInTheDocument(); + expect(tree.getByText(/客服部/)).toBeInTheDocument(); + }); + + it('tree item click highlights without navigation', async () => { + renderHome(); + const tree = within(screen.getByTestId('role-tree')); + const item = tree.getByRole('button', { name: /核价人员/ }); + await userEvent.click(item); + // 高亮(aria-pressed),不触发路由跳转 + expect(item).toHaveAttribute('aria-pressed', 'true'); + expect(screen.getByTestId('loc').textContent).toBe('/'); + }); + + it('common ops user-list click navigates to /usr/users', async () => { + renderHome(); + // 常用操作卡内「用户列表」 + const opsUserList = screen.getByRole('button', { name: '用户列表' }); + await userEvent.click(opsUserList); + expect(screen.getByTestId('loc').textContent).toBe('/usr/users'); + }); + + it('renders footer copyright text', () => { + renderHome(); + expect(screen.getByText(/©Copyright Antler Software/)).toBeInTheDocument(); + expect(screen.getByText(/沪ICP备14034791号-1/)).toBeInTheDocument(); + }); +});