feat: Sprint 2 — Stepper/InlineAlert组件 + 首页TaskCenter任务中心 + /workspace/next-actions API + Money.tsx BatchDetail集成工作流步骤条和质量门禁

This commit is contained in:
selfrelease
2026-07-31 17:52:02 +08:00
parent c181d3fa45
commit 55b6867c9c
6 changed files with 484 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
/**
* TaskCenter 任务中心组件 — 展示待办队列、人员动态和下一步行动
* 从 /dashboard/workspace/next-actions 获取数据,按优先级分组展示
*/
import { useQuery } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import { AlertCircle, Layers, FileText, Heart, ArrowRight, ListTodo } from 'lucide-react'
import api from '../../lib/api'
import { InlineAlert } from '../../components/ui/InlineAlert'
interface NextAction {
id: string
title: string
subtitle?: string
type?: string
level?: string
dueDate?: string
link: string
}
interface ActionGroup {
category: string
priority: 'high' | 'medium' | 'low'
items: NextAction[]
}
const categoryIcons: Record<string, typeof AlertCircle> = {
'待办事项': AlertCircle,
'发薪批次': Layers,
'合同到期': FileText,
'特殊状态': Heart,
}
const priorityColors: Record<string, string> = {
high: 'text-danger',
medium: 'text-warning',
low: 'text-info',
}
/**
* 任务中心 — 首页核心组件,聚合展示需要关注的行动项
*/
export function TaskCenter() {
const { data, isLoading } = useQuery<{ actions: ActionGroup[]; totalCount: number }>({
queryKey: ['next-actions'],
queryFn: async () => {
const res = await api.get('/dashboard/workspace/next-actions') as any
return res.data
},
staleTime: 60_000,
})
if (isLoading) {
return (
<div className="card p-4">
<div className="flex items-center gap-2 mb-3">
<ListTodo className="w-4 h-4 text-brand-600" />
<h3 className="text-sm font-medium text-ink-700"></h3>
</div>
<div className="space-y-2">
{[1, 2, 3].map(i => (
<div key={i} className="h-12 rounded-md bg-surface-muted animate-pulse" />
))}
</div>
</div>
)
}
if (!data || data.totalCount === 0) {
return (
<div className="card p-4">
<div className="flex items-center gap-2 mb-3">
<ListTodo className="w-4 h-4 text-brand-600" />
<h3 className="text-sm font-medium text-ink-700"></h3>
</div>
<InlineAlert type="success" title="暂无待办">
</InlineAlert>
</div>
)
}
return (
<div className="card p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<ListTodo className="w-4 h-4 text-brand-600" />
<h3 className="text-sm font-medium text-ink-700"></h3>
<span className="text-xs text-ink-400">{data.totalCount} </span>
</div>
</div>
<div className="space-y-3">
{data.actions.map((group) => {
const Icon = categoryIcons[group.category] || AlertCircle
return (
<div key={group.category}>
{/* 分类标题 */}
<div className="flex items-center gap-1.5 mb-1.5">
<Icon className={`w-3.5 h-3.5 ${priorityColors[group.priority]}`} />
<span className="text-xs font-medium text-ink-600">{group.category}</span>
<span className="text-xs text-ink-400">({group.items.length})</span>
</div>
{/* 行动项列表 */}
<div className="space-y-1 ml-5">
{group.items.slice(0, 5).map((item) => (
<Link
key={item.id}
to={item.link}
className="flex items-center justify-between gap-2 px-2 py-1.5 rounded-md hover:bg-surface-muted transition-colors group"
>
<div className="flex-1 min-w-0">
<div className="text-sm text-ink-900 truncate">{item.title}</div>
{item.subtitle && (
<div className="text-xs text-ink-400 truncate">{item.subtitle}</div>
)}
</div>
{item.dueDate && (
<span className="text-xs text-ink-400 shrink-0">{item.dueDate}</span>
)}
<ArrowRight className="w-3.5 h-3.5 text-ink-300 group-hover:text-brand-600 shrink-0 transition-colors" />
</Link>
))}
{group.items.length > 5 && (
<Link
to={group.category === '发薪批次' ? '/money' : group.category === '合同到期' ? '/roster' : '/'}
className="block text-xs text-brand-600 hover:underline px-2 py-1"
>
{group.items.length}
</Link>
)}
</div>
</div>
)
})}
</div>
</div>
)
}