feat(工作台): 细化计划进展工时证据
关键改动: - 支持需求覆盖和调研方向开始工作记录 - 日报按计划记录开始时间和进度下次开始时间计算证据 - 更新版本编辑校验、项目展示和 workflow 说明 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user