feat: 初始化义工时长管理系统
- 管理台:活动设置、时长登记、用户管理、数据导出(Excel/PNG) - 用户端:姓名查询个人义工时长与活动记录 - 技术栈:React + Vite + TailwindCSS / Node.js + Express + sql.js (SQLite) - 支持 Docker 部署,附带 Gitea Actions 自动部署工作流 - 响应式界面,兼容移动端与 PC 端 - 默认管理员账号:admin / admin123
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import Button from '../../components/Button';
|
||||
import DataTable from '../../components/DataTable';
|
||||
import { usersApi, errMsg } from '../../api';
|
||||
import { formatDateRange } from '../../utils/export';
|
||||
import type { UserDetail } from '../../types';
|
||||
|
||||
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-gray-50 px-4 py-6 md:py-10">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-5 flex-wrap gap-2">
|
||||
<h1 className="text-xl md:text-2xl font-black tracking-wide">🕒 个人义工时长总览</h1>
|
||||
<Button variant="secondary" onClick={() => navigate('/u')}>← 返回查询</Button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border-2 border-black p-5 mb-5">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 mb-1">姓名</div>
|
||||
<div className="text-lg font-bold">{data.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 mb-1">班级</div>
|
||||
<div className="text-lg font-bold">{data.class || '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 mb-1">义工号</div>
|
||||
<div className="text-lg font-bold">{data.volunteer_no || '—'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 mb-1">所属工作组</div>
|
||||
<div className="text-lg font-bold">{data.group_name || '—'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 pt-5 border-t-2 border-gray-200 grid grid-cols-2 gap-4">
|
||||
<div className="bg-gray-50 border-2 border-gray-200 p-4">
|
||||
<div className="text-xs text-gray-500 mb-1">累计义工时长</div>
|
||||
<div className="text-2xl md:text-3xl font-black text-black">{Number(data.total_hours || 0).toFixed(2)} <span className="text-base font-bold text-gray-600">小时</span></div>
|
||||
</div>
|
||||
<div className="bg-gray-50 border-2 border-gray-200 p-4">
|
||||
<div className="text-xs text-gray-500 mb-1">参与活动数</div>
|
||||
<div className="text-2xl md:text-3xl font-black text-black">{data.activities.length} <span className="text-base font-bold text-gray-600">次</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="font-bold text-lg mb-3">参与活动明细</h2>
|
||||
<DataTable
|
||||
id="user-activities-table"
|
||||
columns={[
|
||||
{ key: 'title', title: '活动名称', render: (r: any) => <span className="font-medium">{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-bold">{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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user