ui: 优化特殊状态台账页面UI

- 统计卡片:添加图标、圆角xl、左图标右数据布局
- 列表卡片:类型图标背景色、hover显示操作按钮、分隔线分区
- 日期字段:改用Field组件双列网格布局,关键日期accent高亮
- 提醒区域:圆点指示器+过期动画pulse+今天到期文案
- 员工信息:独立分区带底部分隔线
This commit is contained in:
freedakgmail
2026-08-18 21:50:55 +08:00
parent 9ac07eba0f
commit a7df4fedfc
+121 -77
View File
@@ -84,10 +84,17 @@ const STATUS_COLORS: Record<string, string> = {
RESOLVED: 'bg-gray-100 text-gray-500', RESOLVED: 'bg-gray-100 text-gray-500',
} }
const ALERT_STYLES: Record<string, { border: string; badge: string; text: string }> = { const ALERT_STYLES: Record<string, { border: string; badge: string; text: string; bg: string; dot: string }> = {
red: { border: 'border-l-4 border-l-red-500', badge: 'bg-red-100 text-red-700', text: 'text-red-600' }, red: { border: 'border-l-4 border-l-red-500', badge: 'bg-red-100 text-red-700', text: 'text-red-600', bg: 'bg-red-50', dot: 'bg-red-500' },
yellow: { border: 'border-l-4 border-l-amber-500', badge: 'bg-amber-100 text-amber-700', text: 'text-amber-600' }, yellow: { border: 'border-l-4 border-l-amber-500', badge: 'bg-amber-100 text-amber-700', text: 'text-amber-600', bg: 'bg-amber-50', dot: 'bg-amber-500' },
green: { border: 'border-l-4 border-l-emerald-500', badge: 'bg-emerald-100 text-emerald-700', text: 'text-emerald-600' }, green: { border: 'border-l-4 border-l-emerald-500', badge: 'bg-emerald-100 text-emerald-700', text: 'text-emerald-600', bg: 'bg-emerald-50', dot: 'bg-emerald-500' },
}
const TYPE_ICON_BG: Record<string, string> = {
PREGNANCY: 'bg-pink-100 text-pink-600',
WORK_INJURY: 'bg-red-100 text-red-600',
MEDICAL_PERIOD: 'bg-orange-100 text-orange-600',
OTHER: 'bg-gray-100 text-gray-600',
} }
function formatDate(d: string | null): string { function formatDate(d: string | null): string {
@@ -260,32 +267,59 @@ export default function SpecialStatus() {
{/* 统计卡片 */} {/* 统计卡片 */}
{stats && ( {stats && (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
<div className="bg-white border border-gray-200 rounded-lg p-3"> <div className="bg-white border border-gray-200 rounded-xl p-3.5 flex items-center gap-3">
<div className="text-xs text-gray-500"></div> <div className="w-9 h-9 rounded-lg bg-gray-100 flex items-center justify-center shrink-0">
<div className="text-xl font-bold text-gray-900">{stats.total}</div> <AlertTriangle className="w-4 h-4 text-gray-500" />
</div>
<div className="min-w-0">
<div className="text-xs text-gray-500"></div>
<div className="text-xl font-bold text-gray-900 leading-tight">{stats.total}</div>
</div>
</div> </div>
<div className="bg-white border border-gray-200 rounded-lg p-3"> <div className="bg-white border border-gray-200 rounded-xl p-3.5 flex items-center gap-3">
<div className="text-xs text-gray-500"></div> <div className="w-9 h-9 rounded-lg bg-emerald-50 flex items-center justify-center shrink-0">
<div className="text-xl font-bold text-emerald-600">{stats.active}</div> <Activity className="w-4 h-4 text-emerald-600" />
</div>
<div className="min-w-0">
<div className="text-xs text-gray-500"></div>
<div className="text-xl font-bold text-emerald-600 leading-tight">{stats.active}</div>
</div>
</div> </div>
<div className="bg-white border border-gray-200 rounded-lg p-3"> <div className="bg-white border border-gray-200 rounded-xl p-3.5 flex items-center gap-3">
<div className="text-xs text-gray-500"></div> <div className="w-9 h-9 rounded-lg bg-amber-50 flex items-center justify-center shrink-0">
<div className="text-xl font-bold text-amber-600">{stats.pending}</div> <Clock className="w-4 h-4 text-amber-600" />
</div>
<div className="min-w-0">
<div className="text-xs text-gray-500"></div>
<div className="text-xl font-bold text-amber-600 leading-tight">{stats.pending}</div>
</div>
</div> </div>
<div className="bg-white border border-red-200 rounded-lg p-3"> <div className="bg-white border border-red-200 rounded-xl p-3.5 flex items-center gap-3">
<div className="text-xs text-gray-500"></div> <div className="w-9 h-9 rounded-lg bg-red-50 flex items-center justify-center shrink-0">
<div className="text-xl font-bold text-red-600">{stats.redAlert}</div> <AlertTriangle className="w-4 h-4 text-red-600" />
</div>
<div className="min-w-0">
<div className="text-xs text-gray-500"></div>
<div className="text-xl font-bold text-red-600 leading-tight">{stats.redAlert}</div>
</div>
</div> </div>
<div className="bg-white border border-amber-200 rounded-lg p-3"> <div className="bg-white border border-amber-200 rounded-xl p-3.5 flex items-center gap-3">
<div className="text-xs text-gray-500"></div> <div className="w-9 h-9 rounded-lg bg-amber-50 flex items-center justify-center shrink-0">
<div className="text-xl font-bold text-amber-600">{stats.yellowAlert}</div> <Clock className="w-4 h-4 text-amber-500" />
</div>
<div className="min-w-0">
<div className="text-xs text-gray-500"></div>
<div className="text-xl font-bold text-amber-600 leading-tight">{stats.yellowAlert}</div>
</div>
</div> </div>
<div className="bg-white border border-gray-200 rounded-lg p-3"> <div className="bg-white border border-gray-200 rounded-xl p-3.5">
<div className="text-xs text-gray-500 mb-1"></div> <div className="text-xs text-gray-500 mb-1.5"></div>
<div className="text-xs text-gray-700"> <div className="flex flex-wrap gap-1.5">
{stats.byType.map((t) => ( {stats.byType.map((t) => (
<span key={t.type} className="mr-2">{t.label}: <b>{t.count}</b></span> <span key={t.type} className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs bg-gray-50 text-gray-600">
{t.label}<b className="text-gray-900">{t.count}</b>
</span>
))} ))}
</div> </div>
</div> </div>
@@ -338,85 +372,94 @@ export default function SpecialStatus() {
const Icon = TYPE_ICONS[item.type] || AlertTriangle const Icon = TYPE_ICONS[item.type] || AlertTriangle
const alert = ALERT_STYLES[item.alertLevel] || ALERT_STYLES.green const alert = ALERT_STYLES[item.alertLevel] || ALERT_STYLES.green
const reminderDays = daysUntil(item.reminderDate) const reminderDays = daysUntil(item.reminderDate)
const isOverdue = reminderDays !== null && reminderDays < 0
return ( return (
<div key={item.id} className={`bg-white border ${alert.border} border-gray-200 rounded-lg p-4`}> <div key={item.id} className={`bg-white border ${alert.border} border-gray-200 rounded-xl p-4 hover:shadow-md transition-shadow group`}>
{/* 头部 */} {/* 头部:类型图标 + 状态 + 操作 */}
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2.5">
<span className={`px-2 py-0.5 rounded text-xs font-medium border ${TYPE_COLORS[item.type] || TYPE_COLORS.OTHER}`}> <div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${TYPE_ICON_BG[item.type] || TYPE_ICON_BG.OTHER}`}>
<Icon className="w-3 h-3 inline mr-1" /> <Icon className="w-4 h-4" />
{TYPE_LABELS[item.type] || item.type} </div>
</span> <div className="flex flex-col gap-1">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${STATUS_COLORS[item.status] || STATUS_COLORS.RESOLVED}`}> <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border w-fit ${TYPE_COLORS[item.type] || TYPE_COLORS.OTHER}`}>
{STATUS_LABELS[item.status] || item.status} {TYPE_LABELS[item.type] || item.type}
</span> </span>
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium w-fit ${STATUS_COLORS[item.status] || STATUS_COLORS.RESOLVED}`}>
{STATUS_LABELS[item.status] || item.status}
</span>
</div>
</div> </div>
<div className="flex gap-1"> <div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
<Link to={`/roster?employeeId=${item.employee.id}`} className="p-1 rounded text-gray-400 hover:text-primary hover:bg-gray-100" title="查看员工详情"> <Link to={`/roster?employeeId=${item.employee.id}`} className="p-1.5 rounded-md text-gray-400 hover:text-primary hover:bg-gray-100" title="查看员工详情">
<Eye className="w-3.5 h-3.5" /> <Eye className="w-3.5 h-3.5" />
</Link> </Link>
<button onClick={() => handleOpenEdit(item)} className="p-1 rounded text-gray-400 hover:text-primary hover:bg-gray-100" title="编辑"> <button onClick={() => handleOpenEdit(item)} className="p-1.5 rounded-md text-gray-400 hover:text-primary hover:bg-gray-100" title="编辑">
<Edit2 className="w-3.5 h-3.5" /> <Edit2 className="w-3.5 h-3.5" />
</button> </button>
<button onClick={() => setDeleteTarget(item)} className="p-1 rounded text-gray-400 hover:text-red-500 hover:bg-gray-100" title="删除"> <button onClick={() => setDeleteTarget(item)} className="p-1.5 rounded-md text-gray-400 hover:text-red-500 hover:bg-gray-100" title="删除">
<Trash2 className="w-3.5 h-3.5" /> <Trash2 className="w-3.5 h-3.5" />
</button> </button>
</div> </div>
</div> </div>
{/* 员工信息 */} {/* 员工信息 */}
<div className="mb-3"> <div className="mb-3 pb-3 border-b border-gray-100">
<Link to={`/roster?employeeId=${item.employee.id}`} className="font-medium text-gray-900 hover:text-primary hover:underline">{item.employee.name}</Link> <div className="flex items-center gap-2">
{item.employee.idCardNumber && <span className="text-xs text-gray-400 font-mono ml-1">{item.employee.idCardNumber}</span>} <Link to={`/roster?employeeId=${item.employee.id}`} className="font-medium text-gray-900 hover:text-primary hover:underline">{item.employee.name}</Link>
<span className="text-sm text-gray-500 ml-2">{item.employee.department}</span> {item.employee.idCardNumber && <span className="text-xs text-gray-400 font-mono">{item.employee.idCardNumber}</span>}
{item.employee.status === 'RESIGNED' && ( {item.employee.status === 'RESIGNED' && (
<span className="ml-2 text-xs text-gray-400"></span> <span className="text-xs text-gray-400 px-1.5 py-0.5 rounded bg-gray-100"></span>
)} )}
</div>
<div className="text-xs text-gray-500 mt-0.5">{item.employee.department}</div>
</div> </div>
{/* 关键日期 */} {/* 关键日期 */}
<div className="space-y-1.5 text-sm"> <div className="space-y-2">
{item.type === 'PREGNANCY' && ( {item.type === 'PREGNANCY' && (
<> <div className="grid grid-cols-2 gap-x-3 gap-y-2">
{item.expectedDueDate && <Row label="预产期" value={formatDate(item.expectedDueDate)} />} {item.expectedDueDate && <Field label="预产期" value={formatDate(item.expectedDueDate)} accent="pink" />}
{item.maternityLeaveStart && <Row label="产假开始" value={formatDate(item.maternityLeaveStart)} />} {item.maternityLeaveStart && <Field label="产假开始" value={formatDate(item.maternityLeaveStart)} />}
{item.maternityLeaveEnd && <Row label="产假结束" value={formatDate(item.maternityLeaveEnd)} />} {item.maternityLeaveEnd && <Field label="产假结束" value={formatDate(item.maternityLeaveEnd)} />}
{item.nursingEndDate && <Row label="哺乳期截止" value={formatDate(item.nursingEndDate)} />} {item.nursingEndDate && <Field label="哺乳期截止" value={formatDate(item.nursingEndDate)} />}
</> </div>
)} )}
{item.type === 'WORK_INJURY' && ( {item.type === 'WORK_INJURY' && (
<> <div className="space-y-2">
{item.injuryDate && <Row label="受伤日期" value={formatDate(item.injuryDate)} />} {item.injuryDate && <Field label="受伤日期" value={formatDate(item.injuryDate)} accent="red" />}
{item.injuryDescription && <Row label="伤情描述" value={item.injuryDescription} />} {item.injuryDescription && <Field label="伤情描述" value={item.injuryDescription} fullWidth />}
{item.certificationDate ? ( {item.certificationDate ? (
<Row label="认定日期" value={formatDate(item.certificationDate)} /> <Field label="认定日期" value={formatDate(item.certificationDate)} />
) : ( ) : (
<Row label="认定日期" value="待认定" valueClass="text-amber-600" /> <Field label="认定日期" value="待认定" valueClass="text-amber-600" />
)} )}
{item.assessmentDate && <Row label="鉴定日期" value={formatDate(item.assessmentDate)} />} <div className="grid grid-cols-2 gap-x-3 gap-y-2">
{item.disabilityLevel && <Row label="伤残等级" value={`${item.disabilityLevel}`} />} {item.assessmentDate && <Field label="鉴定日期" value={formatDate(item.assessmentDate)} />}
</> {item.disabilityLevel && <Field label="伤残等级" value={`${item.disabilityLevel}`} />}
</div>
</div>
)} )}
{item.type === 'MEDICAL_PERIOD' && ( {item.type === 'MEDICAL_PERIOD' && (
<> <div className="grid grid-cols-2 gap-x-3 gap-y-2">
{item.startDate && <Row label="开始日期" value={formatDate(item.startDate)} />} {item.startDate && <Field label="开始日期" value={formatDate(item.startDate)} accent="orange" />}
{item.medicalMonths && <Row label="医疗期" value={`${item.medicalMonths}个月`} />} {item.medicalMonths && <Field label="医疗期" value={`${item.medicalMonths}个月`} />}
{item.medicalPeriodEnd && <Row label="截止日期" value={formatDate(item.medicalPeriodEnd)} />} {item.medicalPeriodEnd && <Field label="截止日期" value={formatDate(item.medicalPeriodEnd)} />}
</> </div>
)} )}
{item.type === 'OTHER' && ( {item.type === 'OTHER' && (
<> <div className="grid grid-cols-2 gap-x-3 gap-y-2">
{item.startDate && <Row label="开始日期" value={formatDate(item.startDate)} />} {item.startDate && <Field label="开始日期" value={formatDate(item.startDate)} />}
{item.endDate && <Row label="预计结束" value={formatDate(item.endDate)} />} {item.endDate && <Field label="预计结束" value={formatDate(item.endDate)} />}
</> </div>
)} )}
</div> </div>
{/* 提醒 */} {/* 提醒 */}
{item.status === 'ACTIVE' && reminderDays !== null && ( {item.status === 'ACTIVE' && reminderDays !== null && (
<div className={`mt-3 pt-3 border-t border-gray-100 flex items-center gap-1.5 text-xs ${alert.text}`}> <div className={`mt-3 pt-3 border-t border-gray-100 flex items-center gap-2 text-xs ${alert.text}`}>
<Clock className="w-3.5 h-3.5" /> <div className={`w-2 h-2 rounded-full ${alert.dot} ${isOverdue ? 'animate-pulse' : ''}`} />
{reminderDays < 0 ? `已过期 ${Math.abs(reminderDays)}` : `距提醒日还有 ${reminderDays}`} {isOverdue ? `已过期 ${Math.abs(reminderDays)}` : reminderDays === 0 ? '今天到期' : `距提醒日还有 ${reminderDays}`}
</div> </div>
)} )}
{item.description && ( {item.description && (
@@ -634,12 +677,13 @@ export default function SpecialStatus() {
) )
} }
/** 信息组件 */ /** 信息字段组件 */
function Row({ label, value, valueClass }: { label: string; value: string; valueClass?: string }) { function Field({ label, value, valueClass, accent, fullWidth }: { label: string; value: string; valueClass?: string; accent?: 'pink' | 'red' | 'orange'; fullWidth?: boolean }) {
const accentColor = accent === 'pink' ? 'text-pink-600' : accent === 'red' ? 'text-red-600' : accent === 'orange' ? 'text-orange-600' : 'text-gray-400'
return ( return (
<div className="flex items-center justify-between"> <div className={fullWidth ? 'col-span-2' : ''}>
<span className="text-gray-500 text-xs">{label}</span> <div className="text-[11px] text-gray-400 mb-0.5">{label}</div>
<span className={`text-gray-900 text-xs font-medium ${valueClass || ''}`}>{value}</span> <div className={`text-xs font-medium ${valueClass || 'text-gray-900'}`}>{value}</div>
</div> </div>
) )
} }