feat: 系统优化Phase2 - 面包屑导航/侧边栏间距/制度公示阅读签收/模板变量中文化/通知类型补全
- 面包屑导航组件,集成至TopNav header - 侧边栏菜单分组间距增大,分组间分隔线 - 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计 - 修复Policies.tsx民主程序推进bug(字段名/API路径/参数) - 用工文本模板变量名英文转中文显示 - 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY) - 通知示例数据补充 - h2标题统一为text-sm font-medium - 新增run.md
This commit is contained in:
@@ -2,8 +2,8 @@ import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'
|
||||
import { Users, AlertTriangle, CheckSquare, DollarSign, ArrowRight, RefreshCw, FileText, Calendar, TrendingUp, Briefcase, Calculator, Wallet, Building2, Receipt, Check, X, Clock, LayoutDashboard, ListTodo, ShieldAlert, UserPlus, AlertCircle, Download, ChevronRight } from 'lucide-react'
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, RadialBarChart, RadialBar, PolarAngleAxis } from 'recharts'
|
||||
import { Users, AlertTriangle, CheckSquare, DollarSign, ArrowRight, RefreshCw, FileText, Calendar, TrendingUp, Briefcase, Calculator, Wallet, Building2, Receipt, Check, X, Clock, LayoutDashboard, ListTodo, ShieldAlert, UserPlus, AlertCircle, Download, ChevronRight, CalendarDays, TrendingDown, ShieldCheck, Lightbulb, BookOpen, Sparkles } from 'lucide-react'
|
||||
import api from '../lib/api'
|
||||
import Card from '../components/ui/Card'
|
||||
import Button from '../components/ui/Button'
|
||||
@@ -56,6 +56,31 @@ export default function Dashboard() {
|
||||
},
|
||||
})
|
||||
|
||||
const currentMonth = new Date().toISOString().slice(0, 7)
|
||||
const { data: calendarData } = useQuery<any>({
|
||||
queryKey: ['calendar', currentMonth],
|
||||
queryFn: async () => {
|
||||
const res = await api.get(`/dashboard/calendar?month=${currentMonth}`) as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
const { data: costAnalysis } = useQuery<any>({
|
||||
queryKey: ['cost-analysis', currentMonth],
|
||||
queryFn: async () => {
|
||||
const res = await api.get(`/dashboard/cost-analysis?month=${currentMonth}`) as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
const { data: complianceScore } = useQuery<any>({
|
||||
queryKey: ['compliance-score'],
|
||||
queryFn: async () => {
|
||||
const res = await api.get('/dashboard/compliance-score') as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
const resolveMutation = useMutation({
|
||||
mutationFn: (id: string) => api.patch(`/dashboard/todos/${id}/resolve`),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['dashboard'] }),
|
||||
@@ -175,6 +200,13 @@ export default function Dashboard() {
|
||||
{ key: 'task' as const, label: '月度任务', icon: ListTodo, badge: taskTodos.length },
|
||||
]
|
||||
|
||||
const priorityConfig: Record<string, { label: string; color: string; bg: string }> = {
|
||||
URGENT: { label: '紧急', color: 'text-red-700', bg: 'bg-red-100' },
|
||||
HIGH: { label: '高', color: 'text-orange-700', bg: 'bg-orange-100' },
|
||||
MEDIUM: { label: '中', color: 'text-amber-700', bg: 'bg-amber-100' },
|
||||
LOW: { label: '低', color: 'text-gray-600', bg: 'bg-gray-100' },
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -191,6 +223,33 @@ export default function Dashboard() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 紧急风险横幅 */}
|
||||
{data.urgentRisk && (
|
||||
<Link to={data.urgentRisk.actionUrl}>
|
||||
<Card className="border-red-300 bg-red-50 hover:bg-red-100 transition-colors cursor-pointer">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-red-200 flex-shrink-0">
|
||||
<AlertTriangle className="w-5 h-5 text-red-700" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-bold text-red-800">紧急风险</span>
|
||||
<span className="px-1.5 py-0.5 rounded text-xs bg-red-200 text-red-800">预估损失 {fmt(data.urgentRisk.estimatedLoss)}</span>
|
||||
{data.urgentRisk.daysUntilDeadline !== null && (
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${data.urgentRisk.daysUntilDeadline <= 0 ? 'bg-red-300 text-red-900' : 'bg-red-200 text-red-800'}`}>
|
||||
{data.urgentRisk.daysUntilDeadline <= 0 ? '已逾期' : `${data.urgentRisk.daysUntilDeadline}天后截止`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-red-700 mt-0.5 truncate">{data.urgentRisk.title}</div>
|
||||
<div className="text-xs text-red-600 mt-0.5">{data.urgentRisk.description}</div>
|
||||
</div>
|
||||
<ArrowRight className="w-5 h-5 text-red-600 flex-shrink-0" />
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Tab 导航 */}
|
||||
<div className="flex gap-1 border-b">
|
||||
{tabs.map((tab) => {
|
||||
@@ -218,6 +277,96 @@ export default function Dashboard() {
|
||||
{/* 概览 Tab */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="space-y-3">
|
||||
{/* 合规健康度评分 + AI 建议卡片流 */}
|
||||
{complianceScore && (
|
||||
<div className="space-y-3">
|
||||
{/* 评分环 + 5 维度 */}
|
||||
<Card>
|
||||
<div className="flex items-center gap-4">
|
||||
{/* SVG 环形评分 */}
|
||||
<div className="relative w-28 h-28 shrink-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RadialBarChart
|
||||
innerRadius="70%"
|
||||
outerRadius="100%"
|
||||
data={[{ value: complianceScore.overallScore, fill: complianceScore.level === 'safe' ? '#16A34A' : complianceScore.level === 'warning' ? '#D97706' : '#C00000' }]}
|
||||
startAngle={90}
|
||||
endAngle={-270}
|
||||
>
|
||||
<PolarAngleAxis type="number" domain={[0, 100]} tick={false} />
|
||||
<RadialBar background dataKey="value" cornerRadius={8} />
|
||||
</RadialBarChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span className={`text-2xl font-bold ${complianceScore.level === 'safe' ? 'text-safe' : complianceScore.level === 'warning' ? 'text-warning' : 'text-danger'}`}>{complianceScore.overallScore}</span>
|
||||
<span className="text-xs text-gray-500">{complianceScore.levelLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* 5 维度评分 */}
|
||||
<div className="flex-1 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{complianceScore.dimensions.map((dim: any) => (
|
||||
<Link key={dim.key} to={dim.key === 'contract' ? '/roster' : dim.key === 'policy' ? '/policies' : dim.key === 'attendance' ? '/attendance' : dim.key === 'salary' ? '/money' : '/social'} className="flex items-center justify-between p-2 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${dim.score >= 85 ? 'bg-safe' : dim.score >= 60 ? 'bg-warning' : 'bg-danger'}`} />
|
||||
<span className="text-xs text-gray-700">{dim.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{dim.todoCount > 0 && <span className="text-xs text-gray-400">{dim.todoCount}待办</span>}
|
||||
<span className={`text-sm font-bold ${dim.score >= 85 ? 'text-safe' : dim.score >= 60 ? 'text-warning' : 'text-danger'}`}>{dim.score}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* AI 建议卡片流 */}
|
||||
{complianceScore.suggestions.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-gray-700">
|
||||
<Sparkles className="w-4 h-4 text-primary" />
|
||||
AI 建议
|
||||
</div>
|
||||
{complianceScore.suggestions.map((s: any, i: number) => {
|
||||
const suggestionConfig: Record<string, { icon: typeof AlertTriangle; bg: string; border: string; iconColor: string; label: string; labelBg: string }> = {
|
||||
danger: { icon: AlertTriangle, bg: 'bg-red-50', border: 'border-red-200', iconColor: 'text-red-600', label: '高风险', labelBg: 'bg-red-100 text-red-700' },
|
||||
warning: { icon: AlertCircle, bg: 'bg-amber-50', border: 'border-amber-200', iconColor: 'text-amber-600', label: '中风险', labelBg: 'bg-amber-100 text-amber-700' },
|
||||
value: { icon: ShieldCheck, bg: 'bg-green-50', border: 'border-green-200', iconColor: 'text-safe', label: '价值', labelBg: 'bg-green-100 text-safe' },
|
||||
knowledge: { icon: BookOpen, bg: 'bg-purple-50', border: 'border-purple-200', iconColor: 'text-purple-600', label: '知识', labelBg: 'bg-purple-100 text-purple-700' },
|
||||
}
|
||||
const config = suggestionConfig[s.type] || { icon: Lightbulb, bg: 'bg-gray-50', border: 'border-gray-200', iconColor: 'text-gray-600', label: '', labelBg: '' }
|
||||
const Icon = config.icon
|
||||
return (
|
||||
<Link key={i} to={s.actionUrl}>
|
||||
<Card className={`${config.bg} ${config.border} border hover:shadow-md transition-shadow cursor-pointer`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`flex items-center justify-center w-8 h-8 rounded-lg ${config.iconColor} bg-white/60 flex-shrink-0`}>
|
||||
<Icon className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${config.labelBg}`}>{config.label}</span>
|
||||
<span className="text-sm font-medium text-gray-900 truncate">{s.title}</span>
|
||||
{s.estimatedLoss && s.estimatedLoss > 0 && (
|
||||
<span className="text-xs text-red-600 font-bold">预估损失 {fmt(s.estimatedLoss)}</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-0.5">{s.description}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-xs text-primary flex-shrink-0">
|
||||
{s.actionLabel}
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
{stats.map((stat) => {
|
||||
@@ -239,7 +388,7 @@ export default function Dashboard() {
|
||||
{/* 当月人力成本 */}
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="font-medium flex items-center gap-1.5"><Wallet className="w-4 h-4 text-primary" />当月人力成本</h2>
|
||||
<h2 className="text-sm font-medium flex items-center gap-1.5"><Wallet className="w-4 h-4 text-primary" />当月人力成本</h2>
|
||||
<span className="text-xs text-gray-400">{payroll?.month}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
|
||||
@@ -259,7 +408,7 @@ export default function Dashboard() {
|
||||
{/* 年度累计人力成本 */}
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="font-medium flex items-center gap-1.5"><TrendingUp className="w-4 h-4 text-primary" />年度累计成本</h2>
|
||||
<h2 className="text-sm font-medium flex items-center gap-1.5"><TrendingUp className="w-4 h-4 text-primary" />年度累计成本</h2>
|
||||
<span className="text-xs text-gray-400">{yearCost?.year}年</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
|
||||
@@ -309,7 +458,7 @@ export default function Dashboard() {
|
||||
{/* 本月工作动态 */}
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="font-medium flex items-center gap-1.5"><Briefcase className="w-4 h-4" />本月工作动态</h2>
|
||||
<h2 className="text-sm font-medium flex items-center gap-1.5"><Briefcase className="w-4 h-4" />本月工作动态</h2>
|
||||
<span className="text-xs text-gray-500">{activities?.month}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||
@@ -328,7 +477,7 @@ export default function Dashboard() {
|
||||
|
||||
{/* 风险分布 */}
|
||||
<Card>
|
||||
<h2 className="font-medium mb-3">风险分布</h2>
|
||||
<h2 className="text-sm font-medium mb-3">风险分布</h2>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-32 h-32 shrink-0">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
@@ -407,8 +556,21 @@ export default function Dashboard() {
|
||||
<Link key={r.id} to={r.actionUrl} className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-50 text-xs">
|
||||
<AlertCircle className={`w-4 h-4 flex-shrink-0 ${r.level === 'high' ? 'text-danger' : 'text-warning'}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="truncate text-gray-800">{r.title}</div>
|
||||
{r.employeeName && <div className="text-gray-500">{r.employeeName}</div>}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="truncate text-gray-800">{r.title}</span>
|
||||
{r.priority && priorityConfig[r.priority] && (
|
||||
<span className={`px-1 py-0.5 rounded text-xs font-medium ${priorityConfig[r.priority].bg} ${priorityConfig[r.priority].color}`}>
|
||||
{priorityConfig[r.priority].label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
{r.employeeName && <span>{r.employeeName}</span>}
|
||||
{r.estimatedLoss > 0 && <span className="text-red-600">损失 {fmt(r.estimatedLoss)}</span>}
|
||||
{r.daysUntilDeadline !== null && r.daysUntilDeadline <= 7 && (
|
||||
<span className="text-red-600">{r.daysUntilDeadline <= 0 ? '已逾期' : `${r.daysUntilDeadline}天`}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="w-3 h-3 text-gray-500" />
|
||||
</Link>
|
||||
@@ -420,6 +582,84 @@ export default function Dashboard() {
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* HR 月度日历 + 人力成本分析 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
{/* 月度日历 */}
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-medium flex items-center gap-1.5"><CalendarDays className="w-4 h-4 text-primary" />本月关键日期</h2>
|
||||
<span className="text-xs text-gray-400">{currentMonth}</span>
|
||||
</div>
|
||||
{calendarData?.events && calendarData.events.length > 0 ? (
|
||||
<div className="space-y-1.5 max-h-64 overflow-y-auto">
|
||||
{calendarData.events.slice(0, 10).map((ev: any, i: number) => (
|
||||
<Link key={i} to={ev.actionUrl || '/'} className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-50 text-xs">
|
||||
<div className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${
|
||||
ev.priority === 'high' ? 'bg-red-500' : ev.priority === 'medium' ? 'bg-amber-500' : 'bg-gray-400'
|
||||
}`} />
|
||||
<span className="text-gray-500 w-20 flex-shrink-0">{ev.date.slice(5)}</span>
|
||||
<span className="text-gray-800 truncate flex-1">{ev.title}</span>
|
||||
</Link>
|
||||
))}
|
||||
{calendarData.events.length > 10 && (
|
||||
<div className="text-xs text-gray-500 text-center pt-1">还有 {calendarData.events.length - 10} 个事件</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-gray-500 text-center py-4">本月暂无关键日期</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 人力成本分析 */}
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-medium flex items-center gap-1.5"><TrendingUp className="w-4 h-4 text-primary" />人力成本分析</h2>
|
||||
<span className="text-xs text-gray-400">{currentMonth}</span>
|
||||
</div>
|
||||
{costAnalysis ? (
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className={`p-2 rounded-lg ${costAnalysis.monthOnMonth?.change >= 0 ? 'bg-red-50' : 'bg-green-50'}`}>
|
||||
<div className="text-xs text-gray-500">环比上月</div>
|
||||
<div className={`text-sm font-bold ${costAnalysis.monthOnMonth?.change >= 0 ? 'text-red-600' : 'text-green-600'}`}>
|
||||
{costAnalysis.monthOnMonth?.change >= 0 ? '+' : ''}{costAnalysis.monthOnMonth?.changePercent?.toFixed(1)}%
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{costAnalysis.monthOnMonth?.change >= 0 ? '↑' : '↓'} {fmt(Math.abs(costAnalysis.monthOnMonth?.change || 0))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`p-2 rounded-lg ${costAnalysis.yearOnYear?.change >= 0 ? 'bg-red-50' : 'bg-green-50'}`}>
|
||||
<div className="text-xs text-gray-500">同比去年</div>
|
||||
<div className={`text-sm font-bold ${costAnalysis.yearOnYear?.change >= 0 ? 'text-red-600' : 'text-green-600'}`}>
|
||||
{costAnalysis.yearOnYear?.change >= 0 ? '+' : ''}{costAnalysis.yearOnYear?.changePercent?.toFixed(1)}%
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{costAnalysis.yearOnYear?.change >= 0 ? '↑' : '↓'} {fmt(Math.abs(costAnalysis.yearOnYear?.change || 0))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-t pt-2">
|
||||
<span className="text-xs text-gray-600">本月人均成本</span>
|
||||
<span className="text-sm font-bold text-primary">{fmt(costAnalysis.current?.perCapita || 0)}</span>
|
||||
</div>
|
||||
{costAnalysis.factors && costAnalysis.factors.length > 0 && (
|
||||
<div className="space-y-1 border-t pt-2">
|
||||
<div className="text-xs font-medium text-gray-600">成本变化归因</div>
|
||||
{costAnalysis.factors.map((f: any, i: number) => (
|
||||
<div key={i} className="flex items-start gap-1.5 text-xs">
|
||||
<span className={`w-1.5 h-1.5 rounded-full mt-1 flex-shrink-0 ${f.impact >= 0 ? 'bg-red-500' : 'bg-green-500'}`} />
|
||||
<span className="text-gray-600 flex-1">{f.description}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-gray-500 text-center py-4">暂无成本分析数据</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -427,7 +667,7 @@ export default function Dashboard() {
|
||||
{activeTab === 'payroll' && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="font-medium flex items-center gap-1.5"><Calculator className="w-4 h-4" />本月薪税费用总览</h2>
|
||||
<h2 className="text-sm font-medium flex items-center gap-1.5"><Calculator className="w-4 h-4" />本月薪税费用总览</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={handleExportPayroll} disabled={!payroll || payroll.payslipCount === 0}>
|
||||
<Download className="w-4 h-4 mr-1" />导出
|
||||
@@ -533,7 +773,7 @@ export default function Dashboard() {
|
||||
{/* 待办列表 */}
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="font-medium">{activeTab === 'risk' ? '风险提醒' : '月度任务'}</h2>
|
||||
<h2 className="text-sm font-medium">{activeTab === 'risk' ? '风险提醒' : '月度任务'}</h2>
|
||||
<span className="text-xs text-gray-500">{filteredTodos.length} 项</span>
|
||||
</div>
|
||||
|
||||
@@ -588,7 +828,20 @@ export default function Dashboard() {
|
||||
<Link to={todo.actionUrl} className="flex items-center gap-2.5 flex-1">
|
||||
<TodoIcon type={todo.type} level={todo.level} />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-gray-800">{todo.title}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs text-gray-800">{todo.title}</span>
|
||||
{todo.priority && priorityConfig[todo.priority] && (
|
||||
<span className={`px-1 py-0.5 rounded text-xs font-medium ${priorityConfig[todo.priority].bg} ${priorityConfig[todo.priority].color}`}>
|
||||
{priorityConfig[todo.priority].label}
|
||||
</span>
|
||||
)}
|
||||
{todo.estimatedLoss > 0 && (
|
||||
<span className="text-xs text-red-600 font-medium">损失 {fmt(todo.estimatedLoss)}</span>
|
||||
)}
|
||||
{todo.daysUntilDeadline !== null && todo.daysUntilDeadline <= 3 && (
|
||||
<span className="text-xs text-red-600">{todo.daysUntilDeadline <= 0 ? '已逾期' : `${todo.daysUntilDeadline}天`}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 flex items-center gap-1"><Clock className="w-3 h-3" />{todo.description}</span>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -622,7 +875,7 @@ export default function Dashboard() {
|
||||
{data.resolvedTodos && data.resolvedTodos.length > 0 && (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="font-medium flex items-center gap-1.5"><CheckSquare className="w-4 h-4 text-safe" />已办事项</h2>
|
||||
<h2 className="text-sm font-medium flex items-center gap-1.5"><CheckSquare className="w-4 h-4 text-safe" />已办事项</h2>
|
||||
<span className="text-xs text-gray-500">{data.resolvedTodos.length} 项</span>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
|
||||
Reference in New Issue
Block a user