feat(版本): 优化概览与只读状态

关键改动:

- 增加需求排序和版本只读状态规则及测试

- 完善版本概览阶段耗时、项目页和工作台展示

- 优化小宝预警请求节流、建议状态和风险过滤

Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
Script Generator
2026-06-30 18:18:18 +08:00
parent 3d3d56697a
commit eef3c8f000
32 changed files with 1072 additions and 289 deletions

View File

@@ -35,6 +35,7 @@ import { addRecommendedVersionMembers, getDefaultRecommendedMemberNames, recomme
import { getRequirementCoverageSummary } from '@/lib/version-plan';
import { buildVersionProgressMap } from '@/lib/version-progress';
import { canSubmitReleaseForm, getReleaseProgressWarning } from '@/lib/version-release';
import { isVersionReadonly } from '@/lib/version-status';
function formatOverviewDateTime(value?: string | null): string {
if (!value) return '-';
@@ -131,6 +132,7 @@ export default function VersionDetailPage() {
[version, plans, requirements, devTasks, testCases],
);
const releaseProgress = version ? (releaseProgressMap[version.id] ?? 0) : 0;
const versionReadonly = version ? isVersionReadonly(version.status) : false;
// 自动同步版本状态:有计划开始时间<=今天,版本应进入对应阶段
useEffect(() => {
@@ -208,6 +210,7 @@ export default function VersionDetailPage() {
const renderActions = () => {
const buttons: { label: string; action: () => void; danger?: boolean; tone?: 'release' }[] = [];
if (versionReadonly) return null;
if (version.status !== 'released' && version.status !== 'closed') {
buttons.push({ label: '发版', action: () => setShowReleaseModal(true), tone: 'release' });
}
@@ -499,18 +502,20 @@ export default function VersionDetailPage() {
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="flex items-center justify-between gap-3 mb-2">
<div className="text-[11px] text-[var(--ink-muted)] font-medium"></div>
<div className="flex items-center gap-2">
<button
onClick={() => setShowRecommendModal(true)}
disabled={!recommendationDataReady}
className="flex items-center gap-1 text-[11px] font-medium text-violet-600 hover:underline disabled:cursor-not-allowed disabled:text-[var(--ink-muted)] disabled:no-underline"
>
<Sparkles className="h-3 w-3" />{recommendationDataReady ? 'AI推荐' : '加载中'}
</button>
<button onClick={() => setShowMemberModal(true)} className="flex items-center gap-1 text-[11px] text-[var(--accent)] hover:underline">
<Settings className="h-3 w-3" />
</button>
</div>
{!versionReadonly && (
<div className="flex items-center gap-2">
<button
onClick={() => setShowRecommendModal(true)}
disabled={!recommendationDataReady}
className="flex items-center gap-1 text-[11px] font-medium text-violet-600 hover:underline disabled:cursor-not-allowed disabled:text-[var(--ink-muted)] disabled:no-underline"
>
<Sparkles className="h-3 w-3" />{recommendationDataReady ? 'AI推荐' : '加载中'}
</button>
<button onClick={() => setShowMemberModal(true)} className="flex items-center gap-1 text-[11px] text-[var(--accent)] hover:underline">
<Settings className="h-3 w-3" />
</button>
</div>
)}
</div>
{(() => {
const membersList = version.members ?? [];
@@ -840,11 +845,17 @@ export default function VersionDetailPage() {
devTasks={devTasks}
versionMembers={version.members ?? []}
currentUserName={user?.name ?? ''}
readOnly={versionReadonly}
onLink={(ids, addedBy) => {
if (versionReadonly) return;
ids.forEach((id) => updateRequirement(id, { versionId: version.id, addedToVersionBy: addedBy }));
}}
onUnlink={(id) => updateRequirement(id, { versionId: undefined, addedToVersionBy: undefined })}
onUnlink={(id) => {
if (versionReadonly) return;
updateRequirement(id, { versionId: undefined, addedToVersionBy: undefined });
}}
onCreateChange={(data) => {
if (versionReadonly) return;
createRequirement({
...data,
productId: version.productId,
@@ -878,7 +889,9 @@ export default function VersionDetailPage() {
versionMembers={version.members ?? []}
linkedRequirements={projectAdoptedReqs}
allRequirements={requirements}
readOnly={versionReadonly}
onCreate={(data) => {
if (versionReadonly) return;
createPlan(data);
if ((pt === 'product') && data.linkedRequirementIds?.length) {
data.linkedRequirementIds.forEach((rid) => updateRequirement(rid, { productOwner: data.owner }));
@@ -891,13 +904,20 @@ export default function VersionDetailPage() {
}
}}
onUpdate={(id, data) => {
if (versionReadonly) return;
updatePlan(id, data);
if ((pt === 'product') && data.linkedRequirementIds && data.owner) {
data.linkedRequirementIds.forEach((rid) => updateRequirement(rid, { productOwner: data.owner }));
}
}}
onComplete={completePlan}
onDelete={deletePlan}
onComplete={(id, result) => {
if (versionReadonly) return;
return completePlan(id, result);
}}
onDelete={(id) => {
if (versionReadonly) return;
deletePlan(id);
}}
/>
);
})()
@@ -909,18 +929,19 @@ export default function VersionDetailPage() {
versionId={version.id}
requirementIds={versionReqs.map((r) => r.id)}
versionDeadline={version.expectedReleaseDate ?? undefined}
readOnly={versionReadonly}
/>
);
})()
) : activeTab === 'testcases' ? (
(() => {
const versionReqs = requirements.filter((r) => r.versionId === version.id);
return <TestCaseTab versionId={version.id} requirementIds={versionReqs.map((r) => r.id)} />;
return <TestCaseTab versionId={version.id} requirementIds={versionReqs.map((r) => r.id)} readOnly={versionReadonly} />;
})()
) : activeTab === 'bugs' ? (
(() => {
const versionReqs = requirements.filter((r) => r.versionId === version.id);
return <BugTab versionId={version.id} requirementIds={versionReqs.map((r) => r.id)} />;
return <BugTab versionId={version.id} requirementIds={versionReqs.map((r) => r.id)} readOnly={versionReadonly} />;
})()
) : (
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-12 flex items-center justify-center">
@@ -929,7 +950,7 @@ export default function VersionDetailPage() {
)}
</div>
{showRecommendModal && (
{showRecommendModal && !versionReadonly && (
<MemberRecommendationModal
groups={memberRecommendationGroups}
members={version.members ?? []}
@@ -943,7 +964,7 @@ export default function VersionDetailPage() {
/>
)}
{showReleaseModal && (
{showReleaseModal && !versionReadonly && (
<ReleaseVersionModal
progress={releaseProgress}
initialDate={formatLocalDate()}
@@ -956,7 +977,7 @@ export default function VersionDetailPage() {
)}
{/* 参与人员设置弹窗 */}
{showMemberModal && (
{showMemberModal && !versionReadonly && (
<MemberSettingModal
members={version.members ?? []}
allMembers={memberCandidates}