# AIHR 前端 UI/UX 优化实施方案 > 配套文档:`docs/ui-ux-review.md`(现状梳理 + 竞品标杆 + 问题诊断) > > 本文档为可执行的实施计划,包含具体文件修改清单、代码示例和验收标准。 > > 日期:2026-07-24 --- ## 目录 - [依赖安装清单](#依赖安装清单) - [Phase 1:基础体验修复(1-2 天)](#phase-1基础体验修复1-2-天) - [Phase 2:信息架构调整(2-3 天)](#phase-2信息架构调整2-3-天) - [Phase 3:性能与组件化(2-3 天)](#phase-3性能与组件化2-3-天) - [Phase 4:视觉与数据可视化(2-3 天)](#phase-4视觉与数据可视化2-3-天) - [Phase 5:a11y 与细节打磨(1-2 天)](#phase-5a11y-与细节打磨1-2-天) - [新增文件清单](#新增文件清单) - [验收检查表](#验收检查表) --- ## 依赖安装清单 ```bash # Phase 1 — Toast npm install sonner # Phase 4 — 数据可视化 npm install recharts # Phase 5 — 虚拟列表 npm install @tanstack/react-virtual ``` --- ## Phase 1:基础体验修复(1-2 天) ### P1.1 全局字号提升 **目标**:正文 12px → 14px,页面标题 → 18px,辅助文字保持 12px。 | 文件 | 修改内容 | 行号参考 | |------|----------|----------| | `src/index.css` | `.btn` text-xs → text-sm;`.input` text-xs → text-sm;`.label` text-xs → text-sm | L24, L39, L42 | | `src/index.css` | `h1` text-base → text-lg;`h2` text-sm → text-base | L17-18 | | `src/components/ui/Button.tsx` | size md: text-xs → text-sm;lg: text-sm → text-base | L18-20 | | `src/components/ui/Input.tsx` | Input/Select text-xs → text-sm | L10, L25 | | `src/components/ui/Pagination.tsx` | text-xs → text-sm(页码、条数信息) | L42, L69, L74 | | `src/components/ui/EmptyState.tsx` | title text-sm → text-base;description text-xs → text-sm | L19-20 | **验收标准**:正文内容 14px,页面标题 18px,辅助文字 12px,按钮 14px。 --- ### P1.2 卡片间距增大 **目标**:增加呼吸感,信息密度从"紧凑"到"舒适"。 | 文件 | 当前 | 目标 | |------|------|------| | `src/components/ui/Card.tsx` | `p-3` | `p-4` | | `src/index.css` `.card` | `p-3` | `p-4` | | `src/pages/Dashboard.tsx` | `space-y-3` / `gap-2` | `space-y-4` / `gap-3` | L157, L197 | | `src/pages/Money.tsx` | `space-y-3` | `space-y-4` | L28 | | `src/pages/Roster.tsx` | 列表行 `py-1.5` | `py-2.5` | 表格行 | | `src/pages/SocialInsurance.tsx` | `space-y-3` | `space-y-4` | | | `src/pages/Settings.tsx` | `space-y-3` | `space-y-4` | L45 | **验收标准**:卡片内边距 16px,页面模块间距 16px,表格行高 ≥ 40px。 --- ### P1.3 引入 Toast(sonner) **安装**:`npm install sonner` **修改文件清单**(37 处 alert/confirm): | 文件 | alert 数量 | confirm 数量 | 行号参考 | |------|-----------|-------------|----------| | `src/App.tsx` | — | — | 顶层添加 `` | | `src/pages/AIAssistant.tsx` | 6 | 0 | L137, L433, L435, L585, L587, L605, L607 | | `src/pages/Money.tsx` | 5 | 3 | L284, L342, L352, L354, L358, L493, L506, L883, L1216, L1255 | | `src/pages/Roster.tsx` | 4 | 1 | L400, L507, L985, L993, L1930, L1937 | | `src/pages/SocialInsurance.tsx` | 7 | 0 | L122, L132, L168, L182, L191, L200, L294 | | `src/pages/Settings.tsx` | 6 | 0 | 搜索结果 | | `src/pages/portal/ContractConfirm.tsx` | 1 | 0 | | **App.tsx 修改**: ```tsx import { Toaster } from 'sonner' export default function App() { return ( <> ... ) } ``` **各页面替换规则**: ```tsx // 旧:alert('已保存到员工档案') // 新:toast.success('已保存到员工档案') // 旧:alert('保存失败:' + msg) // 新:toast.error('保存失败:' + msg) // 旧:alert('不支持的文件格式') // 新:toast.error('不支持的文件格式,请上传 PDF、JPG、PNG 或 HEIC 格式') ``` **验收标准**:全局 `grep -r "alert(" src/` 返回 0 结果,所有操作反馈通过 toast。 --- ### P1.4 批量操作二次确认组件 **新增文件**:`src/components/ui/ConfirmDialog.tsx` ```tsx import Modal from './Modal' import Button from './Button' interface ConfirmDialogProps { open: boolean title: string message: string confirmLabel?: string cancelLabel?: string variant?: 'danger' | 'primary' onConfirm: () => void onCancel: () => void } export default function ConfirmDialog({ open, title, message, confirmLabel = '确认', cancelLabel = '取消', variant = 'danger', onConfirm, onCancel, }: ConfirmDialogProps) { return (

