feat(版本): 优化概览与只读状态
关键改动: - 增加需求排序和版本只读状态规则及测试 - 完善版本概览阶段耗时、项目页和工作台展示 - 优化小宝预警请求节流、建议状态和风险过滤 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -27,8 +27,12 @@ import { formatShortTime, formatWorkHours } from '@/lib/work-hours';
|
||||
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';
|
||||
import { getWorkspaceDailyReport } from '@/lib/workspace-daily-report';
|
||||
import { VERSION_STATUS_BG, VERSION_STATUS_LABEL, getVersionReadonlyNotice, isVersionReadonly, type VersionStatus } from '@/lib/version-status';
|
||||
|
||||
type TabKey = 'all' | 'plan_research' | 'plan_product' | 'plan_ui' | 'devTask' | 'testCase' | 'bug';
|
||||
type WorkspaceVersionContext = { id: string; name: string; productName: string; projectName: string; status: VersionStatus };
|
||||
type TreeVersion = { id: string; name: string; status: VersionStatus; pendingCount: number };
|
||||
type ProductTree = Map<string, { name: string; projects: Map<string, { name: string; versions: TreeVersion[] }> }>;
|
||||
|
||||
const TABS: { key: TabKey; label: string; icon: any }[] = [
|
||||
{ key: 'all', label: '全部', icon: ClipboardList },
|
||||
@@ -77,8 +81,8 @@ export default function WorkspacePage() {
|
||||
const allVersions = useMemo(() => flattenVersions(overview), [overview]);
|
||||
|
||||
const versionMap = useMemo(() => {
|
||||
const map = new Map<string, { id: string; name: string; productName: string; projectName: string }>();
|
||||
allVersions.forEach((v) => map.set(v.id, { id: v.id, name: v.name, productName: v.productName, projectName: v.projectName }));
|
||||
const map = new Map<string, WorkspaceVersionContext>();
|
||||
allVersions.forEach((v) => map.set(v.id, { id: v.id, name: v.name, productName: v.productName, projectName: v.projectName, status: v.status }));
|
||||
return map;
|
||||
}, [allVersions]);
|
||||
|
||||
@@ -96,7 +100,7 @@ export default function WorkspacePage() {
|
||||
// 构建树:只显示跟自己有关的产品/项目/版本
|
||||
const tree = useMemo(() => {
|
||||
const myVersionIds = new Set(workItems.map((i) => i.versionId).filter(Boolean));
|
||||
const productMap = new Map<string, { name: string; projects: Map<string, { name: string; versions: { id: string; name: string; pendingCount: number }[] }> }>();
|
||||
const productMap: ProductTree = new Map();
|
||||
|
||||
allVersions.forEach((v) => {
|
||||
if (!myVersionIds.has(v.id)) return;
|
||||
@@ -106,7 +110,7 @@ export default function WorkspacePage() {
|
||||
const proj = prod.projects.get(v.projectName)!;
|
||||
const pending = workItems.filter((i) => i.versionId === v.id && !i.completed).length;
|
||||
if (!proj.versions.find((ver) => ver.id === v.id)) {
|
||||
proj.versions.push({ id: v.id, name: v.name, pendingCount: pending });
|
||||
proj.versions.push({ id: v.id, name: v.name, status: v.status, pendingCount: pending });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -150,6 +154,9 @@ export default function WorkspacePage() {
|
||||
|
||||
const pendingCount = pendingByTab.all;
|
||||
const completedCount = (selectedVersionId ? workItems.filter((i) => i.versionId === selectedVersionId) : workItems).filter((i) => i.completed).length;
|
||||
const selectedVersion = selectedVersionId ? versionMap.get(selectedVersionId) : undefined;
|
||||
const drawerVersionStatus = drawerItem ? versionMap.get(drawerItem.versionId)?.status : undefined;
|
||||
const drawerReadOnly = drawerVersionStatus ? isVersionReadonly(drawerVersionStatus) : false;
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
@@ -216,9 +223,10 @@ export default function WorkspacePage() {
|
||||
<header className="flex h-14 shrink-0 items-center border-b border-[var(--line)] bg-[var(--bg-card)] px-5">
|
||||
<h2 className="text-[14px] font-semibold text-[var(--ink)]">{TABS.find((t) => t.key === activeTab)?.label}</h2>
|
||||
<span className="ml-2 text-[12px] text-[var(--ink-muted)]">{filteredItems.length} 项</span>
|
||||
{selectedVersionId && (
|
||||
<span className="ml-3 text-[11px] text-[var(--accent)] bg-[var(--accent-soft)] px-2 py-0.5 rounded-full">
|
||||
{versionMap.get(selectedVersionId)?.name}
|
||||
{selectedVersion && (
|
||||
<span className="ml-3 inline-flex items-center gap-1.5 rounded-full bg-[var(--accent-soft)] px-2 py-0.5 text-[11px] text-[var(--accent)]">
|
||||
<span>{selectedVersion.name}</span>
|
||||
<VersionStatusTag status={selectedVersion.status} />
|
||||
</span>
|
||||
)}
|
||||
</header>
|
||||
@@ -234,6 +242,7 @@ export default function WorkspacePage() {
|
||||
<WorkItemCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
versionStatus={versionMap.get(item.versionId)?.status}
|
||||
onNavigate={() => item.versionId && router.push(`/versions/${item.versionId}`)}
|
||||
onClick={() => setDrawerItem(item)}
|
||||
/>
|
||||
@@ -247,16 +256,16 @@ export default function WorkspacePage() {
|
||||
|
||||
{/* Detail Drawers */}
|
||||
{drawerItem && (drawerItem.type === 'plan_research' || drawerItem.type === 'plan_product' || drawerItem.type === 'plan_ui') && (
|
||||
<PlanDetailDrawer planId={drawerItem.id} onClose={() => setDrawerItem(null)} contextLabel={`${drawerItem.productName} / ${drawerItem.projectName} / ${drawerItem.versionName}`} />
|
||||
<PlanDetailDrawer planId={drawerItem.id} readOnly={drawerReadOnly} onClose={() => setDrawerItem(null)} contextLabel={`${drawerItem.productName} / ${drawerItem.projectName} / ${drawerItem.versionName}`} />
|
||||
)}
|
||||
{drawerItem && drawerItem.type === 'devTask' && (
|
||||
<DevTaskDetailDrawer taskId={drawerItem.id} allTaskIds={devTasks.map((t) => t.id)} onClose={() => setDrawerItem(null)} contextLabel={`${drawerItem.productName} / ${drawerItem.projectName} / ${drawerItem.versionName}`} />
|
||||
<DevTaskDetailDrawer taskId={drawerItem.id} allTaskIds={devTasks.map((t) => t.id)} readOnly={drawerReadOnly} onClose={() => setDrawerItem(null)} contextLabel={`${drawerItem.productName} / ${drawerItem.projectName} / ${drawerItem.versionName}`} />
|
||||
)}
|
||||
{drawerItem && drawerItem.type === 'testCase' && (
|
||||
<TestCaseDetailDrawer testCaseId={drawerItem.id} onClose={() => setDrawerItem(null)} onCreateBug={(tcId) => { setDrawerItem(null); setBugFromTestCaseId(tcId); }} contextLabel={`${drawerItem.productName} / ${drawerItem.projectName} / ${drawerItem.versionName}`} />
|
||||
<TestCaseDetailDrawer testCaseId={drawerItem.id} readOnly={drawerReadOnly} onClose={() => setDrawerItem(null)} onCreateBug={(tcId) => { if (!drawerReadOnly) { setDrawerItem(null); setBugFromTestCaseId(tcId); } }} contextLabel={`${drawerItem.productName} / ${drawerItem.projectName} / ${drawerItem.versionName}`} />
|
||||
)}
|
||||
{drawerItem && drawerItem.type === 'bug' && (
|
||||
<BugDetailDrawer bugId={drawerItem.id} onClose={() => setDrawerItem(null)} contextLabel={`${drawerItem.productName} / ${drawerItem.projectName} / ${drawerItem.versionName}`} />
|
||||
<BugDetailDrawer bugId={drawerItem.id} readOnly={drawerReadOnly} onClose={() => setDrawerItem(null)} contextLabel={`${drawerItem.productName} / ${drawerItem.projectName} / ${drawerItem.versionName}`} />
|
||||
)}
|
||||
{bugFromTestCaseId && (
|
||||
<BugCreateModal testCaseId={bugFromTestCaseId} onClose={() => setBugFromTestCaseId(null)} />
|
||||
@@ -267,7 +276,7 @@ export default function WorkspacePage() {
|
||||
|
||||
function ProductNode({ name, prod, selectedVersionId, onSelect }: {
|
||||
name: string;
|
||||
prod: { name: string; projects: Map<string, { name: string; versions: { id: string; name: string; pendingCount: number }[] }> };
|
||||
prod: { name: string; projects: Map<string, { name: string; versions: TreeVersion[] }> };
|
||||
selectedVersionId: string | null;
|
||||
onSelect: (id: string | null) => void;
|
||||
}) {
|
||||
@@ -287,7 +296,7 @@ function ProductNode({ name, prod, selectedVersionId, onSelect }: {
|
||||
|
||||
function ProjectNode({ name, versions, selectedVersionId, onSelect }: {
|
||||
name: string;
|
||||
versions: { id: string; name: string; pendingCount: number }[];
|
||||
versions: TreeVersion[];
|
||||
selectedVersionId: string | null;
|
||||
onSelect: (id: string | null) => void;
|
||||
}) {
|
||||
@@ -306,6 +315,7 @@ function ProjectNode({ name, versions, selectedVersionId, onSelect }: {
|
||||
className={`w-full flex items-center gap-1.5 ml-5 px-2 py-1 rounded text-[11px] transition-colors ${selectedVersionId === v.id ? 'bg-[var(--accent-soft)] text-[var(--accent)] font-medium' : 'text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]'}`}
|
||||
>
|
||||
<span className="flex-1 text-left truncate">{v.name}</span>
|
||||
<VersionStatusTag status={v.status} readonlyOnly />
|
||||
{v.pendingCount > 0 && (
|
||||
<span className="text-[9px] bg-red-500 text-white rounded-full px-1.5 min-w-[16px] text-center">{v.pendingCount}</span>
|
||||
)}
|
||||
@@ -315,7 +325,27 @@ function ProjectNode({ name, versions, selectedVersionId, onSelect }: {
|
||||
);
|
||||
}
|
||||
|
||||
function WorkItemCard({ item, onNavigate, onClick }: { item: WorkItem; onNavigate: () => void; onClick: () => void }) {
|
||||
function VersionStatusTag({ status, readonlyOnly = false }: { status?: VersionStatus; readonlyOnly?: boolean }) {
|
||||
if (!status) return null;
|
||||
const readonlyNotice = getVersionReadonlyNotice(status);
|
||||
if (readonlyOnly && !readonlyNotice) return null;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`shrink-0 rounded-md px-1.5 py-0.5 text-[10px] font-medium leading-none ${VERSION_STATUS_BG[status]}`}
|
||||
title={readonlyNotice ?? VERSION_STATUS_LABEL[status]}
|
||||
>
|
||||
{VERSION_STATUS_LABEL[status]}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkItemCard({ item, versionStatus, onNavigate, onClick }: {
|
||||
item: WorkItem;
|
||||
versionStatus?: VersionStatus;
|
||||
onNavigate: () => void;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const statusBadge = getStatusBadge(item);
|
||||
const isDevTask = item.type === 'devTask';
|
||||
const devTaskRaw = isDevTask ? (item.raw as any) : null;
|
||||
@@ -358,6 +388,7 @@ function WorkItemCard({ item, onNavigate, onClick }: { item: WorkItem; onNavigat
|
||||
<span>{item.projectName}</span>
|
||||
<span className="text-[var(--line)]">/</span>
|
||||
<button onClick={(e) => { e.stopPropagation(); onNavigate(); }} className="text-[var(--accent)] hover:underline">{item.versionName}</button>
|
||||
<VersionStatusTag status={versionStatus} readonlyOnly />
|
||||
{isDevTask && devTimeRange && (
|
||||
<>
|
||||
<span className="ml-2 tabular-nums">{devTimeRange}</span>
|
||||
|
||||
Reference in New Issue
Block a user