Files
TurboHR/docs/ui-ux-review.md
selfrelease 0df8aa77d9 feat: AIHR 智能人力资源管理系统初始提交
- 员工花名册管理(加密存储、导入导出)
- 薪酬管理(发薪批次、薪酬模版、加班费计算、工资条)
- 社保公积金(多城市配置、版本管理、基数调整)
- 解聘管理(6步流程、证据链、工作交接)
- AI 助手(合同审查、风险预测、RAG 知识库)
- Dashboard 仪表盘
- 设置与通知
2026-07-24 13:53:11 +08:00

1148 lines
39 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AIHR 前端 UI/UX 梳理与优化方向
> 针对中小微企业人事/财务人员的使用场景,对现有前端进行全面梳理并提出优化建议。
>
> 日期:2026-07-24
---
## 〇、竞品参考标杆
### 0.1 选型逻辑
AIHR 定位为"中小微企业用工合规助手",核心用户是 1-2 人的 HR/财务兼管人员。选参考标杆时,排除北森(太重、实施 3-6 个月)、Rippling(HR+IT 一体化,偏离核心场景),聚焦以下 4 个标杆产品:
### 0.2 标杆产品矩阵
| 产品 | 定位 | 参考价值 | UI/UX 特点 |
|------|------|----------|------------|
| **BambooHR** | 中端市场 HRIS50-500 人) | **UI/UX 黄金标准** | 46% 用户评价中专门提及界面质量;简洁现代,非 HR 人员也能快速上手;员工自助体验最佳 |
| **Gusto** | 美国小企业薪税合规(<50 人) | **薪税+合规体验** | "set it and forget it" 自动化薪税;界面极简,创始人自己就能跑薪税;$49+$6/人,性价比最高 |
| **飞书人事** | 中国协同办公生态 HR 模块 | **本土化+集成体验** | 与 IM/文档/审批原生打通;组织架构拖拉拽调整;员工点击头像即看全档案;零学习成本 |
| **薪人薪事** | 中国中小企业薪酬社保 SaaS | **薪酬社保深度** | 薪酬社保为核心优势;50-200 人企业首选;界面操作直观,部署速度快 |
### 0.3 各标杆关键设计借鉴
#### BambooHR — UI/UX 第一梯队
| 设计要点 | 借鉴方向 |
|----------|----------|
| 左侧导航栏(非顶部 tab) | 信息层级清晰,可折叠,不占垂直空间 |
| 员工档案卡片式布局 | 头照 + 基本信息 + 标签,视觉温度 |
| Onboarding Checklist | 新员工入职清单,进度条可视化 |
| PTO 申请日历视图 | 请假/考勤用日历而非表格 |
| 报表 + 人员分析 Dashboard | 图表驱动,非纯数字 |
| 移动端体验 | 员工自助(查工资条/请假/签合同)流畅 |
#### Gusto — 薪税合规极简体验
| 设计要点 | 借鉴方向 |
|----------|----------|
| 薪税运行向导(Run Payroll) | 3 步引导:确认人数 → 核对金额 → 一键发放 |
| 合规提醒卡片 | "你需要在本月 15 日前完成社保申报" 式主动提醒 |
| 自动报税 + 状态追踪 | 薪税计算后显示"已申报/待申报/已完成"状态 |
| 透明定价展示 | 套餐/额度在设置页面清晰展示 |
| 员工自助入职 | 员工自己填信息+传材料,HR 只需审核 |
#### 飞书人事 — 本土化集成
| 设计要点 | 借鉴方向 |
|----------|----------|
| 组织架构树 + 拖拽调整 | 部门管理可视化,非纯文本 |
| 人事流程无代码配置 | 审批/通知可自定义,适应不同企业 |
| 全生命周期数据汇聚 | 入职→在职→离职数据在一个档案页 |
| 与 IM 消息打通 | 审批/通知推送到微信/钉钉/飞书 |
| 移动端 = 桌面端功能对等 | 不阉割功能,移动端可完成所有操作 |
#### 薪人薪事 — 薪酬社保深度
| 设计要点 | 借鉴方向 |
|----------|----------|
| 薪酬结构可视化编辑 | 薪酬项拖拽排序,公式可视化 |
| 社保基数自动核定 | 按城市/基数上下限自动计算,非手动填 |
| 工资条一键群发 + 员工确认 | 批量发送 → 员工端查看确认 → 状态追踪 |
| 薪酬变更历史时间线 | 调薪记录以时间线展示,可追溯 |
| 社保公积金月度申报表 | 自动生成申报表,导出 Excel/PDF |
### 0.4 AIHR 对标差距与借鉴优先级
| 维度 | 标杆 | AIHR 现状 | 差距 | 借鉴优先级 |
|------|------|-----------|------|------------|
| **导航架构** | BambooHR(左侧栏) | 顶部 6 tab 平铺 | 无层级、认知负担重 | 高 |
| **字号/间距** | BambooHR / Gusto | text-xs 全局 | 过小,阅读疲劳 | 高 |
| **薪税向导** | Gusto3 步引导) | 4 个子 tab 手动操作 | 缺少引导式流程 | 高 |
| **合规提醒** | Gusto(主动卡片) | Dashboard 待办列表 | 有但不够突出 | 中 |
| **员工档案** | 飞书人事(全生命周期) | 11 个 tab 平铺 | 信息分散,缺概览 | 中 |
| **组织架构** | 飞书人事(可视化树) | 无 | 完全缺失 | 低(中小微部门少) |
| **数据可视化** | BambooHR(图表驱动) | 纯数字+图标 | 无趋势/分布图 | 中 |
| **员工自助端** | BambooHR / 飞书 | 有但无导航 | 页面间无法跳转 | 中 |
| **Toast 反馈** | 所有标杆 | 无 | 操作无即时反馈 | 高 |
| **移动端体验** | BambooHR(功能对等) | 底部 6 tab 拥挤 | 表格不响应式 | 高 |
### 0.5 推荐设计方向总结
> **以 BambooHR 的 UI 质量为视觉标杆,以 Gusto 的薪税合规向导为交互范式,以飞书人事的全生命周期档案为信息架构参考,以薪人薪事的薪酬社保深度为功能标杆。**
具体而言:
1. **导航** → 参考 BambooHR 改为左侧栏(桌面端)+ 底部 4 tab(移动端)
2. **薪税流程** → 参考 Gusto 改为引导式向导,而非 4 个 tab 手动切换
3. **员工档案** → 参考飞书人事,增加"概览"首 tab,汇总关键信息
4. **合规提醒** → 参考 Gusto,在 Dashboard 突出"本月必做"卡片
5. **视觉风格** → 参考 BambooHR,提升字号/间距/色彩温度
---
## 一、现状概览
### 1.1 技术栈
| 层面 | 技术 |
|------|------|
| 框架 | React 18 + TypeScript |
| 构建 | Vite 5 |
| 样式 | Tailwind CSS 3(自定义 tokenprimary/danger/warning/safe/surface |
| 路由 | React Router v6 |
| 状态 | Zustandauth+ TanStack Query(服务端数据) |
| 表单 | react-hook-form + zod |
| 图标 | lucide-react |
| 其他 | qrcode.react, jsPDF, xlsx |
### 1.2 页面结构
```
管理端(AdminLayout
├── TopNav(顶部导航 h-146 个 tab
├── PageContainermax-w-content, px-4
├── MobileTabBar(移动端底部 tab bar
└── OnboardingGuide(首次引导遮罩)
路由页面:
├── / Dashboard(总览:概览/薪税/风险/任务 4 tab)
├── /roster Roster(花名册:员工列表 + 11 个详情 tab)
├── /money Money(薪税:发薪批次/薪酬模版/加班费/工资条 4 tab)
├── /social SocialInsurance(社保公积金:社保/公积金/月度 3 tab)
├── /termination Termination(解聘补偿:5 步向导)
├── /ai-assistant AIAssistantAI 顾问:问答/预测/审查/案例/知识库 5 tab)
├── /settings Settings(设置:企业/用户/套餐/通知/导入 5 tab)
员工端(PortalLayout, max-w-md
├── /portal/login 员工登录
├── /portal/payslip 工资条查看
├── /portal/contract 合同查看
├── /portal/onboarding 入职引导
└── /portal/contract-confirm 合同确认
未挂载页面(文件存在但未路由):
├── Compensation.tsx 薪酬调整
└── Contracts.tsx 合同管理
```
### 1.3 组件库
| 组件 | 说明 |
|------|------|
| Button | 3 variantprimary/secondary/danger)× 3 size |
| Card | 白底圆角阴影,p-3 |
| Input / Select / Label | 基础表单控件,text-xs |
| Modal | 4 size,居中弹窗 |
| Pagination | 分页 |
| EmptyState | 空状态 |
| Signal | 信号灯(风险指示) |
### 1.4 设计 Token
```js
colors: {
primary: { DEFAULT: '#2563EB', light: '#3B82F6', dark: '#1D4ED8' },
danger: '#DC2626',
warning: '#F59E0B',
safe: '#16A34A',
surface: '#F8FAFC',
}
maxWidth: { content: 'none' } // 宽屏不限制
```
---
## 二、问题诊断
### 2.1 信息架构
| 问题 | 影响 | 严重度 |
|------|------|--------|
| **导航扁平,6 个 tab 平铺** | 中小微 HR 只有 1-2 人,6 个顶级 tab + 每页 3-5 个子 tab = 认知负担重 | 高 |
| **Contracts / Compensation 页面未挂载** | 合同管理和薪酬调整功能用户无法访问 | 高 |
| **Dashboard 内嵌薪税 tab** | 与 /money 页面功能重叠,用户困惑入口 | 中 |
| **设置入口隐蔽** | 仅在右上角下拉菜单,中小微用户不易发现 | 中 |
| **员工端无统一导航** | Portal 页面间无法跳转,只能通过链接进入 | 中 |
### 2.2 视觉设计
| 问题 | 影响 | 严重度 |
|------|------|--------|
| **全局字号过小(text-xs 为主)** | 人事/财务人员长时间盯屏疲劳,35+ 用户阅读困难 | 高 |
| **卡片间距紧凑(p-3, gap-2** | 信息密度过高,缺乏呼吸感,重要信息不突出 | 高 |
| **主色 #2563EB 偏冷** | B 端工具感强,缺乏温度,与"中小微企业友好"定位不符 | 中 |
| **max-w-content 为 none** | 超宽屏内容无限拉伸,1920px+ 屏幕阅读体验差 | 中 |
| **无数据可视化** | Dashboard 仅数字+图标卡片,无图表,趋势不直观 | 中 |
| **状态色未体系化** | 各页面自行定义颜色(blue-50/amber-50/cyan-50),未统一 token | 低 |
### 2.3 交互体验
| 问题 | 影响 | 严重度 |
|------|------|--------|
| **无全局 Toast/通知** | 操作成功/失败无即时反馈,用户不确定操作是否生效 | 高 |
| **无 Loading 骨架屏** | 仅"加载中..."文字,长列表加载时白屏 | 中 |
| **Modal 无过渡动画** | 弹窗突现突消失,体验生硬 | 中 |
| **表单无防离开提示** | 填写一半误点导航数据丢失 | 中 |
| **批量操作无确认** | 批量解聘/续签缺少二次确认,误操作风险高 | 高 |
| **搜索无防抖** | 花名册搜索每次按键触发请求,浪费资源 | 低 |
| **无快捷键支持** | 高频操作(搜索/新建/保存)无键盘快捷方式 | 低 |
### 2.4 移动端适配
| 问题 | 影响 | 严重度 |
|------|------|--------|
| **MobileTabBar 6 个 tab 拥挤** | iPhone SE 上每个 tab 仅 53px,图标+文字挤压 | 高 |
| **表格无响应式方案** | 花名册等列表页在手机上横向滚动体验差 | 高 |
| **Modal 在小屏溢出** | max-w-4xl 弹窗在手机上内容拥挤 | 中 |
| **员工端 max-w-md 偏窄** | 工资条信息在 tablet 上浪费空间 | 低 |
### 2.5 可访问性(a11y
| 问题 | 影响 | 严重度 |
|------|------|--------|
| **div onClick 模式** | TopNav 下拉菜单等用 div+onClick,键盘不可达 | 中 |
| **无 aria-label** | 图标按钮无语义标签,屏幕阅读器无法识别 | 中 |
| **对比度不足** | text-gray-400 on bg-white 对比度 ~2.5:1,低于 WCAG AA 4.5:1 | 中 |
| **无 focus-visible 样式** | 键盘导航时无法识别当前焦点元素 | 低 |
### 2.6 性能
| 问题 | 影响 | 严重度 |
|------|------|--------|
| **单文件过大** | Roster.tsx 140KB / Money.tsx 63KB / Termination.tsx 55KB,首屏加载慢 | 高 |
| **无代码分割** | 所有页面打包在一个 chunk,无 lazy loading | 高 |
| **无虚拟列表** | 花名册大量员工时 DOM 节点过多 | 中 |
---
## 三、优化方向
### 3.1 信息架构重组(优先级:高)
**目标:降低认知负担,让 HR 3 秒找到功能入口。**
#### 方案:二级导航 + 功能聚合
```
顶部导航(h-14
├── Logo + 企业名
├── 主导航(4 个入口,合并相关功能)
│ ├── 总览 → Dashboard(保留概览+风险+任务,移除薪税 tab)
│ ├── 员工管理 → Roster + Contracts(合并,花名册内增加合同 tab)
│ ├── 薪税社保 → Money + SocialInsurance(合并为一个页面组)
│ └── AI 顾问 → AIAssistant
├── 右侧:通知铃铛(badge) + 设置齿轮 + 用户头像
移动端底部导航(4 个 tab
├── 总览
├── 员工
├── 薪税
└── AI
```
#### 具体调整
1. **合并 Contracts → Roster**:花名册详情中已有合同 tab,Contracts.tsx 独立页面无入口,应删除或合并
2. **合并 Compensation → Money**:薪酬调整功能应作为薪税页面的一个子 tab
3. **Dashboard 去掉薪税 tab**:避免与 /money 重复,保留概览/风险/任务 3 个 tab
4. **设置入口可见化**:齿轮图标直接放在导航栏,而非藏在下拉菜单
### 3.2 视觉设计升级(优先级:高)
#### 3.2.1 字号体系
```
当前:全局 text-xs (12px) → 目标:text-sm (14px) 为正文基准
层级 当前 目标 用途
h1 (页面标题) text-xs text-base 页面顶部标题
h2 (区块标题) text-sm text-sm 卡片标题
正文/表格 text-xs text-sm 列表、表格、表单
辅助文字 text-xs text-xs 时间戳、备注(保持)
数字大屏 无 text-2xl Dashboard 统计数字
```
#### 3.2.2 间距体系
```
当前:p-3 / gap-2 → 目标:p-4 / gap-3(增加呼吸感)
组件 当前 目标
Card p-3 p-4
页面间距 gap-2 gap-4
表格行高 紧凑 py-2.5(增加行高)
```
#### 3.2.3 主色调暖
```js
// 当前:冷蓝 #2563EB
// 建议:indigo-600 #4F46E5,略带温度,专业且亲和
colors: {
primary: { DEFAULT: '#4F46E5', light: '#6366F1', dark: '#4338CA' },
}
```
#### 3.2.4 内容最大宽度
```js
// 当前:max-w-content = none(无限拉伸)
// 建议:限制 1280px,超宽屏居中
maxWidth: { content: '1280px' }
```
#### 3.2.5 数据可视化
- Dashboard 概览增加 **迷你折线图**(月度薪税趋势)
- 风险分布增加 **环形图**(高/中/低风险占比)
- 引入 `recharts`(轻量,~40KB gzip
### 3.3 交互体验增强(优先级:高)
#### 3.3.1 全局 Toast
```tsx
// 引入 sonner~5KB gzip
import { toast } from 'sonner'
// 成功/失败/警告 统一调用
toast.success('员工已添加')
toast.error('删除失败,请重试')
toast.warning('该操作不可撤销')
```
#### 3.3.2 批量操作二次确认
```tsx
// 危险操作(批量解聘/删除)必须 Dialog 二次确认
<Dialog open={confirmOpen}>
{selectedIds.size}
</Dialog>
```
#### 3.3.3 骨架屏
```tsx
// 替换"加载中..."文字
function TableSkeleton() {
return Array.from({ length: 5 }).map(i => (
<div className="h-10 bg-gray-100 rounded animate-pulse mb-2" />
))
}
```
#### 3.3.4 搜索防抖
```tsx
// 使用 useDeferredValue 或 lodash.debounce
const debouncedSearch = useDeferredValue(search)
// queryKey 使用 debouncedSearch
```
#### 3.3.5 表单防离开
```tsx
// react-hook-form 已支持
const form = useForm({ mode: 'onChange' })
useEffect(() => {
const handler = (e: BeforeUnloadEvent) => {
if (form.formState.isDirty) {
e.preventDefault()
e.returnValue = ''
}
}
window.addEventListener('beforeunload', handler)
return () => window.removeEventListener('beforeunload', handler)
}, [form.formState.isDirty])
```
### 3.4 移动端优化(优先级:高)
#### 3.4.1 底部导航精简为 4 个
```
当前 6 个:总览 / 花名册 / 薪税 / 社保 / 解聘补偿 / AI
目标 4 个:总览 / 员工 / 薪税 / AI
(社保、解聘补偿归入对应主页面子功能)
```
#### 3.4.2 表格响应式
- 手机端:卡片列表模式(每行一个卡片,字段竖排)
- 平板端:紧凑表格(隐藏低优先级列)
- 桌面端:完整表格
#### 3.4.3 Modal 移动端全屏
```tsx
// 移动端 Modal 全屏展示
className={clsx(
'relative bg-white shadow-xl w-full max-h-[90vh] overflow-y-auto',
isMobile ? 'h-screen rounded-none' : 'rounded-lg',
sizeClass,
)}
```
### 3.5 可访问性(优先级:中)
- **替换 div onClick**:下拉菜单用 `<button>` + `aria-expanded`
- **添加 aria-label**:图标按钮统一添加 `aria-label="xxx"`
- **修复对比度**`text-gray-400``text-gray-500`(对比度 ≥ 4.5:1
- **focus-visible**`focus-visible:ring-2 focus-visible:ring-primary`
### 3.6 性能优化(优先级:高)
#### 3.6.1 路由懒加载
```tsx
// App.tsx 改为 lazy import
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Roster = lazy(() => import('./pages/Roster'))
// ...
<Suspense fallback={<PageSkeleton />}>
<Routes>...</Routes>
</Suspense>
```
#### 3.6.2 大文件拆分
| 文件 | 当前大小 | 拆分方向 |
|------|----------|----------|
| Roster.tsx | 140KB | 拆为 RosterList + EmployeeDetail + 子 tab 组件 |
| Money.tsx | 63KB | 拆为 BatchManager + TemplateManager + OvertimeCalc + PayslipManager |
| Termination.tsx | 55KB | 拆为向导各步骤独立组件 |
| Settings.tsx | 46KB | 拆为各 section 独立组件 |
#### 3.6.3 虚拟列表
```tsx
// 花名册员工数 > 100 时使用虚拟列表
import { useVirtualizer } from '@tanstack/react-virtual'
```
---
## 三-A、详细优化实施方案
> 以下为每个 Phase 的具体文件修改清单、代码示例和验收标准。
### Phase 1:基础体验修复(1-2 天)
#### P1.1 全局字号提升
| 文件 | 修改内容 | 行号参考 |
|------|----------|----------|
| `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 sm: text-xs→text-xs(保持)md: text-xs→text-smlg: 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-basedescription text-xs→text-sm | L19-20 |
| `src/components/layout/TopNav.tsx` | tab 文字 text-sm(保持);用户名 text-sm→text-sm | L42 |
| `src/components/layout/MobileTabBar.tsx` | tab label text-xs→text-xs(保持,移动端紧凑) | L33 |
**验收标准**:正文内容 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` | `space-y-4``gap-2``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 |
| `src/pages/AIAssistant.tsx` | `space-y-4`(保持) | L37 |
**验收标准**:卡片内边距 16px,页面模块间距 16px,表格行高 ≥ 40px。
#### P1.3 引入 Toastsonner
**安装**`npm install sonner`
**修改文件清单**
| 文件 | 修改 |
|------|------|
| `src/App.tsx` | 顶层添加 `<Toaster position="top-center" richColors />` |
| `src/pages/AIAssistant.tsx` | 6 处 `alert()``toast.success()` / `toast.error()` | L137, L433, L435, L585, L587, L605, L607 |
| `src/pages/Money.tsx` | 8 处 `alert()``toast.success()` / `toast.error()`3 处 `confirm()` → ConfirmDialog | L284, L342, L352, L354, L358, L493, L506, L883, L1216, L1255 |
| `src/pages/Roster.tsx` | 4 处 `alert()``toast.error()`1 处 `confirm()` → ConfirmDialog | L400, L507, L985, L993, L1930, L1937 |
| `src/pages/SocialInsurance.tsx` | 7 处 `alert()``toast.success()` / `toast.error()` | L122, L132, L168, L182, L191, L200, L294 |
| `src/pages/Settings.tsx` | 6 处 `alert()``toast` | 搜索结果 |
| `src/pages/portal/ContractConfirm.tsx` | 1 处 `alert()``toast` | |
**代码示例**
```tsx
// App.tsx 顶层
import { Toaster } from 'sonner'
export default function App() {
return (
<>
<Routes>...</Routes>
<Toaster position="top-center" richColors closeButton />
</>
)
}
// 各页面替换
// 旧:alert('已保存到员工档案')
// 新:toast.success('已保存到员工档案')
// 旧:alert('保存失败:' + msg)
// 新:toast.error('保存失败:' + msg)
```
**验收标准**:全局 0 处 `alert()`0 处 `confirm()`,所有操作反馈通过 toast 或 Dialog。
#### 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 (
<Modal open={open} onClose={onCancel} title={title} size="sm">
<p className="text-sm text-gray-600 mb-4">{message}</p>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={onCancel}>{cancelLabel}</Button>
<Button variant={variant} onClick={onConfirm}>{confirmLabel}</Button>
</div>
</Modal>
)
}
```
**替换清单**(所有 `confirm()` 调用):
| 文件 | 行号 | 当前代码 | 替换为 |
|------|------|----------|--------|
| Money.tsx | L284 | `confirm('确认删除批次?')` | `<ConfirmDialog>` |
| Money.tsx | L493 | `confirm('确认归档?')` | `<ConfirmDialog>` |
| Money.tsx | L506 | `confirm('确认删除批次?')` | `<ConfirmDialog>` |
| Money.tsx | L1255 | `confirm('确认生成工资条?')` | `<ConfirmDialog>` |
| Roster.tsx | L400 | `confirm('确认撤回离职记录?')` | `<ConfirmDialog>` |
**验收标准**:危险操作弹出 Dialog 而非浏览器原生 confirm,有明确文案说明后果。
#### P1.5 对比度修复
| 文件 | 当前 | 目标 | 说明 |
|------|------|------|------|
| 全局 `text-gray-400` | `text-gray-400` (#9CA3AF) | `text-gray-500` (#6B7280) | 对比度 2.5:1 → 4.6:1 |
| `src/components/ui/Pagination.tsx` | L61, L69 `text-gray-400` | `text-gray-500` | 翻页按钮 |
| `src/pages/Dashboard.tsx` | L229 `text-gray-400` | `text-gray-500` | "等人"文字 |
| `src/components/layout/TopNav.tsx` | L50 `text-gray-400` | `text-gray-500` | ChevronDown 图标 |
**验收标准**:所有文字对比度 ≥ 4.5:1WCAG AA),使用 axe DevTools 验证。
#### 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
// 当前:仅用户下拉菜单
// 目标:通知铃铛(badge) + 设置齿轮 + 用户头像
<div className="flex items-center gap-2 shrink-0">
<Link to="/settings" className="p-1.5 rounded-md hover:bg-gray-100" aria-label="设置">
<Settings className="w-4 h-4 text-gray-600" />
</Link>
<button className="relative p-1.5 rounded-md hover:bg-gray-100" aria-label="通知">
<Bell className="w-4 h-4 text-gray-600" />
{riskCount > 0 && <span className="absolute -top-0.5 -right-0.5 ..." />}
</button>
{/* 用户菜单保持 */}
</div>
```
#### P2.2 移动端底部导航精简为 4
**修改文件**`src/components/layout/MobileTabBar.tsx`
```tsx
// 当前 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 },
]
```
#### P2.3 路由调整
**修改文件**`src/App.tsx`
| 变更 | 说明 |
|------|------|
| `/social` 路由保留但导航不直接暴露 | 作为 `/money` 的子 tab 或独立路由通过页面内跳转 |
| `/termination` 路由保留但导航不直接暴露 | 作为 `/roster` 内的功能入口(花名册 → 解聘操作) |
| Dashboard 移除薪税 tab | `activeTab` 类型从 `'overview' \| 'payroll' \| 'risk' \| 'task'``'overview' \| 'risk' \| 'task'` |
**Dashboard.tsx 修改**
```tsx
// 移除 payroll tab
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 块
```
#### P2.4 合并未挂载页面
| 操作 | 文件 | 说明 |
|------|------|------|
| Compensation → Money | `src/pages/Money.tsx` | 在 tabs 数组增加 `{ key: 'adjust', label: '薪酬调整' }`,渲染 `<CompensationManager />` |
| Contracts → Roster | `src/pages/Contracts.tsx` | Roster 详情已有合同 tab,删除独立 Contracts.tsx 或将其逻辑合并到 Roster 详情的合同 tab |
**Money.tsx 修改**
```tsx
type Tab = 'batch' | 'template' | 'overtime' | 'payslip' | 'adjust'
const tabs = [
{ key: 'batch', label: '发薪批次' },
{ key: 'template', label: '薪酬模版' },
{ key: 'overtime', label: '加班费计算' },
{ key: 'payslip', label: '工资条管理' },
{ key: 'adjust', label: '薪酬调整' }, // 新增
]
// {tab === 'adjust' && <CompensationManager />}
```
**验收标准**:顶部导航 4 个 tab,移动端 4 个 tab,无重复入口,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'))
// ... 其他页面同理
function PageSkeleton() {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 text-primary animate-spin" />
</div>
)
}
export default function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>...</Routes>
</Suspense>
)
}
```
**验收标准**:首屏仅加载 Dashboard chunk,其他页面按需加载,Network 面板可见独立 chunk。
#### P3.2 大文件拆分
**Roster.tsx140KB)拆分方案**
```
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.tsx63KB)拆分方案**
```
src/pages/money/
├── Money.tsx # 主页面:tab 切换
├── BatchManager.tsx # 发薪批次
├── TemplateManager.tsx # 薪酬模版
├── OvertimeCalculator.tsx # 加班费计算
├── PayslipManager.tsx # 工资条管理
└── CompensationManager.tsx # 薪酬调整(从 Compensation.tsx 合入)
```
**Termination.tsx55KB)拆分方案**
```
src/pages/termination/
├── Termination.tsx # 主页面:向导容器
├── StepSelectEmployee.tsx # 步骤1:选择员工
├── StepSelectReason.tsx # 步骤2:解聘方式
├── StepCompliance.tsx # 步骤3:合规检查
├── StepSettlement.tsx # 步骤4:费用结算
└── StepConfirm.tsx # 步骤5:确认完成
```
**Settings.tsx46KB)拆分方案**
```
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 (
<div className="space-y-2">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="h-10 bg-gray-100 rounded animate-pulse" />
))}
</div>
)
}
export function CardSkeleton() {
return (
<div className="p-4 bg-white rounded-lg border border-gray-200">
<div className="h-4 bg-gray-100 rounded w-1/3 mb-3 animate-pulse" />
<div className="h-8 bg-gray-100 rounded w-1/2 animate-pulse" />
</div>
)
}
export function DetailSkeleton() {
return (
<div className="space-y-3">
<div className="h-6 bg-gray-100 rounded w-1/4 animate-pulse" />
<div className="h-4 bg-gray-100 rounded w-full animate-pulse" />
<div className="h-4 bg-gray-100 rounded w-3/4 animate-pulse" />
</div>
)
}
```
**替换清单**13 处 "加载中..."):
| 文件 | 行号 | 替换为 |
|------|------|--------|
| Dashboard.tsx | L112 | `<TableSkeleton rows={4} />` |
| Roster.tsx | L249, L702 | `<TableSkeleton />` |
| Money.tsx | L239, L371, L719, L933, L1268 | `<TableSkeleton />` |
| SocialInsurance.tsx | L314, L623 | `<TableSkeleton rows={3} />` |
| AIAssistant.tsx | L773 | `<TableSkeleton />` |
| portal/Payslip.tsx | L131 | `<CardSkeleton />` |
| portal/MyContract.tsx | L70 | `<CardSkeleton />` |
| portal/ContractConfirm.tsx | L85 | `<CardSkeleton />` |
| Contracts.tsx | L82 | `<TableSkeleton />` |
**验收标准**:加载时显示骨架屏动画,无白屏或纯文字"加载中..."。
#### 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],
// ...
})
```
**验收标准**:快速输入时不会每次按键触发 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'
// 月度薪税趋势迷你折线图
<Card>
<h2 className="font-medium mb-3"></h2>
<ResponsiveContainer width="100%" height={120}>
<LineChart data={data.payrollHistory}>
<XAxis dataKey="month" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 12 }} />
<Tooltip />
<Line type="monotone" dataKey="totalPay" stroke="#4F46E5" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
</Card>
// 风险分布环形图
<Card>
<h2 className="font-medium mb-3"></h2>
<ResponsiveContainer width="100%" height={160}>
<PieChart>
<Pie data={riskData} dataKey="count" nameKey="label" cx="50%" cy="50%" innerRadius={40} outerRadius={60}>
{riskData.map((entry, i) => <Cell key={i} fill={entry.color} />)}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</Card>
```
**验收标准**:Dashboard 概览页有折线图和环形图,图表响应式,tooltip 正常显示。
#### P4.3 Modal 过渡动画
**修改文件**`src/components/ui/Modal.tsx`
```tsx
// 添加 CSS transition
// 方案:利用 Tailwind 的 transition + opacity + scale
// 遮罩层:opacity 0 → 100
// 内容层:scale-95 opacity-0 → scale-100 opacity-100
// 新增 state 控制动画
const [show, setShow] = useState(false)
useEffect(() => {
if (open) {
setShow(true)
} else {
const timer = setTimeout(() => setShow(false), 200)
return () => clearTimeout(timer)
}
}, [open])
if (!show && !open) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className={clsx(
'fixed inset-0 bg-black/40 transition-opacity duration-200',
open ? 'opacity-100' : 'opacity-0'
)} onClick={onClose} />
<div className={clsx(
'relative bg-white rounded-lg shadow-xl w-full max-h-[90vh] overflow-y-auto transition-all duration-200',
open ? 'scale-100 opacity-100' : 'scale-95 opacity-0',
sizeClass,
)}>
{/* 内容不变 */}
</div>
</div>
)
```
**验收标准**:弹窗有淡入+缩放动画,关闭有淡出动画,~200ms。
#### P4.4 移动端表格响应式
**新增组件**`src/components/ui/ResponsiveTable.tsx`
```tsx
interface Column<T> {
key: string
label: string
render?: (row: T) => React.ReactNode
priority: 'high' | 'medium' | 'low' // 高优先级在手机端显示
className?: string
}
interface ResponsiveTableProps<T> {
columns: Column<T>[]
data: T[]
rowKey: (row: T) => string
onRowClick?: (row: T) => void
}
// 桌面端:表格(显示所有列)
// 平板端:表格(隐藏 low priority 列,用 hidden md:table-cell
// 手机端:卡片列表(仅显示 high priority 字段,竖排)
```
**应用页面**Roster、Money(工资条列表)、SocialInsurance(月度申报表)
**验收标准**:iPhone SE 上列表为卡片模式,iPad 上为紧凑表格,桌面端完整表格。
---
### Phase 5a11y 与细节打磨(1-2 天)
#### P5.1 div onClick → button + aria
**修改文件**`src/components/layout/TopNav.tsx`
```tsx
// 当前(L46-50):div + onClick
<div className="relative shrink-0">
<button onClick={() => setMenuOpen(!menuOpen)} className="flex items-center gap-1 ...">
// 目标:添加 aria 属性
<button
onClick={() => setMenuOpen(!menuOpen)}
className="flex items-center gap-1 ..."
aria-expanded={menuOpen}
aria-haspopup="menu"
aria-label="用户菜单"
>
```
#### P5.2 aria-label 覆盖
| 文件 | 位置 | 添加 |
|------|------|------|
| TopNav.tsx | Logo Link | `aria-label="用工合规助手首页"` |
| TopNav.tsx | 设置齿轮 | `aria-label="设置"` |
| TopNav.tsx | 通知铃铛 | `aria-label="通知"` |
| MobileTabBar.tsx | 每个 Link | `aria-label={tab.label}` |
| Modal.tsx | 关闭按钮 | `aria-label="关闭"` |
| Pagination.tsx | 上下页按钮 | `aria-label="上一页"` / `aria-label="下一页"` |
#### P5.3 focus-visible 样式
**修改文件**`src/index.css`
```css
@layer base {
*:focus-visible {
@apply outline-none ring-2 ring-primary ring-offset-1;
}
}
```
#### P5.4 表单防离开
**新增 Hook**`src/hooks/useUnsavedChanges.ts`
```tsx
import { useEffect } from 'react'
export function useUnsavedChanges(isDirty: boolean) {
useEffect(() => {
const handler = (e: BeforeUnloadEvent) => {
if (isDirty) {
e.preventDefault()
e.returnValue = ''
}
}
window.addEventListener('beforeunload', handler)
return () => window.removeEventListener('beforeunload', handler)
}, [isDirty])
}
```
**应用**:所有表单页面(AddEmployee、Settings OrgForm、Termination 向导等)
```tsx
const form = useForm({ mode: 'onChange' })
useUnsavedChanges(form.formState.isDirty)
```
#### P5.5 虚拟列表
**安装**`npm install @tanstack/react-virtual`
**修改文件**`src/pages/Roster.tsx`(当员工数 > 100 时)
```tsx
import { useVirtualizer } from '@tanstack/react-virtual'
// 在员工列表渲染中
const rowVirtualizer = useVirtualizer({
count: employees.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48,
overscan: 5,
})
```
**验收标准**:1000+ 员工时列表滚动流畅,DOM 节点数 < 30。
---
## 四、实施路线图
### Phase 1:基础体验修复(1-2 天)
- [ ] 全局字号提升 text-xs → text-sm
- [ ] 卡片间距 p-3 → p-4,页面 gap-2 → gap-4
- [ ] 引入 sonner Toast,替换所有 alert/console
- [ ] 批量操作增加二次确认 Dialog
- [ ] 修复 text-gray-400 对比度
- [ ] max-w-content 限制为 1280px
### Phase 2:信息架构调整(2-3 天)
- [ ] 顶部导航精简为 4 个入口
- [ ] 移动端底部导航精简为 4 个
- [ ] Dashboard 移除薪税 tab
- [ ] 合并 Contracts → RosterCompensation → Money
- [ ] 设置入口可见化(齿轮图标)
### Phase 3:性能与组件化(2-3 天)
- [ ] 路由懒加载 + Suspense
- [ ] Roster / Money / Settings 大文件拆分
- [ ] 骨架屏替换"加载中..."
- [ ] 搜索防抖
### Phase 4:视觉与数据可视化(2-3 天)
- [ ] 主色调 indigo-600
- [ ] Dashboard 增加迷你折线图 + 环形图(recharts)
- [ ] Modal 过渡动画
- [ ] 移动端表格卡片模式
### Phase 5a11y 与细节打磨(1-2 天)
- [ ] div onClick → button + aria
- [ ] aria-label 覆盖所有图标按钮
- [ ] focus-visible 样式
- [ ] 表单 beforeunload 防离开
- [ ] 虚拟列表(如员工数 > 100)
---
## 五、目标用户画像参考
```
中小微企业 HR/财务(1-2 人兼管)
├── 年龄:28-45 岁,非技术背景
├── 设备:笔记本为主(1366×768 或 1920×1080),偶尔手机查看
├── 使用频率:每月集中使用 2-3 次(发薪/社保/合同续签)
├── 痛点:不懂法规、怕出错、需要快速完成
├── 期望:操作简单 → 结果准确 → 有提醒不遗漏
└── 设计原则:
├── 信息层级清晰(一眼看到"该做什么")
├── 操作有反馈(每步都确认成功/失败)
├── 容错性强(危险操作二次确认,可撤销)
└── 移动端可用(出差/居家时手机审批)
```