feat: 与我相关点击打开侧边详情 + 开发/测试转交功能
一、工作台交互改为侧边栏详情(点击卡片打开): - 调研/产品/UI:新建 PlanDetailDrawer - 子任务勾选、进度条、上传成果(链接/文件) - 开始/提交完成操作、转交功能 - 开发任务:复用 DevTaskDetailDrawer - 状态流转、阻塞管理、前置任务等完整功能 - 测试用例:复用 TestCaseDetailDrawer - 测试步骤、预期结果、关联Bug、提Bug - Bug:复用 BugDetailDrawer - 描述、截图、操作日志、状态转换 二、开发任务增加转交功能: - DevTaskDetailDrawer 顶部增加转交按钮 - 从 members 列表选择新负责人 - 更新 assigneeId 三、测试用例增加转交功能: - TestCaseDetailDrawer 顶部增加转交按钮 - 人员离职等场景可转交 四、版本详情同步: - 所有 Drawer 改动同时生效于版本详情页 (因为复用同一组件) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,10 @@ import { useAuthStore } from '@/stores/useAuthStore';
|
||||
import { flattenVersions } from '@/lib/derive';
|
||||
import { aggregateWorkItems, WORK_ITEM_TYPE_LABEL } from '@/lib/workspace-engine';
|
||||
import type { WorkItem, WorkItemType } from '@/lib/workspace-engine';
|
||||
import { PlanDetailDrawer } from '@/components/version/PlanDetailDrawer';
|
||||
import { DevTaskDetailDrawer } from '@/components/dev-task/DevTaskDetailDrawer';
|
||||
import { TestCaseDetailDrawer } from '@/components/test-case/TestCaseDetailDrawer';
|
||||
import { BugDetailDrawer } from '@/components/bug/BugDetailDrawer';
|
||||
import { DEV_TASK_STATUS_LABEL, DEV_TASK_STATUS_COLOR } from '@/lib/dev-task';
|
||||
import { TEST_CASE_STATUS_LABEL, TEST_CASE_STATUS_COLOR } from '@/lib/test-case';
|
||||
import { BUG_STATUS_LABEL, BUG_STATUS_COLOR, BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug';
|
||||
@@ -39,15 +43,16 @@ const PLAN_STATUS_LABEL: Record<string, string> = { pending: '未开始', in_pro
|
||||
export default function WorkspacePage() {
|
||||
const router = useRouter();
|
||||
const { overview, fetchOverview } = useProductStore();
|
||||
const { plans, fetchPlans, updatePlan } = useVersionPlanStore();
|
||||
const { plans, fetchPlans } = useVersionPlanStore();
|
||||
const { requirements, fetchRequirements } = useRequirementStore();
|
||||
const { tasks: devTasks, fetchTasks, updateTask } = useDevTaskStore();
|
||||
const { testCases, fetchTestCases, updateTestCase } = useTestCaseStore();
|
||||
const { bugs, fetchBugs, updateBug } = useBugStore();
|
||||
const { tasks: devTasks, fetchTasks } = useDevTaskStore();
|
||||
const { testCases, fetchTestCases } = useTestCaseStore();
|
||||
const { bugs, fetchBugs } = useBugStore();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const [activeTab, setActiveTab] = useState<TabKey>('all');
|
||||
const [showCompleted, setShowCompleted] = useState(true);
|
||||
const [selectedVersionId, setSelectedVersionId] = useState<string | null>(null);
|
||||
const [drawerItem, setDrawerItem] = useState<WorkItem | null>(null);
|
||||
|
||||
useEffect(() => { fetchOverview(); }, [fetchOverview]);
|
||||
useEffect(() => { fetchPlans(); }, [fetchPlans]);
|
||||
@@ -121,27 +126,6 @@ export default function WorkspacePage() {
|
||||
const pendingCount = pendingByTab.all;
|
||||
const completedCount = (selectedVersionId ? workItems.filter((i) => i.versionId === selectedVersionId) : workItems).filter((i) => i.completed).length;
|
||||
|
||||
const handleItemAction = (item: WorkItem, action: string) => {
|
||||
if (item.type === 'plan_research' || item.type === 'plan_product' || item.type === 'plan_ui') {
|
||||
if (action === 'start') updatePlan(item.id, { status: 'in_progress' });
|
||||
if (action === 'complete') updatePlan(item.id, { status: 'completed', completedAt: new Date().toISOString() });
|
||||
}
|
||||
if (item.type === 'devTask') {
|
||||
if (action === 'start') updateTask(item.id, { status: 'in_progress' });
|
||||
if (action === 'testing') updateTask(item.id, { status: 'testing' });
|
||||
if (action === 'submit') updateTask(item.id, { status: 'submitted', completedAt: new Date().toISOString() });
|
||||
}
|
||||
if (item.type === 'testCase') {
|
||||
if (action === 'pass') updateTestCase(item.id, { status: 'passed', completedAt: new Date().toISOString() });
|
||||
if (action === 'fail') updateTestCase(item.id, { status: 'failed' });
|
||||
}
|
||||
if (item.type === 'bug') {
|
||||
if (action === 'fix') updateBug(item.id, { status: 'fixing' });
|
||||
if (action === 'fixed') updateBug(item.id, { status: 'fixed' });
|
||||
if (action === 'close') updateBug(item.id, { status: 'closed', closedAt: new Date().toISOString() });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
{/* 左侧第一列:产品/项目/版本树 */}
|
||||
@@ -226,13 +210,27 @@ export default function WorkspacePage() {
|
||||
key={item.id}
|
||||
item={item}
|
||||
onNavigate={() => item.versionId && router.push(`/versions/${item.versionId}`)}
|
||||
onAction={(action) => handleItemAction(item, action)}
|
||||
onClick={() => setDrawerItem(item)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detail Drawers */}
|
||||
{drawerItem && (drawerItem.type === 'plan_research' || drawerItem.type === 'plan_product' || drawerItem.type === 'plan_ui') && (
|
||||
<PlanDetailDrawer planId={drawerItem.id} onClose={() => setDrawerItem(null)} />
|
||||
)}
|
||||
{drawerItem && drawerItem.type === 'devTask' && (
|
||||
<DevTaskDetailDrawer taskId={drawerItem.id} allTaskIds={devTasks.map((t) => t.id)} onClose={() => setDrawerItem(null)} />
|
||||
)}
|
||||
{drawerItem && drawerItem.type === 'testCase' && (
|
||||
<TestCaseDetailDrawer testCaseId={drawerItem.id} onClose={() => setDrawerItem(null)} />
|
||||
)}
|
||||
{drawerItem && drawerItem.type === 'bug' && (
|
||||
<BugDetailDrawer bugId={drawerItem.id} onClose={() => setDrawerItem(null)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -287,12 +285,11 @@ function ProjectNode({ name, versions, selectedVersionId, onSelect }: {
|
||||
);
|
||||
}
|
||||
|
||||
function WorkItemCard({ item, onNavigate, onAction }: { item: WorkItem; onNavigate: () => void; onAction: (action: string) => void }) {
|
||||
function WorkItemCard({ item, onNavigate, onClick }: { item: WorkItem; onNavigate: () => void; onClick: () => void }) {
|
||||
const statusBadge = getStatusBadge(item);
|
||||
const actions = getActions(item);
|
||||
|
||||
return (
|
||||
<div className={`rounded-xl border border-[var(--line)] bg-[var(--bg-card)] px-4 py-3 transition-colors hover:border-[var(--accent)] ${item.completed ? 'opacity-60' : ''}`}>
|
||||
<div onClick={onClick} className={`rounded-xl border border-[var(--line)] bg-[var(--bg-card)] px-4 py-3 transition-colors hover:border-[var(--accent)] cursor-pointer ${item.completed ? 'opacity-60' : ''}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
{item.completed && <CheckCircle2 className="h-4 w-4 text-emerald-500 shrink-0" />}
|
||||
<span className="text-[10px] font-medium px-2 py-0.5 rounded-full bg-[var(--bg-subtle)] text-[var(--ink-muted)] shrink-0">
|
||||
@@ -309,17 +306,7 @@ function WorkItemCard({ item, onNavigate, onAction }: { item: WorkItem; onNaviga
|
||||
{BUG_SEVERITY_LABEL[item.extra.severity as keyof typeof BUG_SEVERITY_LABEL] || ''}
|
||||
</span>
|
||||
)}
|
||||
{/* 操作按钮 */}
|
||||
{actions.length > 0 && (
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{actions.map((a) => (
|
||||
<button key={a.key} onClick={() => onAction(a.key)} className={`h-6 px-2 rounded text-[10px] font-medium transition-colors ${a.style}`}>
|
||||
{a.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<button onClick={onNavigate} className="h-6 w-6 flex items-center justify-center rounded text-[var(--ink-muted)] hover:text-[var(--accent)] hover:bg-[var(--bg-subtle)] shrink-0" title="跳转到版本详情">
|
||||
<button onClick={(e) => { e.stopPropagation(); onNavigate(); }} className="h-6 w-6 flex items-center justify-center rounded text-[var(--ink-muted)] hover:text-[var(--accent)] hover:bg-[var(--bg-subtle)] shrink-0" title="跳转到版本详情">
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -328,7 +315,7 @@ function WorkItemCard({ item, onNavigate, onAction }: { item: WorkItem; onNaviga
|
||||
<span className="text-[var(--line)]">/</span>
|
||||
<span>{item.projectName}</span>
|
||||
<span className="text-[var(--line)]">/</span>
|
||||
<button onClick={onNavigate} className="text-[var(--accent)] hover:underline">{item.versionName}</button>
|
||||
<button onClick={(e) => { e.stopPropagation(); onNavigate(); }} className="text-[var(--accent)] hover:underline">{item.versionName}</button>
|
||||
{item.extra?.dueDate && <span className="ml-2">截止 {item.extra.dueDate}</span>}
|
||||
{item.extra?.startTime && <span className="ml-2">{item.extra.startTime.slice(0, 16).replace('T', ' ')} → {item.extra.endTime?.slice(0, 16).replace('T', ' ')}</span>}
|
||||
</div>
|
||||
@@ -336,34 +323,6 @@ function WorkItemCard({ item, onNavigate, onAction }: { item: WorkItem; onNaviga
|
||||
);
|
||||
}
|
||||
|
||||
function getActions(item: WorkItem): { key: string; label: string; style: string }[] {
|
||||
if (item.completed) return [];
|
||||
|
||||
if (item.type === 'plan_research' || item.type === 'plan_product' || item.type === 'plan_ui') {
|
||||
if (item.status === 'pending') return [{ key: 'start', label: '开始', style: 'text-blue-600 hover:bg-blue-50 border border-blue-200' }];
|
||||
if (item.status === 'in_progress') return [{ key: 'complete', label: '完成', style: 'text-emerald-600 hover:bg-emerald-50 border border-emerald-200' }];
|
||||
}
|
||||
if (item.type === 'devTask') {
|
||||
if (item.status === 'todo') return [{ key: 'start', label: '开始', style: 'text-blue-600 hover:bg-blue-50 border border-blue-200' }];
|
||||
if (item.status === 'in_progress') return [{ key: 'testing', label: '提测', style: 'text-purple-600 hover:bg-purple-50 border border-purple-200' }];
|
||||
if (item.status === 'testing') return [{ key: 'submit', label: '完成', style: 'text-emerald-600 hover:bg-emerald-50 border border-emerald-200' }];
|
||||
}
|
||||
if (item.type === 'testCase') {
|
||||
if (item.status === 'pending' || item.status === 'running' || item.status === 'failed') {
|
||||
return [
|
||||
{ key: 'pass', label: '通过', style: 'text-emerald-600 hover:bg-emerald-50 border border-emerald-200' },
|
||||
{ key: 'fail', label: '失败', style: 'text-red-600 hover:bg-red-50 border border-red-200' },
|
||||
];
|
||||
}
|
||||
}
|
||||
if (item.type === 'bug') {
|
||||
if (item.status === 'open') return [{ key: 'fix', label: '修复中', style: 'text-blue-600 hover:bg-blue-50 border border-blue-200' }];
|
||||
if (item.status === 'fixing') return [{ key: 'fixed', label: '已修复', style: 'text-purple-600 hover:bg-purple-50 border border-purple-200' }];
|
||||
if (item.status === 'fixed' || item.status === 'verifying') return [{ key: 'close', label: '关闭', style: 'text-emerald-600 hover:bg-emerald-50 border border-emerald-200' }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function getStatusBadge(item: WorkItem) {
|
||||
if (item.type === 'devTask') {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user