- 全量替换游戏感Neo-Brutalist为政务专业风:圆角2-16px、薄灰边、柔阴影 - Tailwind主题:primary 10阶调色板 + Noto Sans SC主字体 + fade-in-up动画 - 全局CSS:浅灰F6F8F9背景 + 主色焦点环和滚动条 + card/chip工具类 - 品牌组件Brand.tsx:云图形截断防云盘旧字泄露 + 新标题组合 - 6基础组件 + 8页面:Layout深绿侧栏 Login卡片 Export去五色卡 Activities/Hours/Users 用户端Index/Dashboard - 数据链路回归:u/2张三2h明细正确显示 - tsc无错误 vite build成功
This commit is contained in:
@@ -37,7 +37,6 @@ type Category = {
|
||||
subtitle: string;
|
||||
desc: string;
|
||||
icon: ReactNode;
|
||||
accent: string;
|
||||
};
|
||||
|
||||
const CATEGORIES: Category[] = [
|
||||
@@ -47,8 +46,7 @@ const CATEGORIES: Category[] = [
|
||||
title: '部员名册',
|
||||
subtitle: 'Users',
|
||||
desc: '导出所有部员的班级、姓名、义工号与所属工作组',
|
||||
icon: <IconUsers size={32} />,
|
||||
accent: 'from-blue-500 to-blue-700',
|
||||
icon: <IconUsers size={28} />,
|
||||
},
|
||||
{
|
||||
key: 'include_activities',
|
||||
@@ -56,8 +54,7 @@ const CATEGORIES: Category[] = [
|
||||
title: '活动列表',
|
||||
subtitle: 'Activities',
|
||||
desc: '导出所有活动的标题、时间、参与工作组',
|
||||
icon: <IconCalendar size={32} />,
|
||||
accent: 'from-amber-500 to-amber-700',
|
||||
icon: <IconCalendar size={28} />,
|
||||
},
|
||||
{
|
||||
key: 'include_hours',
|
||||
@@ -65,8 +62,7 @@ const CATEGORIES: Category[] = [
|
||||
title: '义工时长明细',
|
||||
subtitle: 'All Hours',
|
||||
desc: '所有部员在所有活动中的时长登记记录(含备注)',
|
||||
icon: <IconTimer size={32} />,
|
||||
accent: 'from-black to-gray-800',
|
||||
icon: <IconTimer size={28} />,
|
||||
},
|
||||
{
|
||||
key: 'include_user_totals',
|
||||
@@ -74,8 +70,7 @@ const CATEGORIES: Category[] = [
|
||||
title: '部员总时长',
|
||||
subtitle: 'User Totals',
|
||||
desc: '所有部员各自的累计总时长与活动数(按时长降序排名)',
|
||||
icon: <IconClock size={32} />,
|
||||
accent: 'from-green-500 to-green-700',
|
||||
icon: <IconClock size={28} />,
|
||||
},
|
||||
{
|
||||
key: 'include_user_activities',
|
||||
@@ -83,8 +78,7 @@ const CATEGORIES: Category[] = [
|
||||
title: '部员活动明细',
|
||||
subtitle: 'User Activities',
|
||||
desc: '每位部员参与过的所有活动明细(活动较多时文件较大)',
|
||||
icon: <IconEye size={32} />,
|
||||
accent: 'from-red-500 to-red-700',
|
||||
icon: <IconEye size={28} />,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -222,13 +216,13 @@ export default function Export() {
|
||||
<div className="space-y-6">
|
||||
<header className="mb-2">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="shrink-0 p-3 bg-black text-white shadow-lg shadow">
|
||||
<IconExport size={32} />
|
||||
<div className="shrink-0 p-3 bg-primary-50 text-primary-600 rounded-lg shadow-sm">
|
||||
<IconExport size={28} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl md:text-3xl font-black tracking-tight">数据导出中心</h1>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
勾选需要的导出类别(已选 <span className="font-black text-blue-700">{count}</span> 项),
|
||||
<h1 className="section-title">数据导出中心</h1>
|
||||
<p className="muted mt-1">
|
||||
勾选需要的导出类别(已选 <span className="font-semibold text-primary-600">{count}</span> 项),
|
||||
生成预览后一键导出为 Excel 表格或 PNG 图片
|
||||
</p>
|
||||
</div>
|
||||
@@ -241,31 +235,33 @@ export default function Export() {
|
||||
return (
|
||||
<label
|
||||
key={c.key}
|
||||
className={`group relative block cursor-pointer transition-all duration-150 ${active ? '-translate-x-0.5 -translate-y-0.5' : 'hover:-translate-x-px hover:-translate-y-px'}`}
|
||||
className={`group relative block cursor-pointer transition-all duration-150 ${active ? '' : 'hover:shadow-md'}`}
|
||||
style={{ animationDelay: `${idx * 40}ms` }}
|
||||
>
|
||||
<input type="checkbox" className="sr-only" checked={active} onChange={() => toggle(c.key)} />
|
||||
<div className={`h-full border-2 ${active ? 'border-black shadow-lg shadow bg-white' : 'border-black bg-white shadow-sm hover:shadow'}`}>
|
||||
<div className="relative overflow-hidden">
|
||||
<div className={`flex items-start gap-4 p-4 pb-3 bg-gradient-to-br ${active ? c.accent : 'from-gray-50 to-gray-100'}`}>
|
||||
<div className={`shrink-0 p-3 border-2 ${active ? 'border-white text-white' : 'border-black bg-white text-black shadow-sm'}`}>
|
||||
<div className={`h-full card transition-all ${active ? 'border-primary-500 ring-1 ring-primary-500/20' : ''}`}>
|
||||
<div className="relative overflow-hidden rounded-lg">
|
||||
<div className={`flex items-start gap-4 p-4 pb-3 ${active ? 'bg-primary-50' : 'bg-gray-50/70 border-b border-gray-100'}`}>
|
||||
<div className={`shrink-0 p-2.5 rounded-md border transition-all ${active ? 'border-primary-300 bg-white text-primary-600 shadow-sm' : 'border-gray-200 bg-white text-gray-700'}`}>
|
||||
{c.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`text-xs font-mono font-bold tracking-wider ${active ? 'text-white/85' : 'text-gray-500'}`}>- {c.tag} -</div>
|
||||
<div className={`text-lg md:text-xl font-black tracking-tight leading-tight ${active ? 'text-white' : 'text-black'}`}>{c.title}</div>
|
||||
<div className={`text-[11px] md:text-xs uppercase font-bold tracking-wider mt-0.5 ${active ? 'text-white/85' : 'text-gray-500'} font-display italic`}>
|
||||
<div className="flex items-center gap-2 mb-0.5">
|
||||
<span className={`chip-primary`}>{c.tag}</span>
|
||||
</div>
|
||||
<div className={`text-lg font-semibold tracking-tight leading-tight ${active ? 'text-gray-900' : 'text-gray-800'}`}>{c.title}</div>
|
||||
<div className={`text-[11px] md:text-xs font-mono tracking-wider mt-0.5 ${active ? 'text-primary-600' : 'text-gray-500'}`}>
|
||||
{c.subtitle}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1 shrink-0">
|
||||
<div className={`w-7 h-7 grid place-items-center border-2 transition-all ${active ? 'border-white text-black bg-white' : 'border-black text-white'}`}>
|
||||
{active ? 'OK' : ''}
|
||||
<div className={`w-6 h-6 grid place-items-center rounded border transition-all ${active ? 'border-primary-500 text-white bg-primary-500' : 'border-gray-300 text-transparent bg-white'}`}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 pt-3 bg-white">
|
||||
<div className="text-sm text-gray-700 leading-relaxed">{c.desc}</div>
|
||||
<div className="text-sm text-gray-600 leading-relaxed">{c.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -274,9 +270,9 @@ export default function Export() {
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="paper p-4 md:p-5 bg-white">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-3 mb-4">
|
||||
<div className="text-sm text-gray-600">
|
||||
<section className="card p-4 md:p-5">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
||||
<div className="muted">
|
||||
操作:先勾选上方类别卡片,生成预览后导出文件。
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap justify-end">
|
||||
@@ -294,55 +290,55 @@ export default function Export() {
|
||||
</section>
|
||||
|
||||
{preview && (
|
||||
<div ref={previewRef} className="paper bg-white p-3 md:p-6 space-y-8">
|
||||
<div className="border-b-2 border-black pb-4 text-center">
|
||||
<div className="font-display text-3xl md:text-4xl font-black tracking-tight italic">Volunteer Data Report</div>
|
||||
<div className="text-sm text-gray-600 mt-1">义工数据汇总导出 | 生成时间:{new Date().toLocaleString('zh-CN')}</div>
|
||||
<div ref={previewRef} className="card bg-white p-3 md:p-6 space-y-8">
|
||||
<div className="border-b border-gray-200 pb-4 text-center">
|
||||
<div className="text-xl md:text-2xl font-semibold tracking-tight text-gray-800">宣传部义工时长记录系统 · 数据导出报告</div>
|
||||
<div className="muted mt-1.5">义工数据汇总导出 | 生成时间:{new Date().toLocaleString('zh-CN')}</div>
|
||||
</div>
|
||||
|
||||
{preview.users?.length ? (
|
||||
<section className="animate-stagger-in">
|
||||
<h3 className="font-black text-xl mb-3 border-l-4 border-blue-600 pl-3 inline-flex items-center gap-2">
|
||||
<span className="chip">01</span> 部员名册({preview.users.length} 人)
|
||||
<section className="animate-fade-in-up">
|
||||
<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">01</span> 部员名册({preview.users.length} 人)
|
||||
</h3>
|
||||
<MiniTable heads={['序号', '班级', '姓名', '义工号', '工作组']} rows={preview.users.map((u, i) => [i + 1, u.class, u.name, u.volunteer_no || '-', u.group_name || '-'])} />
|
||||
</section>
|
||||
) : null}
|
||||
{preview.activities?.length ? (
|
||||
<section className="animate-stagger-in">
|
||||
<h3 className="font-black text-xl mb-3 border-l-4 border-amber-600 pl-3 inline-flex items-center gap-2">
|
||||
<span className="chip">02</span> 活动列表({preview.activities.length} 个)
|
||||
<section className="animate-fade-in-up">
|
||||
<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">02</span> 活动列表({preview.activities.length} 个)
|
||||
</h3>
|
||||
<MiniTable heads={['序号', '活动标题', '时间', '参与工作组']} rows={preview.activities.map((a, i) => [i + 1, a.title, formatDateRange(a.start_time, a.end_time), a.groups_name || '全部'])} />
|
||||
</section>
|
||||
) : null}
|
||||
{preview.hours?.length ? (
|
||||
<section className="animate-stagger-in">
|
||||
<h3 className="font-black text-xl mb-3 border-l-4 border-black pl-3 inline-flex items-center gap-2">
|
||||
<span className="chip">03</span> 全部时长明细({preview.hours.length} 条)
|
||||
<section className="animate-fade-in-up">
|
||||
<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">03</span> 全部时长明细({preview.hours.length} 条)
|
||||
</h3>
|
||||
<MiniTable heads={['班级', '姓名', '活动', '时长(h)', '备注']} rows={preview.hours.map((h: any) => [h.class, h.name, h.activity_title, Number(h.hours || 0).toFixed(2), h.remark || '-'])} />
|
||||
</section>
|
||||
) : null}
|
||||
{preview.user_totals?.length ? (
|
||||
<section className="animate-stagger-in">
|
||||
<h3 className="font-black text-xl mb-3 border-l-4 border-green-600 pl-3 inline-flex items-center gap-2">
|
||||
<span className="chip">04</span> 部员总时长排行
|
||||
<section className="animate-fade-in-up">
|
||||
<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">04</span> 部员总时长排行
|
||||
</h3>
|
||||
<MiniTable heads={['排名', '班级', '姓名', '工作组', '活动数', '累计时长(h)']} rows={preview.user_totals.map((u, i) => [i + 1, u.class, u.name, u.group_name || '-', u.activity_count, Number(u.total_hours || 0).toFixed(2)])} />
|
||||
</section>
|
||||
) : null}
|
||||
{preview.user_activities?.length ? (
|
||||
<section className="animate-stagger-in">
|
||||
<h3 className="font-black text-xl mb-3 border-l-4 border-red-600 pl-3 inline-flex items-center gap-2">
|
||||
<span className="chip">05</span> 部员参加的活动
|
||||
<section className="animate-fade-in-up">
|
||||
<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">05</span> 部员参加的活动
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{preview.user_activities.map(u => (
|
||||
<div key={u.id} className="border-2 border-black bg-gray-50 shadow-sm">
|
||||
<div className="bg-black text-white px-4 py-2 border-b-2 border-black font-black flex items-center justify-between flex-wrap gap-2">
|
||||
<span className="font-black">{u.class} {u.name}{u.group_name ? ` (${u.group_name})` : ''}</span>
|
||||
<span className="text-xs font-bold text-gray-300 font-mono tracking-wider">共 {u.activities.length} 次活动</span>
|
||||
<div key={u.id} className="card overflow-hidden">
|
||||
<div className="bg-[#275354] text-white px-4 py-2.5 border-b border-gray-200 font-semibold flex items-center justify-between flex-wrap gap-2">
|
||||
<span>{u.class} {u.name}{u.group_name ? ` (${u.group_name})` : ''}</span>
|
||||
<span className="text-xs font-medium text-white/85 font-mono tracking-wider">共 {u.activities.length} 次活动</span>
|
||||
</div>
|
||||
{u.activities.length === 0 ? (
|
||||
<div className="p-3 text-sm text-gray-500">未参与任何活动</div>
|
||||
@@ -362,12 +358,12 @@ export default function Export() {
|
||||
|
||||
function MiniTable({ heads, rows }: { heads: string[]; rows: any[][] }) {
|
||||
return (
|
||||
<div className="w-full overflow-x-auto border-2 border-black bg-white">
|
||||
<div className="w-full overflow-x-auto border border-gray-200 bg-white rounded-lg">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-100 border-b-2 border-black">
|
||||
<tr className="bg-gray-100 border-b border-gray-200">
|
||||
{heads.map((h, i) => (
|
||||
<th key={i} className="px-3 py-2.5 text-left font-black border border-gray-200 whitespace-nowrap bg-gray-900 text-white">
|
||||
<th key={i} className="px-3 py-2.5 text-left font-semibold whitespace-nowrap text-gray-800">
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
@@ -375,9 +371,9 @@ function MiniTable({ heads, rows }: { heads: string[]; rows: any[][] }) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r, ri) => (
|
||||
<tr key={ri} className={ri % 2 ? 'bg-gray-50/70 border-b border-gray-200 hover:bg-amber-50 transition-colors' : 'bg-white border-b border-gray-200 hover:bg-amber-50 transition-colors'}>
|
||||
<tr key={ri} className={`border-b border-gray-200 ${ri % 2 ? 'bg-gray-50' : 'bg-white'} hover:bg-primary-50 transition-colors`}>
|
||||
{r.map((c, ci) => (
|
||||
<td key={ci} className="px-3 py-2 border border-gray-200 whitespace-nowrap">
|
||||
<td key={ci} className="px-3 py-2 whitespace-nowrap text-gray-700">
|
||||
{c}
|
||||
</td>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user