From a84a8b19acef01eb149863e107ddff6283ad8fea Mon Sep 17 00:00:00 2001 From: Trae Assistant Date: Sun, 4 Oct 2026 23:17:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(export):=20=E6=96=B0=E5=A2=9E=E6=B4=BB?= =?UTF-8?q?=E5=8A=A8=E6=97=B6=E9=95=BF=E6=80=BB=E8=A1=A8=EF=BC=88=E7=9F=A9?= =?UTF-8?q?=E9=98=B5=EF=BC=89+=20=E7=A7=BB=E9=99=A4Logo=20-=20=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=20POST=20/api/export=20=E6=96=B0=E5=A2=9E=20include?= =?UTF-8?q?=5Factivity=5Fmatrix=EF=BC=9A=E9=83=A8=E5=91=98=C3=97=E6=B4=BB?= =?UTF-8?q?=E5=8A=A8=E7=9F=A9=E9=98=B5=E8=A1=8C=EF=BC=8C=E5=88=97=3D?= =?UTF-8?q?=E6=89=80=E6=9C=89=E6=B4=BB=E5=8A=A8=EF=BC=88start=5Ftime=20DES?= =?UTF-8?q?C=E6=8E=92=EF=BC=89=EF=BC=8C=5Ftotal=E5=B0=BE=E5=88=97=20-=20?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=20Export=20=E6=96=B0=E5=A2=9E=2006=20?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E3=80=8C=E6=B4=BB=E5=8A=A8=E6=97=B6=E9=95=BF?= =?UTF-8?q?=E6=80=BB=E8=A1=A8=EF=BC=88=E7=9F=A9=E9=98=B5=EF=BC=89=E3=80=8D?= =?UTF-8?q?=EF=BC=9A=E9=83=A8=E5=91=98+=E4=B9=89=E5=B7=A5=E5=8F=B7=20?= =?UTF-8?q?=E5=B7=A6=E5=9B=BA=E5=AE=9A=E5=88=97=EF=BC=8C=E6=B4=BB=E5=8A=A8?= =?UTF-8?q?=E5=88=97160px=E5=AE=BD=E8=A1=A8=E5=A4=B4=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E6=97=A5=E6=9C=9F=E5=89=AF=E6=A0=87=E9=A2=98=EF=BC=8C=E5=80=BC?= =?UTF-8?q?>0=E9=AB=98=E4=BA=AE=E4=B8=BB=E8=89=B2+=E6=B5=85=E5=BA=95?= =?UTF-8?q?=EF=BC=8C=E6=80=BB=E6=97=B6=E9=95=BFbg-primary-50=20-=20Excel?= =?UTF-8?q?=20=E6=96=B0=E5=A2=9E=E3=80=8C=E6=B4=BB=E5=8A=A8=E6=97=B6?= =?UTF-8?q?=E9=95=BF=E6=80=BB=E8=A1=A8(=E7=9F=A9=E9=98=B5)=E3=80=8DSheet?= =?UTF-8?q?=EF=BC=9A=E5=88=97=E5=90=8D=3D=E6=B4=BB=E5=8A=A8=E6=A0=87?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E6=9C=AB=E5=88=97=E6=80=BB=E6=97=B6=E9=95=BF?= =?UTF-8?q?=20-=20=E7=A7=BB=E9=99=A4Brand=E7=BB=84=E4=BB=B6=E6=89=80?= =?UTF-8?q?=E6=9C=89img=20Logo=E6=B8=B2=E6=9F=93=EF=BC=8C=E7=BA=AF?= =?UTF-8?q?=E6=A0=87=E9=A2=98=EF=BC=8C=E5=87=8F=E5=B0=8F=E4=BE=A7=E6=A0=8F?= =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E7=AB=AF=E5=A4=B4=E9=83=A8=E8=A7=86=E8=A7=89?= =?UTF-8?q?=E5=99=AA=E9=9F=B3=20-=20=E5=89=8D=E5=90=8E=E7=AB=AF=20tsc=20--?= =?UTF-8?q?noEmit=20exit=200=20+=20vite=20build=20exit=200=EF=BC=9B?= =?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=20E2E=20=E9=AA=8C=E8=AF=81=E7=9F=A9?= =?UTF-8?q?=E9=98=B5=E5=BC=A0=E4=B8=89/=E6=A0=A1=E5=9B=AD=E5=BF=97?= =?UTF-8?q?=E6=84=BF=E5=AE=A3=E4=BC=A0=E6=B4=BB=E5=8A=A8=3D2.00/=E6=80=BB?= =?UTF-8?q?=E6=97=B6=E9=95=BF=202.00=20h?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/Brand.tsx | 15 ++---- client/src/pages/admin/Export.tsx | 78 +++++++++++++++++++++++++++++++ client/src/types/index.ts | 4 ++ server/src/routes/export.ts | 33 ++++++++++++- 4 files changed, 118 insertions(+), 12 deletions(-) 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); });