feat: 完成23项系统优化 - 花名册社保状态列/身份证复制/附件类型扩展, 用工办理姓名检索/直接提交/文书查看/批量证明, 风险中心跳转筛选+批量处理, 日历7/15/35天分组+逾期统计, 考勤单条编辑+按人导出, 工资条查看状态+工资流水导出, 辞职申请附件上传, 交接清单PDF下载, 操作完成下一步引导, 休假审批入口, 人效成本分部门

This commit is contained in:
freedakgmail
2026-08-04 22:55:03 +08:00
parent 338af5eee9
commit 5604d02de9
41 changed files with 2104 additions and 482 deletions
+106 -42
View File
@@ -1,80 +1,144 @@
import { useState, useEffect } from 'react'
import { X, ArrowRight } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { X, ArrowRight, Home, Users, Calculator, CalendarCheck, ShieldAlert, Bot } from 'lucide-react'
const STORAGE_KEY = 'hr-onboarding-completed'
const STORAGE_KEY = 'hr-onboarding-dismissed'
const steps = [
const modules = [
{
icon: '🏠',
title: '这里看风险',
description: '首页展示企业用工风险总览,红色代表高风险项,点击「去处理」直接跳转操作。',
icon: Home,
color: 'text-blue-600',
bg: 'bg-blue-50',
title: '工作台',
desc: '风险总览、待办事项、日历事件',
path: '/',
},
{
icon: '',
title: '这里管花名册',
description: '花名册页面管理员工档案、劳动合同、附件,以及违纪、考勤、培训、绩效记录,可生成仲裁证据链。',
icon: Users,
color: 'text-indigo-600',
bg: 'bg-indigo-50',
title: '团队管理',
desc: '花名册、用工办理、离职管理、特殊状态',
path: '/roster',
},
{
icon: '💰',
title: '这里算薪税',
description: '薪税页面提供加班费、双倍工资、社保公积金计算器和工资条管理,输入参数实时计算。',
icon: Calculator,
color: 'text-amber-600',
bg: 'bg-amber-50',
title: '薪酬管理',
desc: '发薪批次、工资条、社保公积金、薪酬分析',
path: '/money',
},
{
icon: CalendarCheck,
color: 'text-green-600',
bg: 'bg-green-50',
title: '考勤时间',
desc: '考勤打卡、排班管理、休假审批',
path: '/attendance',
},
{
icon: ShieldAlert,
color: 'text-red-600',
bg: 'bg-red-50',
title: '合规风控',
desc: '风险中心、证据链、规章制度、用工体检',
path: '/risk-center',
},
{
icon: Bot,
color: 'text-purple-600',
bg: 'bg-purple-50',
title: 'AI 助手',
desc: '智能咨询、合同审查、判赔预测、人力分析',
path: '/ai-assistant',
},
]
export function isOnboardingDismissed() {
return localStorage.getItem(STORAGE_KEY) === '1'
}
export function dismissOnboarding() {
localStorage.setItem(STORAGE_KEY, '1')
}
export function resetOnboarding() {
localStorage.removeItem(STORAGE_KEY)
}
export default function OnboardingGuide() {
const [visible, setVisible] = useState(false)
const [step, setStep] = useState(0)
const [dontShow, setDontShow] = useState(false)
const navigate = useNavigate()
useEffect(() => {
const completed = localStorage.getItem(STORAGE_KEY)
if (!completed) {
if (!isOnboardingDismissed()) {
setVisible(true)
}
}, [])
const close = () => {
localStorage.setItem(STORAGE_KEY, '1')
if (dontShow) dismissOnboarding()
setVisible(false)
}
const goTo = (path: string) => {
if (dontShow) dismissOnboarding()
setVisible(false)
navigate(path)
}
if (!visible) return null
const current = steps[step]
const isLast = step === steps.length - 1
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-xl shadow-xl max-w-sm w-full mx-4 overflow-hidden">
<div className="flex justify-end p-2">
<div className="bg-white rounded-xl shadow-xl max-w-lg w-full mx-4 overflow-hidden">
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-100">
<h2 className="text-base font-semibold">使</h2>
<button onClick={close} className="text-gray-400 hover:text-gray-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="px-6 pb-6">
<div className="text-5xl text-center mb-4">{current.icon}</div>
<h2 className="text-lg font-semibold text-center mb-2">{current.title}</h2>
<p className="text-sm text-gray-600 text-center mb-6">{current.description}</p>
{/* 进度指示器 */}
<div className="flex justify-center gap-1.5 mb-6">
{steps.map((_, i) => (
<div
key={i}
className={`h-1.5 rounded-full transition-all ${i === step ? 'w-6 bg-primary' : 'w-1.5 bg-gray-300'}`}
/>
))}
<div className="px-5 py-4">
<p className="text-sm text-gray-500 mb-4"> 6 </p>
<div className="grid grid-cols-2 gap-3">
{modules.map((m) => {
const Icon = m.icon
return (
<button
key={m.title}
onClick={() => goTo(m.path)}
className="flex items-start gap-3 p-3 rounded-lg border border-gray-100 hover:border-primary/30 hover:bg-primary/[0.02] transition-all text-left"
>
<div className={`flex items-center justify-center w-9 h-9 rounded-lg ${m.bg} ${m.color} shrink-0`}>
<Icon className="w-4.5 h-4.5" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-800">{m.title}</div>
<div className="text-xs text-gray-500 mt-0.5 leading-relaxed">{m.desc}</div>
</div>
</button>
)
})}
</div>
<div className="flex justify-between">
{step > 0 ? (
<button onClick={() => setStep(step - 1)} className="text-sm text-gray-500"></button>
) : <span />}
<div className="mt-4 flex items-center justify-between">
<label className="flex items-center gap-2 text-sm text-gray-500 cursor-pointer">
<input
type="checkbox"
checked={dontShow}
onChange={(e) => setDontShow(e.target.checked)}
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary/10"
/>
</label>
<button
onClick={() => isLast ? close() : setStep(step + 1)}
className="flex items-center gap-1 text-sm font-medium text-primary"
onClick={close}
className="flex items-center gap-1 px-4 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors"
>
{isLast ? '开始使用' : '下一步'}
{!isLast && <ArrowRight className="w-4 h-4" />}
使
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>