fix: 胶囊进度按子任务加权 + 调研任务清单必填+预设选项
1. 胶囊进度计算改为子任务加权:
- 分子 = 所有plan中已完成的子任务数
- 分母 = 所有plan的子任务总数
- completed的plan视为其子任务全部完成
- 没有子任务的plan按1项计算(不会导致分母为0)
- 例:4个completed(共8子任务) + 1个pending(2子任务)
= 8/(8+2) = 80%
2. 调研任务清单必须至少1项:
- handleSubmit 加校验:tasks.length === 0 时 return
- 红色提示:"请至少添加一项任务"
3. 预设可选任务选项(空列表时显示):
- 竞品分析、用户访谈、数据调研
- 技术可行性分析、市场调研、需求分析
- 点击即添加,也可自定义输入
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -303,27 +303,33 @@ export default function VersionDetailPage() {
|
|||||||
|
|
||||||
const calcGroupProgress = (group: typeof vPlans, type: 'research' | 'product' | 'ui') => {
|
const calcGroupProgress = (group: typeof vPlans, type: 'research' | 'product' | 'ui') => {
|
||||||
if (group.length === 0) return 0;
|
if (group.length === 0) return 0;
|
||||||
// 每个 plan 按权重贡献进度:completed=1, in_progress=子任务完成率, pending=0
|
// 分子=已完成子任务数,分母=所有plan的子任务总数
|
||||||
let totalWeight = 0;
|
// completed 的 plan:其子任务全算已完成
|
||||||
|
let totalItems = 0;
|
||||||
|
let doneItems = 0;
|
||||||
for (const p of group) {
|
for (const p of group) {
|
||||||
if (p.status === 'completed') {
|
if (type === 'research') {
|
||||||
totalWeight += 1;
|
const tasks = p.tasks || [];
|
||||||
} else if (p.status === 'in_progress') {
|
const count = Math.max(tasks.length, 1);
|
||||||
if (type === 'research') {
|
totalItems += count;
|
||||||
const tasks = p.tasks || [];
|
if (p.status === 'completed') {
|
||||||
if (tasks.length > 0) {
|
doneItems += count;
|
||||||
totalWeight += tasks.filter((t) => t.status === 'completed').length / tasks.length;
|
} else {
|
||||||
}
|
doneItems += tasks.filter((t) => t.status === 'completed').length;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const linked = p.linkedRequirementIds || [];
|
||||||
|
const count = Math.max(linked.length, 1);
|
||||||
|
totalItems += count;
|
||||||
|
if (p.status === 'completed') {
|
||||||
|
doneItems += count;
|
||||||
} else {
|
} else {
|
||||||
const linked = p.linkedRequirementIds || [];
|
|
||||||
const completed = p.completedRequirementIds || [];
|
const completed = p.completedRequirementIds || [];
|
||||||
if (linked.length > 0) {
|
doneItems += completed.filter((id) => linked.includes(id)).length;
|
||||||
totalWeight += completed.filter((id) => linked.includes(id)).length / linked.length;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Math.round((totalWeight / group.length) * 100);
|
return totalItems > 0 ? Math.round((doneItems / totalItems) * 100) : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPlanStatus = (group: typeof vPlans): 'idle' | 'active' | 'done' => {
|
const getPlanStatus = (group: typeof vPlans): 'idle' | 'active' | 'done' => {
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ function PlanFormModal({ initial, planType, versionId, versionDeadline, currentU
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!title.trim() || !endTime) return;
|
if (!title.trim() || !endTime) return;
|
||||||
if (isOverdue && !overdueReason.trim()) return;
|
if (isOverdue && !overdueReason.trim()) return;
|
||||||
|
if (planType === 'research' && tasks.length === 0) return;
|
||||||
onSubmit({
|
onSubmit({
|
||||||
versionId,
|
versionId,
|
||||||
type: planType,
|
type: planType,
|
||||||
@@ -326,7 +327,25 @@ function PlanFormModal({ initial, planType, versionId, versionDeadline, currentU
|
|||||||
)}
|
)}
|
||||||
{planType === 'research' && (
|
{planType === 'research' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1.5 block">任务清单</label>
|
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1.5 block">
|
||||||
|
任务清单 <span className="text-red-500">*</span>
|
||||||
|
<span className="text-[10px] text-[var(--ink-muted)] ml-1">至少添加一项</span>
|
||||||
|
</label>
|
||||||
|
{/* 预设选项 */}
|
||||||
|
{tasks.length === 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1.5 mb-2">
|
||||||
|
{['竞品分析', '用户访谈', '数据调研', '技术可行性分析', '市场调研', '需求分析'].map((preset) => (
|
||||||
|
<button
|
||||||
|
key={preset}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTasks([...tasks, { id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 5)}`, title: preset, status: 'pending' }])}
|
||||||
|
className="h-6 px-2.5 rounded-md text-[11px] border border-dashed border-[var(--line)] text-[var(--ink-soft)] hover:border-[var(--accent)] hover:text-[var(--accent)] transition-colors"
|
||||||
|
>
|
||||||
|
+ {preset}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="space-y-1.5 mb-2">
|
<div className="space-y-1.5 mb-2">
|
||||||
{tasks.map((task, i) => (
|
{tasks.map((task, i) => (
|
||||||
<div key={task.id} className="flex items-center gap-2 rounded-lg bg-[var(--bg-subtle)] px-3 py-1.5">
|
<div key={task.id} className="flex items-center gap-2 rounded-lg bg-[var(--bg-subtle)] px-3 py-1.5">
|
||||||
@@ -340,11 +359,14 @@ function PlanFormModal({ initial, planType, versionId, versionDeadline, currentU
|
|||||||
value={newTaskTitle}
|
value={newTaskTitle}
|
||||||
onChange={(e) => setNewTaskTitle(e.target.value)}
|
onChange={(e) => setNewTaskTitle(e.target.value)}
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); if (newTaskTitle.trim()) { setTasks([...tasks, { id: `task-${Date.now()}`, title: newTaskTitle.trim(), status: 'pending' }]); setNewTaskTitle(''); } } }}
|
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); if (newTaskTitle.trim()) { setTasks([...tasks, { id: `task-${Date.now()}`, title: newTaskTitle.trim(), status: 'pending' }]); setNewTaskTitle(''); } } }}
|
||||||
placeholder="输入任务名称,回车添加"
|
placeholder="自定义任务名称,回车添加"
|
||||||
className="flex-1 h-8 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none"
|
className="flex-1 h-8 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<button type="button" onClick={() => { if (newTaskTitle.trim()) { setTasks([...tasks, { id: `task-${Date.now()}`, title: newTaskTitle.trim(), status: 'pending' }]); setNewTaskTitle(''); } }} className="h-8 px-3 rounded-lg text-[12px] font-medium bg-[var(--bg-subtle)] text-[var(--ink-soft)] hover:bg-[var(--line)]">添加</button>
|
<button type="button" onClick={() => { if (newTaskTitle.trim()) { setTasks([...tasks, { id: `task-${Date.now()}`, title: newTaskTitle.trim(), status: 'pending' }]); setNewTaskTitle(''); } }} className="h-8 px-3 rounded-lg text-[12px] font-medium bg-[var(--bg-subtle)] text-[var(--ink-soft)] hover:bg-[var(--line)]">添加</button>
|
||||||
</div>
|
</div>
|
||||||
|
{tasks.length === 0 && (
|
||||||
|
<div className="text-[11px] text-red-500 mt-1">请至少添加一项任务</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user