feat(export): 新增活动时长总表(矩阵)+ 移除Logo
Deploy to Server / deploy (push) Canceled after 14m38s

- 后端 POST /api/export 新增 include_activity_matrix:部员×活动矩阵行,列=所有活动(start_time DESC排),_total尾列
- 前端 Export 新增 06 卡片「活动时长总表(矩阵)」:部员+义工号 左固定列,活动列160px宽表头显示日期副标题,值>0高亮主色+浅底,总时长bg-primary-50
- Excel 新增「活动时长总表(矩阵)」Sheet:列名=活动标题,末列总时长
- 移除Brand组件所有img Logo渲染,纯标题,减小侧栏移动端头部视觉噪音
- 前后端 tsc --noEmit exit 0 + vite build exit 0;浏览器 E2E 验证矩阵张三/校园志愿宣传活动=2.00/总时长 2.00 h
This commit is contained in:
Trae Assistant
2026-10-04 23:17:19 +08:00
parent 1a51a854a3
commit a84a8b19ac
4 changed files with 118 additions and 12 deletions
+78
View File
@@ -20,6 +20,7 @@ type Opts = {
include_hours: boolean;
include_user_totals: boolean;
include_user_activities: boolean;
include_activity_matrix: boolean;
};
const defaultOpts: Opts = {
@@ -28,6 +29,7 @@ const defaultOpts: Opts = {
include_hours: true,
include_user_totals: true,
include_user_activities: false,
include_activity_matrix: false,
};
type Category = {
@@ -80,6 +82,14 @@ const CATEGORIES: Category[] = [
desc: '每位部员参与过的所有活动明细(活动较多时文件较大)',
icon: <IconEye size={28} />,
},
{
key: 'include_activity_matrix',
tag: '06',
title: '活动时长总表(矩阵)',
subtitle: 'Activity Matrix',
desc: '行是部员(姓名+义工号),列是所有活动;多人同活动在同一列,展示每位部员的对应时长',
icon: <IconExport size={28} />,
},
];
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() {
</div>
</section>
) : null}
{preview.activity_matrix?.activities?.length ? (
<section className="card p-5">
<h3 className="font-semibold text-lg mb-3 border-l-4 border-primary-500 pl-3 inline-flex items-center gap-2 text-gray-800">
<span className="chip-primary">06</span> 活动时长总表(矩阵)
<span className="chip ml-2">{preview.activity_matrix.users.length} 人 × {preview.activity_matrix.activities.length} 活动</span>
</h3>
<div className="overflow-x-auto">
<table className="min-w-full text-sm border-collapse border border-gray-200 rounded-lg bg-white">
<thead>
<tr className="bg-gray-100 border-b border-gray-200">
<th className="px-3 py-2.5 text-left font-semibold whitespace-nowrap border-r border-gray-200 text-gray-800 sticky left-0 bg-gray-100 z-10">部员</th>
<th className="px-3 py-2.5 text-left font-semibold whitespace-nowrap border-r border-gray-200 text-gray-800 sticky left-[96px] bg-gray-100 z-10">义工号</th>
{preview.activity_matrix.activities.map((a, i) => (
<th
key={a.id}
className={`px-3 py-2.5 text-center font-semibold whitespace-nowrap text-gray-800 ${i < preview.activity_matrix!.activities!.length - 1 ? 'border-r border-gray-200' : ''}`}
style={{ minWidth: 160 }}
title={`${a.title}\n${String(a.start_time || '').slice(0, 16)}`}
>
<div className="font-semibold">{a.title}</div>
<div className="mt-0.5 text-[11px] text-gray-500 font-normal">{String(a.start_time || '').slice(0, 10)}</div>
</th>
))}
<th className="px-3 py-2.5 text-center font-semibold whitespace-nowrap border-l border-gray-200 text-gray-800 bg-primary-50/60">总时长</th>
</tr>
</thead>
<tbody>
{preview.activity_matrix.users.map((u, ri) => (
<tr key={u.id} className={`border-b border-gray-200 ${ri % 2 ? 'bg-gray-50' : 'bg-white'} hover:bg-primary-50 transition-colors`}>
<td className="px-3 py-2 whitespace-nowrap border-r border-gray-200 font-medium text-gray-800 sticky left-0 z-10" style={{ background: ri % 2 ? '#f9fafb' : '#fff' }}>{u.name}</td>
<td className="px-3 py-2 whitespace-nowrap border-r border-gray-200 font-mono text-xs text-gray-600 sticky left-[96px] z-10" style={{ background: ri % 2 ? '#f9fafb' : '#fff' }}>{u.volunteer_no || '-'}</td>
{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 (
<td
key={a.id}
className={`px-3 py-2 text-center whitespace-nowrap ${cls} ${i < preview.activity_matrix!.activities!.length - 1 ? 'border-r border-gray-200/70' : ''}`}
>
{v > 0 ? v.toFixed(2) : '—'}
</td>
);
})}
<td className="px-3 py-2 whitespace-nowrap text-center border-l border-gray-200 font-semibold bg-primary-50/70 text-primary-700">
{Number(u._total || 0).toFixed(2)} h
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
) : null}
</div>
)}
</div>