From 6b965429ce4d111afb9edc77e7e24770eefcf4a7 Mon Sep 17 00:00:00 2001 From: Script Generator Date: Fri, 26 Jun 2026 14:40:37 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(workspace):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E6=97=A5=E6=8A=A5=E6=B4=BE=E7=94=9F=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/lib/workspace-daily-report.test.ts | 123 ++++++++++++++++++++ apps/web/lib/workspace-daily-report.ts | 63 ++++++++++ 2 files changed, 186 insertions(+) create mode 100644 apps/web/lib/workspace-daily-report.test.ts create mode 100644 apps/web/lib/workspace-daily-report.ts diff --git a/apps/web/lib/workspace-daily-report.test.ts b/apps/web/lib/workspace-daily-report.test.ts new file mode 100644 index 0000000..ed19733 --- /dev/null +++ b/apps/web/lib/workspace-daily-report.test.ts @@ -0,0 +1,123 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import type { TaskWorklog } from './task-worklog'; +import type { WorkItem } from './workspace-engine'; +import { getWorkspaceDailyReport } from './workspace-daily-report'; + +const workItems: WorkItem[] = [ + { + id: 'task-1', + type: 'devTask', + title: '实现登录接口', + status: 'in_progress', + completed: false, + productName: 'FTB', + projectName: '项目管理', + versionName: 'V1.0', + versionId: 'version-1', + raw: {} as any, + }, + { + id: 'task-2', + type: 'bug', + title: '修复菜单错位', + status: 'fixing', + completed: false, + productName: 'FTB', + projectName: '项目管理', + versionName: 'V1.0', + versionId: 'version-1', + raw: {} as any, + }, +]; + +const worklogs: TaskWorklog[] = [ + { + id: 'wl-1', + taskId: 'task-1', + userId: '张三', + date: '2026-06-26', + hours: 1.5, + workContent: '完成登录接口联调', + createdAt: '2026-06-26T02:00:00.000Z', + }, + { + id: 'wl-2', + taskId: 'task-2', + userId: '张三', + date: '2026-06-26', + hours: 0.5, + workContent: '定位菜单错位原因', + createdAt: '2026-06-26T03:00:00.000Z', + }, + { + id: 'wl-other-user', + taskId: 'task-1', + userId: '李四', + date: '2026-06-26', + hours: 8, + workContent: '其他人的日报', + createdAt: '2026-06-26T04:00:00.000Z', + }, + { + id: 'wl-other-date', + taskId: 'task-1', + userId: '张三', + date: '2026-06-25', + hours: 8, + workContent: '昨天的日报', + createdAt: '2026-06-25T04:00:00.000Z', + }, +]; + +test('getWorkspaceDailyReport filters by current user and date', () => { + const report = getWorkspaceDailyReport({ + worklogs, + workItems, + userId: '张三', + date: '2026-06-26', + }); + + assert.equal(report.totalHours, 2); + assert.equal(report.totalCount, 2); + assert.deepEqual(report.items.map((item) => item.id), ['wl-2', 'wl-1']); +}); + +test('getWorkspaceDailyReport resolves task title and workspace context', () => { + const report = getWorkspaceDailyReport({ + worklogs, + workItems, + userId: '张三', + date: '2026-06-26', + }); + + assert.equal(report.items[0].taskTitle, '修复菜单错位'); + assert.equal(report.items[0].productName, 'FTB'); + assert.equal(report.items[0].projectName, '项目管理'); + assert.equal(report.items[0].versionName, 'V1.0'); +}); + +test('getWorkspaceDailyReport keeps logs whose task is no longer visible', () => { + const report = getWorkspaceDailyReport({ + worklogs: [ + { + id: 'wl-missing', + taskId: 'deleted-task', + userId: '张三', + date: '2026-06-26', + hours: 0.5, + workContent: '处理历史任务', + createdAt: '2026-06-26T01:00:00.000Z', + }, + ], + workItems: [], + userId: '张三', + date: '2026-06-26', + }); + + assert.equal(report.totalHours, 0.5); + assert.equal(report.totalCount, 1); + assert.equal(report.items[0].taskTitle, '未知任务'); + assert.equal(report.items[0].workContent, '处理历史任务'); +}); diff --git a/apps/web/lib/workspace-daily-report.ts b/apps/web/lib/workspace-daily-report.ts new file mode 100644 index 0000000..eb6d343 --- /dev/null +++ b/apps/web/lib/workspace-daily-report.ts @@ -0,0 +1,63 @@ +import type { TaskWorklog } from './task-worklog'; +import type { WorkItem } from './workspace-engine'; + +export interface WorkspaceDailyReportItem { + id: string; + taskId: string; + taskTitle: string; + workContent: string; + hours: number; + createdAt: string; + productName?: string; + projectName?: string; + versionName?: string; +} + +export interface WorkspaceDailyReport { + date: string; + totalHours: number; + totalCount: number; + items: WorkspaceDailyReportItem[]; +} + +interface GetWorkspaceDailyReportInput { + worklogs: TaskWorklog[]; + workItems: WorkItem[]; + userId: string; + date: string; +} + +export function getWorkspaceDailyReport({ + worklogs, + workItems, + userId, + date, +}: GetWorkspaceDailyReportInput): WorkspaceDailyReport { + const workItemMap = new Map(workItems.map((item) => [item.id, item])); + + const items = worklogs + .filter((log) => log.userId === userId && log.date === date) + .sort((a, b) => b.createdAt.localeCompare(a.createdAt)) + .map((log) => { + const item = workItemMap.get(log.taskId); + + return { + id: log.id, + taskId: log.taskId, + taskTitle: item?.title ?? '未知任务', + workContent: log.workContent, + hours: log.hours, + createdAt: log.createdAt, + productName: item?.productName, + projectName: item?.projectName, + versionName: item?.versionName, + }; + }); + + return { + date, + totalHours: items.reduce((sum, item) => sum + item.hours, 0), + totalCount: items.length, + items, + }; +} From 17dd4afada79a0baa0cc39b0e7577e57cc535b0e Mon Sep 17 00:00:00 2001 From: Script Generator Date: Fri, 26 Jun 2026 14:58:32 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(workspace):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BB=8A=E6=97=A5=E6=97=A5=E6=8A=A5=E4=BE=A7=E6=A0=8F=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/workspace/DailyReportPanel.tsx | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 apps/web/components/workspace/DailyReportPanel.tsx diff --git a/apps/web/components/workspace/DailyReportPanel.tsx b/apps/web/components/workspace/DailyReportPanel.tsx new file mode 100644 index 0000000..089c8af --- /dev/null +++ b/apps/web/components/workspace/DailyReportPanel.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { CalendarDays, ClipboardList, Clock3 } from 'lucide-react'; +import type { WorkspaceDailyReport } from '@/lib/workspace-daily-report'; +import { formatWorkHours, formatWorkHoursShort } from '@/lib/work-hours'; + +interface Props { + report: WorkspaceDailyReport; +} + +export function DailyReportPanel({ report }: Props) { + return ( + + ); +} From 952e13677c9998751d3dff17231a6847cf219790 Mon Sep 17 00:00:00 2001 From: Script Generator Date: Fri, 26 Jun 2026 15:10:44 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix(workspace):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BB=8A=E6=97=A5=E6=97=A5=E6=8A=A5=E4=BE=A7=E6=A0=8F=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/workspace/DailyReportPanel.tsx | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/apps/web/components/workspace/DailyReportPanel.tsx b/apps/web/components/workspace/DailyReportPanel.tsx index 089c8af..39ba275 100644 --- a/apps/web/components/workspace/DailyReportPanel.tsx +++ b/apps/web/components/workspace/DailyReportPanel.tsx @@ -46,22 +46,26 @@ export function DailyReportPanel({ report }: Props) { ) : (
- {report.items.map((item) => ( -
-
-

{item.workContent}

- - {formatWorkHoursShort(item.hours)} - + {report.items.map((item) => { + const contextLabel = [item.productName, item.projectName, item.versionName].filter(Boolean).join(' / '); + + return ( +
+
+

{item.workContent}

+ + {formatWorkHoursShort(item.hours)} + +
+

{item.taskTitle}

+ {contextLabel && ( +

+ {contextLabel} +

+ )}
-

{item.taskTitle}

- {(item.productName || item.projectName || item.versionName) && ( -

- {[item.productName, item.projectName, item.versionName].filter(Boolean).join(' / ')} -

- )} -
- ))} + ); + })}
)}
From 8c3fec221b3547020a6208d6d07416a479c88e7e Mon Sep 17 00:00:00 2001 From: Script Generator Date: Fri, 26 Jun 2026 15:19:42 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat(workspace):=20=E5=9C=A8=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8F=B0=E6=8E=A5=E5=85=A5=E4=BB=8A=E6=97=A5=E6=97=A5?= =?UTF-8?q?=E6=8A=A5=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/workspace/page.tsx | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/web/app/workspace/page.tsx b/apps/web/app/workspace/page.tsx index 11d7d91..23cc555 100644 --- a/apps/web/app/workspace/page.tsx +++ b/apps/web/app/workspace/page.tsx @@ -10,9 +10,11 @@ import { useDevTaskStore } from '@/stores/useDevTaskStore'; import { useTestCaseStore } from '@/stores/useTestCaseStore'; import { useBugStore } from '@/stores/useBugStore'; import { useAuthStore } from '@/stores/useAuthStore'; +import { useTaskWorklogStore } from '@/stores/useTaskWorklogStore'; import { flattenVersions } from '@/lib/derive'; import { aggregateWorkItems, WORK_ITEM_TYPE_LABEL } from '@/lib/workspace-engine'; import type { WorkItem, WorkItemType } from '@/lib/workspace-engine'; +import { DailyReportPanel } from '@/components/workspace/DailyReportPanel'; import { PlanDetailDrawer } from '@/components/version/PlanDetailDrawer'; import { DevTaskDetailDrawer } from '@/components/dev-task/DevTaskDetailDrawer'; import { TestCaseDetailDrawer } from '@/components/test-case/TestCaseDetailDrawer'; @@ -23,6 +25,7 @@ import { formatDateTime } from '@/lib/format'; import { formatShortTime, formatWorkHours } from '@/lib/work-hours'; import { TEST_CASE_STATUS_LABEL, TEST_CASE_STATUS_COLOR } from '@/lib/test-case'; import { BUG_STATUS_LABEL, BUG_STATUS_COLOR, BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug'; +import { getWorkspaceDailyReport } from '@/lib/workspace-daily-report'; type TabKey = 'all' | 'plan_research' | 'plan_product' | 'plan_ui' | 'devTask' | 'testCase' | 'bug'; @@ -51,6 +54,7 @@ export default function WorkspacePage() { const { tasks: devTasks, fetchTasks } = useDevTaskStore(); const { testCases, fetchTestCases } = useTestCaseStore(); const { bugs, fetchBugs } = useBugStore(); + const { worklogs, fetchWorklogs } = useTaskWorklogStore(); const user = useAuthStore((s) => s.user); const [activeTab, setActiveTab] = useState('all'); const [showCompleted, setShowCompleted] = useState(true); @@ -64,6 +68,7 @@ export default function WorkspacePage() { useEffect(() => { fetchTasks(); }, [fetchTasks]); useEffect(() => { fetchTestCases(); }, [fetchTestCases]); useEffect(() => { fetchBugs(); }, [fetchBugs]); + useEffect(() => { fetchWorklogs(); }, [fetchWorklogs]); const userName = user?.name ?? ''; const allVersions = useMemo(() => flattenVersions(overview), [overview]); @@ -113,6 +118,18 @@ export default function WorkspacePage() { return items.sort((a, b) => (a.completed !== b.completed ? (a.completed ? 1 : -1) : 0)); }, [workItems, activeTab, showCompleted, selectedVersionId]); + const today = useMemo(() => new Date().toISOString().slice(0, 10), []); + + const dailyReport = useMemo(() => + getWorkspaceDailyReport({ + worklogs, + workItems, + userId: userName, + date: today, + }), + [worklogs, workItems, userName, today] + ); + // 待办数量按 tab 分(受 version filter 影响) const pendingByTab = useMemo(() => { const base = selectedVersionId ? workItems.filter((i) => i.versionId === selectedVersionId) : workItems; @@ -191,7 +208,7 @@ export default function WorkspacePage() { {/* 右侧:任务列表 */} -
+

{TABS.find((t) => t.key === activeTab)?.label}

{filteredItems.length} 项 @@ -222,6 +239,8 @@ export default function WorkspacePage() {
+ + {/* Detail Drawers */} {drawerItem && (drawerItem.type === 'plan_research' || drawerItem.type === 'plan_product' || drawerItem.type === 'plan_ui') && ( setDrawerItem(null)} contextLabel={`${drawerItem.productName} / ${drawerItem.projectName} / ${drawerItem.versionName}`} /> From d9b0236d1c5a4f00f057f4706247c068c939b251 Mon Sep 17 00:00:00 2001 From: Script Generator Date: Fri, 26 Jun 2026 15:43:31 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix(workspace):=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E6=97=A5=E6=9C=9F=E7=94=9F=E6=88=90=E4=BB=8A?= =?UTF-8?q?=E6=97=A5=E6=97=A5=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/workspace/page.tsx | 4 ++-- apps/web/components/dev-task/WorklogPanel.tsx | 3 ++- apps/web/lib/format.test.ts | 12 ++++++++++++ apps/web/lib/format.ts | 7 +++++++ 4 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 apps/web/lib/format.test.ts diff --git a/apps/web/app/workspace/page.tsx b/apps/web/app/workspace/page.tsx index 23cc555..d1c36b8 100644 --- a/apps/web/app/workspace/page.tsx +++ b/apps/web/app/workspace/page.tsx @@ -21,7 +21,7 @@ import { TestCaseDetailDrawer } from '@/components/test-case/TestCaseDetailDrawe import { BugDetailDrawer } from '@/components/bug/BugDetailDrawer'; import { BugCreateModal } from '@/components/bug/BugCreateModal'; import { DEV_TASK_STATUS_LABEL, DEV_TASK_STATUS_COLOR, getEstimateHours, getActualHours } from '@/lib/dev-task'; -import { formatDateTime } from '@/lib/format'; +import { formatDateTime, formatLocalDate } from '@/lib/format'; import { formatShortTime, formatWorkHours } from '@/lib/work-hours'; import { TEST_CASE_STATUS_LABEL, TEST_CASE_STATUS_COLOR } from '@/lib/test-case'; import { BUG_STATUS_LABEL, BUG_STATUS_COLOR, BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug'; @@ -118,7 +118,7 @@ export default function WorkspacePage() { return items.sort((a, b) => (a.completed !== b.completed ? (a.completed ? 1 : -1) : 0)); }, [workItems, activeTab, showCompleted, selectedVersionId]); - const today = useMemo(() => new Date().toISOString().slice(0, 10), []); + const today = useMemo(() => formatLocalDate(), []); const dailyReport = useMemo(() => getWorkspaceDailyReport({ diff --git a/apps/web/components/dev-task/WorklogPanel.tsx b/apps/web/components/dev-task/WorklogPanel.tsx index 5060ba7..8603332 100644 --- a/apps/web/components/dev-task/WorklogPanel.tsx +++ b/apps/web/components/dev-task/WorklogPanel.tsx @@ -5,6 +5,7 @@ import { Plus, Trash2 } from 'lucide-react'; import { useTaskWorklogStore } from '@/stores/useTaskWorklogStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { getTaskWorklogs } from '@/lib/task-worklog'; +import { formatLocalDate } from '@/lib/format'; interface Props { taskId: string; @@ -17,7 +18,7 @@ export function WorklogPanel({ taskId }: Props) { const taskLogs = getTaskWorklogs(worklogs, taskId); const totalHours = taskLogs.reduce((sum, w) => sum + w.hours, 0); - const today = new Date().toISOString().slice(0, 10); + const today = formatLocalDate(); const [date, setDate] = useState(today); const [hours, setHours] = useState(1); const [workContent, setWorkContent] = useState(''); diff --git a/apps/web/lib/format.test.ts b/apps/web/lib/format.test.ts new file mode 100644 index 0000000..1a1e11a --- /dev/null +++ b/apps/web/lib/format.test.ts @@ -0,0 +1,12 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { formatLocalDate } from './format'; + +test('formatLocalDate formats a local calendar date as YYYY-MM-DD', () => { + assert.equal(formatLocalDate(new Date(2026, 5, 26, 23, 59)), '2026-06-26'); +}); + +test('formatLocalDate pads month and day', () => { + assert.equal(formatLocalDate(new Date(2026, 0, 2, 8, 5)), '2026-01-02'); +}); diff --git a/apps/web/lib/format.ts b/apps/web/lib/format.ts index f07f010..9aa7fe0 100644 --- a/apps/web/lib/format.ts +++ b/apps/web/lib/format.ts @@ -34,6 +34,13 @@ export function formatDate(iso?: string | null): string { /** * 格式化为 MM-DD HH:mm(紧凑版,本地时区) */ +export function formatLocalDate(date: Date = new Date()): string { + const yyyy = date.getFullYear(); + const mm = String(date.getMonth() + 1).padStart(2, '0'); + const dd = String(date.getDate()).padStart(2, '0'); + return `${yyyy}-${mm}-${dd}`; +} + export function formatDateTimeShort(iso?: string | null): string { if (!iso) return '-'; const d = new Date(iso); From 6f13ff325ba143a21d5ac1877eaab4e7d4446719 Mon Sep 17 00:00:00 2001 From: Script Generator Date: Fri, 26 Jun 2026 15:50:30 +0800 Subject: [PATCH 6/6] =?UTF-8?q?docs(format):=20=E4=BF=AE=E6=AD=A3=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E6=97=A5=E6=9C=9F=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/lib/format.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/lib/format.ts b/apps/web/lib/format.ts index 9aa7fe0..c78dc9c 100644 --- a/apps/web/lib/format.ts +++ b/apps/web/lib/format.ts @@ -32,7 +32,7 @@ export function formatDate(iso?: string | null): string { } /** - * 格式化为 MM-DD HH:mm(紧凑版,本地时区) + * 格式化为 YYYY-MM-DD(本地日期,用于 date input / 今日筛选) */ export function formatLocalDate(date: Date = new Date()): string { const yyyy = date.getFullYear(); @@ -41,6 +41,9 @@ export function formatLocalDate(date: Date = new Date()): string { return `${yyyy}-${mm}-${dd}`; } +/** + * 格式化为 MM-DD HH:mm(紧凑版,本地时区) + */ export function formatDateTimeShort(iso?: string | null): string { if (!iso) return '-'; const d = new Date(iso);