feat: 与我相关加宽布局 + 任务可直接完成
1. 加宽两栏布局: - 左一(树):w-52 → w-64 - 左二(环节):w-48 → w-56 - 右侧更舒适,不再拥挤 2. 待办任务可直接在工作台完成,无需跳转: - 调研/产品方案/UI:开始 → 完成 - 开发任务:开始 → 提测 → 完成 - 测试用例:通过 / 失败 - Bug:修复中 → 已修复 → 关闭 - 每个状态显示对应的下一步操作按钮 - 已完成的不显示操作按钮 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,11 +39,11 @@ const PLAN_STATUS_LABEL: Record<string, string> = { pending: '未开始', in_pro
|
|||||||
export default function WorkspacePage() {
|
export default function WorkspacePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { overview, fetchOverview } = useProductStore();
|
const { overview, fetchOverview } = useProductStore();
|
||||||
const { plans, fetchPlans } = useVersionPlanStore();
|
const { plans, fetchPlans, updatePlan } = useVersionPlanStore();
|
||||||
const { requirements, fetchRequirements } = useRequirementStore();
|
const { requirements, fetchRequirements } = useRequirementStore();
|
||||||
const { tasks: devTasks, fetchTasks } = useDevTaskStore();
|
const { tasks: devTasks, fetchTasks, updateTask } = useDevTaskStore();
|
||||||
const { testCases, fetchTestCases } = useTestCaseStore();
|
const { testCases, fetchTestCases, updateTestCase } = useTestCaseStore();
|
||||||
const { bugs, fetchBugs } = useBugStore();
|
const { bugs, fetchBugs, updateBug } = useBugStore();
|
||||||
const user = useAuthStore((s) => s.user);
|
const user = useAuthStore((s) => s.user);
|
||||||
const [activeTab, setActiveTab] = useState<TabKey>('all');
|
const [activeTab, setActiveTab] = useState<TabKey>('all');
|
||||||
const [showCompleted, setShowCompleted] = useState(true);
|
const [showCompleted, setShowCompleted] = useState(true);
|
||||||
@@ -121,10 +121,31 @@ export default function WorkspacePage() {
|
|||||||
const pendingCount = pendingByTab.all;
|
const pendingCount = pendingByTab.all;
|
||||||
const completedCount = (selectedVersionId ? workItems.filter((i) => i.versionId === selectedVersionId) : workItems).filter((i) => i.completed).length;
|
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 (
|
return (
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
{/* 左侧第一列:产品/项目/版本树 */}
|
{/* 左侧第一列:产品/项目/版本树 */}
|
||||||
<div className="w-52 shrink-0 border-r border-[var(--line)] bg-[var(--bg-card)] flex flex-col">
|
<div className="w-64 shrink-0 border-r border-[var(--line)] bg-[var(--bg-card)] flex flex-col">
|
||||||
<div className="flex h-14 items-center px-4 border-b border-[var(--line)]">
|
<div className="flex h-14 items-center px-4 border-b border-[var(--line)]">
|
||||||
<h1 className="text-[14px] font-semibold text-[var(--ink)]">与我相关</h1>
|
<h1 className="text-[14px] font-semibold text-[var(--ink)]">与我相关</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,7 +167,7 @@ export default function WorkspacePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 左侧第二列:环节分类 */}
|
{/* 左侧第二列:环节分类 */}
|
||||||
<div className="w-48 shrink-0 border-r border-[var(--line)] bg-[var(--bg-card)] flex flex-col">
|
<div className="w-56 shrink-0 border-r border-[var(--line)] bg-[var(--bg-card)] flex flex-col">
|
||||||
<div className="flex h-14 items-center px-4 border-b border-[var(--line)]">
|
<div className="flex h-14 items-center px-4 border-b border-[var(--line)]">
|
||||||
<div className="flex items-center gap-3 text-[11px]">
|
<div className="flex items-center gap-3 text-[11px]">
|
||||||
<span className="text-[var(--ink-muted)]">待办 <span className="font-medium text-[var(--ink)]">{pendingCount}</span></span>
|
<span className="text-[var(--ink-muted)]">待办 <span className="font-medium text-[var(--ink)]">{pendingCount}</span></span>
|
||||||
@@ -201,7 +222,12 @@ export default function WorkspacePage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{filteredItems.map((item) => (
|
{filteredItems.map((item) => (
|
||||||
<WorkItemCard key={item.id} item={item} onNavigate={() => item.versionId && router.push(`/versions/${item.versionId}`)} />
|
<WorkItemCard
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
onNavigate={() => item.versionId && router.push(`/versions/${item.versionId}`)}
|
||||||
|
onAction={(action) => handleItemAction(item, action)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -261,8 +287,9 @@ function ProjectNode({ name, versions, selectedVersionId, onSelect }: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkItemCard({ item, onNavigate }: { item: WorkItem; onNavigate: () => void }) {
|
function WorkItemCard({ item, onNavigate, onAction }: { item: WorkItem; onNavigate: () => void; onAction: (action: string) => void }) {
|
||||||
const statusBadge = getStatusBadge(item);
|
const statusBadge = getStatusBadge(item);
|
||||||
|
const actions = getActions(item);
|
||||||
|
|
||||||
return (
|
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 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' : ''}`}>
|
||||||
@@ -282,6 +309,16 @@ function WorkItemCard({ item, onNavigate }: { item: WorkItem; onNavigate: () =>
|
|||||||
{BUG_SEVERITY_LABEL[item.extra.severity as keyof typeof BUG_SEVERITY_LABEL] || ''}
|
{BUG_SEVERITY_LABEL[item.extra.severity as keyof typeof BUG_SEVERITY_LABEL] || ''}
|
||||||
</span>
|
</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={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" />
|
<ExternalLink className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@@ -299,6 +336,34 @@ function WorkItemCard({ item, onNavigate }: { item: WorkItem; onNavigate: () =>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function getStatusBadge(item: WorkItem) {
|
||||||
if (item.type === 'devTask') {
|
if (item.type === 'devTask') {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user