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

@@ -24,6 +24,7 @@ interface Props {
planId: string;
onClose: () => void;
contextLabel?: string;
readOnly?: boolean;
}
const STATUS_STYLE: Record<string, string> = { pending: 'bg-zinc-100 text-zinc-600', in_progress: 'bg-blue-50 text-blue-600', completed: 'bg-emerald-50 text-emerald-600' };
@@ -48,7 +49,7 @@ function getFailureLabels(types?: ProductPlanReviewFailureType[]): string[] {
.filter(Boolean) as string[];
}
export function PlanDetailDrawer({ planId, onClose, contextLabel }: Props) {
export function PlanDetailDrawer({ planId, onClose, contextLabel, readOnly = false }: Props) {
const { plans, updatePlan, completePlan } = useVersionPlanStore();
const { requirements } = useRequirementStore();
const { members } = useMemberStore();
@@ -74,13 +75,14 @@ export function PlanDetailDrawer({ planId, onClose, contextLabel }: Props) {
? getResearchDirectionProgressSummary(plan).percent
: getRequirementCoverageSummary(plan).percent;
const linkedReqs = (plan.linkedRequirementIds || []).map((id) => requirements.find((r) => r.id === id)).filter(Boolean) as { id: string; code: string; title: string }[];
const canEditCoverage = canEditPlanRequirementCoverage(plan);
const canEditCoverage = !readOnly && canEditPlanRequirementCoverage(plan);
const currentUserName = user?.name ?? plan.owner;
const productPlanKind = plan.type === 'product' ? getProductPlanKind(plan) : undefined;
const isProductDesignPlan = productPlanKind === 'design';
const isProductReviewPlan = productPlanKind === 'review';
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
if (readOnly) return;
const file = e.target.files?.[0];
if (!file) return;
setFileName(file.name);
@@ -90,6 +92,7 @@ export function PlanDetailDrawer({ planId, onClose, contextLabel }: Props) {
};
const toggleFailureType = (type: ProductPlanReviewFailureType) => {
if (readOnly) return;
const next = new Set(reviewFailureTypes);
if (next.has(type)) next.delete(type);
else next.add(type);
@@ -97,6 +100,7 @@ export function PlanDetailDrawer({ planId, onClose, contextLabel }: Props) {
};
const handleSubmitResult = () => {
if (readOnly) return;
let payload: PlanResultPayload | null = null;
if (isProductReviewPlan) {
@@ -140,6 +144,7 @@ export function PlanDetailDrawer({ planId, onClose, contextLabel }: Props) {
};
const handleTransfer = () => {
if (readOnly) return;
if (!transferTo) return;
updatePlan(plan.id, { owner: transferTo });
setShowTransfer(false);
@@ -272,7 +277,7 @@ export function PlanDetailDrawer({ planId, onClose, contextLabel }: Props) {
)}
{/* Transfer Section */}
{showTransfer && (
{showTransfer && !readOnly && (
<div className="rounded-lg border border-[var(--line)] p-3 space-y-2">
<div className="text-[11px] font-medium text-[var(--ink-muted)]"></div>
<FilterSelect
@@ -289,7 +294,7 @@ export function PlanDetailDrawer({ planId, onClose, contextLabel }: Props) {
)}
{/* Complete with result */}
{showComplete && (
{showComplete && !readOnly && (
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-3 space-y-2">
<div className="text-[11px] font-medium text-emerald-700">{getSubmitActionLabel(plan)}</div>
{isProductReviewPlan ? (
@@ -382,7 +387,7 @@ export function PlanDetailDrawer({ planId, onClose, contextLabel }: Props) {
</div>
{/* Footer Actions */}
{plan.status !== 'completed' && (
{plan.status !== 'completed' && !readOnly && (
<>
<div className="flex items-center gap-2 px-5 py-3 border-t border-[var(--line)] shrink-0">
{plan.status === 'pending' && (