소스 검색

docs: 添加个人 OKR 执行可视化计划

wangkangyjy 3 일 전
부모
커밋
5bae508e13
1개의 변경된 파일262개의 추가작업 그리고 0개의 파일을 삭제
  1. 262 0
      docs/superpowers/plans/2026-07-14-personal-okr-execution-visualization.md

+ 262 - 0
docs/superpowers/plans/2026-07-14-personal-okr-execution-visualization.md

@@ -0,0 +1,262 @@
+# 个人 OKR 执行可视化实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 修复个人 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/yimeng/okr/dto/OkrExecutionTimelineDto.java` — 时间线增加 KR 单位。
+- 修改:`backend/src/main/java/com/yimeng/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/yimeng/okr/service/OkrServiceTest.java` — 本人时间线边界测试。
+
+### 任务 1:锁定周期与接口调用回归
+
+**文件:**
+- 修改:`frontend/src/__tests__/workspace-navigation-contract.test.js`
+- 修改:`frontend/src/__tests__/open-design-page-contract.test.js`
+
+- [ ] **步骤 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')
+})
+```
+
+- [ ] **步骤 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')
+})
+```
+
+- [ ] **步骤 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`
+
+- [ ] **步骤 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 }
+}
+```
+
+- [ ] **步骤 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`,并把 `<router-link :to="item.to">` 改为 `<router-link :to="periodAwareTarget(item)">`。
+
+- [ ] **步骤 3:让两个个人页面监听有效周期**
+
+```js
+const { effectivePeriodId } = useCurrentPeriodId()
+watch(effectivePeriodId, periodId => {
+  if (periodId) fetchData(periodId)
+}, { immediate: true })
+```
+
+“我的 OKR”保留自身周期切换 UI,但切换时同步 query;执行页不再使用只执行一次的 `onMounted(fetchData)`。
+
+- [ ] **步骤 4:运行周期契约测试验证通过**
+
+运行:`cd frontend && npm test -- --run src/__tests__/workspace-navigation-contract.test.js`
+
+预期:PASS。
+
+### 任务 3:收窄个人时间线后端契约
+
+**文件:**
+- 修改:`backend/src/main/java/com/yimeng/okr/dto/OkrExecutionTimelineDto.java`
+- 修改:`backend/src/main/java/com/yimeng/okr/service/OkrService.java`
+- 修改:`backend/src/test/java/com/yimeng/okr/service/OkrServiceTest.java`
+
+- [ ] **步骤 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());
+```
+
+- [ ] **步骤 2:运行测试验证失败**
+
+运行:`cd backend && mvn -Dtest=OkrServiceTest#getExecutionTimelineOnlyReturnsCurrentUsersObjectives test`
+
+预期:FAIL,现有实现会调用 `getSubordinateOkrs` 并合并下属。
+
+- [ ] **步骤 3:实现最小后端修复**
+
+```java
+List<OkrDetailDto> myOkrList = new ArrayList<>(getMyOkrSet(periodId, actorId));
+```
+
+删除下属合并逻辑;在 `KrTimelineItem` 增加 `private String unit;`,映射时执行 `item.setUnit(kr.getUnit())`。
+
+- [ ] **步骤 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`
+
+- [ ] **步骤 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')
+```
+
+- [ ] **步骤 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,正常执行在深紫与浅青之间交替。
+
+- [ ] **步骤 3:在“我的 OKR”复用组件**
+
+用 `ObjectiveProgressCard` 替换现有 Objective 表格区,点击更新事件继续调用现有 `openKrDrawer(kr)`,不修改创建与审核表单。
+
+- [ ] **步骤 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 有计划日期就必须显示轨道。
+
+- [ ] **步骤 5:区分错误和空数据**
+
+请求失败设置 `error`;请求成功但无 Objective 设置 `empty`。空态文案明确提示“当前账号在该周期没有个人 OKR”,不显示“网络错误”。
+
+- [ ] **步骤 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/**`
+
+- [ ] **步骤 1:运行前端完整验证**
+
+运行:
+
+```bash
+cd frontend
+npm test -- --run
+npm run verify:theme
+npm run verify:scoring
+npm run build
+```
+
+预期:全部测试、契约检查和 Vite 构建通过。
+
+- [ ] **步骤 2:运行后端完整验证**
+
+运行:`cd backend && mvn clean test`
+
+预期:0 failures、0 errors。
+
+- [ ] **步骤 3:执行浏览器视觉验收**
+
+使用有个人 OKR 的账号打开 `/okr/my?periodId=1` 和 `/okr/execution?periodId=1`,捕获桌面与移动端截图。将用户参考图和实现截图横向合成后检查:Objective 边界、KR 行密度、进度条宽度、当前/目标值、状态色、更新入口及移动端溢出。
+
+- [ ] **步骤 4:更新 Design QA**
+
+`design-qa.md` 必须记录参考图、实现图、已修复差异,并包含 `final result: passed`;存在 P0/P1/P2 时继续修改和重新截图。
+
+- [ ] **步骤 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 产物。