feat: 分页组件、Dashboard待办图标、归档与工资条解耦、总览数据优化

- 新增公用 Pagination 组件,Roster/Money/Dashboard 列表加分页
- Dashboard 待办按类型显示不同图标(合同/薪资/解聘/月度)
- 待办分为「风险提醒」「月度任务」两个顶层 tab
- 归档与工资条生成解耦:归档只锁定批次,工资条单独生成
- 工资条管理新增「从批次汇总生成」按钮
- Dashboard 总览优先从已归档批次 BatchEntry 汇总数据
- 新增工资条生成待办提醒,生成后自动标记完成
- 修复高风险统计只含 CONTRACT/TERMINATION 类型
- 修复月度任务去重逻辑覆盖 SALARY 类型
This commit is contained in:
freedakgmail
2026-07-23 13:42:28 +08:00
parent 820579e98d
commit 2a09d31ccc
20 changed files with 2300 additions and 235 deletions
+43 -16
View File
@@ -1,21 +1,40 @@
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import { Users, AlertTriangle, CheckSquare, DollarSign, ArrowRight, RefreshCw, FileText, Calendar, TrendingUp, Briefcase, Calculator, Wallet, Building2, Receipt, Check, X, Clock, LayoutDashboard, ListTodo } from 'lucide-react'
import { Users, AlertTriangle, CheckSquare, DollarSign, ArrowRight, RefreshCw, FileText, Calendar, TrendingUp, Briefcase, Calculator, Wallet, Building2, Receipt, Check, X, Clock, LayoutDashboard, ListTodo, ShieldAlert } from 'lucide-react'
import api from '../lib/api'
import Card from '../components/ui/Card'
import Button from '../components/ui/Button'
import EmptyState from '../components/ui/EmptyState'
import Signal from '../components/ui/Signal'
import Pagination from '../components/ui/Pagination'
import type { DashboardData } from '../types'
function fmt(n: number) {
return `¥${n.toLocaleString(undefined, { maximumFractionDigits: 2 })}`
return `¥${(n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
const TODO_ICON_CONFIG: Record<string, { icon: typeof FileText; color: string; bg: string }> = {
CONTRACT: { icon: FileText, color: 'text-blue-600', bg: 'bg-blue-50' },
SALARY: { icon: DollarSign, color: 'text-amber-600', bg: 'bg-amber-50' },
TERMINATION: { icon: ShieldAlert, color: 'text-red-600', bg: 'bg-red-50' },
MONTHLY: { icon: Calendar, color: 'text-purple-600', bg: 'bg-purple-50' },
}
function TodoIcon({ type, level }: { type: string; level: string }) {
const config = TODO_ICON_CONFIG[type] || TODO_ICON_CONFIG.MONTHLY
const Icon = config.icon
return (
<div className={`flex items-center justify-center w-8 h-8 rounded-lg ${config.bg} ${config.color} flex-shrink-0`}>
<Icon className="w-4 h-4" />
</div>
)
}
export default function Dashboard() {
const [todoPage, setTodoPage] = useState(1)
const [todoPageSize, setTodoPageSize] = useState(10)
const queryClient = useQueryClient()
const [activeTab, setActiveTab] = useState<'overview' | 'payroll' | 'todos'>('overview')
const [activeTab, setActiveTab] = useState<'overview' | 'payroll' | 'risk' | 'task'>('overview')
const { data, isLoading, refetch, isFetching } = useQuery<DashboardData>({
queryKey: ['dashboard'],
queryFn: async () => {
@@ -34,6 +53,10 @@ export default function Dashboard() {
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['dashboard'] }),
})
const riskTodos = data?.todos.filter((t) => t.type === 'CONTRACT' || t.type === 'TERMINATION') || []
const taskTodos = data?.todos.filter((t) => t.type === 'MONTHLY' || t.type === 'SALARY') || []
const filteredTodos = activeTab === 'risk' ? riskTodos : taskTodos
if (isLoading) {
return <div className="text-center py-8 text-gray-400">...</div>
}
@@ -75,7 +98,8 @@ export default function Dashboard() {
const tabs = [
{ key: 'overview' as const, label: '概览', icon: LayoutDashboard, badge: data.stats.todoCount },
{ key: 'payroll' as const, label: '薪税', icon: Calculator, badge: payroll?.payslipCount ?? 0 },
{ key: 'todos' as const, label: '待办', icon: ListTodo, badge: data.todos.length },
{ key: 'risk' as const, label: '风险提醒', icon: AlertTriangle, badge: riskTodos.length },
{ key: 'task' as const, label: '月度任务', icon: ListTodo, badge: taskTodos.length },
]
return (
@@ -274,27 +298,29 @@ export default function Dashboard() {
</Card>
)}
{/* 待办 Tab */}
{activeTab === 'todos' && (
{/* 风险提醒 Tab */}
{(activeTab === 'risk' || activeTab === 'task') && (
<div className="space-y-4">
{/* 待办列表 */}
<Card>
<div className="flex items-center justify-between mb-4">
<h2 className="font-semibold"></h2>
<span className="text-sm text-gray-400">{data.todos.length} </span>
<h2 className="font-semibold">{activeTab === 'risk' ? '风险提醒' : '月度任务'}</h2>
<span className="text-sm text-gray-400">{filteredTodos.length} </span>
</div>
{data.todos.length === 0 ? (
<EmptyState title="暂无待办" description="所有风险项已处理完毕" />
{filteredTodos.length === 0 ? (
<EmptyState title="暂无待办" description="所有项已处理完毕" />
) : (
<div className="space-y-2">
{data.todos.map((todo) => (
<>
<Pagination page={todoPage} pageSize={todoPageSize} total={filteredTodos.length} onPageChange={setTodoPage} onPageSizeChange={(s) => { setTodoPageSize(s); setTodoPage(1) }} />
<div className="space-y-2">
{filteredTodos.slice((todoPage - 1) * todoPageSize, todoPage * todoPageSize).map((todo) => (
<div
key={todo.id}
className="flex items-center justify-between px-3 py-3 rounded-md hover:bg-gray-50 transition-colors"
>
<Link to={todo.actionUrl} className="flex items-center gap-3 flex-1">
<Signal level={todo.level} />
<TodoIcon type={todo.type} level={todo.level} />
<div className="flex flex-col">
<span className="text-sm text-gray-800">{todo.title}</span>
<span className="text-xs text-gray-400 flex items-center gap-1"><Clock className="w-3 h-3" />{todo.description}</span>
@@ -320,7 +346,8 @@ export default function Dashboard() {
</div>
</div>
))}
</div>
</div>
</>
)}
</Card>
@@ -338,7 +365,7 @@ export default function Dashboard() {
className="flex items-center justify-between px-3 py-3 rounded-md bg-gray-50"
>
<Link to={todo.actionUrl} className="flex items-center gap-3 flex-1">
<Check className="w-4 h-4 text-safe" />
<TodoIcon type={todo.type} level={todo.level} />
<div className="flex flex-col">
<span className="text-sm text-gray-600 line-through">{todo.title}</span>
<span className="text-xs text-gray-400">{todo.description}</span>