From 58fe2b1ea8c50b15f3ce7d8f7305f926d2bbbdd7 Mon Sep 17 00:00:00 2001 From: zyc <1439655764@qq.com> Date: Fri, 10 Apr 2026 15:07:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20OKR=E7=9C=8B=E6=9D=BF=E9=87=8D=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=20+=20=E5=91=A8=E5=88=87=E6=8D=A2=20+=20=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E9=80=89=E6=97=A5=E6=9C=9F=20+=20=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OKR看板:按项目分组大卡片 + 目标/任务折叠展开 + 统计栏 + 按项目筛选 - KR状态文本标签(已取消/已暂停/已延期)替代emoji - 本周关键结果支持前后周切换(‹ 本周 ›)+ 日期范围显示 - 恢复暂停任务时必须选择新截止日期 - 项目列表仓库名只显示短名不显示完整URL - 项目详情标题合并到进度条卡片内 Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/src/routes/okr.ts | 5 +- backend/src/routes/overview.ts | 13 +- backend/src/routes/projects.ts | 9 + backend/src/services/okr.ts | 10 +- frontend/src/api/okr.ts | 4 +- frontend/src/views/GitActivity.vue | 2 +- frontend/src/views/OKR.vue | 482 ++++++++++++--------------- frontend/src/views/Overview.vue | 40 ++- frontend/src/views/ProjectDetail.vue | 109 ++++-- frontend/src/views/ProjectList.vue | 24 +- 10 files changed, 376 insertions(+), 322 deletions(-) diff --git a/backend/src/routes/okr.ts b/backend/src/routes/okr.ts index 7fce6f6..a085f00 100644 --- a/backend/src/routes/okr.ts +++ b/backend/src/routes/okr.ts @@ -104,12 +104,15 @@ okrRoutes.post('/okr/key-results/:id/pause', ); // POST /api/okr/key-results/:id/resume — 恢复 +const resumeSchema = z.object({ newEndDate: z.string().min(1) }); okrRoutes.post('/okr/key-results/:id/resume', requireRole('admin', 'manager', 'developer'), + zValidator('json', resumeSchema), async (c) => { const krId = c.req.param('id'); + const { newEndDate } = c.req.valid('json'); const user = c.get('user'); - const result = await okrService.resumeKR(krId, user.sub, user.displayName); + const result = await okrService.resumeKR(krId, newEndDate, user.sub, user.displayName); return c.json({ code: 0, data: result, message: 'success' }); } ); diff --git a/backend/src/routes/overview.ts b/backend/src/routes/overview.ts index 957f3b4..d2aa780 100644 --- a/backend/src/routes/overview.ts +++ b/backend/src/routes/overview.ts @@ -153,9 +153,10 @@ overviewRoutes.get('/overview', async (c) => { }); } - // 6. 本周待完成 KR(截止日期在本周范围内,且未完成的) - const weekStart = dayjs().startOf('week'); - const weekEnd = dayjs().endOf('week'); + // 6. 指定周的 KR(支持 weekOffset 参数,0=本周,1=下周,-1=上周) + const weekOffset = parseInt(c.req.query('weekOffset') || '0'); + const weekStart = dayjs().startOf('week').add(weekOffset, 'week'); + const weekEnd = dayjs().endOf('week').add(weekOffset, 'week'); const allKRsRaw = await db.select().from(keyResults); const allObjsMap = new Map((await db.select().from(objectives)).map(o => [o.id, o])); @@ -296,7 +297,11 @@ overviewRoutes.get('/overview', async (c) => { weeklyCodeActivity, okrProgress, urgentKRs, - weeklyKRStats: { total: weeklyTotal, completed: weeklyCompleted, avgProgress: weeklyAvgProgress }, + weeklyKRStats: { + total: weeklyTotal, completed: weeklyCompleted, avgProgress: weeklyAvgProgress, + weekOffset, + weekLabel: weekStart.format('MM/DD') + ' ~ ' + weekEnd.format('MM/DD'), + }, overdueKRs: overdueList, recentCommits: await getRecentCommits(), }, diff --git a/backend/src/routes/projects.ts b/backend/src/routes/projects.ts index 8cd4072..993be96 100644 --- a/backend/src/routes/projects.ts +++ b/backend/src/routes/projects.ts @@ -282,6 +282,15 @@ projectRoutes.get('/projects/:id', async (c) => { milestones: milestoneData, taskMatrix, gitActivity, + recentCommitList: recentCommits + .sort((a, b) => dayjs(b.committedAt).valueOf() - dayjs(a.committedAt).valueOf()) + .slice(0, 15) + .map(c => ({ + sha: c.sha?.slice(0, 7) || '', + message: (c.message || '').split('\n')[0].slice(0, 60), + authorName: c.userId ? (userMap.get(c.userId) || c.authorName) : c.authorName, + committedAt: c.committedAt instanceof Date ? c.committedAt.toISOString() : c.committedAt, + })), okr: { objectives: okrData, overallProgress: avgOKRProgress, diff --git a/backend/src/services/okr.ts b/backend/src/services/okr.ts index a7e8fe1..261f810 100644 --- a/backend/src/services/okr.ts +++ b/backend/src/services/okr.ts @@ -301,16 +301,18 @@ export async function pauseKR(krId: string, reason: string, operatorId: string, // ── 恢复 ── -export async function resumeKR(krId: string, operatorId: string, operatorName: string) { +export async function resumeKR(krId: string, newEndDate: string, operatorId: string, operatorName: string) { const kr = await db.query.keyResults.findFirst({ where: eq(keyResults.id, krId) }); if (!kr) throw new AppError(40404, 'Key result not found', 404); - await db.update(keyResults).set({ status: 'active', updatedAt: new Date() }) + const oldEndDate = kr.endDate || '未设置'; + await db.update(keyResults).set({ status: 'active', endDate: newEndDate, updatedAt: new Date() }) .where(eq(keyResults.id, krId)); - await addKRLog(krId, 'resumed', '恢复为进行中', operatorId, operatorName); + await addKRLog(krId, 'resumed', `恢复为进行中,截止日期 ${oldEndDate} → ${newEndDate}`, operatorId, operatorName); await recalcObjectiveProgress(kr.objectiveId); - return { id: krId, status: 'active' }; + await recalcObjectiveDates(kr.objectiveId); + return { id: krId, status: 'active', endDate: newEndDate }; } // ── 取消 ── diff --git a/frontend/src/api/okr.ts b/frontend/src/api/okr.ts index f69bacf..14e6ec9 100644 --- a/frontend/src/api/okr.ts +++ b/frontend/src/api/okr.ts @@ -32,8 +32,8 @@ export function pauseKRApi(krId: string, data: { reason: string }) { return request.post(`/api/okr/key-results/${krId}/pause`, data); } -export function resumeKRApi(krId: string) { - return request.post(`/api/okr/key-results/${krId}/resume`); +export function resumeKRApi(krId: string, data: { newEndDate: string }) { + return request.post(`/api/okr/key-results/${krId}/resume`, data); } export function cancelKRApi(krId: string, data: { reason: string }) { diff --git a/frontend/src/views/GitActivity.vue b/frontend/src/views/GitActivity.vue index bfd9e12..81fb76f 100644 --- a/frontend/src/views/GitActivity.vue +++ b/frontend/src/views/GitActivity.vue @@ -53,7 +53,7 @@ const weeklyOptions = computed(() => { const found = (w.byUser || []).find((u: any) => u.userId === userId); return found?.commits || 0; }), - itemStyle: { color: CHART_COLORS[i % CHART_COLORS.length], borderRadius: i === userSet.size - 1 ? [3, 3, 0, 0] : [0, 0, 0, 0] }, + itemStyle: { color: ['#3B5998', '#0D9668', '#D4920A', '#7C4DBA', '#DC2626', '#06B6D4', '#8B5CF6', '#EC4899'][i % 8], borderRadius: i === userSet.size - 1 ? [3, 3, 0, 0] : [0, 0, 0, 0] }, })); return { diff --git a/frontend/src/views/OKR.vue b/frontend/src/views/OKR.vue index eceda9b..a641cda 100644 --- a/frontend/src/views/OKR.vue +++ b/frontend/src/views/OKR.vue @@ -1,318 +1,272 @@ diff --git a/frontend/src/views/Overview.vue b/frontend/src/views/Overview.vue index 9ff4a65..1acf378 100644 --- a/frontend/src/views/Overview.vue +++ b/frontend/src/views/Overview.vue @@ -13,11 +13,24 @@ const router = useRouter(); const loading = ref(true); const overviewData = ref(null); const projectOptions = ref>([]); +const weekOffset = ref(0); + +function weekLabel(offset: number) { + if (offset === 0) return '本周'; + if (offset === 1) return '下周'; + if (offset === -1) return '上周'; + return offset > 0 ? `${offset}周后` : `${-offset}周前`; +} + +async function changeWeek(delta: number) { + weekOffset.value += delta; + await loadData(); +} async function loadData(filters?: { period?: string; projectIds?: string[] }) { loading.value = true; try { - const params: any = {}; + const params: any = { weekOffset: weekOffset.value }; if (filters?.period) params.period = filters.period; if (filters?.projectIds) params.projectIds = filters.projectIds.join(','); @@ -132,13 +145,21 @@ function formatCommitTime(isoStr: string) {
@@ -238,6 +259,11 @@ function formatCommitTime(isoStr: string) { .badge-paused { background: rgba(212,146,10,0.08); color: #B47D08; } .badge-active { background: rgba(59,89,152,0.08); color: var(--color-primary-hex); } +.week-nav { font-size: 18px; font-weight: 700; color: var(--color-text-muted); cursor: pointer; padding: 0 4px; user-select: none; line-height: 1; } +.week-nav:hover { color: var(--color-primary-hex); } +.week-reset { font-size: 12px; color: var(--color-primary-hex); cursor: pointer; } +.week-reset:hover { text-decoration: underline; } + .urgent-kr-row { display: flex; align-items: center; gap: 12px; } .urgent-kr-left { flex: 1; min-width: 0; } .urgent-kr-title { font-size: 13px; font-weight: 500; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } diff --git a/frontend/src/views/ProjectDetail.vue b/frontend/src/views/ProjectDetail.vue index 208e19b..c825d9e 100644 --- a/frontend/src/views/ProjectDetail.vue +++ b/frontend/src/views/ProjectDetail.vue @@ -185,6 +185,11 @@ const reasonAction = ref<'pause' | 'cancel'>('pause'); const reasonKRId = ref(''); const reasonText = ref(''); +// 恢复弹窗 +const showResumeModal = ref(false); +const resumeKRId = ref(''); +const resumeEndDate = ref(null); + // 日志弹窗 const showLogsModal = ref(false); const logsKRTitle = ref(''); @@ -201,7 +206,9 @@ function handleKRMenu(key: string, kr: any) { reasonText.value = ''; showReasonModal.value = true; } else if (key === 'resume') { - doResume(kr.id); + resumeKRId.value = kr.id; + resumeEndDate.value = null; + showResumeModal.value = true; } } @@ -236,10 +243,15 @@ async function doReasonAction() { } catch { message.error('操作失败'); } } -async function doResume(krId: string) { +async function doResume() { + if (!resumeEndDate.value) { + message.warning('请选择新的截止日期'); + return; + } try { - await resumeKRApi(krId); + await resumeKRApi(resumeKRId.value, { newEndDate: tsToDateStr(resumeEndDate.value) }); message.success('已恢复'); + showResumeModal.value = false; loadData(); } catch { message.error('恢复失败'); } } @@ -286,6 +298,18 @@ const { chartRef: gitMiniRef } = useECharts(gitMiniChartOptions); function clamp(v: number) { return Math.min(Math.max(v, 0), 100); } +function formatTime(isoStr: string) { + if (!isoStr) return ''; + const d = new Date(isoStr); + const now = new Date(); + const diffH = Math.floor((now.getTime() - d.getTime()) / 3600000); + const diffD = Math.floor((now.getTime() - d.getTime()) / 86400000); + if (diffH < 1) return '刚刚'; + if (diffH < 24) return diffH + '小时前'; + if (diffD < 7) return diffD + '天前'; + return d.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }); +} + // 判断当前用户是否可以编辑该 Objective(创建者本人 或 admin) function canEditObj(obj: any): boolean { if (authStore.isAdmin) return true; @@ -297,16 +321,14 @@ function canEditObj(obj: any): boolean {