2026-07-14-personal-okr-execution-visualization.md 10 KB

个人 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:编写失败的周期继承测试

    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:编写失败的执行页数据契约测试

    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,侧边栏仍使用固定字符串路径,执行页仍依赖单个 myOkronMounted

任务 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

    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

    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:让两个个人页面监听有效周期

    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:

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:实现最小后端修复

    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:扩展失败的视觉契约

    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 为 objectiveobjectiveIndexinteractive;emit 为 update-progress。进度计算使用 baseline-aware 公式:

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 主视图

    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:运行前端完整验证

运行:

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:同步和重新打包

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