fix(前端): 修复成员表头与加班导出

关键改动:

- 成员管理表格改为卡片内滚动,并固定每个表头单元格

- 加班导出按当前月份筛选导出记录

- 增加成员表头与加班导出月份过滤测试

Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
Script Generator
2026-07-02 17:02:54 +08:00
parent 58b9f993ea
commit e906daf228
5 changed files with 117 additions and 41 deletions

View File

@@ -155,49 +155,51 @@ function MembersPageContent() {
</div>
</header>
<div className="flex-1 overflow-y-auto bg-[var(--bg)] px-5 py-4">
<div className="flex-1 overflow-hidden bg-[var(--bg)] px-5 py-4">
{filteredMembers.length === 0 ? (
<div className="rounded-2xl border border-dashed border-[var(--line)] bg-[var(--bg-card)] py-20 text-center">
<p className="text-[13px] font-medium text-[var(--ink-soft)]"></p>
</div>
) : (
<div className="rounded-2xl border border-[var(--line)] bg-[var(--bg-card)] shadow-[var(--shadow-sm)]">
<table className="w-full text-left text-[13px]">
<thead className="sticky top-0 z-10 bg-[var(--bg-subtle)]">
<tr className="border-b border-[var(--line)] bg-[var(--bg-subtle)]">
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-right text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
</tr>
</thead>
<tbody>
{filteredMembers.map((m) => (
<tr key={m.id} className="border-b border-[var(--line-soft)] last:border-0 hover:bg-[var(--bg-subtle)] transition-colors">
<td className="px-4 py-3 font-medium text-[var(--ink)]">{m.name}</td>
<td className="px-4 py-3 font-mono text-[var(--ink-soft)]">{m.username ?? '-'}</td>
<td className="px-4 py-3">
<span className="inline-flex items-center rounded-md bg-[var(--bg-subtle)] px-2 py-0.5 text-[11px] font-medium text-[var(--ink-soft)]">{roleName(m.roleId)}</span>
</td>
<td className="px-4 py-3 text-[var(--ink-soft)]">{departments.find((d) => d.id === m.departmentId)?.name ?? '-'}</td>
<td className="px-4 py-3 text-[var(--ink-soft)]">{m.email}</td>
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-1">
<button onClick={() => { setEditingMember(m); setShowMemberModal(true); }} className="h-6 px-2 rounded text-[11px] font-medium text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]"></button>
{!isSystemAdminMember(m) && (
<button onClick={() => deleteMember(m.id)} className="h-6 px-2 rounded text-[11px] font-medium text-red-500 hover:bg-red-50"></button>
)}
{isSystemAdminMember(m) && (
<span className="h-6 px-2 inline-flex items-center rounded text-[11px] font-medium text-amber-700 bg-amber-50"></span>
)}
</div>
</td>
<div className="h-full overflow-hidden rounded-2xl border border-[var(--line)] bg-[var(--bg-card)] shadow-[var(--shadow-sm)]">
<div className="h-full overflow-y-auto">
<table className="w-full text-left text-[13px]">
<thead className="bg-[var(--bg-subtle)]">
<tr className="bg-[var(--bg-subtle)]">
<th className="sticky top-0 z-20 border-b border-[var(--line)] bg-[var(--bg-subtle)] px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="sticky top-0 z-20 border-b border-[var(--line)] bg-[var(--bg-subtle)] px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="sticky top-0 z-20 border-b border-[var(--line)] bg-[var(--bg-subtle)] px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="sticky top-0 z-20 border-b border-[var(--line)] bg-[var(--bg-subtle)] px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="sticky top-0 z-20 border-b border-[var(--line)] bg-[var(--bg-subtle)] px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="sticky top-0 z-20 border-b border-[var(--line)] bg-[var(--bg-subtle)] px-4 py-2.5 text-right text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{filteredMembers.map((m) => (
<tr key={m.id} className="border-b border-[var(--line-soft)] last:border-0 hover:bg-[var(--bg-subtle)] transition-colors">
<td className="px-4 py-3 font-medium text-[var(--ink)]">{m.name}</td>
<td className="px-4 py-3 font-mono text-[var(--ink-soft)]">{m.username ?? '-'}</td>
<td className="px-4 py-3">
<span className="inline-flex items-center rounded-md bg-[var(--bg-subtle)] px-2 py-0.5 text-[11px] font-medium text-[var(--ink-soft)]">{roleName(m.roleId)}</span>
</td>
<td className="px-4 py-3 text-[var(--ink-soft)]">{departments.find((d) => d.id === m.departmentId)?.name ?? '-'}</td>
<td className="px-4 py-3 text-[var(--ink-soft)]">{m.email}</td>
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-1">
<button onClick={() => { setEditingMember(m); setShowMemberModal(true); }} className="h-6 px-2 rounded text-[11px] font-medium text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]"></button>
{!isSystemAdminMember(m) && (
<button onClick={() => deleteMember(m.id)} className="h-6 px-2 rounded text-[11px] font-medium text-red-500 hover:bg-red-50"></button>
)}
{isSystemAdminMember(m) && (
<span className="h-6 px-2 inline-flex items-center rounded text-[11px] font-medium text-amber-700 bg-amber-50"></span>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>

View File

@@ -8,7 +8,7 @@ import { useRequirementStore } from '@/stores/useRequirementStore';
import { useAuthStore } from '@/stores/useAuthStore';
import { useMemberStore } from '@/stores/useMemberStore';
import { flattenProjects, flattenVersions } from '@/lib/derive';
import { calcDuration, filterOvertimeRecordsForViewer } from '@/lib/overtime';
import { calcDuration, filterOvertimeRecordsForExportMonth, filterOvertimeRecordsForViewer } from '@/lib/overtime';
import type { OvertimeRecord } from '@/lib/overtime';
import { Pagination, usePagination } from '@/components/Pagination';
import { DictDrawer } from '@/components/requirement/DictDrawer';
@@ -133,7 +133,9 @@ function OvertimePageContent() {
const handleExport = () => {
const header = ['项目', '版本', '加班人', '开始时间', '结束时间', '时长(h)', '加班原因', '备注'];
const rows = filtered.map((r) => [
const exportRecords = filterOvertimeRecordsForExportMonth(visibleRecords, monthFilter)
.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
const rows = exportRecords.map((r) => [
projectName(r.projectId),
versionName(r.versionId),
r.person,