From a560634091209434d550451a5b044dd490da3e0f Mon Sep 17 00:00:00 2001 From: Script Generator Date: Thu, 11 Jun 2026 18:02:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(dev-task):=20=E5=BC=80=E5=8F=91=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=A8=A1=E5=9D=97=20V1=20=E5=AE=8C=E6=95=B4=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 核心库:dev-task.ts(类型+状态机+进度计算)、task-worklog.ts(工时)、task-category.ts(字典)、work-item.ts(聚合契约) - Store:useDevTaskStore(CRUD+状态流转)、useTaskWorklogStore(工时记录)、useTaskCategoryStore(字典管理) - UI组件:DevTaskTab、DevTaskCreateModal、DevTaskDetailDrawer、DevTaskRow、WorklogPanel、StatusBadge、CategoryChip - 集成:版本详情页"开发任务"Tab、"与我相关"工作台开发任务分组、任务类型管理页 - 设计文档:spec + 实施计划 Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/app/admin/categories/page.tsx | 73 + apps/web/app/versions/[id]/page.tsx | 11 + apps/web/app/workspace/page.tsx | 61 +- apps/web/components/dev-task/CategoryChip.tsx | 18 + .../dev-task/DevTaskCreateModal.tsx | 148 ++ .../dev-task/DevTaskDetailDrawer.tsx | 131 ++ apps/web/components/dev-task/DevTaskRow.tsx | 47 + apps/web/components/dev-task/DevTaskTab.tsx | 104 ++ apps/web/components/dev-task/StatusBadge.tsx | 12 + apps/web/components/dev-task/WorklogPanel.tsx | 85 + apps/web/lib/dev-task.ts | 93 ++ apps/web/lib/task-category.ts | 36 + apps/web/lib/task-worklog.ts | 33 + apps/web/lib/work-item.ts | 49 + apps/web/stores/useDevTaskStore.ts | 109 ++ apps/web/stores/useTaskCategoryStore.ts | 65 + apps/web/stores/useTaskWorklogStore.ts | 57 + ...2026-06-09-version-module-phase1-design.md | 187 +++ .../2026-06-11-dev-task-module-design.md | 357 +++++ .../plans/2026-06-11-dev-task-module.md | 1384 +++++++++++++++++ 20 files changed, 3054 insertions(+), 6 deletions(-) create mode 100644 apps/web/app/admin/categories/page.tsx create mode 100644 apps/web/components/dev-task/CategoryChip.tsx create mode 100644 apps/web/components/dev-task/DevTaskCreateModal.tsx create mode 100644 apps/web/components/dev-task/DevTaskDetailDrawer.tsx create mode 100644 apps/web/components/dev-task/DevTaskRow.tsx create mode 100644 apps/web/components/dev-task/DevTaskTab.tsx create mode 100644 apps/web/components/dev-task/StatusBadge.tsx create mode 100644 apps/web/components/dev-task/WorklogPanel.tsx create mode 100644 apps/web/lib/dev-task.ts create mode 100644 apps/web/lib/task-category.ts create mode 100644 apps/web/lib/task-worklog.ts create mode 100644 apps/web/lib/work-item.ts create mode 100644 apps/web/stores/useDevTaskStore.ts create mode 100644 apps/web/stores/useTaskCategoryStore.ts create mode 100644 apps/web/stores/useTaskWorklogStore.ts create mode 100644 docs/specs/2026-06-09-version-module-phase1-design.md create mode 100644 docs/specs/2026-06-11-dev-task-module-design.md create mode 100644 docs/superpowers/plans/2026-06-11-dev-task-module.md diff --git a/apps/web/app/admin/categories/page.tsx b/apps/web/app/admin/categories/page.tsx new file mode 100644 index 0000000..69fe688 --- /dev/null +++ b/apps/web/app/admin/categories/page.tsx @@ -0,0 +1,73 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Plus, Pencil, Trash2, Shield } from 'lucide-react'; +import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore'; +import { CATEGORY_GROUP_LABEL } from '@/lib/task-category'; +import type { CategoryGroup } from '@/lib/task-category'; + +export default function CategoriesPage() { + const { categories, fetchCategories, addCategory, updateCategory, deleteCategory } = useTaskCategoryStore(); + useEffect(() => { fetchCategories(); }, [fetchCategories]); + + const [newName, setNewName] = useState(''); + const [newGroup, setNewGroup] = useState('development'); + const [editingId, setEditingId] = useState(null); + const [editName, setEditName] = useState(''); + + const groups: CategoryGroup[] = ['development', 'testing', 'implementation', 'other']; + + const handleAdd = () => { + if (!newName.trim()) return; + addCategory(newName.trim(), newGroup); + setNewName(''); + }; + + const handleSaveEdit = (id: string) => { + if (editName.trim()) updateCategory(id, { name: editName.trim() }); + setEditingId(null); + }; + + return ( +
+

