import { ReactNode, useRef, useState } from 'react'; import Button from '../../components/Button'; import { exportApi, errMsg } from '../../api'; import { exportToExcel, exportTableToImage, formatDateRange, utc8DateStamp, utc8DateTimeStamp } from '../../utils/export'; import type { ExportResult } from '../../types'; import { IconExport, IconFileExcel, IconFileImage, IconUsers, IconCalendar, IconTimer, IconClock, IconEye, } from '../../components/Icons'; type Opts = { include_users: boolean; include_activities: boolean; include_hours: boolean; include_user_totals: boolean; include_user_activities: boolean; include_activity_matrix: boolean; }; const defaultOpts: Opts = { include_users: true, include_activities: true, include_hours: true, include_user_totals: true, include_user_activities: false, include_activity_matrix: false, }; type Category = { key: keyof Opts; tag: string; title: string; subtitle: string; desc: string; icon: ReactNode; }; const CATEGORIES: Category[] = [ { key: 'include_users', tag: '01', title: '部员名册', subtitle: 'Users', desc: '导出所有部员的班级、姓名、义工号与所属工作组', icon: , }, { key: 'include_activities', tag: '02', title: '活动列表', subtitle: 'Activities', desc: '导出所有活动的标题、时间、参与工作组', icon: , }, { key: 'include_hours', tag: '03', title: '义工时长明细', subtitle: 'All Hours', desc: '所有部员在所有活动中的时长登记记录(含备注)', icon: , }, { key: 'include_user_totals', tag: '04', title: '部员总时长', subtitle: 'User Totals', desc: '所有部员各自的累计总时长与活动数(按时长降序排名)', icon: , }, { key: 'include_user_activities', tag: '05', title: '部员活动明细', subtitle: 'User Activities', desc: '每位部员参与过的所有活动明细(活动较多时文件较大)', icon: , }, { key: 'include_activity_matrix', tag: '06', title: '活动时长总表(矩阵)', subtitle: 'Activity Matrix', desc: '行是部员(姓名+义工号),列是所有活动;多人同活动在同一列,展示每位部员的对应时长', icon: , }, ]; export default function Export() { const [opts, setOpts] = useState(defaultOpts); const [loading, setLoading] = useState(false); const [preview, setPreview] = useState(null); const previewRef = useRef(null); const toggle = (k: keyof Opts) => setOpts(o => ({ ...o, [k]: !o[k] })); const anySelected = Object.values(opts).some(Boolean); const count = Object.values(opts).filter(Boolean).length; const load = async () => { if (!anySelected) { alert('请至少选择一项导出内容'); return; } setLoading(true); setPreview(null); (async () => { try { const d = await exportApi.data(opts); setPreview(d); } catch (e) { alert(errMsg(e)); } finally { setLoading(false); } })(); }; const doExcel = () => { if (!preview) { alert('请先生成预览'); return; } const sheets: Array<{ name: string; data: any[] }> = []; if (preview.users?.length) { sheets.push({ name: '部员名册', data: preview.users.map((u, i) => ({ '序号': i + 1, '班级': u.class, '姓名': u.name, '义工号': u.volunteer_no || '-', '所属工作组': u.group_name || '-', })), }); } if (preview.activities?.length) { sheets.push({ name: '活动列表', data: preview.activities.map((a, i) => ({ '序号': i + 1, '活动标题': a.title, '内容': a.content || '', '开始时间': a.start_time, '结束时间': a.end_time, '参与工作组': a.groups_name || '', '创建时间': (a.created_at || '').slice(0, 16), })), }); } if (preview.hours?.length) { sheets.push({ name: '全部时长明细', data: preview.hours.map((h: any, i: number) => ({ '序号': i + 1, '班级': h.class, '姓名': h.name, '义工号': h.volunteer_no || '-', '工作组': h.group_name || '-', '活动名称': h.activity_title, '活动时间': formatDateRange(h.start_time, h.end_time), '时长(小时)': Number(h.hours || 0).toFixed(2), '备注': h.remark || '-', '登记时间': (h.created_at || '').slice(0, 16), })), }); } if (preview.user_totals?.length) { sheets.push({ name: '部员总时长', data: preview.user_totals.map((u, i) => ({ '排名': i + 1, '班级': u.class, '姓名': u.name, '义工号': u.volunteer_no || '-', '工作组': u.group_name || '-', '参与活动数': u.activity_count, '累计时长(小时)': Number(u.total_hours || 0).toFixed(2), })), }); } if (preview.user_activities?.length) { const flat: any[] = []; preview.user_activities.forEach(u => { if (u.activities.length === 0) { flat.push({ '班级': u.class, '姓名': u.name, '义工号': u.volunteer_no || '-', '工作组': u.group_name || '-', '活动名称': '(未参与任何活动)', '时间': '', '时长(小时)': '', '备注': '', }); } u.activities.forEach(a => { flat.push({ '班级': u.class, '姓名': u.name, '义工号': u.volunteer_no || '-', '工作组': u.group_name || '-', '活动名称': a.title, '时间': formatDateRange(a.start_time, a.end_time), '时长(小时)': Number(a.hours || 0).toFixed(2), '备注': a.remark || '-', }); }); }); 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, `义工数据-全部导出-${utc8DateStamp()}.xlsx`); }; const doImage = async () => { if (!previewRef.current) return; await exportTableToImage(previewRef.current, `义工数据-全部导出-${utc8DateStamp()}.png`); }; return (

数据导出中心

勾选需要的导出类别(已选 {count} 项), 生成预览后一键导出为 Excel 表格或 PNG 图片

{CATEGORIES.map((c, idx) => { const active = opts[c.key]; return ( ); })}
操作:先勾选上方类别卡片,生成预览后导出文件。
{preview && (
宣传部义工时长记录系统 · 数据导出报告
义工数据汇总导出 | 生成时间:{utc8DateTimeStamp()}
{preview.users?.length ? (

01 部员名册({preview.users.length} 人)

[i + 1, u.class, u.name, u.volunteer_no || '-', u.group_name || '-'])} />
) : null} {preview.activities?.length ? (

02 活动列表({preview.activities.length} 个)

[i + 1, a.title, formatDateRange(a.start_time, a.end_time), a.groups_name || '全部'])} />
) : null} {preview.hours?.length ? (

03 全部时长明细({preview.hours.length} 条)

[i + 1, h.class, h.name, h.volunteer_no || '-', h.group_name || '-', h.activity_title, Number(h.hours || 0).toFixed(2), h.remark || '-'])} />
) : null} {preview.user_totals?.length ? (

04 部员总时长排行

[i + 1, u.class, u.name, u.volunteer_no || '-', u.group_name || '-', u.activity_count, Number(u.total_hours || 0).toFixed(2)])} />
) : null} {preview.user_activities?.length ? (

05 部员参加的活动

{preview.user_activities.map(u => (
{u.class} {u.name} 义工号:{u.volunteer_no || '—'} {u.group_name ? 工作组:{u.group_name} : null}
共 {u.activities.length} 次活动
{u.activities.length === 0 ? (
未参与任何活动
) : ( [a.title, formatDateRange(a.start_time, a.end_time), Number(a.hours || 0).toFixed(2), a.remark || '-'])} /> )}
))}
) : 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)}
) : null}
)}
); } function MiniTable({ heads, rows }: { heads: string[]; rows: any[][] }) { return (
{heads.map((h, i) => ( ))} {rows.map((r, ri) => ( {r.map((c, ci) => ( ))} ))}
{h}
{c}
); }