# 个人 OKR 执行可视化实现计划 > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [x]`)语法来跟踪进度。 **目标:** 修复个人 OKR 页面周期与接口数据链路,并按 Open Design 参考图交付可更新、可追溯的 Objective/KR 进度可视化。 **架构:** 以 workspace store 的当前周期为唯一周期来源,由路由 query 持久化;个人页面监听有效周期并加载本人全部 Objective。抽取一个 ObjectiveProgressCard 组件复用在“我的 OKR”和“执行进度”,后端 execution-timeline 收窄为本人数据并补齐单位字段。 **技术栈:** Vue 3、Pinia、Vue Router、Element Plus、Vitest、Spring Boot、MyBatis-Plus、JUnit 5、Mockito --- ## 文件结构 - 创建:`frontend/src/components/okr/ObjectiveProgressCard.vue` — Objective/KR 主进度卡片。 - 创建:`frontend/src/composables/useCurrentPeriodId.js` — URL 与 workspace 当前周期归一化。 - 修改:`frontend/src/components/shell/CohereSidebar.vue` — 周期相关链接携带 query。 - 修改:`frontend/src/views/okr/MyOkrView.vue` — 使用统一周期并渲染进度卡片。 - 修改:`frontend/src/views/okr/OkrExecutionView.vue` — 加载多 Objective、渲染主进度卡和时间线。 - 修改:`frontend/src/api/index.js` — 作为两个页面唯一 OKR API 客户端。 - 修改:`backend/src/main/java/com/emoon/okr/dto/OkrExecutionTimelineDto.java` — 时间线增加 KR 单位。 - 修改:`backend/src/main/java/com/emoon/okr/service/OkrService.java` — 时间线仅聚合当前用户。 - 修改:`frontend/src/__tests__/workspace-navigation-contract.test.js` — 周期继承契约。 - 修改:`frontend/src/__tests__/open-design-page-contract.test.js` — 进度可视化契约。 - 修改:`backend/src/test/java/com/emoon/okr/service/OkrServiceTest.java` — 本人时间线边界测试。 ### 任务 1:锁定周期与接口调用回归 **文件:** - 修改:`frontend/src/__tests__/workspace-navigation-contract.test.js` - 修改:`frontend/src/__tests__/open-design-page-contract.test.js` - [x] **步骤 1:编写失败的周期继承测试** ```js it('keeps the selected period on personal OKR navigation', () => { const sidebar = readFileSync(resolve(process.cwd(), 'src/components/shell/CohereSidebar.vue'), 'utf8') expect(sidebar).toContain('periodAwareTarget') expect(sidebar).toContain('workspace.periodId') }) ``` - [x] **步骤 2:编写失败的执行页数据契约测试** ```js it('loads all personal objectives and the personal execution timeline', () => { const page = source('okr/OkrExecutionView.vue') expect(page).toContain('okrApi.getMyOkrSet(periodId)') expect(page).toContain('okrApi.getExecutionTimeline(periodId)') expect(page).toContain('ObjectiveProgressCard') expect(page).toContain('watch(effectivePeriodId') }) ``` - [x] **步骤 3:运行测试验证失败** 运行:`cd frontend && npm test -- --run src/__tests__/workspace-navigation-contract.test.js src/__tests__/open-design-page-contract.test.js` 预期:FAIL,侧边栏仍使用固定字符串路径,执行页仍依赖单个 `myOkr` 与 `onMounted`。 ### 任务 2:建立统一周期上下文 **文件:** - 创建:`frontend/src/composables/useCurrentPeriodId.js` - 修改:`frontend/src/components/shell/CohereSidebar.vue` - 修改:`frontend/src/views/okr/MyOkrView.vue` - 修改:`frontend/src/views/okr/OkrExecutionView.vue` - [x] **步骤 1:实现有效周期 composable** ```js import { computed, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useWorkspaceStore } from '@/stores/workspace' export function useCurrentPeriodId() { const route = useRoute() const router = useRouter() const workspace = useWorkspaceStore() const effectivePeriodId = computed(() => { const queryId = Number(route.query.periodId) return Number.isFinite(queryId) && queryId > 0 ? queryId : workspace.periodId }) watch(() => workspace.periodId, id => { if (id && !route.query.periodId) router.replace({ query: { ...route.query, periodId: id } }) }, { immediate: true }) return { effectivePeriodId } } ``` - [x] **步骤 2:让侧边栏周期页面继承当前 query** ```js function periodAwareTarget(item) { if (!item.periodAware || !workspace.periodId) return item.to return { path: item.to, query: { periodId: workspace.periodId } } } ``` 将“我的 OKR”“团队 OKR”“OKR 对齐”“执行进度”和绩效周期页面标记为 `periodAware: true`,并把 `` 改为 ``。 - [x] **步骤 3:让两个个人页面监听有效周期** ```js const { effectivePeriodId } = useCurrentPeriodId() watch(effectivePeriodId, periodId => { if (periodId) fetchData(periodId) }, { immediate: true }) ``` “我的 OKR”保留自身周期切换 UI,但切换时同步 query;执行页不再使用只执行一次的 `onMounted(fetchData)`。 - [x] **步骤 4:运行周期契约测试验证通过** 运行:`cd frontend && npm test -- --run src/__tests__/workspace-navigation-contract.test.js` 预期:PASS。 ### 任务 3:收窄个人时间线后端契约 **文件:** - 修改:`backend/src/main/java/com/emoon/okr/dto/OkrExecutionTimelineDto.java` - 修改:`backend/src/main/java/com/emoon/okr/service/OkrService.java` - 修改:`backend/src/test/java/com/emoon/okr/service/OkrServiceTest.java` - [x] **步骤 1:编写失败的本人范围测试** 构造当前用户 Objective 和直属下属 Objective,调用 `getExecutionTimeline(periodId, actorId)`,断言结果只包含当前用户 KR: ```java assertEquals(List.of(100L), dto.getItems().stream() .map(OkrExecutionTimelineDto.KrTimelineItem::getKrId).toList()); verify(userMapper, never()).selectList(any()); ``` - [x] **步骤 2:运行测试验证失败** 运行:`cd backend && mvn -Dtest=OkrServiceTest#getExecutionTimelineOnlyReturnsCurrentUsersObjectives test` 预期:FAIL,现有实现会调用 `getSubordinateOkrs` 并合并下属。 - [x] **步骤 3:实现最小后端修复** ```java List myOkrList = new ArrayList<>(getMyOkrSet(periodId, actorId)); ``` 删除下属合并逻辑;在 `KrTimelineItem` 增加 `private String unit;`,映射时执行 `item.setUnit(kr.getUnit())`。 - [x] **步骤 4:运行后端测试验证通过** 运行:`cd backend && mvn -Dtest=OkrServiceTest test` 预期:全部 OkrServiceTest 通过。 ### 任务 4:实现 Open Design Objective/KR 进度卡片 **文件:** - 创建:`frontend/src/components/okr/ObjectiveProgressCard.vue` - 修改:`frontend/src/views/okr/MyOkrView.vue` - 修改:`frontend/src/views/okr/OkrExecutionView.vue` - 修改:`frontend/src/__tests__/open-design-page-contract.test.js` - [x] **步骤 1:扩展失败的视觉契约** ```js expect(page).toContain('objective-progress-card') expect(page).toContain('objective-progress-card__kr') expect(page).toContain('objective-progress-card__track') expect(page).toContain('objective-progress-card__fill') expect(page).toContain('update-progress') ``` - [x] **步骤 2:实现共享进度卡组件** 组件 props 为 `objective`、`objectiveIndex`、`interactive`;emit 为 `update-progress`。进度计算使用 baseline-aware 公式: ```js const progressOf = kr => { const baseline = Number(kr.baselineValue || 0) const current = Number(kr.currentValue || 0) const target = Number(kr.targetValue || 0) if (target === baseline) return current === target ? 100 : 0 return Math.max(0, Math.min(100, Math.round((current - baseline) / (target - baseline) * 100))) } ``` 模板按参考图渲染 O 编号、标题、权重、描述,以及每条 KR 的编号、标题、当前值、目标值、单位、状态标签和横向进度条。阻塞/延期为 danger,正常执行在深紫与浅青之间交替。 - [x] **步骤 3:在“我的 OKR”复用组件** 用 `ObjectiveProgressCard` 替换现有 Objective 表格区,点击更新事件继续调用现有 `openKrDrawer(kr)`,不修改创建与审核表单。 - [x] **步骤 4:重构执行页为多 Objective 主视图** ```js const myOkrs = ref([]) const [objectives, timeline] = await Promise.all([ okrApi.getMyOkrSet(periodId), okrApi.getExecutionTimeline(periodId) ]) myOkrs.value = objectives || [] timelineItems.value = timeline?.items || [] ``` 主区域先渲染全部 `ObjectiveProgressCard`,再渲染计划时间线。即使没有 progressHistory,只要 KR 有计划日期就必须显示轨道。 - [x] **步骤 5:区分错误和空数据** 请求失败设置 `error`;请求成功但无 Objective 设置 `empty`。空态文案明确提示“当前账号在该周期没有个人 OKR”,不显示“网络错误”。 - [x] **步骤 6:运行前端目标测试** 运行:`cd frontend && npm test -- --run src/__tests__/open-design-page-contract.test.js src/__tests__/workspace-navigation-contract.test.js` 预期:PASS。 ### 任务 5:全量验证、视觉 QA 与单 JAR 同步 **文件:** - 修改:`design-qa.md` - 生成:`docs/audit/open-design-2026-07-14/execution-*.png` - 同步:`backend/src/main/resources/static/**` - [x] **步骤 1:运行前端完整验证** 运行: ```bash cd frontend npm test -- --run npm run verify:theme npm run verify:scoring npm run build ``` 预期:全部测试、契约检查和 Vite 构建通过。 - [x] **步骤 2:运行后端完整验证** 运行:`cd backend && mvn clean test` 预期:0 failures、0 errors。 - [x] **步骤 3:执行浏览器视觉验收** 使用有个人 OKR 的账号打开 `/okr/my?periodId=1` 和 `/okr/execution?periodId=1`,捕获桌面与移动端截图。将用户参考图和实现截图横向合成后检查:Objective 边界、KR 行密度、进度条宽度、当前/目标值、状态色、更新入口及移动端溢出。 - [x] **步骤 4:更新 Design QA** `design-qa.md` 必须记录参考图、实现图、已修复差异,并包含 `final result: passed`;存在 P0/P1/P2 时继续修改和重新截图。 - [x] **步骤 5:同步和重新打包** ```bash rm -rf backend/src/main/resources/static/assets cp -R frontend/dist/. backend/src/main/resources/static/ cmp frontend/dist/index.html backend/src/main/resources/static/index.html cd backend && mvn package -DskipTests ``` 预期:`cmp` 退出码 0,JAR 中包含最新 ObjectiveProgressCard 与 OkrExecutionView 产物。