任务类型管理

+ +
+ setNewName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleAdd(); }} placeholder="新类型名称" className="flex-1 h-9 rounded-lg border border-[var(--line)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" /> + + +
+ + {groups.map((g) => { + const items = categories.filter((c) => c.group === g); + if (items.length === 0) return null; + return ( +
+

{CATEGORY_GROUP_LABEL[g]}

+
+ {items.map((cat) => ( +
+ {cat.color && } + {editingId === cat.id ? ( + setEditName(e.target.value)} onBlur={() => handleSaveEdit(cat.id)} onKeyDown={(e) => { if (e.key === 'Enter') handleSaveEdit(cat.id); }} autoFocus className="flex-1 h-7 rounded border border-[var(--line)] px-2 text-[13px] focus:border-[var(--accent)] focus:outline-none" /> + ) : ( + {cat.name} + )} + {cat.isSystem && } + {!cat.isSystem && ( + <> + + + + )} +
+ ))} +
+
+ ); + })} +
+ ); +} diff --git a/apps/web/app/versions/[id]/page.tsx b/apps/web/app/versions/[id]/page.tsx index a9c63c2..785a600 100644 --- a/apps/web/app/versions/[id]/page.tsx +++ b/apps/web/app/versions/[id]/page.tsx @@ -17,6 +17,7 @@ import { REQ_STATUS_LABEL, REQ_STATUS_COLOR } from '@/lib/requirement'; import { OVERTIME_REASON_LABEL } from '@/lib/overtime'; import { VersionRequirementsTab } from '@/components/version/VersionRequirementsTab'; import { PlanTab } from '@/components/version/PlanTab'; +import { DevTaskTab } from '@/components/dev-task/DevTaskTab'; import { useVersionPlanStore } from '@/stores/useVersionPlanStore'; import { useAuthStore } from '@/stores/useAuthStore'; @@ -450,6 +451,16 @@ export default function VersionDetailPage() { /> ); })() + ) : activeTab === 'tasks' ? ( + (() => { + const versionReqs = requirements.filter((r) => r.versionId === version.id); + return ( + r.id)} + /> + ); + })() ) : (
功能开发中,敬请期待 diff --git a/apps/web/app/workspace/page.tsx b/apps/web/app/workspace/page.tsx index e3fffcd..cf370c2 100644 --- a/apps/web/app/workspace/page.tsx +++ b/apps/web/app/workspace/page.tsx @@ -2,22 +2,27 @@ import { useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { Search, FileText, Palette, Layout, ClipboardList, Check, ExternalLink, Link2, FileUp } from 'lucide-react'; +import { Search, FileText, Palette, Layout, ClipboardList, Check, ExternalLink, Link2, FileUp, Code2 } from 'lucide-react'; import { useProductStore } from '@/stores/useProductStore'; import { useVersionPlanStore } from '@/stores/useVersionPlanStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; +import { useDevTaskStore } from '@/stores/useDevTaskStore'; +import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { flattenVersions } from '@/lib/derive'; import { calcPlanProgress, calcLinkedReqProgress } from '@/lib/version-plan'; +import { DEV_TASK_STATUS_LABEL, DEV_TASK_STATUS_COLOR, formatHours } from '@/lib/dev-task'; import type { PlanTask, VersionPlan } from '@/lib/version-plan'; +import type { DevTask } from '@/lib/dev-task'; -type TabKey = 'all' | 'research' | 'product' | 'ui'; +type TabKey = 'all' | 'research' | 'product' | 'ui' | 'devTask'; const TABS: { key: TabKey; label: string; icon: any }[] = [ { key: 'all', label: '全部待办', icon: ClipboardList }, { key: 'research', label: '调研', icon: Search }, { key: 'product', label: '产品方案', icon: FileText }, { key: 'ui', label: 'UI设计', icon: Palette }, + { key: 'devTask', label: '开发任务', icon: Code2 }, ]; export default function WorkspacePage() { @@ -25,6 +30,8 @@ export default function WorkspacePage() { const { overview, fetchOverview } = useProductStore(); const { plans, fetchPlans, updatePlan, completePlan } = useVersionPlanStore(); const { requirements, fetchRequirements } = useRequirementStore(); + const { tasks: devTasks, fetchTasks } = useDevTaskStore(); + const { categories, fetchCategories } = useTaskCategoryStore(); const user = useAuthStore((s) => s.user); const [activeTab, setActiveTab] = useState('all'); const [completingPlan, setCompletingPlan] = useState(null); @@ -32,6 +39,8 @@ export default function WorkspacePage() { useEffect(() => { fetchOverview(); }, [fetchOverview]); useEffect(() => { fetchPlans(); }, [fetchPlans]); useEffect(() => { fetchRequirements(); }, [fetchRequirements]); + useEffect(() => { fetchTasks(); }, [fetchTasks]); + useEffect(() => { fetchCategories(); }, [fetchCategories]); const userName = user?.name ?? ''; const allVersions = useMemo(() => flattenVersions(overview), [overview]); @@ -42,14 +51,21 @@ export default function WorkspacePage() { [plans, userName] ); + // 我负责的所有未完成开发任务 + const myDevTasks = useMemo(() => + devTasks.filter((t) => t.assigneeId === userName && t.status !== 'done'), + [devTasks, userName] + ); + const counts = { - all: myPlans.length, + all: myPlans.length + myDevTasks.length, research: myPlans.filter((p) => p.type === 'research').length, product: myPlans.filter((p) => p.type === 'product').length, ui: myPlans.filter((p) => p.type === 'ui').length, + devTask: myDevTasks.length, }; - const filtered = activeTab === 'all' ? myPlans : myPlans.filter((p) => p.type === activeTab); + const filtered = activeTab === 'all' ? myPlans : activeTab === 'devTask' ? [] : myPlans.filter((p) => p.type === activeTab); const today = new Date().toISOString().slice(0, 10); const toggleTask = (plan: VersionPlan, task: PlanTask) => { @@ -97,11 +113,44 @@ export default function WorkspacePage() {

{TABS.find((t) => t.key === activeTab)?.label}

- {filtered.length} 项 + {activeTab === 'devTask' ? myDevTasks.length : filtered.length} 项
- {filtered.length === 0 ? ( + {activeTab === 'devTask' ? ( + myDevTasks.length === 0 ? ( +
+

暂无开发任务

+
+ ) : ( + myDevTasks.map((task) => { + const cat = categories.find((c) => c.id === task.categoryId); + const req = requirements.find((r) => r.id === task.requirementId); + return ( +
+
+
+ + {DEV_TASK_STATUS_LABEL[task.status]} + + {task.isBlocked && 阻塞} + {task.taskNo} + {task.title} +
+ {task.priority} +
+
+ {req && {req.code} {req.title}} + {cat && {cat.name}} + 预计 {formatHours(task.estimateHours)} + {task.actualHours > 0 && 已投入 {task.actualHours}h} + {task.dueDate && 截止 {task.dueDate}} +
+
+ ); + }) + ) + ) : filtered.length === 0 ? (

暂无待办事项

diff --git a/apps/web/components/dev-task/CategoryChip.tsx b/apps/web/components/dev-task/CategoryChip.tsx new file mode 100644 index 0000000..72dde82 --- /dev/null +++ b/apps/web/components/dev-task/CategoryChip.tsx @@ -0,0 +1,18 @@ +'use client'; + +import type { TaskCategory } from '@/lib/task-category'; + +export function CategoryChip({ category }: { category?: TaskCategory }) { + if (!category) return 未分类; + return ( + + {category.name} + + ); +} diff --git a/apps/web/components/dev-task/DevTaskCreateModal.tsx b/apps/web/components/dev-task/DevTaskCreateModal.tsx new file mode 100644 index 0000000..32a32a6 --- /dev/null +++ b/apps/web/components/dev-task/DevTaskCreateModal.tsx @@ -0,0 +1,148 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import { X } from 'lucide-react'; +import { useDevTaskStore } from '@/stores/useDevTaskStore'; +import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore'; +import { useRequirementStore } from '@/stores/useRequirementStore'; +import { useMemberStore } from '@/stores/useMemberStore'; +import { useAuthStore } from '@/stores/useAuthStore'; +import type { Priority } from '@/lib/derive'; + +interface Props { + versionId: string; + requirementIds: string[]; + onClose: () => void; + onCreated?: () => void; +} + +export function DevTaskCreateModal({ versionId, requirementIds, onClose, onCreated }: Props) { + const { createTask, tasks } = useDevTaskStore(); + const { categories } = useTaskCategoryStore(); + const { requirements } = useRequirementStore(); + const { members } = useMemberStore(); + const user = useAuthStore((s) => s.user); + + const versionReqs = useMemo( + () => requirements.filter((r) => requirementIds.includes(r.id)), + [requirements, requirementIds], + ); + const versionTasks = useMemo( + () => tasks.filter((t) => requirementIds.includes(t.requirementId)), + [tasks, requirementIds], + ); + + const [title, setTitle] = useState(''); + const [requirementId, setRequirementId] = useState(versionReqs[0]?.id || ''); + const [categoryId, setCategoryId] = useState(categories[0]?.id || ''); + const [assigneeId, setAssigneeId] = useState(''); + const [priority, setPriority] = useState('P2'); + const [estimateHours, setEstimateHours] = useState(8); + const [dueDate, setDueDate] = useState(''); + const [predecessorIds, setPredecessorIds] = useState([]); + const [description, setDescription] = useState(''); + + const selectedReq = versionReqs.find((r) => r.id === requirementId); + const effectivePriority = selectedReq?.priority || priority; + const canSubmit = title.trim() && requirementId && categoryId && assigneeId && estimateHours > 0; + + const handleSubmit = () => { + if (!canSubmit) return; + createTask({ + requirementId, + title: title.trim(), + description: description.trim() || undefined, + categoryId, + assigneeId, + reviewerId: undefined, + priority: effectivePriority, + estimateHours, + startDate: undefined, + dueDate: dueDate || undefined, + completedAt: undefined, + status: 'todo', + blockReason: undefined, + blockedById: undefined, + predecessorIds: predecessorIds.length > 0 ? predecessorIds : undefined, + riskLevel: undefined, + createdBy: user?.name || '系统', + }); + onCreated?.(); + onClose(); + }; + + return ( +
+
e.stopPropagation()}> +
+

新建开发任务

+ +
+
+
+ + setTitle(e.target.value)} className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" placeholder="例如:排班界面开发" /> +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + setEstimateHours(parseFloat(e.target.value) || 0)} className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" /> +
+
+ + setDueDate(e.target.value)} className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" /> +
+
+ {versionTasks.length > 0 && ( +
+ +
+ {versionTasks.map((t) => ( + + ))} +
+
+ )} +
+ +