Deploy to Server / deploy (push) Canceled after 14m40s
- 全量替换游戏感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成功
97 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|