{message}

) } ``` **替换清单**(所有 `confirm()` 调用): | 文件 | 行号 | 当前代码 | 替换为 | |------|------|----------|--------| | Money.tsx | L284 | `confirm('确认删除批次?')` | `` | | Money.tsx | L493 | `confirm('确认归档?')` | `` | | Money.tsx | L506 | `confirm('确认删除批次?')` | `` | | Money.tsx | L1255 | `confirm('确认生成工资条?')` | `` | | Roster.tsx | L400 | `confirm('确认撤回离职记录?')` | `` | **使用示例**: ```tsx const [confirmOpen, setConfirmOpen] = useState(false) // 触发 onClick={() => setConfirmOpen(true)} // 渲染 { deleteBatchMutation.mutate(batch.id); setConfirmOpen(false) }} onCancel={() => setConfirmOpen(false)} /> ``` **验收标准**:危险操作弹出 Dialog 而非浏览器原生 confirm,有明确文案说明后果。 --- ### P1.5 对比度修复 **目标**:所有文字对比度 ≥ 4.5:1(WCAG AA)。 | 文件 | 当前 | 目标 | 说明 | |------|------|------|------| | 全局 `text-gray-400` | #9CA3AF (2.5:1) | `text-gray-500` #6B7280 (4.6:1) | 全局替换 | | `src/components/ui/Pagination.tsx` | L61, L69 | `text-gray-500` | 翻页按钮 | | `src/pages/Dashboard.tsx` | L229 | `text-gray-500` | "等人"文字 | | `src/components/layout/TopNav.tsx` | L50 | `text-gray-500` | ChevronDown 图标 | **验收标准**:使用 axe DevTools 扫描,0 个对比度违规。 --- ### P1.6 内容宽度限制 | 文件 | 修改 | |------|------| | `tailwind.config.js` | `maxWidth: { content: '1280px' }`(当前 `none`) | **验收标准**:1920px 屏幕内容居中,最大宽度 1280px,两侧留白。 --- ## Phase 2:信息架构调整(2-3 天) ### P2.1 顶部导航精简为 4 入口 **修改文件**:`src/components/layout/TopNav.tsx` ```tsx // 当前 6 个 tab const tabs = [ { path: '/', label: '总览' }, { path: '/roster', label: '花名册' }, { path: '/money', label: '薪税' }, { path: '/social', label: '社保公积金' }, { path: '/termination', label: '解聘补偿' }, { path: '/ai-assistant', label: 'AI顾问' }, ] // 目标 4 个 tab const tabs = [ { path: '/', label: '总览' }, { path: '/roster', label: '员工管理' }, { path: '/money', label: '薪税社保' }, { path: '/ai-assistant', label: 'AI顾问' }, ] ``` **右侧操作区修改**: ```tsx import { Settings, Bell } from 'lucide-react' // 当前:仅用户下拉菜单 // 目标:通知铃铛(badge) + 设置齿轮 + 用户头像
{/* 用户菜单保持 */}
``` **验收标准**:顶部导航 4 个 tab + 设置齿轮 + 通知铃铛 + 用户菜单。 --- ### P2.2 移动端底部导航精简为 4 **修改文件**:`src/components/layout/MobileTabBar.tsx` ```tsx import { Home, Users, Calculator, Bot } from 'lucide-react' // 当前 6 个 → 目标 4 个 const tabs = [ { path: '/', label: '总览', icon: Home }, { path: '/roster', label: '员工', icon: Users }, { path: '/money', label: '薪税', icon: Calculator }, { path: '/ai-assistant', label: 'AI', icon: Bot }, ] ``` **验收标准**:iPhone SE 上每个 tab ≥ 80px 宽度,图标+文字不挤压。 --- ### P2.3 路由调整 **修改文件**:`src/App.tsx` | 变更 | 说明 | |------|------| | `/social` 路由保留 | 导航不直接暴露,作为 `/money` 的子 tab 或页面内跳转 | | `/termination` 路由保留 | 导航不直接暴露,作为 `/roster` 内的功能入口 | **修改文件**:`src/pages/Dashboard.tsx` ```tsx // 移除 payroll tab type Tab = 'overview' | 'risk' | 'task' // 移除 'payroll' const tabs = [ { key: 'overview' as const, label: '概览', icon: LayoutDashboard, badge: data.stats.todoCount }, { key: 'risk' as const, label: '风险提醒', icon: AlertTriangle, badge: riskTodos.length }, { key: 'task' as const, label: '月度任务', icon: ListTodo, badge: taskTodos.length }, ] // 删除 activeTab === 'payroll' 相关的所有 JSX 块 // 删除 payrollSummary / payrollItems / deductionItems 等相关变量 ``` **验收标准**:Dashboard 3 个 tab(概览/风险/任务),无薪税重复入口。 --- ### P2.4 合并未挂载页面 | 操作 | 源文件 | 目标文件 | 说明 | |------|--------|----------|------| | Compensation → Money | `src/pages/Compensation.tsx` | `src/pages/Money.tsx` | 作为薪税页面的子 tab | | Contracts → Roster | `src/pages/Contracts.tsx` | `src/pages/Roster.tsx` | Roster 详情已有合同 tab,删除或合并 | **Money.tsx 修改**: ```tsx type Tab = 'batch' | 'template' | 'overtime' | 'payslip' | 'adjust' const tabs: { key: Tab; label: string }[] = [ { key: 'batch', label: '发薪批次' }, { key: 'template', label: '薪酬模版' }, { key: 'overtime', label: '加班费计算' }, { key: 'payslip', label: '工资条管理' }, { key: 'adjust', label: '薪酬调整' }, // 新增 ] // 渲染 {tab === 'adjust' && } ``` **验收标准**:Contracts/Compensation 功能可访问,无孤立页面。 --- ## Phase 3:性能与组件化(2-3 天) ### P3.1 路由懒加载 **修改文件**:`src/App.tsx` ```tsx import { lazy, Suspense } from 'react' import { Loader2 } from 'lucide-react' const Dashboard = lazy(() => import('./pages/Dashboard')) const Roster = lazy(() => import('./pages/Roster')) const Money = lazy(() => import('./pages/Money')) const SocialInsurance = lazy(() => import('./pages/SocialInsurance')) const Termination = lazy(() => import('./pages/Termination')) const AIAssistant = lazy(() => import('./pages/AIAssistant')) const Settings = lazy(() => import('./pages/Settings')) const Login = lazy(() => import('./pages/auth/Login')) const Register = lazy(() => import('./pages/auth/Register')) const ForgotPassword = lazy(() => import('./pages/auth/ForgotPassword')) const PortalLogin = lazy(() => import('./pages/portal/PortalLogin')) const Payslip = lazy(() => import('./pages/portal/Payslip')) const MyContract = lazy(() => import('./pages/portal/MyContract')) const Onboarding = lazy(() => import('./pages/portal/Onboarding')) const ContractConfirm = lazy(() => import('./pages/portal/ContractConfirm')) function PageSkeleton() { return (
) } export default function App() { return ( }> ... ) } ``` **验收标准**:首屏仅加载 Dashboard chunk,其他页面按需加载,Network 面板可见独立 chunk。 --- ### P3.2 大文件拆分 #### Roster.tsx(140KB → 拆分为 17 个文件) ``` src/pages/roster/ ├── Roster.tsx # 主页面:列表 + 搜索 + 筛选 + 分页 ├── EmployeeDetail.tsx # 详情面板:tab 切换容器 ├── tabs/ │ ├── BasicInfoTab.tsx # 基本信息 │ ├── ContractTab.tsx # 合同信息 │ ├── PayslipTab.tsx # 工资条 │ ├── OvertimeTab.tsx # 加班记录 │ ├── DisciplinaryTab.tsx # 违纪记录 │ ├── AttendanceTab.tsx # 考勤记录 │ ├── TrainingTab.tsx # 培训记录 │ ├── PerformanceTab.tsx # 绩效记录 │ ├── TerminationTab.tsx # 解聘记录 │ ├── AttachmentTab.tsx # 附件管理 │ └── EvidenceTab.tsx # 仲裁证据链 ├── AddEmployeeModal.tsx # 新增员工弹窗 ├── ResignModal.tsx # 离职弹窗 ├── RehireModal.tsx # 重新入职弹窗 ├── SalaryModal.tsx # 调薪弹窗 ├── DeptModal.tsx # 调岗弹窗 └── BatchRenewModal.tsx # 批量续签弹窗 ``` #### Money.tsx(63KB → 拆分为 6 个文件) ``` src/pages/money/ ├── Money.tsx # 主页面:tab 切换 ├── BatchManager.tsx # 发薪批次 ├── TemplateManager.tsx # 薪酬模版 ├── OvertimeCalculator.tsx # 加班费计算 ├── PayslipManager.tsx # 工资条管理 └── CompensationManager.tsx # 薪酬调整(从 Compensation.tsx 合入) ``` #### Termination.tsx(55KB → 拆分为 6 个文件) ``` src/pages/termination/ ├── Termination.tsx # 主页面:向导容器 ├── StepSelectEmployee.tsx # 步骤1:选择员工 ├── StepSelectReason.tsx # 步骤2:解聘方式 ├── StepCompliance.tsx # 步骤3:合规检查 ├── StepSettlement.tsx # 步骤4:费用结算 └── StepConfirm.tsx # 步骤5:确认完成 ``` #### Settings.tsx(46KB → 拆分为 6 个文件) ``` src/pages/settings/ ├── Settings.tsx # 主页面:section 切换 ├── OrgSettings.tsx # 企业信息 ├── UserSettings.tsx # 用户管理 ├── PlanSettings.tsx # 套餐 ├── NotificationSettings.tsx # 通知设置 └── ImportSettings.tsx # 数据导入 ``` **验收标准**:单个文件不超过 500 行,每个子组件独立可测。 --- ### P3.3 骨架屏组件 **新增文件**:`src/components/ui/Skeleton.tsx` ```tsx export function TableSkeleton({ rows = 5 }: { rows?: number }) { return (
{Array.from({ length: rows }).map((_, i) => (
))}
) } export function CardSkeleton() { return (
) } export function DetailSkeleton() { return (
) } ``` **替换清单**(13 处 "加载中..."): | 文件 | 行号 | 替换为 | |------|------|--------| | Dashboard.tsx | L112 | `` | | Roster.tsx | L249, L702 | `` | | Money.tsx | L239, L371, L719, L933, L1268 | `` | | SocialInsurance.tsx | L314, L623 | `` | | AIAssistant.tsx | L773 | `` | | portal/Payslip.tsx | L131 | `` | | portal/MyContract.tsx | L70 | `` | | portal/ContractConfirm.tsx | L85 | `` | | Contracts.tsx | L82 | `` | **验收标准**:`grep -r "加载中" src/` 返回 0 结果,加载时显示骨架屏动画。 --- ### P3.4 搜索防抖 **修改文件**:`src/pages/Roster.tsx` ```tsx import { useDeferredValue } from 'react' const [search, setSearch] = useState('') const deferredSearch = useDeferredValue(search) // queryKey 使用 deferredSearch 而非 search const { data: rosterData } = useQuery({ queryKey: ['roster', page, pageSize, deferredSearch, filterStatus, filterContractStatus], queryFn: async () => { const params: any = { page, pageSize } if (deferredSearch) params.search = deferredSearch // ... }, }) ``` **验收标准**:快速输入时不会每次按键触发 API 请求,停止输入 ~200ms 后才发请求。 --- ## Phase 4:视觉与数据可视化(2-3 天) ### P4.1 主色调暖 **修改文件**:`tailwind.config.js` ```js // 当前:冷蓝 primary: { DEFAULT: '#2563EB', light: '#3B82F6', dark: '#1D4ED8' } // 目标:indigo-600(略带紫调,专业且亲和) primary: { DEFAULT: '#4F46E5', light: '#6366F1', dark: '#4338CA' } ``` **影响范围**:所有使用 `text-primary`、`bg-primary`、`border-primary` 的组件自动生效。 **验收标准**:主色从冷蓝变为 indigo,与 Tailwind indigo-600 色卡一致。 --- ### P4.2 Dashboard 数据可视化 **安装**:`npm install recharts` **修改文件**:`src/pages/Dashboard.tsx` 在概览 tab 的统计卡片下方增加: ```tsx import { LineChart, Line, ResponsiveContainer, XAxis, YAxis, Tooltip, PieChart, Pie, Cell } from 'recharts' // 月度薪税趋势迷你折线图

