feat(版本): 优化概览与只读状态
关键改动: - 增加需求排序和版本只读状态规则及测试 - 完善版本概览阶段耗时、项目页和工作台展示 - 优化小宝预警请求节流、建议状态和风险过滤 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -32,6 +32,7 @@ interface Props {
|
||||
allTaskIds: string[];
|
||||
onClose: () => void;
|
||||
contextLabel?: string;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
function defaultPlanStartLocal(): string {
|
||||
@@ -46,7 +47,7 @@ function defaultPlanEndLocal(): string {
|
||||
return isoToLocal(d.toISOString());
|
||||
}
|
||||
|
||||
export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel }: Props) {
|
||||
export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel, readOnly = false }: Props) {
|
||||
const { tasks, changeStatus, setBlocked, deleteTask, updateTask } = useDevTaskStore();
|
||||
const addProgressNote = useWorkActivityStore((s) => s.addProgressNote);
|
||||
const { categories } = useTaskCategoryStore();
|
||||
@@ -93,12 +94,14 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel
|
||||
const planEstimateHours = planStartBeforeEnd ? calcWorkHours(planStartISO, planEndISO) : 0;
|
||||
|
||||
const openPlanInput = () => {
|
||||
if (readOnly) return;
|
||||
setPlanStartLocal(task.expectedStartAt ? isoToLocal(task.expectedStartAt) : defaultPlanStartLocal());
|
||||
setPlanEndLocal(task.expectedEndAt ? isoToLocal(task.expectedEndAt) : defaultPlanEndLocal());
|
||||
setShowPlanInput(true);
|
||||
};
|
||||
|
||||
const handleSavePlan = () => {
|
||||
if (readOnly) return;
|
||||
const assigneeId = task.assigneeId || currentUserName;
|
||||
if (!assigneeId) {
|
||||
alert('领取前需要先登录或选择负责人');
|
||||
@@ -115,6 +118,7 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel
|
||||
};
|
||||
|
||||
const handleTransition = (to: DevTaskStatus) => {
|
||||
if (readOnly) return;
|
||||
if (to === 'in_progress' && !startReady) {
|
||||
openPlanInput();
|
||||
return;
|
||||
@@ -138,17 +142,20 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel
|
||||
};
|
||||
|
||||
const handleBlock = () => {
|
||||
if (readOnly) return;
|
||||
if (!blockReason.trim()) return;
|
||||
setBlocked(task.id, true, blockReason.trim());
|
||||
setShowBlockInput(false);
|
||||
};
|
||||
|
||||
const handleUnblock = () => {
|
||||
if (readOnly) return;
|
||||
setBlocked(task.id, false);
|
||||
setBlockReason('');
|
||||
};
|
||||
|
||||
const handleProgressNote = () => {
|
||||
if (readOnly) return;
|
||||
const note = progressNote.trim();
|
||||
const blocker = progressBlocker.trim();
|
||||
const delayRisk = progressDelayRisk.trim();
|
||||
@@ -184,15 +191,15 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel
|
||||
<span className="text-[14px] font-semibold text-[var(--ink)] truncate max-w-[240px]">{task.title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{task.status !== 'submitted' && (
|
||||
{!readOnly && task.status !== 'submitted' && (
|
||||
<button onClick={() => setShowTransfer(!showTransfer)} className="p-1.5 rounded-lg hover:bg-blue-50 text-[var(--ink-muted)] hover:text-blue-500" title="转交"><ArrowRightLeft className="h-4 w-4" /></button>
|
||||
)}
|
||||
<button onClick={() => { if (confirm('确定删除此任务?')) { deleteTask(task.id); onClose(); } }} className="p-1.5 rounded-lg hover:bg-red-50 text-[var(--ink-muted)] hover:text-red-500" title="删除"><Trash2 className="h-4 w-4" /></button>
|
||||
{!readOnly && <button onClick={() => { if (confirm('确定删除此任务?')) { deleteTask(task.id); onClose(); } }} className="p-1.5 rounded-lg hover:bg-red-50 text-[var(--ink-muted)] hover:text-red-500" title="删除"><Trash2 className="h-4 w-4" /></button>}
|
||||
<button onClick={onClose} className="p-1.5 rounded-lg hover:bg-[var(--bg-subtle)]"><X className="h-4 w-4 text-[var(--ink-muted)]" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showTransfer && (
|
||||
{showTransfer && !readOnly && (
|
||||
<div className="mx-5 mt-3 rounded-lg border border-[var(--line)] p-3 flex items-center gap-2">
|
||||
<span className="text-[11px] text-[var(--ink-muted)] shrink-0">转交给:</span>
|
||||
<FilterSelect
|
||||
@@ -239,7 +246,7 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel
|
||||
)}
|
||||
</div>
|
||||
|
||||
{visibleNextStatuses.length > 0 && !showDelayInput && (
|
||||
{visibleNextStatuses.length > 0 && !showDelayInput && !readOnly && (
|
||||
<div className="flex items-center gap-2 pt-1 flex-wrap">
|
||||
<ChevronRight className="h-3.5 w-3.5 text-[var(--ink-muted)]" />
|
||||
{visibleNextStatuses.map((s) => (
|
||||
@@ -250,7 +257,7 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel
|
||||
</div>
|
||||
)}
|
||||
|
||||
{task.status === 'todo' && !startReady && !showPlanInput && (
|
||||
{task.status === 'todo' && !startReady && !showPlanInput && !readOnly && (
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<button
|
||||
onClick={openPlanInput}
|
||||
@@ -265,7 +272,7 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPlanInput && (
|
||||
{showPlanInput && !readOnly && (
|
||||
<div className="rounded-lg border border-orange-200 bg-orange-50 p-3 space-y-3">
|
||||
<div className="text-[11px] font-medium text-orange-700">
|
||||
{needsClaim ? '领取并填写计划' : '填写计划'}
|
||||
@@ -305,7 +312,7 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showDelayInput && (
|
||||
{showDelayInput && !readOnly && (
|
||||
<div className="rounded-lg border border-orange-200 bg-orange-50 p-3 space-y-2">
|
||||
<div className="flex items-center gap-1.5 text-[11px] text-orange-700">
|
||||
<AlertTriangle className="h-3.5 w-3.5" />
|
||||
@@ -326,23 +333,23 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel
|
||||
<AlertTriangle className="h-3.5 w-3.5 shrink-0 mt-0.5" />
|
||||
<span>{task.blockReason}</span>
|
||||
</div>
|
||||
<button onClick={handleUnblock} className="text-[11px] text-emerald-600 font-medium hover:underline">解除阻塞</button>
|
||||
{!readOnly && <button onClick={handleUnblock} className="text-[11px] text-emerald-600 font-medium hover:underline">解除阻塞</button>}
|
||||
</div>
|
||||
) : (
|
||||
showBlockInput ? (
|
||||
showBlockInput && !readOnly ? (
|
||||
<div className="flex gap-2">
|
||||
<input value={blockReason} onChange={(e) => setBlockReason(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleBlock(); }} placeholder="阻塞原因" className="flex-1 h-8 rounded-lg border border-[var(--line)] px-3 text-[12px] focus:border-red-400 focus:outline-none" autoFocus />
|
||||
<button onClick={handleBlock} disabled={!blockReason.trim()} className="h-8 px-3 rounded-lg text-[11px] font-medium bg-red-500 text-white disabled:opacity-50">确认</button>
|
||||
<button onClick={() => setShowBlockInput(false)} className="h-8 px-2 text-[11px] text-[var(--ink-muted)]">取消</button>
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={() => setShowBlockInput(true)} className="text-[11px] text-red-500 font-medium hover:underline">标记阻塞</button>
|
||||
!readOnly && <button onClick={() => setShowBlockInput(true)} className="text-[11px] text-red-500 font-medium hover:underline">标记阻塞</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{task.status !== 'todo' && task.status !== 'submitted' && (
|
||||
{task.status !== 'todo' && task.status !== 'submitted' && !readOnly && (
|
||||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4 space-y-3">
|
||||
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide">今日进展</div>
|
||||
<textarea
|
||||
|
||||
Reference in New Issue
Block a user