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) => (
+
+ {a.title}
+ {String(a.start_time || '').slice(0, 10)}
+ |
+ ))}
+ 总时长 |
+
+
+
+ {preview.activity_matrix.users.map((u, ri) => (
+
+ | {u.name} |
+ {u.volunteer_no || '-'} |
+ {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 (
+
+ {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);
});