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

39 KiB
Raw Permalink Blame History

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

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 主色调暖

// 当前:冷蓝 #2563EB
// 建议:indigo-600 #4F46E5,略带温度,专业且亲和
colors: {
  primary: { DEFAULT: '#4F46E5', light: '#6366F1', dark: '#4338CA' },
}

3.2.4 内容最大宽度

// 当前:max-w-content = none(无限拉伸)
// 建议:限制 1280px,超宽屏居中
maxWidth: { content: '1280px' }

3.2.5 数据可视化

  • Dashboard 概览增加 迷你折线图(月度薪税趋势)
  • 风险分布增加 环形图(高/中/低风险占比)
  • 引入 recharts(轻量,~40KB gzip

3.3 交互体验增强(优先级:高)

3.3.1 全局 Toast

// 引入 sonner~5KB gzip
import { toast } from 'sonner'

// 成功/失败/警告 统一调用
toast.success('员工已添加')
toast.error('删除失败,请重试')
toast.warning('该操作不可撤销')

3.3.2 批量操作二次确认

// 危险操作(批量解聘/删除)必须 Dialog 二次确认
<Dialog open={confirmOpen}>
  确认对 {selectedIds.size} 名员工执行解聘操作?
  此操作不可撤销。
</Dialog>

3.3.3 骨架屏

// 替换"加载中..."文字
function TableSkeleton() {
  return Array.from({ length: 5 }).map(i => (
    <div className="h-10 bg-gray-100 rounded animate-pulse mb-2" />
  ))
}

3.3.4 搜索防抖

// 使用 useDeferredValue 或 lodash.debounce
const debouncedSearch = useDeferredValue(search)
// queryKey 使用 debouncedSearch

3.3.5 表单防离开

// 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 移动端全屏

// 移动端 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-400text-gray-500(对比度 ≥ 4.5:1
  • focus-visiblefocus-visible:ring-2 focus-visible:ring-primary

3.6 性能优化(优先级:高)

3.6.1 路由懒加载

// 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 虚拟列表

// 花名册员工数 > 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-lgh2 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-4gap-2gap-3
src/pages/Money.tsx space-y-3 space-y-4
src/pages/Roster.tsx 列表行 py-1.5py-2.5 表格行
src/pages/SocialInsurance.tsx space-y-3space-y-4
src/pages/Settings.tsx space-y-3space-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()
src/pages/Money.tsx 8 处 alert()toast.success() / toast.error()3 处 confirm() → ConfirmDialog
src/pages/Roster.tsx 4 处 alert()toast.error()1 处 confirm() → ConfirmDialog
src/pages/SocialInsurance.tsx 7 处 alert()toast.success() / toast.error()
src/pages/Settings.tsx 6 处 alert()toast
src/pages/portal/ContractConfirm.tsx 1 处 alert()toast

代码示例

// 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

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

// 当前 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顾问' },
]

右侧操作区修改

// 当前:仅用户下拉菜单
// 目标:通知铃铛(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

// 当前 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 修改

// 移除 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 修改

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

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

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(及任何有搜索的页面)

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

// 当前
primary: { DEFAULT: '#2563EB', light: '#3B82F6', dark: '#1D4ED8' }
// 目标(indigo-600,略带紫调,专业且亲和)
primary: { DEFAULT: '#4F46E5', light: '#6366F1', dark: '#4338CA' }

影响范围:所有使用 text-primarybg-primaryborder-primary 的组件自动生效,无需逐文件修改。

验收标准:主色从冷蓝变为 indigo,视觉感受更温暖,与 Tailwind indigo-600 色卡一致。

P4.2 Dashboard 数据可视化

安装npm install recharts

修改文件src/pages/Dashboard.tsx

在概览 tab 的统计卡片下方增加:

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

// 添加 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

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

// 当前(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

@layer base {
  *:focus-visible {
    @apply outline-none ring-2 ring-primary ring-offset-1;
  }
}

P5.4 表单防离开

新增 Hooksrc/hooks/useUnsavedChanges.ts

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 向导等)

const form = useForm({ mode: 'onChange' })
useUnsavedChanges(form.formState.isDirty)

P5.5 虚拟列表

安装npm install @tanstack/react-virtual

修改文件src/pages/Roster.tsx(当员工数 > 100 时)

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 次(发薪/社保/合同续签)
├── 痛点:不懂法规、怕出错、需要快速完成
├── 期望:操作简单 → 结果准确 → 有提醒不遗漏
└── 设计原则:
    ├── 信息层级清晰(一眼看到"该做什么")
    ├── 操作有反馈(每步都确认成功/失败)
    ├── 容错性强(危险操作二次确认,可撤销)
    └── 移动端可用(出差/居家时手机审批)