月度薪税趋势

// 风险分布环形图

风险分布

{riskData.map((entry, i) => )}
``` **验收标准**:Dashboard 概览页有折线图和环形图,图表响应式,tooltip 正常显示。 --- ### P4.3 Modal 过渡动画 **修改文件**:`src/components/ui/Modal.tsx` ```tsx import { ReactNode, useEffect, useState } from 'react' import { X } from 'lucide-react' import clsx from 'clsx' export default function Modal({ open, onClose, title, children, className, size = 'md' }: ModalProps) { const [show, setShow] = useState(false) useEffect(() => { if (open) { setShow(true) } else { const timer = setTimeout(() => setShow(false), 200) return () => clearTimeout(timer) } }, [open]) // body overflow 控制(保持原有逻辑) useEffect(() => { document.body.style.overflow = open ? 'hidden' : '' return () => { document.body.style.overflow = '' } }, [open]) if (!show && !open) return null return (
{/* title + children 保持不变 */}
) } ``` **验收标准**:弹窗有淡入+缩放动画,关闭有淡出动画,~200ms。 --- ### P4.4 移动端表格响应式 **新增组件**:`src/components/ui/ResponsiveTable.tsx` ```tsx import { ReactNode } from 'react' import clsx from 'clsx' interface Column { key: string label: string render?: (row: T) => ReactNode priority: 'high' | 'medium' | 'low' className?: string } interface ResponsiveTableProps { columns: Column[] data: T[] rowKey: (row: T) => string onRowClick?: (row: T) => void } export default function ResponsiveTable({ columns, data, rowKey, onRowClick }: ResponsiveTableProps) { return ( <> {/* 桌面端/平板:表格 */} {columns.map(col => ( ))} {data.map(row => ( onRowClick?.(row)}> {columns.map(col => ( ))} ))}
{col.label}
{col.render ? col.render(row) : (row as any)[col.key]}
{/* 手机端:卡片列表 */}
{data.map(row => { const highCols = columns.filter(c => c.priority === 'high') return (
onRowClick?.(row)}> {highCols.map(col => (
{col.label} {col.render ? col.render(row) : (row as any)[col.key]}
))}
) })}
) } ``` **应用页面**:Roster、Money(工资条列表)、SocialInsurance(月度申报表) **验收标准**:iPhone SE 上列表为卡片模式,iPad 上为表格,桌面端完整表格。 --- ## Phase 5:a11y 与细节打磨(1-2 天) ### P5.1 div onClick → button + aria **修改文件**:`src/components/layout/TopNav.tsx` ```tsx // 添加 aria 属性