|
|
@@ -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 产物。
|