diff --git a/client/src/components/Brand.tsx b/client/src/components/Brand.tsx index 173edc1..2253baf 100644 --- a/client/src/components/Brand.tsx +++ b/client/src/components/Brand.tsx @@ -14,20 +14,13 @@ export default function Brand({ className = '', }: BrandProps) { const sizes = { - sm: { logo: 'w-[28px] h-auto', title: 'text-sm font-semibold' }, - md: { logo: 'w-[36px] h-auto', title: 'text-base font-bold' }, - lg: { logo: 'w-[44px] h-auto', title: 'text-xl font-bold tracking-tight' }, + sm: { title: 'text-sm font-semibold' }, + md: { title: 'text-base font-bold' }, + lg: { title: 'text-xl font-bold tracking-tight' }, }[size]; return ( -
-
- 宣传部义工 -
+
{showTitle && !condensed && ( , }, + { + key: 'include_activity_matrix', + tag: '06', + title: '活动时长总表(矩阵)', + subtitle: 'Activity Matrix', + desc: '行是部员(姓名+义工号),列是所有活动;多人同活动在同一列,展示每位部员的对应时长', + icon: , + }, ]; export default function Export() { @@ -204,6 +214,18 @@ export default function Export() { }); sheets.push({ name: '部员活动明细', data: flat }); } + if (preview.activity_matrix?.activities?.length && preview.activity_matrix?.users?.length) { + const { activities, users } = preview.activity_matrix; + const data = users.map((u) => { + const row: any = { '部员': u.name, '义工号': u.volunteer_no || '-' }; + for (const a of activities) { + row[a.title] = Number(u[String(a.id)] || 0).toFixed(2); + } + row['总时长'] = Number(u._total || 0).toFixed(2); + return row; + }); + sheets.push({ name: '活动时长总表(矩阵)', data }); + } exportToExcel(sheets, `义工数据-全部导出-${new Date().toISOString().slice(0, 10)}.xlsx`); }; @@ -350,6 +372,62 @@ export default function Export() {
) : null} + + {preview.activity_matrix?.activities?.length ? ( +
+

+ 06 活动时长总表(矩阵) + {preview.activity_matrix.users.length} 人 × {preview.activity_matrix.activities.length} 活动 +

+
+ + + + + + {preview.activity_matrix.activities.map((a, i) => ( + + ))} + + + + + {preview.activity_matrix.users.map((u, ri) => ( + + + + {preview.activity_matrix!.activities!.map((a, i) => { + const v = Number(u[String(a.id)] || 0); + const cls = v > 0 + ? 'text-primary-700 font-semibold bg-primary-50/40' + : 'text-gray-400'; + return ( + + ); + })} + + + ))} + +
部员义工号 +
{a.title}
+
{String(a.start_time || '').slice(0, 10)}
+
总时长
{u.name}{u.volunteer_no || '-'} + {v > 0 ? v.toFixed(2) : '—'} + + {Number(u._total || 0).toFixed(2)} h +
+
+
+ ) : null}
)} diff --git a/client/src/types/index.ts b/client/src/types/index.ts index db7927f..0ae1a9d 100644 --- a/client/src/types/index.ts +++ b/client/src/types/index.ts @@ -94,4 +94,8 @@ export interface ExportResult { hours?: any[]; user_totals?: UserTotal[]; user_activities?: UserActivities[]; + activity_matrix?: { + activities: Array<{ id: number; title: string; start_time: string }>; + users: Array>; + }; } diff --git a/server/src/routes/export.ts b/server/src/routes/export.ts index fadff45..c51b2d1 100644 --- a/server/src/routes/export.ts +++ b/server/src/routes/export.ts @@ -5,7 +5,7 @@ import { authMiddleware, AuthRequest } from '../middleware/auth'; const router = Router(); router.post('/', authMiddleware, (req: AuthRequest, res) => { - const { include_users, include_activities, include_hours, include_user_totals, include_user_activities } = req.body || {}; + const { include_users, include_activities, include_hours, include_user_totals, include_user_activities, include_activity_matrix } = req.body || {}; const db = getDb(); const result: any = {}; @@ -65,6 +65,37 @@ router.post('/', authMiddleware, (req: AuthRequest, res) => { return { ...u, activities: acts }; }); } + if (include_activity_matrix) { + const activities = db.prepareCompat(` + SELECT id, title, start_time FROM activities ORDER BY start_time DESC, id DESC + `).all() as any[]; + const users = db.prepareCompat(` + SELECT u.id, u.class, u.name, u.volunteer_no, g.name AS group_name + FROM users u LEFT JOIN work_groups g ON u.group_id = g.id + ORDER BY u.class, u.name + `).all() as any[]; + const hoursRows = db.prepareCompat(` + SELECT user_id, activity_id, hours FROM hours + `).all() as any[]; + const matrix: Record = {}; + for (const r of hoursRows) { + const key = `${r.user_id}|${r.activity_id}`; + matrix[key] = (matrix[key] || 0) + Number(r.hours || 0); + } + result.activity_matrix = { + activities, + users: users.map((u) => { + const row: Record = { id: u.id, class: u.class, name: u.name, volunteer_no: u.volunteer_no, group_name: u.group_name }; + for (const a of activities) { + row[String(a.id)] = Number((matrix[`${u.id}|${a.id}`] || 0).toFixed(2)); + } + row._total = Number( + activities.reduce((s: number, a: any) => s + (Number(row[String(a.id)]) || 0), 0).toFixed(2) + ); + return row; + }), + }; + } res.json(result); });