# AIHR 前端 UI/UX 梳理与优化方向

> 针对中小微企业人事/财务人员的使用场景，对现有前端进行全面梳理并提出优化建议。
>
> 日期：2026-07-24

---

## 〇、竞品参考标杆

### 0.1 选型逻辑

AIHR 定位为"中小微企业用工合规助手"，核心用户是 1-2 人的 HR/财务兼管人员。选参考标杆时，排除北森（太重、实施 3-6 个月）、Rippling（HR+IT 一体化，偏离核心场景），聚焦以下 4 个标杆产品：

### 0.2 标杆产品矩阵

| 产品 | 定位 | 参考价值 | UI/UX 特点 |
|------|------|----------|------------|
| **BambooHR** | 中端市场 HRIS（50-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 全局 | 过小，阅读疲劳 | 高 |
| **薪税向导** | Gusto（3 步引导） | 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（自定义 token：primary/danger/warning/safe/surface） |
| 路由 | React Router v6 |
| 状态 | Zustand（auth）+ TanStack Query（服务端数据） |
| 表单 | react-hook-form + zod |
| 图标 | lucide-react |
| 其他 | qrcode.react, jsPDF, xlsx |

### 1.2 页面结构

```
管理端（AdminLayout）
├── TopNav（顶部导航 h-14，6 个 tab）
├── PageContainer（max-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  AIAssistant（AI 顾问：问答/预测/审查/案例/知识库 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 variant（primary/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-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 |
| `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 引入 Toast（sonner）

**安装**：`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:1（WCAG 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.tsx（140KB）拆分方案**：

```
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）拆分方案**：

```
src/pages/money/
├── Money.tsx               # 主页面：tab 切换
├── BatchManager.tsx        # 发薪批次
├── TemplateManager.tsx     # 薪酬模版
├── OvertimeCalculator.tsx  # 加班费计算
├── PayslipManager.tsx      # 工资条管理
└── CompensationManager.tsx # 薪酬调整（从 Compensation.tsx 合入）
```

**Termination.tsx（55KB）拆分方案**：

```
src/pages/termination/
├── Termination.tsx         # 主页面：向导容器
├── StepSelectEmployee.tsx  # 步骤1：选择员工
├── StepSelectReason.tsx    # 步骤2：解聘方式
├── StepCompliance.tsx      # 步骤3：合规检查
├── StepSettlement.tsx      # 步骤4：费用结算
└── StepConfirm.tsx         # 步骤5：确认完成
```

**Settings.tsx（46KB）拆分方案**：

```
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 5：a11y 与细节打磨（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 → Roster，Compensation → Money
- [ ] 设置入口可见化（齿轮图标）

### Phase 3：性能与组件化（2-3 天）

- [ ] 路由懒加载 + Suspense
- [ ] Roster / Money / Settings 大文件拆分
- [ ] 骨架屏替换"加载中..."
- [ ] 搜索防抖

### Phase 4：视觉与数据可视化（2-3 天）

- [ ] 主色调 indigo-600
- [ ] Dashboard 增加迷你折线图 + 环形图（recharts）
- [ ] Modal 过渡动画
- [ ] 移动端表格卡片模式

### Phase 5：a11y 与细节打磨（1-2 天）

- [ ] div onClick → button + aria
- [ ] aria-label 覆盖所有图标按钮
- [ ] focus-visible 样式
- [ ] 表单 beforeunload 防离开
- [ ] 虚拟列表（如员工数 > 100）

---

## 五、目标用户画像参考

```
中小微企业 HR/财务（1-2 人兼管）
├── 年龄：28-45 岁，非技术背景
├── 设备：笔记本为主（1366×768 或 1920×1080），偶尔手机查看
├── 使用频率：每月集中使用 2-3 次（发薪/社保/合同续签）
├── 痛点：不懂法规、怕出错、需要快速完成
├── 期望：操作简单 → 结果准确 → 有提醒不遗漏
└── 设计原则：
    ├── 信息层级清晰（一眼看到"该做什么"）
    ├── 操作有反馈（每步都确认成功/失败）
    ├── 容错性强（危险操作二次确认，可撤销）
    └── 移动端可用（出差/居家时手机审批）
```
