feat(版本): 记录计划需求进展活动
关键改动: - 记录产品/UI 计划需求覆盖进展,并写入工作活动与实体日志 - 日报和小宝预警补充需求进展活动证据 - 调整版本列表与需求覆盖面板展示,并补充测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -12,6 +12,7 @@ export const WORK_ACTIVITY_ACTION_LABEL: Record<WorkActivityAction, string> = {
|
||||
version_plan_created: '新建计划',
|
||||
version_plan_started: '开始计划',
|
||||
version_plan_completed: '完成计划',
|
||||
version_plan_requirement_progress: '需求进度',
|
||||
dev_task_created: '新建开发任务',
|
||||
dev_task_started: '开始开发',
|
||||
dev_task_self_testing: '进入自测',
|
||||
|
||||
@@ -163,6 +163,8 @@ test('updates requirement coverage, syncs legacy completed ids, and creates a pl
|
||||
assert.equal(next.logs?.[0]?.type, 'requirement_progress');
|
||||
assert.equal(next.logs?.[0]?.actor, 'PM');
|
||||
assert.equal(next.logs?.[0]?.requirementCode, 'QY0001');
|
||||
assert.equal(next.logs?.[0]?.completedContent, '完成移动端主流程');
|
||||
assert.equal(next.logs?.[0]?.remainingContent, 'PC 端筛选规则未完成');
|
||||
});
|
||||
|
||||
test('validates requirement coverage record drafts by status', () => {
|
||||
@@ -178,3 +180,16 @@ test('validates requirement coverage record drafts by status', () => {
|
||||
assert.equal(canSaveRequirementCoverageDraft!('partial', '完成主流程', '补充异常状态'), true);
|
||||
assert.equal(canSaveRequirementCoverageDraft!('not_started'), false);
|
||||
});
|
||||
|
||||
test('hides coverage record action after a requirement is completed', () => {
|
||||
const canOpenRequirementCoverageRecord = (versionPlan as any).canOpenRequirementCoverageRecord as undefined | ((
|
||||
status: string,
|
||||
canEdit: boolean,
|
||||
) => boolean);
|
||||
assert.equal(typeof canOpenRequirementCoverageRecord, 'function');
|
||||
|
||||
assert.equal(canOpenRequirementCoverageRecord!('not_started', true), true);
|
||||
assert.equal(canOpenRequirementCoverageRecord!('partial', true), true);
|
||||
assert.equal(canOpenRequirementCoverageRecord!('completed', true), false);
|
||||
assert.equal(canOpenRequirementCoverageRecord!('partial', false), false);
|
||||
});
|
||||
|
||||
@@ -67,6 +67,8 @@ export interface VersionPlanLog {
|
||||
requirementCode?: string;
|
||||
requirementTitle?: string;
|
||||
coverageStatus?: RequirementCoverageStatus;
|
||||
completedContent?: string;
|
||||
remainingContent?: string;
|
||||
aiTarget?: AgentDecomposeTarget;
|
||||
aiStatus?: AiDecomposeLogStatus;
|
||||
}
|
||||
@@ -194,6 +196,13 @@ export function canSaveRequirementCoverageDraft(
|
||||
return false;
|
||||
}
|
||||
|
||||
export function canOpenRequirementCoverageRecord(
|
||||
status: RequirementCoverageStatus,
|
||||
canEdit: boolean,
|
||||
): boolean {
|
||||
return canEdit && status !== 'completed';
|
||||
}
|
||||
|
||||
export function appendPlanLog(plan: VersionPlan, draft: PlanLogDraft): VersionPlanLog[] {
|
||||
const createdAt = draft.createdAt ?? new Date().toISOString();
|
||||
const log: VersionPlanLog = {
|
||||
@@ -260,6 +269,8 @@ export function updateRequirementCoverage(
|
||||
requirementCode: input.requirementCode,
|
||||
requirementTitle: input.requirementTitle,
|
||||
coverageStatus: input.status,
|
||||
completedContent: nextCoverage.completedContent,
|
||||
remainingContent: nextCoverage.remainingContent,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
makeDevTaskStatusActivity,
|
||||
makeTestCaseStatusActivity,
|
||||
makeVersionPlanCompletedActivity,
|
||||
makeVersionPlanRequirementProgressActivity,
|
||||
} from './work-activity-factory';
|
||||
|
||||
function devTask(patch: Partial<DevTask> = {}): DevTask {
|
||||
@@ -105,6 +106,32 @@ test('makeVersionPlanCompletedActivity marks product plan completion as delivery
|
||||
assert.equal(activity.summary, '完成产品方案:提交产品方案');
|
||||
});
|
||||
|
||||
test('makeVersionPlanRequirementProgressActivity records partial requirement coverage details', () => {
|
||||
const activity = makeVersionPlanRequirementProgressActivity(plan({ type: 'ui', title: '绘制个人中心' }), {
|
||||
id: 'log-1',
|
||||
type: 'requirement_progress',
|
||||
createdAt: '2026-06-30T03:00:00.000Z',
|
||||
actor: '张三',
|
||||
title: 'QY-001 个人中心更新为部分完成',
|
||||
detail: '已完成:头像和基本信息\n剩余:安全设置入口',
|
||||
requirementId: 'req-1',
|
||||
requirementCode: 'QY-001',
|
||||
requirementTitle: '个人中心',
|
||||
coverageStatus: 'partial',
|
||||
completedContent: '头像和基本信息',
|
||||
remainingContent: '安全设置入口',
|
||||
});
|
||||
|
||||
assert.equal(activity?.action, 'version_plan_requirement_progress');
|
||||
assert.equal(activity?.category, 'progress');
|
||||
assert.equal(activity?.sourceType, 'version_plan');
|
||||
assert.equal(activity?.sourceId, 'plan-1');
|
||||
assert.equal(activity?.occurredAt, '2026-06-30T03:00:00.000Z');
|
||||
assert.equal(activity?.summary, '推进UI设计需求:QY-001 个人中心(部分完成)');
|
||||
assert.equal(activity?.metadata?.completedContent, '头像和基本信息');
|
||||
assert.equal(activity?.metadata?.remainingContent, '安全设置入口');
|
||||
});
|
||||
|
||||
test('makeBugStatusActivity maps fixing and fixed actions', () => {
|
||||
const fixing = makeBugStatusActivity(bug({ status: 'open' }), 'open', 'fixing', '张三');
|
||||
const fixed = makeBugStatusActivity(bug(), 'fixing', 'fixed', '张三');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Bug, BugStatus } from './bug';
|
||||
import type { DevTask, DevTaskStatus } from './dev-task';
|
||||
import type { TestCase, TestCaseStatus } from './test-case';
|
||||
import type { VersionPlan } from './version-plan';
|
||||
import { REQUIREMENT_COVERAGE_LABEL, type VersionPlan, type VersionPlanLog } from './version-plan';
|
||||
import type { WorkActivityDraft } from './work-activity';
|
||||
|
||||
const PLAN_TYPE_LABEL: Record<VersionPlan['type'], string> = {
|
||||
@@ -46,6 +46,33 @@ export function makeVersionPlanCompletedActivity(plan: VersionPlan, actorId: str
|
||||
};
|
||||
}
|
||||
|
||||
export function makeVersionPlanRequirementProgressActivity(plan: VersionPlan, log: VersionPlanLog): WorkActivityDraft | undefined {
|
||||
if (log.type !== 'requirement_progress') return undefined;
|
||||
if (plan.type !== 'product' && plan.type !== 'ui') return undefined;
|
||||
|
||||
const requirementLabel = [log.requirementCode, log.requirementTitle].filter(Boolean).join(' ') || '需求';
|
||||
const statusLabel = log.coverageStatus ? REQUIREMENT_COVERAGE_LABEL[log.coverageStatus] : '进度更新';
|
||||
|
||||
return {
|
||||
actorId: log.actor,
|
||||
sourceType: 'version_plan',
|
||||
sourceId: plan.id,
|
||||
action: 'version_plan_requirement_progress',
|
||||
category: 'progress',
|
||||
title: plan.title,
|
||||
summary: `推进${PLAN_TYPE_LABEL[plan.type]}需求:${requirementLabel}(${statusLabel})`,
|
||||
occurredAt: log.createdAt,
|
||||
metadata: {
|
||||
requirementId: log.requirementId,
|
||||
requirementCode: log.requirementCode,
|
||||
requirementTitle: log.requirementTitle,
|
||||
coverageStatus: log.coverageStatus,
|
||||
completedContent: log.completedContent,
|
||||
remainingContent: log.remainingContent,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function makeDevTaskCreatedActivity(task: DevTask, actorId: string): WorkActivityDraft {
|
||||
return {
|
||||
actorId,
|
||||
|
||||
@@ -6,6 +6,7 @@ export type WorkActivityAction =
|
||||
| 'version_plan_created'
|
||||
| 'version_plan_started'
|
||||
| 'version_plan_completed'
|
||||
| 'version_plan_requirement_progress'
|
||||
| 'dev_task_created'
|
||||
| 'dev_task_started'
|
||||
| 'dev_task_self_testing'
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
filterXiaobaoRiskWarnings,
|
||||
filterXiaobaoWarningVersions,
|
||||
formatRemainingWork,
|
||||
getRiskScoreTone,
|
||||
getXiaobaoWarningRiskCount,
|
||||
sanitizeRiskInsight,
|
||||
} from './xiaobao-warning-view';
|
||||
@@ -135,6 +136,12 @@ test('formatRemainingWork keeps hours and adds work-day conversion', () => {
|
||||
assert.equal(formatRemainingWork(0.5), '0.5h / 0.1天');
|
||||
});
|
||||
|
||||
test('getRiskScoreTone follows Xiaobao warning score thresholds', () => {
|
||||
assert.equal(getRiskScoreTone(75), 'danger');
|
||||
assert.equal(getRiskScoreTone(55), 'warn');
|
||||
assert.equal(getRiskScoreTone(54), 'ok');
|
||||
});
|
||||
|
||||
test('sanitizeRiskInsight filters invalid page refresh suggested actions', () => {
|
||||
const result = sanitizeRiskInsight({
|
||||
summary: '风险上升',
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface XiaobaoWarningRiskListFilter {
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
export type RiskScoreTone = 'danger' | 'warn' | 'ok';
|
||||
|
||||
export function filterXiaobaoWarningVersions(
|
||||
versions: VersionWithContext[],
|
||||
filter: XiaobaoWarningVersionFilter,
|
||||
@@ -56,6 +58,12 @@ export function getXiaobaoWarningRiskCount(risks: XiaobaoVersionRisk[]): number
|
||||
return filterXiaobaoRiskWarnings(risks).length;
|
||||
}
|
||||
|
||||
export function getRiskScoreTone(score: number): RiskScoreTone {
|
||||
if (score >= 75) return 'danger';
|
||||
if (score >= 55) return 'warn';
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
export function formatRemainingWork(hours: number): string {
|
||||
const safeHours = Number.isFinite(hours) && hours > 0 ? hours : 0;
|
||||
const days = safeHours / WORK_HOURS.hoursPerDay;
|
||||
|
||||
Reference in New Issue
Block a user