feat: 计划时间改为年月日时分 + 到期自动开始 + 耗时统计同步
1. 日期选择改为 datetime-local(年月日时分): - 计划开始时间/截止时间均精确到分钟 - 展示时显示完整时间而非仅日期 2. 到期自动开始逻辑: - 当前时间 >= plan.startTime 且 status=pending → 自动视为开始 - 自动触发 store 更新 status=in_progress + actualStartAt - 用户仍可在计划开始前手动点击"开始"按钮 - 按钮改为"提前开始",已到期的不再显示按钮 3. 耗时计算统一用实际时间: - 耗时 = actualStartAt 到 completedAt(或当前时间) - 自动开始的 plan 使用 startTime 作为 effectiveStartAt - 概览总耗时/开始日期计算也同步纳入自动开始的 plan 4. checkbox 与自动开始联动: - 自动开始后 checkbox 可交互(不需要手动点开始) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,25 +56,33 @@ export function PlanTab({ plans, versionId, versionDeadline, currentUserName, pl
|
||||
<div className="space-y-3">
|
||||
{typePlans.map((plan) => {
|
||||
const now = new Date().toISOString();
|
||||
// 自动开始:如果到了计划开始日期且状态还是 pending,视为已开始
|
||||
const autoStarted = plan.status === 'pending' && plan.startTime && new Date(plan.startTime) <= new Date();
|
||||
const effectiveStatus = autoStarted ? 'in_progress' : plan.status;
|
||||
const effectiveStartAt = plan.actualStartAt || (autoStarted ? plan.startTime : null);
|
||||
// 耗时用实际时间戳计算
|
||||
const dur = plan.status === 'completed' && plan.completedAt && plan.actualStartAt
|
||||
? calcPlanDuration(plan.actualStartAt, plan.completedAt)
|
||||
: plan.status === 'in_progress' && plan.actualStartAt
|
||||
? calcPlanDuration(plan.actualStartAt, now)
|
||||
: effectiveStatus === 'in_progress' && effectiveStartAt
|
||||
? calcPlanDuration(effectiveStartAt, now)
|
||||
: { days: 0, hours: 0 };
|
||||
const durText = dur.days > 0 || dur.hours > 0 ? formatDuration(dur.days, dur.hours) : '-';
|
||||
// 如果自动开始了,触发 store 更新(副作用)
|
||||
if (autoStarted && !plan.actualStartAt) {
|
||||
onUpdate(plan.id, { status: 'in_progress' });
|
||||
}
|
||||
return (
|
||||
<div key={plan.id} className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="text-[13px] font-medium text-[var(--ink)]">{plan.title}</span>
|
||||
<span className={`inline-flex items-center rounded-md px-2 py-0.5 text-[10px] font-medium border ${STATUS_STYLE[plan.status]}`}>
|
||||
{STATUS_LABEL[plan.status]}
|
||||
<span className={`inline-flex items-center rounded-md px-2 py-0.5 text-[10px] font-medium border ${STATUS_STYLE[effectiveStatus]}`}>
|
||||
{STATUS_LABEL[effectiveStatus]}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-[12px] text-[var(--ink-soft)]">
|
||||
<span>计划:{plan.startTime.slice(0, 10)} → {plan.endTime.slice(0, 10)}</span>
|
||||
<span>计划:{plan.startTime.slice(0, 16).replace('T', ' ')} → {plan.endTime.slice(0, 16).replace('T', ' ')}</span>
|
||||
{plan.actualStartAt && <span className="text-blue-600">实际开始:{plan.actualStartAt.slice(0, 16).replace('T', ' ')}</span>}
|
||||
{plan.completedAt && <span className="text-green-600">完成:{plan.completedAt.slice(0, 16).replace('T', ' ')}</span>}
|
||||
<span className="font-medium text-[var(--ink)]">已耗时 {durText}</span>
|
||||
@@ -106,14 +114,14 @@ export function PlanTab({ plans, versionId, versionDeadline, currentUserName, pl
|
||||
{plan.tasks.map((task) => (
|
||||
<div key={task.id} className="flex items-center gap-2">
|
||||
<button
|
||||
disabled={plan.status !== 'in_progress'}
|
||||
disabled={plan.status !== 'in_progress' && !autoStarted}
|
||||
onClick={() => {
|
||||
if (plan.status !== 'in_progress') return;
|
||||
if (plan.status !== 'in_progress' && !autoStarted) return;
|
||||
const nextStatus = task.status === 'completed' ? 'pending' : 'completed';
|
||||
const updatedTasks = plan.tasks!.map((t) => t.id === task.id ? { ...t, status: nextStatus as PlanTask['status'] } : t);
|
||||
onUpdate(plan.id, { tasks: updatedTasks });
|
||||
}}
|
||||
className={`h-4 w-4 rounded border flex items-center justify-center shrink-0 transition-colors ${plan.status !== 'in_progress' ? 'opacity-40 cursor-not-allowed' : ''} ${task.status === 'completed' ? 'bg-[var(--accent)] border-[var(--accent)]' : 'border-[var(--line)]'}`}
|
||||
className={`h-4 w-4 rounded border flex items-center justify-center shrink-0 transition-colors ${plan.status !== 'in_progress' && !autoStarted ? 'opacity-40 cursor-not-allowed' : ''} ${task.status === 'completed' ? 'bg-[var(--accent)] border-[var(--accent)]' : 'border-[var(--line)]'}`}
|
||||
>
|
||||
{task.status === 'completed' && <Check className="h-2.5 w-2.5 text-white" strokeWidth={3} />}
|
||||
</button>
|
||||
@@ -142,14 +150,14 @@ export function PlanTab({ plans, versionId, versionDeadline, currentUserName, pl
|
||||
return req ? (
|
||||
<div key={rid} className="flex items-center gap-2">
|
||||
<button
|
||||
disabled={plan.status !== 'in_progress'}
|
||||
disabled={plan.status !== 'in_progress' && !autoStarted}
|
||||
onClick={() => {
|
||||
if (plan.status !== 'in_progress') return;
|
||||
if (plan.status !== 'in_progress' && !autoStarted) return;
|
||||
const current = plan.completedRequirementIds || [];
|
||||
const next = isDone ? current.filter((id) => id !== rid) : [...current, rid];
|
||||
onUpdate(plan.id, { completedRequirementIds: next });
|
||||
}}
|
||||
className={`h-4 w-4 rounded border flex items-center justify-center shrink-0 transition-colors ${plan.status !== 'in_progress' ? 'opacity-40 cursor-not-allowed' : ''} ${isDone ? 'bg-[var(--accent)] border-[var(--accent)]' : 'border-[var(--line)]'}`}
|
||||
className={`h-4 w-4 rounded border flex items-center justify-center shrink-0 transition-colors ${plan.status !== 'in_progress' && !autoStarted ? 'opacity-40 cursor-not-allowed' : ''} ${isDone ? 'bg-[var(--accent)] border-[var(--accent)]' : 'border-[var(--line)]'}`}
|
||||
>
|
||||
{isDone && <Check className="h-2.5 w-2.5 text-white" strokeWidth={3} />}
|
||||
</button>
|
||||
@@ -169,8 +177,8 @@ export function PlanTab({ plans, versionId, versionDeadline, currentUserName, pl
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 ml-3">
|
||||
{plan.status === 'pending' && (
|
||||
<button onClick={() => onUpdate(plan.id, { status: 'in_progress' })} className="h-7 px-2 flex items-center gap-1 rounded-md text-[11px] font-medium text-blue-600 hover:bg-blue-50 border border-blue-200" title="开始">
|
||||
{plan.status === 'pending' && !autoStarted && (
|
||||
<button onClick={() => onUpdate(plan.id, { status: 'in_progress' })} className="h-7 px-2 flex items-center gap-1 rounded-md text-[11px] font-medium text-blue-600 hover:bg-blue-50 border border-blue-200" title="提前开始">
|
||||
<Play className="h-3 w-3" />开始
|
||||
</button>
|
||||
)}
|
||||
@@ -235,11 +243,11 @@ function PlanFormModal({ initial, planType, versionId, versionDeadline, currentU
|
||||
onClose: () => void;
|
||||
onSubmit: (data: any) => void;
|
||||
}) {
|
||||
const now = new Date().toISOString().slice(0, 10);
|
||||
const now = new Date().toISOString().slice(0, 16);
|
||||
const [title, setTitle] = useState(initial?.title ?? '');
|
||||
const [owner] = useState(initial?.owner ?? currentUserName);
|
||||
const [startTime, setStartTime] = useState(initial?.startTime?.slice(0, 10) ?? now);
|
||||
const [endTime, setEndTime] = useState(initial?.endTime?.slice(0, 10) ?? '');
|
||||
const [startTime, setStartTime] = useState(initial?.startTime?.slice(0, 16) ?? now);
|
||||
const [endTime, setEndTime] = useState(initial?.endTime?.slice(0, 16) ?? '');
|
||||
const [remark, setRemark] = useState(initial?.remark ?? '');
|
||||
const [tasks, setTasks] = useState<PlanTask[]>(initial?.tasks ?? []);
|
||||
const [newTaskTitle, setNewTaskTitle] = useState('');
|
||||
@@ -287,12 +295,12 @@ function PlanFormModal({ initial, planType, versionId, versionDeadline, currentU
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block">开始日期</label>
|
||||
<input type="date" value={startTime} onChange={(e) => setStartTime(e.target.value)} className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
|
||||
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block">计划开始时间</label>
|
||||
<input type="datetime-local" value={startTime} onChange={(e) => setStartTime(e.target.value)} className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block">结束日期</label>
|
||||
<input type="date" value={endTime} onChange={(e) => setEndTime(e.target.value)} required className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
|
||||
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block">计划截止时间</label>
|
||||
<input type="datetime-local" value={endTime} onChange={(e) => setEndTime(e.target.value)} required className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
{versionDeadline && (
|
||||
|
||||
Reference in New Issue
Block a user