feat(版本): 优化概览与只读状态
关键改动: - 增加需求排序和版本只读状态规则及测试 - 完善版本概览阶段耗时、项目页和工作台展示 - 优化小宝预警请求节流、建议状态和风险过滤 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user