6.19日从 V1 骨架到 V14 验收前端跟随业务迭代逐步演进而非一开始就设计完美架构。1. API 层的演进从单一调用到复杂业务流程V1-V2 时期API 层只是简单的 CRUD 调用export async function fetchProjects() { const { data } await apiClient.getProject[](/projects); return data; }V8 之后API 层开始承载复杂业务流程- 团队准入审批 申请 → 审批 → 拒绝 → 撤销 → 批量操作- GitHub 同步 配置 → 预览 → 应用 → 历史- 评论状态流转 确认 → 忽略 → 发布review.ts从 50 行增长到 540 行体现了业务复杂度的累积技术要点 - API 函数命名遵循业务语义 previewTeamGitHubSync → applyTeamGitHubSync - 参数类型从简单对象演进为嵌套结构- 响应类型从单一实体演进为聚合结果如 TeamGitHubSyncApplyResult 2. 状态管理的演进从 auth 到团队治理V1 只有简单的登录状态export const useAuthStore defineStore(auth, { state: () ({ user: null }), });V8 之后状态开始承载复杂业务- 团队准入申请 requestFilters 、 selectedRequestIdsByTeam- GitHub 同步 syncConfigForm 、 teamSyncPreview 、 teamSyncHistory- 定时刷新 syncHistoryTimer 的生命周期管理TeamsView.vue的 script 部分超过 1000 行其中状态定义占了约 100 行。技术要点 - 使用 reactive 管理表单状态而非分散的 ref- 定时器生命周期与组件生命周期绑定 onMounted / onBeforeUnmount - 状态持久化到 localStorage如仓库筛选3. 路由守卫的演进从简单跳转到会话恢复V1-V4 时期路由只是简单的页面映射。V8.1 引入 GitHub 真实身份后路由守卫开始承担认证职责router.beforeEach(async (to) { const authStore useAuthStore(pinia); await authStore.restoreSession(); // 页面刷新后恢复登录状态 if (to.meta.requiresAuth !authStore.isAuthenticated) { return { name: login, query: { next: to.fullPath } }; } });技术要点 - restoreSession的幂等设计已初始化则跳过- next参数支持登录后跳转回原目标页面- 401响应拦截器自动跳转登录页4. 组件设计的演进从 StatCard 到嵌套弹窗V1-V2 只有简单的 StatCardtemplate section classpanel metric-card div classlabel{{ label }}/div div classvalue{{ value }}/div /section /templateV8 之后组件开始承载复杂交互- 团队详情弹窗 5 个 Tab 关键词搜索 二级弹窗- GitHub 同步弹窗 预览 → 应用 → 历史 定时刷新- 审查任务详情页 LLM 诊断 分块进度 问题分组 草稿评论技术要点- 弹窗嵌套使用 append-to-body 避免层级问题- Tab 切换时按需加载数据规则/技能/规范详情- 折叠面板实现渐进式信息展示5. 测试的演进从零到关键路径覆盖V9-V10 引入测试- analytics.spec.ts趋势图表算法测试- draftCommentState.spec.ts状态机测试- router.spec.ts路由守卫测试技术要点 - 优先测试纯函数如 buildTrendPolyline - 状态机逻辑单独抽取为 draftCommentState.ts 便于测试- 路由守卫测试覆盖认证跳转和会话恢复6. 类型定义的演进从简单实体到复杂聚合V1-V2 时期类型定义只有基础实体// V1-V2基础实体 interface Project { id: number; name: string; repository_bindings: RepositoryBinding[]; }V8 之后类型开始承载复杂聚合结构// V8复杂聚合 interface ReviewTaskDetail extends ReviewTaskSummary { changed_files: ChangedFile[]; findings: Finding[]; draft_comments: DraftComment[]; review_chunks: ReviewTaskChunk[]; command_runs: ReviewCommandRun[]; review_summary: ReviewSummary | null; llm_diagnostics: ReviewLLMDiagnostics | null; } interface TeamGitHubSyncResult { source_member_count: number; added_count: number; removed_count: number; pending_invite_count: number; add_candidates: TeamGitHubSyncAddCandidate[]; remove_candidates: TeamGitHubSyncRemoveCandidate[]; pending_invites: string[]; skipped_owner_members: string[]; }index.ts从 50 行增长到 676 行包含 40 个类型定义。7. 翻译函数的演进从硬编码到统一映射V1-V2 时期状态翻译散落在各组件中。V9 之后统一抽取到display.ts // 统一的翻译函数 export function translateReviewTaskStatus(status: string) { const labels: Recordstring, string { PENDING: 待处理, PROCESSING: 处理中, ANALYZED: 已分析, GOVERNED: 已生成建议, PUBLISHED: 已发布, FAILED: 执行失败, }; return labels[status] ?? status; } export function translateDraftCommentStatus(status: string) { const labels: Recordstring, string { DRAFT: 未确认, PENDING_CONFIRMATION: 未确认, CONFIRMED: 确认待发布, IGNORED: 已忽略, PUBLISHED: 已发布, }; return labels[status] ?? status; }演进要点 - 250 行的翻译函数覆盖 20 种状态/类型- 统一的 fallback 处理 labels[status] ?? status- 支持可选参数 translateTeamRole(role: string | null | undefined)8. 表单状态管理的演进从分散 ref 到 reactiveV1-V2 时期表单状态使用分散的 ref // V1-V2分散的 ref const projectName ref(); const owner ref(); const repo ref();V8 之后使用 reactive 管理表单状态// V8reactive 表单 const manualForm reactive({ project_name: , owner: , repo: , default_branch: main, }); const scopeForm reactive({ uses_custom_configuration: false, rule_ids: [] as number[], skill_ids: [] as number[], norm_mapping_ids: [] as number[], });演进要点 - reactive 适合表单场景修改时无需 .value- 重置函数统一管理 resetManualForm() 、 resetOAuthForm()- 类型注解 [] as number[] 明确数组类型9. 加载状态的演进从单一 loading 到多状态V1-V2 时期只有单一 loading 状态const loading ref(false);V8 之后需要区分多种操作状态// ProjectsView.vue 的加载状态 const loading ref(false); const saving ref(false); const scopeSaving ref(false); const oauthLoading ref(false); const repoLoading ref(false); const branchLoading ref(false); const oauthBindingSaving ref(false); const verifyingBindingId refnumber | null(null); const diagnosingBindingId refnumber | null(null); const syncingBindingId refnumber | null(null); const recreatingBindingId refnumber | null(null); const teamSavingBindingId refnumber | null(null); const teamRequestBindingId refnumber | null(null);演进要点 - 按操作类型区分 loading 状态- 按实体 ID 区分 loading 状态如 verifyingBindingId - 按钮绑定对应 loading :loadingverifyingBindingId binding.id10. 测试策略的演进从零到关键路径覆盖V9-V10 引入测试优先测试三类内容纯函数测试 // analytics.spec.ts describe(analytics polyline, () { it(returns empty string for empty trend, () { expect(buildTrendPolyline([], (point) point.created_tasks)).toBe(); }); it(builds chart points and keeps them within viewport, () { const trend [ mockPoint({ date: 2026-03-20, created_tasks: 1 }), mockPoint({ date: 2026-03-21, created_tasks: 3 }), mockPoint({ date: 2026-03-22, created_tasks: 2 }), ]; const points buildTrendPolyline(trend, (point) point.created_tasks, { width: 200, height: 100, padding: 10, }); expect(points).toBe(10,63 100,10 190,37); }); });状态机测试 // draftCommentState.spec.ts describe(draftCommentState, () { it(allows confirm and ignore only for unconfirmed statuses, () { expect(canConfirmDraftComment(DRAFT)).toBe(true); expect(canConfirmDraftComment(PENDING_CONFIRMATION)).toBe(true); expect(canConfirmDraftComment(CONFIRMED)).toBe(false); }); it(allows publish only for confirmed status, () { expect(canPublishDraftComment(CONFIRMED)).toBe(true); expect(canPublishDraftComment(DRAFT)).toBe(false); }); });路由测试 // router.spec.ts describe(router, () { it(registers the core MVP pages, () { const routeNames router.getRoutes().map((route) route.name); expect(routeNames).toEqual( expect.arrayContaining([ dashboard, projects, review-tasks, review-task-detail, comments, ]) ); }); });测试策略 - 纯函数优先测试无依赖、易断言- 状态机逻辑单独抽取便于测试- 路由配置测试覆盖核心页面
山东大学软件学院创新实训——CodeGaurd(七)
6.19日从 V1 骨架到 V14 验收前端跟随业务迭代逐步演进而非一开始就设计完美架构。1. API 层的演进从单一调用到复杂业务流程V1-V2 时期API 层只是简单的 CRUD 调用export async function fetchProjects() { const { data } await apiClient.getProject[](/projects); return data; }V8 之后API 层开始承载复杂业务流程- 团队准入审批 申请 → 审批 → 拒绝 → 撤销 → 批量操作- GitHub 同步 配置 → 预览 → 应用 → 历史- 评论状态流转 确认 → 忽略 → 发布review.ts从 50 行增长到 540 行体现了业务复杂度的累积技术要点 - API 函数命名遵循业务语义 previewTeamGitHubSync → applyTeamGitHubSync - 参数类型从简单对象演进为嵌套结构- 响应类型从单一实体演进为聚合结果如 TeamGitHubSyncApplyResult 2. 状态管理的演进从 auth 到团队治理V1 只有简单的登录状态export const useAuthStore defineStore(auth, { state: () ({ user: null }), });V8 之后状态开始承载复杂业务- 团队准入申请 requestFilters 、 selectedRequestIdsByTeam- GitHub 同步 syncConfigForm 、 teamSyncPreview 、 teamSyncHistory- 定时刷新 syncHistoryTimer 的生命周期管理TeamsView.vue的 script 部分超过 1000 行其中状态定义占了约 100 行。技术要点 - 使用 reactive 管理表单状态而非分散的 ref- 定时器生命周期与组件生命周期绑定 onMounted / onBeforeUnmount - 状态持久化到 localStorage如仓库筛选3. 路由守卫的演进从简单跳转到会话恢复V1-V4 时期路由只是简单的页面映射。V8.1 引入 GitHub 真实身份后路由守卫开始承担认证职责router.beforeEach(async (to) { const authStore useAuthStore(pinia); await authStore.restoreSession(); // 页面刷新后恢复登录状态 if (to.meta.requiresAuth !authStore.isAuthenticated) { return { name: login, query: { next: to.fullPath } }; } });技术要点 - restoreSession的幂等设计已初始化则跳过- next参数支持登录后跳转回原目标页面- 401响应拦截器自动跳转登录页4. 组件设计的演进从 StatCard 到嵌套弹窗V1-V2 只有简单的 StatCardtemplate section classpanel metric-card div classlabel{{ label }}/div div classvalue{{ value }}/div /section /templateV8 之后组件开始承载复杂交互- 团队详情弹窗 5 个 Tab 关键词搜索 二级弹窗- GitHub 同步弹窗 预览 → 应用 → 历史 定时刷新- 审查任务详情页 LLM 诊断 分块进度 问题分组 草稿评论技术要点- 弹窗嵌套使用 append-to-body 避免层级问题- Tab 切换时按需加载数据规则/技能/规范详情- 折叠面板实现渐进式信息展示5. 测试的演进从零到关键路径覆盖V9-V10 引入测试- analytics.spec.ts趋势图表算法测试- draftCommentState.spec.ts状态机测试- router.spec.ts路由守卫测试技术要点 - 优先测试纯函数如 buildTrendPolyline - 状态机逻辑单独抽取为 draftCommentState.ts 便于测试- 路由守卫测试覆盖认证跳转和会话恢复6. 类型定义的演进从简单实体到复杂聚合V1-V2 时期类型定义只有基础实体// V1-V2基础实体 interface Project { id: number; name: string; repository_bindings: RepositoryBinding[]; }V8 之后类型开始承载复杂聚合结构// V8复杂聚合 interface ReviewTaskDetail extends ReviewTaskSummary { changed_files: ChangedFile[]; findings: Finding[]; draft_comments: DraftComment[]; review_chunks: ReviewTaskChunk[]; command_runs: ReviewCommandRun[]; review_summary: ReviewSummary | null; llm_diagnostics: ReviewLLMDiagnostics | null; } interface TeamGitHubSyncResult { source_member_count: number; added_count: number; removed_count: number; pending_invite_count: number; add_candidates: TeamGitHubSyncAddCandidate[]; remove_candidates: TeamGitHubSyncRemoveCandidate[]; pending_invites: string[]; skipped_owner_members: string[]; }index.ts从 50 行增长到 676 行包含 40 个类型定义。7. 翻译函数的演进从硬编码到统一映射V1-V2 时期状态翻译散落在各组件中。V9 之后统一抽取到display.ts // 统一的翻译函数 export function translateReviewTaskStatus(status: string) { const labels: Recordstring, string { PENDING: 待处理, PROCESSING: 处理中, ANALYZED: 已分析, GOVERNED: 已生成建议, PUBLISHED: 已发布, FAILED: 执行失败, }; return labels[status] ?? status; } export function translateDraftCommentStatus(status: string) { const labels: Recordstring, string { DRAFT: 未确认, PENDING_CONFIRMATION: 未确认, CONFIRMED: 确认待发布, IGNORED: 已忽略, PUBLISHED: 已发布, }; return labels[status] ?? status; }演进要点 - 250 行的翻译函数覆盖 20 种状态/类型- 统一的 fallback 处理 labels[status] ?? status- 支持可选参数 translateTeamRole(role: string | null | undefined)8. 表单状态管理的演进从分散 ref 到 reactiveV1-V2 时期表单状态使用分散的 ref // V1-V2分散的 ref const projectName ref(); const owner ref(); const repo ref();V8 之后使用 reactive 管理表单状态// V8reactive 表单 const manualForm reactive({ project_name: , owner: , repo: , default_branch: main, }); const scopeForm reactive({ uses_custom_configuration: false, rule_ids: [] as number[], skill_ids: [] as number[], norm_mapping_ids: [] as number[], });演进要点 - reactive 适合表单场景修改时无需 .value- 重置函数统一管理 resetManualForm() 、 resetOAuthForm()- 类型注解 [] as number[] 明确数组类型9. 加载状态的演进从单一 loading 到多状态V1-V2 时期只有单一 loading 状态const loading ref(false);V8 之后需要区分多种操作状态// ProjectsView.vue 的加载状态 const loading ref(false); const saving ref(false); const scopeSaving ref(false); const oauthLoading ref(false); const repoLoading ref(false); const branchLoading ref(false); const oauthBindingSaving ref(false); const verifyingBindingId refnumber | null(null); const diagnosingBindingId refnumber | null(null); const syncingBindingId refnumber | null(null); const recreatingBindingId refnumber | null(null); const teamSavingBindingId refnumber | null(null); const teamRequestBindingId refnumber | null(null);演进要点 - 按操作类型区分 loading 状态- 按实体 ID 区分 loading 状态如 verifyingBindingId - 按钮绑定对应 loading :loadingverifyingBindingId binding.id10. 测试策略的演进从零到关键路径覆盖V9-V10 引入测试优先测试三类内容纯函数测试 // analytics.spec.ts describe(analytics polyline, () { it(returns empty string for empty trend, () { expect(buildTrendPolyline([], (point) point.created_tasks)).toBe(); }); it(builds chart points and keeps them within viewport, () { const trend [ mockPoint({ date: 2026-03-20, created_tasks: 1 }), mockPoint({ date: 2026-03-21, created_tasks: 3 }), mockPoint({ date: 2026-03-22, created_tasks: 2 }), ]; const points buildTrendPolyline(trend, (point) point.created_tasks, { width: 200, height: 100, padding: 10, }); expect(points).toBe(10,63 100,10 190,37); }); });状态机测试 // draftCommentState.spec.ts describe(draftCommentState, () { it(allows confirm and ignore only for unconfirmed statuses, () { expect(canConfirmDraftComment(DRAFT)).toBe(true); expect(canConfirmDraftComment(PENDING_CONFIRMATION)).toBe(true); expect(canConfirmDraftComment(CONFIRMED)).toBe(false); }); it(allows publish only for confirmed status, () { expect(canPublishDraftComment(CONFIRMED)).toBe(true); expect(canPublishDraftComment(DRAFT)).toBe(false); }); });路由测试 // router.spec.ts describe(router, () { it(registers the core MVP pages, () { const routeNames router.getRoutes().map((route) route.name); expect(routeNames).toEqual( expect.arrayContaining([ dashboard, projects, review-tasks, review-task-detail, comments, ]) ); }); });测试策略 - 纯函数优先测试无依赖、易断言- 状态机逻辑单独抽取便于测试- 路由配置测试覆盖核心页面