Files
VolunteerManage-for-byw/client/src/pages/user/Dashboard.tsx
T
Trae Assistant 1a51a854a3
Deploy to Server / deploy (push) Canceled after 14m40s
feat(ui): 专业风UI改造 主色#275354 + Brand系统标题统一
- 全量替换游戏感Neo-Brutalist为政务专业风:圆角2-16px、薄灰边、柔阴影
- Tailwind主题:primary 10阶调色板 + Noto Sans SC主字体 + fade-in-up动画
- 全局CSS:浅灰F6F8F9背景 + 主色焦点环和滚动条 + card/chip工具类
- 品牌组件Brand.tsx:云图形截断防云盘旧字泄露 + 新标题组合
- 6基础组件 + 8页面:Layout深绿侧栏 Login卡片 Export去五色卡 Activities/Hours/Users 用户端Index/Dashboard
- 数据链路回归:u/2张三2h明细正确显示
- tsc无错误  vite build成功
2026-10-04 23:05:39 +08:00

97 lines
4.4 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import Button from '../../components/Button';
import DataTable from '../../components/DataTable';
import Brand from '../../components/Brand';
import { usersApi, errMsg } from '../../api';
import { formatDateRange } from '../../utils/export';
import type { UserDetail } from '../../types';
import { IconClock, IconArrowLeft } from '../../components/Icons';
export default function UserDashboard() {
const { id } = useParams();
const navigate = useNavigate();
const [data, setData] = useState<UserDetail | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
if (!id) return;
setLoading(true);
usersApi.detail(Number(id)).then(d => {
setData(d);
setLoading(false);
}).catch(e => {
setError(errMsg(e, '加载失败'));
setLoading(false);
});
}, [id]);
if (loading) return <div className="p-8 text-center text-gray-500">加载中...</div>;
if (error) return <div className="p-8 text-center"><p className="text-red-600 mb-4">{error}</p><Button onClick={() => navigate('/u')}>返回查询</Button></div>;
if (!data) return null;
return (
<div className="min-h-screen bg-paper px-4 py-6 md:py-10">
<div className="max-w-5xl mx-auto">
<div className="mb-5 flex items-center justify-between flex-wrap gap-2">
<Brand variant="dark" size="sm" showTitle={true} />
<Button variant="secondary" onClick={() => navigate('/u')}><span className="inline-flex items-center gap-2"><IconArrowLeft size={18} />返回查询</span></Button>
</div>
<div className="mb-5 flex items-start gap-2 animate-fade-in-up">
<div className="shrink-0 p-2 bg-primary-50 text-primary-600 rounded-md mt-0.5">
<IconClock size={26} />
</div>
<h1 className="section-title mt-1">个人义工时长总览</h1>
</div>
<div className="card p-5 mb-5 animate-fade-in-up">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<div className="muted mb-1">姓名</div>
<div className="text-lg font-semibold text-gray-800">{data.name}</div>
</div>
<div>
<div className="muted mb-1">班级</div>
<div className="text-lg font-semibold text-gray-800">{data.class || '—'}</div>
</div>
<div>
<div className="muted mb-1">义工号</div>
<div className="text-lg font-semibold text-gray-800">{data.volunteer_no || '—'}</div>
</div>
<div>
<div className="muted mb-1">所属工作组</div>
<div className="text-lg font-semibold text-gray-800">{data.group_name || '—'}</div>
</div>
</div>
<div className="mt-5 pt-5 border-t border-gray-200 grid grid-cols-2 gap-4">
<div className="card p-4 bg-primary-50/50">
<div className="muted mb-1">累计义工时长</div>
<div className="text-2xl md:text-3xl font-semibold text-primary-600">{Number(data.total_hours || 0).toFixed(2)} <span className="text-base font-medium text-gray-600">小时</span></div>
</div>
<div className="card p-4">
<div className="muted mb-1">参与活动数</div>
<div className="text-2xl md:text-3xl font-semibold text-gray-800">{data.activities.length} <span className="text-base font-medium text-gray-600">次</span></div>
</div>
</div>
</div>
<h2 className="font-semibold text-lg mb-3 text-gray-800">参与活动明细</h2>
<DataTable
id="user-activities-table"
columns={[
{ key: 'title', title: '活动名称', render: (r: any) => <span className="font-medium text-gray-800">{r.title}</span> },
{ key: 'time', title: '活动时间', render: (r: any) => formatDateRange(r.start_time, r.end_time) },
{ key: 'hours', title: '时长(小时)', render: (r: any) => <span className="font-semibold text-primary-600">{Number(r.hours || 0).toFixed(2)}</span> },
{ key: 'remark', title: '备注', render: (r: any) => r.remark || '—' },
]}
data={data.activities}
rowKey={(r: any) => r.id}
emptyText="暂无参与的活动记录"
/>
</div>
</div>
);
}