feat(工作台): 细化计划进展工时证据

关键改动:

- 支持需求覆盖和调研方向开始工作记录

- 日报按计划记录开始时间和进度下次开始时间计算证据

- 更新版本编辑校验、项目展示和 workflow 说明

Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
Script Generator
2026-07-01 11:02:38 +08:00
parent e5ce2d221e
commit 8947aeaac7
18 changed files with 1125 additions and 94 deletions

View File

@@ -14,6 +14,8 @@ import {
getRequirementCoverageStatus,
getRequirementCoverageSummary,
REQUIREMENT_COVERAGE_LABEL,
startResearchDirectionWork,
startRequirementCoverageWork,
updateResearchDirectionProgress,
updateRequirementCoverage,
} from '@/lib/version-plan';
@@ -116,10 +118,19 @@ export function PlanRequirementCoveragePanel({ plan, requirements, canEdit, curr
setRemainingContent('');
};
const canSavePartial = canSaveRequirementCoverageDraft('partial', completedContent, remainingContent);
const startCoverage = (req: RequirementOption) => {
if (!canEdit) return;
const patch = startRequirementCoverageWork(plan, {
requirementId: req.id,
updatedBy: currentUserName,
});
onUpdate(plan.id, patch);
};
const saveCoverage = (req: RequirementOption, status: Extract<RequirementCoverageStatus, 'partial' | 'completed'>) => {
if (!canEdit || !canSaveRequirementCoverageDraft(status, completedContent, remainingContent)) return;
const coverage = getRequirementCoverage(plan, req.id);
const workStartedAt = coverage?.currentWorkStartedAt;
if (!canEdit || !canSaveRequirementCoverageDraft(status, completedContent, remainingContent, workStartedAt)) return;
const patch = updateRequirementCoverage(plan, {
requirementId: req.id,
status,
@@ -155,7 +166,11 @@ export function PlanRequirementCoveragePanel({ plan, requirements, canEdit, curr
const status = getRequirementCoverageStatus(plan, req.id);
const coverage = getRequirementCoverage(plan, req.id);
const isEditing = editingRequirementId === req.id;
const canOpenRecord = canOpenRequirementCoverageRecord(status, canEdit);
const hasStartedWork = Boolean(coverage?.currentWorkStartedAt);
const canOpenRecord = canOpenRequirementCoverageRecord(status, canEdit) && hasStartedWork;
const canStartWork = canEdit && status !== 'completed' && !hasStartedWork;
const canSaveCompleted = canSaveRequirementCoverageDraft('completed', undefined, undefined, coverage?.currentWorkStartedAt);
const canSavePartial = canSaveRequirementCoverageDraft('partial', completedContent, remainingContent, coverage?.currentWorkStartedAt);
return (
<div key={req.id} className="rounded-md px-2 py-1.5 hover:bg-[var(--bg-card)]">
<div className="flex min-w-0 items-start gap-2">
@@ -168,19 +183,31 @@ export function PlanRequirementCoveragePanel({ plan, requirements, canEdit, curr
<span className="min-w-0 truncate text-[12px] font-medium text-[var(--ink)]" title={req.title}>{req.title}</span>
{req.isHistorical && <span className="shrink-0 rounded bg-orange-50 px-1.5 py-0.5 text-[10px] text-orange-600"></span>}
</div>
{(coverage?.completedContent || coverage?.remainingContent) && (
{(coverage?.completedContent || coverage?.remainingContent || coverage?.currentWorkStartedAt) && (
<div className="mt-1 space-y-0.5 text-[11px] leading-4 text-[var(--ink-soft)]">
{coverage.completedContent && <div className="line-clamp-2">{coverage.completedContent}</div>}
{coverage.remainingContent && <div className="line-clamp-2 text-amber-700">{coverage.remainingContent}</div>}
{coverage.currentWorkStartedAt && <div className="text-blue-600">{formatDateTime(coverage.currentWorkStartedAt)}</div>}
</div>
)}
</div>
{canEdit && (canOpenRecord || isEditing) && (
{canEdit && status !== 'completed' && (
<div className="flex shrink-0 items-center gap-1.5">
{canStartWork && (
<button
type="button"
onClick={() => startCoverage(req)}
className="rounded-md bg-blue-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-blue-700"
>
</button>
)}
<button
type="button"
onClick={() => isEditing ? closeEditor() : openEditor(req)}
className="rounded-md border border-[var(--line)] px-2 py-1 text-[11px] font-medium text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]"
onClick={() => canOpenRecord && (isEditing ? closeEditor() : openEditor(req))}
disabled={!canOpenRecord}
title={!hasStartedWork ? '请先开始任务' : undefined}
className="rounded-md border border-[var(--line)] px-2 py-1 text-[11px] font-medium text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)] disabled:cursor-not-allowed disabled:opacity-50"
>
{isEditing ? '收起' : '记录'}
</button>
@@ -207,7 +234,8 @@ export function PlanRequirementCoveragePanel({ plan, requirements, canEdit, curr
<button
type="button"
onClick={() => saveCoverage(req, 'completed')}
className="h-7 rounded-md bg-emerald-600 px-3 text-[11px] font-medium text-white hover:bg-emerald-700"
disabled={!canSaveCompleted}
className="h-7 rounded-md bg-emerald-600 px-3 text-[11px] font-medium text-white hover:bg-emerald-700 disabled:opacity-50"
>
</button>
@@ -260,10 +288,17 @@ export function PlanResearchDirectionPanel({ plan, canEdit, currentUserName, onU
setRemainingContent('');
};
const canSavePartial = canSaveRequirementCoverageDraft('partial', completedContent, remainingContent);
const startDirection = (task: PlanTask) => {
if (!canEdit) return;
const patch = startResearchDirectionWork(plan, {
taskId: task.id,
updatedBy: currentUserName,
});
onUpdate(plan.id, patch);
};
const saveDirection = (task: PlanTask, status: Extract<RequirementCoverageStatus, 'partial' | 'completed'>) => {
if (!canEdit || !canSaveRequirementCoverageDraft(status, completedContent, remainingContent)) return;
if (!canEdit || !canSaveRequirementCoverageDraft(status, completedContent, remainingContent, task.currentWorkStartedAt)) return;
const patch = updateResearchDirectionProgress(plan, {
taskId: task.id,
status,
@@ -296,7 +331,11 @@ export function PlanResearchDirectionPanel({ plan, canEdit, currentUserName, onU
{tasks.map((task) => {
const status = getResearchDirectionStatus(task);
const isEditing = editingTaskId === task.id;
const canOpenRecord = canOpenRequirementCoverageRecord(status, canEdit);
const hasStartedWork = Boolean(task.currentWorkStartedAt);
const canOpenRecord = canOpenRequirementCoverageRecord(status, canEdit) && hasStartedWork;
const canStartWork = canEdit && status !== 'completed' && !hasStartedWork;
const canSaveCompleted = canSaveRequirementCoverageDraft('completed', undefined, undefined, task.currentWorkStartedAt);
const canSavePartial = canSaveRequirementCoverageDraft('partial', completedContent, remainingContent, task.currentWorkStartedAt);
return (
<div key={task.id} className="rounded-md px-2 py-1.5 hover:bg-[var(--bg-card)]">
<div className="flex min-w-0 items-start gap-2">
@@ -305,19 +344,31 @@ export function PlanResearchDirectionPanel({ plan, canEdit, currentUserName, onU
</span>
<div className="min-w-0 flex-1">
<div className="truncate text-[12px] font-medium text-[var(--ink)]" title={task.title}>{task.title}</div>
{(task.completedContent || task.remainingContent) && (
{(task.completedContent || task.remainingContent || task.currentWorkStartedAt) && (
<div className="mt-1 space-y-0.5 text-[11px] leading-4 text-[var(--ink-soft)]">
{task.completedContent && <div className="line-clamp-2">{task.completedContent}</div>}
{task.remainingContent && <div className="line-clamp-2 text-amber-700">{task.remainingContent}</div>}
{task.currentWorkStartedAt && <div className="text-blue-600">{formatDateTime(task.currentWorkStartedAt)}</div>}
</div>
)}
</div>
{canEdit && (canOpenRecord || isEditing) && (
{canEdit && status !== 'completed' && (
<div className="flex shrink-0 items-center gap-1.5">
{canStartWork && (
<button
type="button"
onClick={() => startDirection(task)}
className="rounded-md bg-blue-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-blue-700"
>
</button>
)}
<button
type="button"
onClick={() => isEditing ? closeEditor() : openEditor(task)}
className="rounded-md border border-[var(--line)] px-2 py-1 text-[11px] font-medium text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]"
onClick={() => canOpenRecord && (isEditing ? closeEditor() : openEditor(task))}
disabled={!canOpenRecord}
title={!hasStartedWork ? '请先开始任务' : undefined}
className="rounded-md border border-[var(--line)] px-2 py-1 text-[11px] font-medium text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)] disabled:cursor-not-allowed disabled:opacity-50"
>
{isEditing ? '收起' : '记录'}
</button>
@@ -344,7 +395,8 @@ export function PlanResearchDirectionPanel({ plan, canEdit, currentUserName, onU
<button
type="button"
onClick={() => saveDirection(task, 'completed')}
className="h-7 rounded-md bg-emerald-600 px-3 text-[11px] font-medium text-white hover:bg-emerald-700"
disabled={!canSaveCompleted}
className="h-7 rounded-md bg-emerald-600 px-3 text-[11px] font-medium text-white hover:bg-emerald-700 disabled:opacity-50"
>
</button>
@@ -460,6 +512,11 @@ export function PlanLogTimeline({ logs, className = '', fillHeight = false }: Lo
<div className="whitespace-pre-wrap">{log.detail}</div>
</div>
)}
{log.workStartedAt && (
<div className="mt-2 inline-flex rounded-md bg-blue-50 px-2 py-1 text-[11px] font-medium text-blue-700">
{formatDateTime(log.workStartedAt)}
</div>
)}
</div>
</div>
);