feat: 完成优化1-6全部功能 — Portal安全/AI增强/Settings导入导出/社保公积金版本化

- 优化-1: 社保公积金独立配置+版本化缴费记录+多城市支持+迁移脚本
- 优化-2: AI流式输出/RAG集成/风险角标/审计日志/二维码/批量续签/忘记密码/语音输入/PDF导出/速率限制/套餐人数上限
- 优化-3: 批次重命名/费用实时预览/模拟版本管理/续签合规预检/社保重置/搜索分页/批量解聘/到期预警/税率试算
- 优化-4: 会话历史/待办批量/结果关联档案/风险下钻/预测上下文/附件校验/Tab级联/薪税导出
- 优化-5: 表单回填/用户编辑禁用/导入预览/选择性导出/通知测试/错误日志导出/脱敏导出/gzip压缩
- 优化-6: 工资条确认通知HR/AI上下文增强/电子签名/用量限制修复/入职文件上传/RAG管理/工资趋势/用量事务/验证码加固/审查结构化/链接撤回/超时机制/确认重发/案例转待办
This commit is contained in:
freedakgmail
2026-07-24 07:58:25 +08:00
parent f1c72f3eb0
commit e01bad4815
17 changed files with 1721 additions and 141 deletions
+69 -2
View File
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import { DollarSign, Check } from 'lucide-react'
import { DollarSign, Check, TrendingUp, Download } from 'lucide-react'
import api from '../../lib/api'
import Card from '../../components/ui/Card'
import Button from '../../components/ui/Button'
@@ -20,6 +20,7 @@ portalApi.interceptors.request.use((config: any) => {
export default function Payslip() {
const queryClient = useQueryClient()
const [month, setMonth] = useState(new Date().toISOString().slice(0, 7))
const [showHistory, setShowHistory] = useState(false)
const { data, isLoading } = useQuery<any>({
queryKey: ['payslip', month],
@@ -29,6 +30,14 @@ export default function Payslip() {
},
})
const { data: history } = useQuery<any[]>({
queryKey: ['payslip-history'],
queryFn: async () => {
const res = await portalApi.get('/payslip/history') as any
return res.data?.data ?? []
},
})
const confirmMutation = useMutation({
mutationFn: (id: string) => portalApi.post(`/payslip/${id}/confirm`),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['payslip'] }),
@@ -36,6 +45,31 @@ export default function Payslip() {
const employee = JSON.parse(localStorage.getItem('portalEmployee') || '{}')
const handleExport = () => {
if (!history || history.length === 0) return
const headers = ['月份', '基本工资', '加班费', '津贴', '扣款', '应发合计', '确认状态']
const rows = history.map((p: any) => [
p.month,
p.baseSalary,
p.overtimePay,
p.allowance,
p.deduction,
p.totalPay,
p.confirmedAt ? '已确认' : '未确认',
])
const csv = [headers, ...rows].map(r => r.join(',')).join('\n')
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `工资条_${employee.name || '员工'}_${new Date().toISOString().slice(0, 10)}.csv`
a.click()
URL.revokeObjectURL(url)
}
const sortedHistory = [...(history || [])].sort((a: any, b: any) => a.month.localeCompare(b.month))
const maxPay = Math.max(...sortedHistory.map((p: any) => Number(p.totalPay) || 0), 1)
return (
<div className="min-h-screen bg-gray-50 px-4 py-6">
<div className="max-w-md mx-auto">
@@ -50,15 +84,48 @@ export default function Payslip() {
</div>
</div>
<div className="mb-4">
<div className="flex items-center gap-2 mb-4">
<input
type="month"
value={month}
onChange={(e) => setMonth(e.target.value)}
className="px-3 py-2 rounded-md border border-gray-300 text-sm"
/>
<button
onClick={() => setShowHistory(!showHistory)}
className="flex items-center gap-1 px-3 py-2 rounded-md bg-gray-100 text-xs font-medium"
>
<TrendingUp className="w-4 h-4" />
</button>
<button
onClick={handleExport}
disabled={!history || history.length === 0}
className="flex items-center gap-1 px-3 py-2 rounded-md bg-gray-100 text-xs font-medium disabled:opacity-50"
>
<Download className="w-4 h-4" />
</button>
</div>
{showHistory && sortedHistory.length > 0 && (
<Card className="mb-4">
<h3 className="text-xs font-medium mb-3"> {sortedHistory.length} </h3>
<div className="space-y-2">
{sortedHistory.map((p: any) => (
<div key={p.id} className="flex items-center gap-2">
<span className="text-xs text-gray-500 w-16 flex-shrink-0">{p.month}</span>
<div className="flex-1 bg-gray-100 rounded-full h-5 relative overflow-hidden">
<div
className="bg-primary h-full rounded-full transition-all"
style={{ width: `${(Number(p.totalPay) / maxPay) * 100}%` }}
/>
</div>
<span className="text-xs font-medium w-20 text-right">¥{fmt(Number(p.totalPay))}</span>
</div>
))}
</div>
</Card>
)}
<Card>
{isLoading ? (
<div className="text-center py-8 text-gray-400">...</div>