diff --git a/apps/web/app/admin/members/page.tsx b/apps/web/app/admin/members/page.tsx
index 6719a71..4977ff0 100644
--- a/apps/web/app/admin/members/page.tsx
+++ b/apps/web/app/admin/members/page.tsx
@@ -155,49 +155,51 @@ function MembersPageContent() {
-
+
{filteredMembers.length === 0 ? (
) : (
-
-
-
-
- | 姓名 |
- 用户名 |
- 角色 |
- 部门 |
- 企业邮箱 |
- 操作 |
-
-
-
- {filteredMembers.map((m) => (
-
- | {m.name} |
- {m.username ?? '-'} |
-
- {roleName(m.roleId)}
- |
- {departments.find((d) => d.id === m.departmentId)?.name ?? '-'} |
- {m.email} |
-
-
-
- {!isSystemAdminMember(m) && (
-
- )}
- {isSystemAdminMember(m) && (
- 系统账号
- )}
-
- |
+
+
+
+
+
+ | 姓名 |
+ 用户名 |
+ 角色 |
+ 部门 |
+ 企业邮箱 |
+ 操作 |
- ))}
-
-
+
+
+ {filteredMembers.map((m) => (
+
+ | {m.name} |
+ {m.username ?? '-'} |
+
+ {roleName(m.roleId)}
+ |
+ {departments.find((d) => d.id === m.departmentId)?.name ?? '-'} |
+ {m.email} |
+
+
+
+ {!isSystemAdminMember(m) && (
+
+ )}
+ {isSystemAdminMember(m) && (
+ 系统账号
+ )}
+
+ |
+
+ ))}
+
+
+
)}
diff --git a/apps/web/app/overtime/page.tsx b/apps/web/app/overtime/page.tsx
index d27ca89..4e90f7c 100644
--- a/apps/web/app/overtime/page.tsx
+++ b/apps/web/app/overtime/page.tsx
@@ -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,
diff --git a/apps/web/lib/member-page-header.test.ts b/apps/web/lib/member-page-header.test.ts
new file mode 100644
index 0000000..5d6bef3
--- /dev/null
+++ b/apps/web/lib/member-page-header.test.ts
@@ -0,0 +1,28 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import { join } from 'node:path';
+
+test('members table pins each header cell above scrolling rows', () => {
+ const source = readFileSync(join(process.cwd(), 'app/admin/members/page.tsx'), 'utf8');
+ const headerClassNames = Array.from(
+ source.matchAll(//g),
+ (match) => match[1],
+ );
+
+ assert.equal(headerClassNames.length, 6);
+ for (const className of headerClassNames) {
+ assert.match(className, /\bsticky\b/);
+ assert.match(className, /\btop-0\b/);
+ assert.match(className, /\bz-20\b/);
+ assert.match(className, /bg-\[var\(--bg-subtle\)\]/);
+ }
+});
+
+test('members table scrolls inside the table card instead of the padded page area', () => {
+ const source = readFileSync(join(process.cwd(), 'app/admin/members/page.tsx'), 'utf8');
+
+ assert.match(source, /className="flex-1 overflow-hidden bg-\[var\(--bg\)\] px-5 py-4"/);
+ assert.match(source, /className="h-full overflow-hidden rounded-2xl border border-\[var\(--line\)\] bg-\[var\(--bg-card\)\] shadow-\[var\(--shadow-sm\)\]"/);
+ assert.match(source, /className="h-full overflow-y-auto"/);
+});
diff --git a/apps/web/lib/overtime.test.ts b/apps/web/lib/overtime.test.ts
index 9b6a7bf..a1c8928 100644
--- a/apps/web/lib/overtime.test.ts
+++ b/apps/web/lib/overtime.test.ts
@@ -1,12 +1,13 @@
-import assert from 'node:assert/strict';
import test from 'node:test';
+import assert from 'node:assert/strict';
import {
+ filterOvertimeRecordsForExportMonth,
filterOvertimeRecordsForViewer,
type OvertimeRecord,
} from './overtime';
-import type { Department, Member, RoleItem } from './members';
import type { AuthUser } from './auth-user';
+import type { Department, Member, RoleItem } from './members';
const departments: Department[] = [
{ id: 'dept-tech', name: 'Tech', order: 1, createdAt: '2024-01-01' },
@@ -28,6 +29,29 @@ const records: OvertimeRecord[] = [
record('ot-pm', 'PM Carol'),
];
+const exportRecords: OvertimeRecord[] = [
+ {
+ id: 'july-1',
+ projectId: 'project-1',
+ person: 'Alice',
+ startTime: '2026-07-01T19:00:00',
+ endTime: '2026-07-01T21:00:00',
+ duration: 2,
+ reasonId: 'reason-1',
+ createdAt: '2026-07-01',
+ },
+ {
+ id: 'august-1',
+ projectId: 'project-1',
+ person: 'Bob',
+ startTime: '2026-08-02T19:00:00',
+ endTime: '2026-08-02T21:00:00',
+ duration: 2,
+ reasonId: 'reason-1',
+ createdAt: '2026-08-02',
+ },
+];
+
test('filterOvertimeRecordsForViewer limits non-admin viewers to their department tree', () => {
const visible = filterOvertimeRecordsForViewer(records, {
viewer: toAuthUser(members[0]),
@@ -50,6 +74,18 @@ test('filterOvertimeRecordsForViewer lets wildcard admin view every department',
assert.deepEqual(visible.map((item) => item.id), ['ot-front', 'ot-back', 'ot-pm']);
});
+test('filters export records by selected month', () => {
+ const result = filterOvertimeRecordsForExportMonth(exportRecords, '2026-07');
+
+ assert.deepEqual(result.map((record) => record.id), ['july-1']);
+});
+
+test('keeps all export records when all months are selected', () => {
+ const result = filterOvertimeRecordsForExportMonth(exportRecords, '');
+
+ assert.deepEqual(result.map((record) => record.id), ['july-1', 'august-1']);
+});
+
function member(id: string, name: string, departmentId: string, roleId: string): Member {
return {
id,
diff --git a/apps/web/lib/overtime.ts b/apps/web/lib/overtime.ts
index ee2ebe4..7b022d0 100644
--- a/apps/web/lib/overtime.ts
+++ b/apps/web/lib/overtime.ts
@@ -118,6 +118,14 @@ export function calcDuration(start: string, end: string): number {
return Math.round(total * 10) / 10;
}
+export function filterOvertimeRecordsForExportMonth(
+ records: OvertimeRecord[],
+ monthFilter: string,
+): OvertimeRecord[] {
+ if (!monthFilter) return [...records];
+ return records.filter((record) => record.startTime.slice(0, 7) === monthFilter);
+}
+
function collectDepartmentTreeIds(departments: Department[], departmentId: string): Set {
const ids = new Set([departmentId]);
let changed = true;
|