Files
TurboHR/frontend/src/components/HelpModal.tsx
T

920 lines
54 KiB
TypeScript
Raw 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.
import { useState, useEffect, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { HelpCircle, Search, ChevronDown, ChevronRight, Sparkles, Bell,
Home, Users, FileText, Calculator, Bot, Calendar,
Settings, Lightbulb, AlertTriangle, CheckCircle, Phone, RotateCcw, ShieldAlert, TrendingDown } from 'lucide-react'
import Modal from './ui/Modal'
import { aiApi } from '../lib/api-services'
import { resetOnboarding } from './OnboardingGuide'
import clsx from 'clsx'
interface HelpCategory {
id: string
title: string
icon: typeof Home
articles: HelpArticle[]
}
interface HelpArticle {
id: string
question: string
answer: string
steps?: string[]
tip?: string
warning?: string
}
const categories: HelpCategory[] = [
{
id: 'whats-new',
title: '近期更新',
icon: Sparkles,
articles: [
{
id: 'update-20260811-overview',
question: '2026年8月11日优化:16项问题全面修复',
answer: '本次更新覆盖花名册、离职管理、薪税管理、社保公积金、商业保险、员工福利、规章制度、文本模板、电子签署、绩效考核、违纪记录、特殊员工、证据链等全部模块,共修复16项问题(P0紧急5项 + P1重要8项 + P2优化2项 + 待确认1项)。\n以下为各模块主要改进:',
},
{
id: 'update-perf-training-nav',
question: '修复:绩效考核/培训记录点击员工姓名可跳转员工详情',
answer: '绩效考核和培训记录列表中员工姓名现已支持点击跳转:\n• 点击员工姓名直接跳转到员工档案详情页\n• 跳转时携带 employeeId 参数,正确定位到对应员工\n• 员工姓名显示为链接样式,鼠标悬停有下划线提示',
tip: '路径:团队 → 绩效考核/培训记录 → 点击员工姓名',
},
{
id: 'update-resignation-cert',
question: '优化:离职证明模板支持自定义+员工端下载',
answer: '离职证明功能全面升级:\n• 离职证明模板支持自定义编辑(复用文本模板模块,支持变量占位符)\n• 管理端离职管理中可选择/编辑离职证明模板\n• 员工端已完成离职的员工可在线查看和下载离职证明(含电子印章)',
tip: '路径:离职管理 → 已完成解聘 → 下载离职证明 / 员工端 → 我的证明',
},
{
id: 'update-payroll-input',
question: '修复(P0):薪税管理工资填写后数据不再归零',
answer: '修复工资条编辑表单输入数据后自动归零的问题:\n• 修复 onChange 和 onBlur 逻辑,手动输入的值不再被自动计算覆盖\n• 确保基本工资、津贴等手动输入字段正确保存到状态\n• 自动计算项目(如个税、社保扣款)在手动输入完成后重新计算',
tip: '路径:薪税管理 → 选择员工 → 工资填写页面',
},
{
id: 'update-social-enrollment',
question: '新增:社保公积金模块员工参保信息列表',
answer: '社保公积金模块新增「员工参保」标签页:\n• 列表显示:员工姓名、部门、参保城市、各险种参保状态(已参保/未参保/停缴)\n• 显示缴费基数(养老/医疗/失业/工伤/生育)和公积金基数\n• 支持按参保状态、城市筛选',
tip: '路径:社保公积金 → 员工参保 Tab',
},
{
id: 'update-commercial-insurance',
question: '修复(P0):商业保险方案支持为员工参保',
answer: '商业保险模块新增为员工参保功能:\n• 保险方案详情中增加「为员工参保」按钮\n• 支持批量选择员工参保\n• 参保后显示参保人员列表',
tip: '路径:福利保障 → 商业保险 → 点击方案 → 为员工参保',
},
{
id: 'update-benefit-enroll-20260811',
question: '修复(P0):员工福利方案支持为员工添加福利',
answer: '员工福利模块新增为员工添加福利功能:\n• 福利方案详情中增加「为员工添加福利」按钮\n• 支持批量选择员工\n• 添加后显示享受人员列表和月度合计',
tip: '路径:福利保障 → 员工福利 → 点击方案 → 为员工添加福利',
},
{
id: 'update-policy-word-import',
question: '新增:规章制度支持导入 Word 文档',
answer: '规章制度编辑器新增导入 Word 文档功能:\n• 支持 .docx 格式 Word 文档导入\n• 使用 mammoth 库自动将 Word 内容转为 HTML\n• 导入后可在编辑器中继续修改\n• 保留原有格式(标题、段落、列表等)',
tip: '路径:规章制度 → 新建 → 导入 Word 文档按钮',
},
{
id: 'update-template-format',
question: '修复:文本模板下载 Word 文档保留完整格式',
answer: '文本模板下载的 Word 文档现已保留完整格式:\n• 使用完整的 Word HTML 格式(含 style 定义)\n• 保留标题、段落间距、字体大小、对齐方式等格式\n• 下载文件名使用模板名称,不再使用随机文件名',
tip: '路径:文本模板 → 下载 Word',
},
{
id: 'update-template-variables',
question: '修复:模板下载时变量占位符正确替换为实际数据',
answer: '模板下载时自动替换变量占位符:\n• 下载 Word 文档时自动替换模板中的 {{变量}} 占位符\n• 从员工详情页发起下载时自动填充该员工信息\n• 修复了 token 参数被误当作模板变量的问题',
tip: '路径:文本模板 → 填写变量 → 下载 / 员工详情 → 下载模板',
},
{
id: 'update-esign-employees',
question: '修复(P0):电子签署发起时员工下拉框正常显示',
answer: '修复电子签署发起签署时员工下拉框为空的问题:\n• 发起签署弹窗中员工下拉框正确加载在职员工列表\n• 显示员工姓名和部门信息\n• 修复员工列表数据获取逻辑',
tip: '路径:电子签署 → 发起签署 → 选择员工',
},
{
id: 'update-performance-reviewer',
question: '优化:新增绩效记录支持填写考评人',
answer: '绩效考核新增记录表单增加考评人字段:\n• 新增「考评人」必填输入框\n• 考评人信息保存到 reviewer 字段\n• 列表中显示考评人信息',
tip: '路径:团队 → 绩效考核 → 新增 → 考评人',
},
{
id: 'update-disciplinary-detail',
question: '优化:违纪记录新增处罚执行细节',
answer: '违纪记录新增处罚执行管理功能:\n• 违纪记录新增「执行细节」字段,支持填写处罚方式、处罚日期等\n• 违纪记录列表增加「执行细节」列\n• 表单中增加 textarea 用于详细记录处罚执行情况',
tip: '路径:团队 → 违纪记录 → 新增 → 执行细节',
},
{
id: 'update-special-status-detail',
question: '修复:特殊员工列表支持查看员工详情',
answer: '特殊员工列表新增查看详情功能:\n• 每条记录右上角新增「查看」按钮(眼睛图标)\n• 点击员工姓名也可跳转到员工档案详情页\n• 跳转时携带 employeeId 参数,正确定位到对应员工',
tip: '路径:团队 → 特殊员工 → 点击查看按钮或员工姓名',
},
{
id: 'update-benefit-summary',
question: '修复:员工福利汇总正确显示参保人员和月度合计',
answer: '员工福利汇总数据修复:\n• 新增福利方案并添加员工后,员工汇总正确显示\n• 月度合计金额正确计算所有有效福利方案\n• 汇总Tab显示参保人员姓名、部门、福利项和月度合计',
tip: '路径:福利保障 → 员工福利 → 员工汇总 Tab',
},
{
id: 'update-evidence-hash',
question: '修复(P0):证据链验证完整性全部通过',
answer: '证据链哈希验证逻辑修复:\n• 修复哈希计算中 key 排序问题,改为递归排序所有层级的 key\n• 验证时自动修复因 PostgreSQL JSONB key 重排或哈希算法升级导致的不一致\n• 历史数据验证全部通过,不再出现误报篡改',
tip: '路径:合规 → 证据链条 → 验证全部完整性',
},
{
id: 'update-2026-batch-overview',
question: '2026年8月批量优化:28项问题一次性修复',
answer: '本次更新覆盖员工福利、离职管理、考勤管理、证据链、规章制度、文本模板、花名册、用工办理、证明开具、培训记录、绩效考核等全部模块,共修复28项问题(P0紧急6项 + P1重要16项 + P2优化6项)。\n以下为各模块主要改进:',
},
{
id: 'update-benefit-enroll',
question: '修复:福利方案创建后无法添加享受人员',
answer: '修复福利方案批量参保功能:\n• 商业保险和员工福利模块均支持批量参保\n• 可选择多名员工一次性加入福利方案\n• 参保时自动记录生效日期和缴费金额',
tip: '路径:员工福利 → 福利方案 → 批量参保',
},
{
id: 'update-termination-cert',
question: '修复:离职证明下载内容为乱码',
answer: '修复离职证明下载后打开为乱码的问题:\n• 使用 Word HTML 格式生成 .doc 文件,设置 charset=utf-8\n• 指定 SimSun(宋体)字体,确保中文正常显示\n• 离职证明内容包含:员工姓名、证件号码、入职日期、离职日期、企业名称等',
tip: '路径:离职管理 → 已完成解聘 → 下载离职证明',
},
{
id: 'update-attendance-template',
question: '优化:考勤导入模板合并Sheet,减少重复录入',
answer: '考勤月度导入模板从多个Sheet合并为单Sheet:\n• 考勤记录和加班记录合并为「考勤与加班」一个Sheet\n• 不再需要分别填写考勤Sheet和加班Sheet\n• 导入时自动识别合并Sheet或独立Sheet,兼容旧模板\n• 减少重复录入姓名和证件号码',
tip: '路径:考勤管理 → 考勤确认 → 导入考勤 → 下载模板',
},
{
id: 'update-attendance-makeup',
question: '修复:补卡无法修改未打卡状态',
answer: '修复每日出勤中补卡功能:\n• 补卡弹窗支持修改签到时间、签退时间、考勤状态\n• 可手动修正迟到、早退、缺勤等异常状态\n• 补卡操作记录备注信息,方便后续审计',
tip: '路径:考勤管理 → 每日出勤 → 补卡按钮',
},
{
id: 'update-attachment-view',
question: '新增:合同附件支持在线查看和删除',
answer: '员工档案附件管理新增在线查看和删除功能:\n• 点击眼睛图标可在线预览附件(PDF、图片等)\n• 点击下载图标可下载附件文件\n• 点击删除图标可删除传错的附件\n• 支持身份证、银行卡、学历证书、合同扫描件等多种类型',
tip: '路径:员工档案 → 附件管理',
},
{
id: 'update-page-size',
question: '修复:花名册每页条数选择无反应',
answer: '修复花名册列表切换每页显示条数后不生效的问题:\n• 分页组件改为使用本地 pageSize 状态而非服务端返回值\n• 切换条数后立即重置到第一页并重新加载数据',
tip: '路径:花名册列表底部 → 每页条数下拉框',
},
{
id: 'update-termination-export',
question: '优化:离职管理导出支持筛选条件',
answer: '离职管理导出数据新增筛选条件:\n• 支持按状态、部门、关键词筛选\n• 新增日期范围筛选(开始日期至结束日期)\n• 导出时携带当前筛选条件,只导出符合条件的数据',
tip: '路径:离职管理 → 筛选条件 → 导出按钮',
},
{
id: 'update-termination-cancel',
question: '新增:已提交离职数据可撤回,无用数据可删除',
answer: '离职管理新增撤回和删除功能:\n• 已提交的离职草稿可撤销(非已完成状态均可撤回)\n• 草稿状态和已撤销状态的记录可删除\n• 撤回和删除操作均记录审计日志',
tip: '路径:离职管理 → 草稿列表 → 撤销/删除按钮',
},
{
id: 'update-overtime-calc',
question: '优化:加班费自动计算,无需重复导入',
answer: '考勤导入时自动计算加班费,薪税模块直接读取:\n• 导入考勤数据时根据加班倍率配置自动计算加班费\n• 月度考勤报表自动汇总加班时长和加班费\n• 薪税管理发放工资时自动读取已计算的加班费\n• 不再需要在薪税模块重复导入加班数据',
tip: '路径:考勤管理 → 导入考勤(自动计算加班费)→ 薪税管理(自动读取)',
},
{
id: 'update-overtime-summary',
question: '修复:个人考勤记录添加后加班汇总不显示',
answer: '修复添加个人考勤记录后月度报表加班汇总不更新的问题:\n• 月度报表从 overtimeRecord 表读取加班汇总数据\n• 个人考勤记录中的加班时长自动累计到月度汇总',
tip: '路径:考勤管理 → 月度报表',
},
{
id: 'update-evidence-verify',
question: '优化:证据链验证显示异常项详情',
answer: '证据链「验证全部完整性」功能增强:\n• 验证结果新增异常项详情列表\n• 每条异常项显示:证据类型、关联ID、描述、创建时间\n• 方便快速定位被篡改的证据链记录',
tip: '路径:证据链管理 → 验证全部完整性',
},
{
id: 'update-policy-remind',
question: '新增:规章制度签收催办和未签收人员查看',
answer: '规章制度签收管理新增催办和未签收人员列表:\n• 签收统计中显示已签收和未签收人数\n• 可展开查看未签收人员明细列表\n• 一键催办功能:向所有未签收员工发送系统内通知提醒\n• 催办通知记录在通知管理中可查看',
tip: '路径:规章制度 → 点击制度 → 阅读统计 → 一键催办',
},
{
id: 'update-template-import',
question: '新增:模板支持导入 Word 文档',
answer: '文本模板编辑新增导入 Word 文档功能:\n• 支持 .docx 格式 Word 文档导入\n• 使用 mammoth 库自动将 Word 内容转为 HTML\n• 导入后可在编辑器中继续修改变量占位符\n• 保留原有格式(标题、段落、列表等)',
tip: '路径:文本模板 → 新建/编辑模板 → 导入 Word 文档按钮',
},
{
id: 'update-social-cap',
question: '修复:选择参保地后社保基数自动封上下限',
answer: '花名册添加员工时选择参保城市后自动封顶/保底社保基数:\n• 选择参保城市后自动调用社保计算接口\n• 社保基数超过上限自动封顶,低于下限自动保底\n• 公积金基数同样自动封顶/保底\n• 显示封顶/保底提示信息',
tip: '路径:花名册 → 添加员工 → 选择参保城市',
},
{
id: 'update-validation-detail',
question: '优化:录入校验失败显示具体字段和错误原因',
answer: '员工信息录入校验失败时显示字段级错误信息:\n• 后端 Zod 校验返回具体字段名和错误原因\n• 前端解析错误详情,逐条列出校验失败的字段\n• 添加员工弹窗和编辑表单均支持详细错误提示',
tip: '路径:花名册 → 添加/编辑员工 → 校验失败时显示',
},
{
id: 'update-position-field',
question: '修复:花名册录入时新增职务/岗位字段',
answer: '添加员工表单新增「职务/岗位」输入框:\n• 在姓名和部门旁边新增职务字段\n• 录入时可直接填写岗位信息\n• 与花名册列表中的职务列对应',
tip: '路径:花名册 → 添加员工 → 职务/岗位',
},
{
id: 'update-social-detail',
question: '新增:花名册员工详情显示社保费用分险种明细',
answer: '员工详情薪税信息中新增社保费用分险种明细展示:\n• 按养老、医疗、失业、工伤、生育分别显示企业和个人缴费金额及比例\n• 显示社保基数是否已封顶/保底\n• 医保基数与养老基数不同时单独提示\n• 与社保模块计算结果保持一致',
tip: '路径:员工档案 → 薪税信息 → 社保费用明细',
},
{
id: 'update-modal-noclose',
question: '优化:录入弹窗防止误关闭丢失已填信息',
answer: '添加员工等录入弹窗防止误操作关闭:\n• Modal 组件新增 closeOnOverlayClick 属性\n• 添加员工弹窗设置为点击遮罩层不关闭\n• 防止误点击弹窗外部导致已填信息丢失',
tip: '路径:花名册 → 添加员工弹窗',
},
{
id: 'update-idcard-dedup',
question: '新增:用工办理按证件号码查重',
answer: '创建员工时自动按证件号码查重:\n• 后端创建员工前先检查证件号码是否已存在\n• 如果已存在,返回已有员工姓名、部门、在职状态\n• 前端显示明确的重复提示信息,避免重复录入',
tip: '路径:用工办理 → 入职办理 / 花名册 → 添加员工',
},
{
id: 'update-income-cert-select',
question: '优化:收入证明支持员工下拉选择并自动填充',
answer: '用工办理中收入证明和离职证明支持员工下拉选择:\n• 员工字段从手动输入改为下拉搜索选择\n• 选择员工后自动填充:姓名、证件号码、职务、月收入、入职日期、部门、手机号\n• 减少手动输入,避免信息不一致',
tip: '路径:用工办理 → 收入证明/离职证明 → 选择员工',
},
{
id: 'update-training-batch',
question: '新增:培训记录支持批量选择员工',
answer: '培训记录新增批量选择模式:\n• 支持切换单选/批量模式\n• 批量模式下可搜索姓名/部门并勾选多名员工\n• 一次为多名员工添加相同培训记录\n• 显示已选择员工数量',
tip: '路径:团队 → 培训记录 → 新增 → 切换为批量',
},
{
id: 'update-workprocess-dedup',
question: '优化:用工办理中离职/解聘流程统一归入离职管理',
answer: '用工办理模块与离职管理模块功能去重:\n• 用工办理中保留入职、转正、调岗、合同相关流程\n• 离职、解聘相关流程统一在「离职管理」模块处理\n• 清理用工办理中已废弃的流程类型定义',
tip: '路径:用工办理(入职类流程)→ 离职管理(离职类流程)',
},
{
id: 'update-social-input',
question: '优化:社保基数输入框支持直接覆盖',
answer: '社保和公积金基数输入框优化:\n• 自动填充的默认值改为 placeholder 显示,不再回填\n• 点击输入框时自动全选当前值,方便直接覆盖\n• 清空输入框后不再回退到月工资默认值\n• 添加员工和重新雇佣弹窗均已优化',
tip: '路径:花名册 → 添加员工 → 社保/公积金基数输入框',
},
{
id: 'update-payslip-entry',
question: '优化:花名册员工详情薪税入口改名为「查看薪资历史」',
answer: '员工详情中薪税模块入口按钮优化:\n• 按钮名称从「薪税模块」改为「查看薪资历史」,语义更明确\n• 跳转时携带员工ID和tab参数,直接定位到该员工的工资条\n• 与员工个人薪资关联,不再跳转到薪税批次列表',
tip: '路径:员工档案 → 薪酬社保 → 查看薪资历史',
},
{
id: 'update-roster-guide',
question: '优化:花名册添加员工后引导前往用工办理',
answer: '花名册添加员工成功后增加引导提示:\n• 添加成功后 toast 提示「员工已添加」\n• 提供「前往用工办理」快捷操作按钮\n• 引导用户使用用工办理完成完整入职流程(合同签署等)\n• 花名册保留快速添加入口,用工办理提供完整流程',
tip: '路径:花名册 → 添加员工 → 成功提示 → 前往用工办理',
},
{
id: 'update-performance-template',
question: '新增:绩效考核支持自定义模板和维度评分',
answer: '绩效考核模块新增绩效模板管理:\n• 支持定义自定义考核维度(如工作能力、态度、业绩等)\n• 每个维度可设置权重和满分分值\n• 考核时按模板填写各维度得分,系统按权重自动计算总分\n• 保留简单评分模式作为默认,自定义模板作为高级功能\n• 绩效模板支持增删改查,可设置默认模板',
tip: '路径:员工档案 → 绩效考核 → 新增 → 选择绩效模板',
},
],
},
{
id: 'home',
title: '首页',
icon: Home,
articles: [
{
id: 'system-intro',
question: '本系统能帮企业做什么?',
answer: '「安职通」是一站式人力资源管理平台,覆盖员工全生命周期管理,帮助企业高效管理人事业务的同时确保合规运营:\n• 员工管理:入职登记、合同签订、转正调岗、离职解聘\n• 薪税管理:工资计算、个税申报、社保公积金缴纳\n• 考勤管理:排班打卡、加班统计、休假记录、月度报表\n• 合同管理:电子合同、到期提醒、续签流程\n• 风险管控:自动扫描法律风险、合规预警、判赔预测\n• AI 助手:劳动法咨询、智能问答、文档生成',
},
{
id: 'compliance',
question: '系统如何保障用工合规?',
answer: '系统从以下维度帮助企业实现合规管理:\n• 合同合规:自动提醒合同到期续签,检测未签合同风险(入职1个月内未签合同需支付双倍工资)\n• 薪酬合规:自动计算个税、社保扣款,确保发薪准确无误\n• 考勤合规:记录加班时长,预警超时加班风险,留存考勤证据\n• 解聘合规:自动计算经济补偿金,生成规范解聘协议,降低劳动争议风险\n• 社保合规:跟踪社保缴纳情况,提醒漏缴断缴\n• 风险预警:统一风险中心实时扫描所有数据,按高/中/低分级预警',
tip: '建议每周查看风险中心,每月核对薪税和考勤数据,确保合规无遗漏。',
},
{
id: 'workflow',
question: '日常人事工作流程是怎样的?',
answer: '系统覆盖企业日常人事管理的完整流程:',
steps: [
'入职:添加员工信息 → 签订合同 → 设置社保 → 安排排班',
'日常:考勤打卡 → 加班审批 → 休假管理 → 补卡修正',
'月度:导入考勤 → 确认考勤 → 计算工资 → 发放工资条 → 缴纳社保公积金 → 个税申报',
'合同:到期提醒 → 续签合同 → 合同确认',
'离职:发起解聘 → 计算补偿金 → 生成协议 → 完成离职',
],
},
{
id: 'value',
question: '使用系统能带来什么价值?',
answer: '• 提效:自动化算薪、考勤统计、合同管理,减少 80% 人工操作\n• 降险:法律风险自动检测预警,避免因疏忽导致的劳动纠纷和罚款\n• 省心:到期提醒、月度任务提醒,不再遗漏关键时间节点\n• 透明:员工可通过手机端查看工资条、合同、考勤记录,信息透明\n• 合规:所有操作留存记录,满足劳动法合规要求,应对审计无忧',
},
],
},
{
id: 'start',
title: '快速入门',
icon: Lightbulb,
articles: [
{
id: 'what-is',
question: '「安职通」是什么?',
answer: '这是一个帮您管理员工、合同、工资和社保的工具。您可以把它理解为一个「人事小助手」,帮您把繁琐的人事工作变得简单。比如:记录员工信息、提醒合同到期、计算工资社保、生成法律文档等。',
},
{
id: 'first-steps',
question: '我第一次使用,该从哪里开始?',
answer: '建议按以下顺序操作,就像填表格一样简单:',
steps: [
'第一步:点击左侧菜单「员工管理」,添加您的员工信息(姓名、手机号、入职日期等)',
'第二步:在员工详情里填写合同信息(合同类型、签订日期、到期日期等)',
'第三步:在「社保管理」中设置您公司的社保缴纳基数和比例',
'第四步:在「薪税管理」中创建发薪批次,系统会自动帮您计算工资',
'第五步:有不懂的随时点这个「?」图标查看帮助',
],
tip: '不用担心填错,所有信息都可以随时修改。',
},
{
id: 'mobile',
question: '我可以在手机上使用吗?',
answer: '可以!用手机浏览器打开本网站即可。手机版会自动显示底部导航栏,方便您随时查看。建议添加到手机桌面,像 App 一样使用。',
steps: [
'苹果手机:Safari 打开 → 点击底部分享按钮 → 「添加到主屏幕」',
'安卓手机:Chrome 打开 → 点击右上角菜单 → 「添加到主屏幕」',
],
},
],
},
{
id: 'employee',
title: '员工管理',
icon: Users,
articles: [
{
id: 'add-employee',
question: '怎么添加新员工?',
answer: '就像填一张入职登记表:',
steps: [
'点击左侧菜单「员工管理」',
'点击右上角「添加员工」按钮',
'填写员工姓名、手机号、入职日期等基本信息',
'点击「保存」即可',
],
tip: '带 * 号的是必填项,其他可以以后再补。',
},
{
id: 'edit-employee',
question: '怎么修改员工信息?',
answer: '在员工列表中,点击员工姓名即可进入详情页,然后点击右上角「编辑」按钮修改信息。',
},
{
id: 'employee-status',
question: '员工离职了怎么处理?',
answer: '请到「解聘管理」页面处理离职流程,系统会自动帮您计算经济补偿金、生成解聘协议书等法律文件。不要直接删除员工记录,保留记录有助于日后查证。',
warning: '直接删除员工会导致该员工的所有历史记录丢失,包括合同、工资单等。',
},
{
id: 'training-records',
question: '培训记录怎么管理?',
answer: '在左侧菜单「团队」分组下点击「培训记录」进入管理页面:\n• 点击「新增」按钮选择员工,填写培训主题、日期、讲师、时长等信息\n• 保存后记录状态为「待签收」,员工可在员工端「我的记录」中签收或拒绝\n• 列表显示签收状态(待签收/已签收/拒绝签收),支持按员工姓名搜索',
tip: '开启「电子签署设置 → 培训记录电子签」后,员工签收时需走电子签署流程,签收记录自动进入证据链。',
},
{
id: 'performance-records',
question: '绩效考核怎么录入和管理?',
answer: '在左侧菜单「团队」分组下点击「绩效考核」进入管理页面:\n• 点击「新增」选择员工,填写考核周期、得分、等级、结果、评语等\n• 保存后员工可在员工端查看并签字确认\n• 列表显示签字状态(待签字/已签字),支持按员工姓名搜索',
tip: '开启「电子签署设置 → 绩效考核电子签」后,员工签字时需走电子签署流程。',
},
{
id: 'disciplinary-records',
question: '违纪记录怎么管理?',
answer: '在左侧菜单「团队」分组下点击「违纪记录」进入管理页面:\n• 点击「新增」选择员工,填写违纪日期、类型、描述、严重程度、处理方式等\n• 可填写见证人信息,保存后员工可在员工端查看并签字确认\n• 列表显示签字状态(待签字/已签字),支持按员工姓名搜索',
warning: '违纪记录是劳动仲裁重要证据,建议如实记录并确保员工签字确认。开启电子签后签字记录自动进入证据链。',
},
],
},
{
id: 'contract',
title: '合同管理',
icon: FileText,
articles: [
{
id: 'contract-types',
question: '合同类型有哪些?我该选哪个?',
answer: '常见的合同类型:\n• 固定期限合同:有明确到期日,比如签 3 年\n• 无固定期限合同:没有到期日,长期雇佣\n• 完成任务合同:以完成某项工作为期限\n• 未签合同:还没签合同的情况',
warning: '员工入职 1 个月内必须签订书面合同,否则企业需要支付双倍工资。',
},
{
id: 'contract-expiry',
question: '合同快到期了,系统会提醒我吗?',
answer: '会的!系统会自动检测即将到期的合同,并在顶部通知铃铛处显示提醒数字。您也可以在「通知管理」页面查看所有提醒。默认提前 30 天提醒,您可以在通知设置中修改天数。',
},
{
id: 'contract-confirm',
question: '什么是合同确认?',
answer: '合同确认是指员工通过手机查看并确认自己的劳动合同内容。系统会生成一个链接,员工用手机打开即可查看合同详情并确认签字。您可以在员工详情的「合同信息」标签页中发起确认。',
},
],
},
{
id: 'salary',
title: '工资和社保',
icon: Calculator,
articles: [
{
id: 'create-payroll',
question: '怎么发工资?',
answer: '系统帮您自动化算薪:',
steps: [
'点击左侧菜单「薪税管理」',
'点击「创建发薪批次」',
'选择月份和批次类型(正常工资/补发等)',
'系统自动从员工信息中拉取数据并计算',
'确认无误后点击「发放」',
],
tip: '系统会自动计算个税、社保扣款等,您只需要核对数字即可。',
},
{
id: 'social-insurance',
question: '社保基数和比例怎么设置?',
answer: '在「社保管理」页面,您可以设置公司所在地的社保缴纳基数上下限和各项比例(养老、医疗、失业、工伤、生育)。这些数据通常每年会调整一次,请关注当地社保局通知。',
warning: '社保基数和比例各地不同,请以当地社保局公布的最新标准为准。',
},
{
id: 'payslip',
question: '员工怎么查看工资条?',
answer: '系统提供员工端(手机访问),员工可以通过手机查看自己的工资条。您在发薪后,系统会自动生成工资条,员工登录员工端即可查看。',
},
{
id: 'overtime',
question: '加班费怎么算?',
answer: '系统会根据您设置的加班倍率自动计算。默认规则:\n• 工作日加班:1.5 倍\n• 休息日加班:2 倍\n• 法定节假日加班:3 倍\n您可以在薪税管理的加班配置中修改倍率。',
},
],
},
{
id: 'attendance',
title: '考勤管理',
icon: Calendar,
articles: [
{
id: 'attendance-overview',
question: '考勤管理有哪些功能?',
answer: '考勤管理包含 6 个子功能:\n• 考勤确认:导入考勤数据后批量确认并发布给员工\n• 班次管理:设置早班、晚班、弹性班等班次规则\n• 排班:按日期为员工分配班次,支持批量排班\n• 每日出勤:查看当日打卡情况,支持补卡修正\n• 月度报表:汇总月度出勤、迟到、加班数据\n• 休假记录:管理员工请假信息',
},
{
id: 'shift-setup',
question: '怎么设置班次?',
answer: '在考勤管理「班次管理」标签页中,点击「新增班次」按钮,设置班次名称、上下班时间、弹性时长和休息时长。每个班次可以设置不同颜色方便区分。',
tip: '常见班次:早班 08:00-17:00、晚班 14:00-23:00、弹性班 09:00-18:00(弹性30分钟)。',
},
{
id: 'schedule',
question: '怎么给员工排班?',
answer: '在考勤管理「排班」标签页中:',
steps: [
'选择日期',
'在员工列表中,未排班的员工行内有班次下拉框',
'选择班次后点击「排班」按钮即可',
'也可以点击「批量排班」按钮,勾选多个员工一次性分配班次',
],
tip: '支持按姓名或部门搜索,按部门筛选快速定位员工。',
},
{
id: 'attendance-import',
question: '怎么导入考勤数据?',
answer: '在考勤管理「考勤确认」标签页中,点击「导入考勤」按钮,下载模板填写后上传。系统会自动匹配员工并生成考勤记录。',
tip: '证件号码优先匹配,未填时用姓名匹配。',
},
{
id: 'attendance-correct',
question: '员工漏打卡了怎么办?',
answer: '在「每日出勤」标签页中,找到对应员工,点击「补卡」按钮,手动填写签到/签退时间和状态即可修正记录。',
},
],
},
{
id: 'risk',
title: '风险中心',
icon: ShieldAlert,
articles: [
{
id: 'what-is-risk',
question: '风险中心是什么?',
answer: '统一风险中心会自动扫描您的员工、合同、薪酬、社保等数据,汇总所有潜在风险。包括:合同到期未续签、未签合同、试用期超长、薪酬异常、社保漏缴、退休提醒等。风险分为高、中、低三个等级,建议优先处理高风险项。',
tip: '访问路径:左侧菜单「风险中心」或直接访问 /risk-center。',
},
{
id: 'how-to-fix',
question: '发现风险后怎么处理?',
answer: '在风险中心页面,每个风险项右侧有快捷操作按钮(如「续签」「转正」「处理」),点击即可跳转到对应页面处理。处理完成后风险会自动消除。',
tip: '建议每周查看一次风险中心,及时处理避免法律纠纷。',
},
{
id: 'risk-types',
question: '有哪些类型的风险?',
answer: '系统目前检测以下风险类型:\n• 合同风险:到期未续签、未签合同\n• 薪酬风险:薪资异常波动\n• 解聘风险:可能存在劳动争议\n• 月度任务:发薪、社保、公积金、个税等截止日提醒\n• 入职手续:入职材料不完整\n• 退休提醒:员工即将达到退休年龄',
},
],
},
{
id: 'termination',
title: '解聘管理',
icon: TrendingDown,
articles: [
{
id: 'termination-process',
question: '员工离职怎么处理?',
answer: '在「解聘管理」页面处理离职流程:',
steps: [
'点击「发起解聘」选择员工',
'填写解聘原因、离职日期等信息',
'系统自动计算经济补偿金',
'生成解聘协议书等法律文件',
'确认后完成解聘流程',
],
warning: '不要直接删除员工记录,保留记录有助于日后查证和合规。',
},
{
id: 'compensation',
question: '经济补偿金怎么算?',
answer: '系统根据员工工龄和月均工资自动计算经济补偿金:\n• 每满一年支付一个月工资\n• 六个月以上不满一年按一年算\n• 不满六个月支付半个月工资\n• 月工资按离职前12个月平均工资计算',
tip: '工资高于当地社平工资3倍的,按3倍封顶,最长补偿12年。',
},
],
},
{
id: 'ai',
title: 'AI 助手',
icon: Bot,
articles: [
{
id: 'what-is-ai',
question: 'AI 助手能帮我做什么?',
answer: 'AI 助手可以回答劳动法相关问题,比如:\n• 「员工试用期最多多久?」\n• 「经济补偿金怎么计算?」\n• 「未婚怀孕能休产假吗?」\n• 「加班费怎么算?」\n就像问一个专业律师一样,用大白话问就行。',
},
{
id: 'ai-predict',
question: '判赔预测是什么?',
answer: '如果您要解聘员工,AI 可以根据员工的信息(工资、工龄、合同等)预测可能需要支付的经济补偿金金额,帮您提前做好预算。在「AI 助手」页面选择「判赔预测」模式即可使用。',
warning: 'AI 预测结果仅供参考,实际金额请以律师建议或法院判决为准。',
},
{
id: 'ai-limit',
question: 'AI 回答准确吗?',
answer: 'AI 基于劳动法律法规训练,回答具有较高参考价值。但法律问题往往涉及具体细节,建议将 AI 回答作为参考,重要决策请咨询专业律师。',
},
],
},
{
id: 'settings',
title: '通知和设置',
icon: Settings,
articles: [
{
id: 'notification',
question: '怎么设置提醒?',
answer: '在「设置」页面的「通知设置」标签中可以配置:\n• 合同到期提前提醒天数\n• 未签合同提醒\n• 加班超时提醒\n• 工资条发布通知\n• 月度事务提醒(发薪日、社保日、公积金日、个税日)\n• 企业微信 Webhook 推送\n• 邮件通知\n点击顶部通知铃铛图标可以查看所有未读提醒。',
},
{
id: 'salary-dashboard',
question: '薪酬分析看板有什么用?',
answer: '薪酬分析看板在「薪税管理」页面中,提供:\n• 薪酬概览(员工总数、月均薪酬、中位数、年度总薪酬)\n• 部门薪酬对比(含人均薪酬排名)\n• 月度薪酬趋势(同比环比变化)\n帮助您了解薪酬分布情况,辅助预算决策。',
},
{
id: 'change-password',
question: '怎么修改密码?',
answer: '点击右上角您的用户名 → 「设置」页面中可以修改密码。建议定期更换密码以保护账号安全。',
},
{
id: 'company-info',
question: '怎么修改公司信息?',
answer: '在「设置」页面可以修改公司名称、行业、规模等基本信息。这些信息会影响风险检测的准确性,请如实填写。',
},
],
},
{
id: 'faq',
title: '常见问题',
icon: Phone,
articles: [
{
id: 'data-safe',
question: '我的数据安全吗?',
answer: '您的数据存储在加密的云端服务器上,只有您本人登录后才能查看。我们不会将您的数据分享给任何第三方。所有敏感信息(如证件号码)都经过加密存储。',
},
{
id: 'data-export',
question: '可以导出数据吗?',
answer: '可以。在员工管理页面可以导出员工名单为 Excel 文件。工资批次可以导出为 Excel 方便财务对账。考勤管理支持导出每日出勤和月度报表为 CSV 文件。',
},
{
id: 'multi-user',
question: '可以多人同时使用吗?',
answer: '可以。在「设置」页面可以添加多个 HR 账号,不同账号可以设置不同权限。比如一个管理员、几个普通 HR。',
},
{
id: 'support',
question: '遇到问题怎么办?',
answer: '您可以:\n1. 点击页面右上角的「?」图标查看帮助文档\n2. 在「AI 助手」中直接提问\n3. 联系客服:support@hr8ai.com',
},
],
},
]
interface RAGResult {
title: string
content: string
source: string
category: string
similarity: number
}
export default function HelpModal({ open, onClose }: { open: boolean; onClose: () => void }) {
const navigate = useNavigate()
const [activeCategory, setActiveCategory] = useState(categories[0].id)
const [expandedArticle, setExpandedArticle] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [ragResults, setRagResults] = useState<RAGResult[] | null>(null)
const [ragLoading, setRagLoading] = useState(false)
const [ragError, setRagError] = useState(false)
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
// RAG 语义搜索(防抖 300ms
useEffect(() => {
if (!searchQuery.trim()) {
setRagResults(null)
setRagError(false)
return
}
if (debounceTimer.current) clearTimeout(debounceTimer.current)
debounceTimer.current = setTimeout(async () => {
setRagLoading(true)
setRagError(false)
try {
const res = await aiApi.ragHelpSearch(searchQuery, 8) as any
setRagResults(res?.results || [])
} catch {
setRagError(true)
setRagResults([])
} finally {
setRagLoading(false)
}
}, 300)
return () => { if (debounceTimer.current) clearTimeout(debounceTimer.current) }
}, [searchQuery])
const isSearching = searchQuery.trim().length > 0
const filteredCategories = isSearching
? categories.map(c => ({
...c,
articles: c.articles.filter(
a => a.question.includes(searchQuery) || a.answer.includes(searchQuery)
),
})).filter(c => c.articles.length > 0)
: categories
const currentCategory = filteredCategories.find(c => c.id === activeCategory) || filteredCategories[0]
return (
<Modal open={open} onClose={onClose} title="使用帮助" size="lg" className="p-0">
{/* 搜索框 */}
<div className="border-b border-gray-200 px-4 py-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder="搜索问题,比如「怎么添加员工」「合同到期」..."
className="w-full pl-9 pr-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
</div>
<div className="flex flex-col md:flex-row min-h-[400px] max-h-[60vh]">
{/* 左侧分类导航 */}
<div className="md:w-48 md:border-r border-gray-200 md:flex-shrink-0">
{/* 移动端:横向滚动 */}
<div className="md:hidden flex gap-1 overflow-x-auto p-2 border-b border-gray-200">
{filteredCategories.map(cat => {
const Icon = cat.icon
return (
<button
key={cat.id}
onClick={() => setActiveCategory(cat.id)}
className={clsx(
'flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs whitespace-nowrap flex-shrink-0',
activeCategory === cat.id ? 'bg-primary/10 text-primary font-medium' : 'text-gray-600'
)}
>
<Icon className="w-3.5 h-3.5" />
{cat.title}
</button>
)
})}
</div>
{/* 桌面端:纵向列表 */}
<div className="hidden md:block py-2">
{filteredCategories.map(cat => {
const Icon = cat.icon
return (
<button
key={cat.id}
onClick={() => setActiveCategory(cat.id)}
className={clsx(
'flex items-center gap-2 w-full px-3 py-2 text-sm transition-colors text-left',
activeCategory === cat.id
? 'bg-primary/10 text-primary font-medium border-r-2 border-primary'
: 'text-gray-600 hover:bg-gray-50'
)}
>
<Icon className="w-4 h-4 flex-shrink-0" />
<span className="truncate">{cat.title}</span>
</button>
)
})}
</div>
</div>
{/* 右侧文章列表 */}
<div className="flex-1 overflow-y-auto p-4">
{/* RAG 语义搜索结果模式 */}
{isSearching && ragResults !== null ? (
<>
<h3 className="text-sm font-semibold text-gray-900 mb-3 flex items-center gap-2">
<Sparkles className="w-4 h-4 text-primary" />
AI
{ragLoading && <span className="text-xs font-normal text-gray-400">...</span>}
</h3>
{ragError ? (
/* RAG 不可用时回退到关键词匹配 */
<>
<p className="text-xs text-gray-400 mb-3">AI </p>
{filteredCategories.length > 0 ? (
<div className="space-y-2">
{filteredCategories.flatMap(c => c.articles).map(article => (
<div key={article.id} className="border border-gray-200 rounded-lg overflow-hidden">
<button
onClick={() => setExpandedArticle(expandedArticle === article.id ? null : article.id)}
className="flex items-center justify-between w-full px-3 py-2.5 text-left hover:bg-gray-50 transition-colors"
>
<span className="text-sm font-medium text-gray-800 pr-2">{article.question}</span>
{expandedArticle === article.id ? <ChevronDown className="w-4 h-4 text-gray-400 flex-shrink-0" /> : <ChevronRight className="w-4 h-4 text-gray-400 flex-shrink-0" />}
</button>
{expandedArticle === article.id && (
<div className="px-3 pb-3 pt-1 text-sm text-gray-600">
{article.answer.split('\n').map((line, i) => (
<p key={i} className={line.startsWith('•') ? 'pl-2' : ''}>{line}</p>
))}
</div>
)}
</div>
))}
</div>
) : (
<div className="text-center py-12 text-gray-400">
<Search className="w-8 h-8 mx-auto mb-2" />
<p className="text-sm"></p>
<p className="text-xs mt-1"></p>
</div>
)}
</>
) : ragResults.length > 0 ? (
<div className="space-y-2">
{ragResults.map((r, i) => (
<div key={i} className="border border-gray-200 rounded-lg overflow-hidden">
<button
onClick={() => setExpandedArticle(expandedArticle === `rag-${i}` ? null : `rag-${i}`)}
className="flex items-center justify-between w-full px-3 py-2.5 text-left hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-2 pr-2 min-w-0">
<span className="text-sm font-medium text-gray-800 truncate">{r.title}</span>
<span className="text-xs text-gray-400 flex-shrink-0">{(r.similarity * 100).toFixed(0)}% </span>
</div>
{expandedArticle === `rag-${i}` ? <ChevronDown className="w-4 h-4 text-gray-400 flex-shrink-0" /> : <ChevronRight className="w-4 h-4 text-gray-400 flex-shrink-0" />}
</button>
{expandedArticle === `rag-${i}` && (
<div className="px-3 pb-3 pt-1 text-sm text-gray-600 space-y-2">
<p>{r.content}</p>
<div className="flex items-center gap-2 text-xs text-gray-400">
<span className="px-1.5 py-0.5 rounded bg-gray-100">{r.category}</span>
<span>{r.source}</span>
</div>
</div>
)}
</div>
))}
</div>
) : (
<div className="text-center py-12 text-gray-400">
<Search className="w-8 h-8 mx-auto mb-2" />
<p className="text-sm"></p>
<p className="text-xs mt-1"> AI </p>
</div>
)}
</>
) : (
/* 正常分类浏览模式 */
<>
{currentCategory ? (
<>
<h3 className="text-sm font-semibold text-gray-900 mb-3 flex items-center gap-2">
{(() => {
const Icon = currentCategory.icon
return <Icon className="w-4 h-4 text-primary" />
})()}
{currentCategory.title}
</h3>
<div className="space-y-2">
{currentCategory.articles.map(article => {
const expanded = expandedArticle === article.id
return (
<div key={article.id} className="border border-gray-200 rounded-lg overflow-hidden">
<button
onClick={() => setExpandedArticle(expanded ? null : article.id)}
className="flex items-center justify-between w-full px-3 py-2.5 text-left hover:bg-gray-50 transition-colors"
>
<span className="text-sm font-medium text-gray-800 pr-2">{article.question}</span>
{expanded ? <ChevronDown className="w-4 h-4 text-gray-400 flex-shrink-0" /> : <ChevronRight className="w-4 h-4 text-gray-400 flex-shrink-0" />}
</button>
{expanded && (
<div className="px-3 pb-3 pt-1 text-sm text-gray-600 space-y-2">
{article.answer.split('\n').map((line, i) => (
<p key={i} className={line.startsWith('•') ? 'pl-2' : ''}>{line}</p>
))}
{article.steps && (
<div className="bg-blue-50 rounded-md p-3 space-y-1.5">
<p className="text-xs font-medium text-blue-700"></p>
{article.steps.map((step, i) => (
<div key={i} className="flex gap-2 text-xs text-blue-900">
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-blue-200 text-blue-700 flex items-center justify-center font-medium">{i + 1}</span>
<span className="pt-0.5">{step}</span>
</div>
))}
</div>
)}
{article.tip && (
<div className="flex items-start gap-2 bg-green-50 rounded-md p-2.5">
<CheckCircle className="w-4 h-4 text-green-600 flex-shrink-0 mt-0.5" />
<p className="text-xs text-green-800">{article.tip}</p>
</div>
)}
{article.warning && (
<div className="flex items-start gap-2 bg-amber-50 rounded-md p-2.5">
<AlertTriangle className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
<p className="text-xs text-amber-800">{article.warning}</p>
</div>
)}
</div>
)}
</div>
)
})}
</div>
</>
) : (
<div className="text-center py-12 text-gray-400">
<Search className="w-8 h-8 mx-auto mb-2" />
<p className="text-sm"></p>
<p className="text-xs mt-1"></p>
</div>
)}
</>
)}
</div>
</div>
{/* 底部联系方式 */}
<div className="border-t border-gray-200 px-4 py-2.5 flex items-center justify-between text-xs text-gray-500">
<div className="flex items-center gap-3">
<span className="flex items-center gap-1.5">
<HelpCircle className="w-3.5 h-3.5" />
AI
</span>
<button
onClick={() => { resetOnboarding(); onClose(); navigate('/'); setTimeout(() => window.location.reload(), 100) }}
className="flex items-center gap-1 text-primary hover:underline"
>
<RotateCcw className="w-3 h-3" />
</button>
</div>
<span>support@hr8ai.com</span>
</div>
</Modal>
)
}