feat(版本): 完善研发计划与预警已读

关键改动:

- 增加版本表单、发布校验和调研方向进度规则

- 扩展小宝预警已读状态、风险签名和今日证据

- 补充组长权限、加班查看范围、活动记录与相关测试

Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
Script Generator
2026-06-30 16:48:58 +08:00
parent 04520bd8c5
commit 3d3d56697a
41 changed files with 1701 additions and 121 deletions

View File

@@ -20,6 +20,14 @@ import { buildVersionProgressMap } from '@/lib/version-progress';
import { useXiaobaoWarningRisks } from '@/hooks/useXiaobaoWarningRisks';
import type { XiaobaoVersionRisk } from '@/lib/xiaobao-risk';
import { getRiskScoreTone } from '@/lib/xiaobao-warning-view';
import {
VERSION_DEVELOPMENT_TYPE_OPTIONS,
canSubmitNewVersionForm,
getRecommendedExpectedReleaseDate,
getRecommendedWorkDays,
getVersionDevelopmentTypeOption,
type VersionDevelopmentType,
} from '@/lib/version-form';
import { Pagination, usePagination } from '@/components/Pagination';
type Priority = 'P0' | 'P1' | 'P2' | 'P3' | 'P4';
@@ -462,7 +470,7 @@ type IterationType = 'major' | 'minor' | 'patch';
interface NewVersionModalProps {
overview: any[];
onClose: () => void;
onCreate: (productId: string, data: { name: string; status: VersionStatus; priority?: Priority; expectedReleaseDate?: string; members?: any[] }) => void;
onCreate: (productId: string, data: { name: string; status: VersionStatus; priority?: Priority; expectedReleaseDate: string; members?: any[] }) => void;
currentUserName: string;
}
@@ -510,6 +518,8 @@ function NewVersionModal({ overview, onClose, onCreate, currentUserName }: NewVe
const [projectName, setProjectName] = useState('');
const [iterationType, setIterationType] = useState<IterationType | ''>('');
const [versionNumber, setVersionNumber] = useState('');
const [developmentType, setDevelopmentType] = useState<VersionDevelopmentType | ''>('');
const [productDesignCompleted, setProductDesignCompleted] = useState(false);
const [status] = useState<VersionStatus>('developing');
const [priority, setPriority] = useState<Priority>('P2');
const [expectedReleaseDate, setExpectedReleaseDate] = useState('');
@@ -526,6 +536,9 @@ function NewVersionModal({ overview, onClose, onCreate, currentUserName }: NewVe
setProjectName('');
setIterationType('');
setVersionNumber('');
setDevelopmentType('');
setProductDesignCompleted(false);
setExpectedReleaseDate('');
};
// When project changes
@@ -535,6 +548,9 @@ function NewVersionModal({ overview, onClose, onCreate, currentUserName }: NewVe
setProjectName(proj?.name ?? '');
setIterationType('');
setVersionNumber('');
setDevelopmentType('');
setProductDesignCompleted(false);
setExpectedReleaseDate('');
};
// When iteration type changes, auto-generate version number
@@ -552,7 +568,33 @@ function NewVersionModal({ overview, onClose, onCreate, currentUserName }: NewVe
setVersionNumber(cleaned);
};
const canSubmit = productId && projectId && versionNumber && !submitting;
const applyRecommendedReleaseDate = (type: VersionDevelopmentType, productDesigned: boolean) => {
setExpectedReleaseDate(getRecommendedExpectedReleaseDate(type, new Date(), { productDesignCompleted: productDesigned }));
};
const handleDevelopmentTypeChange = (type: VersionDevelopmentType) => {
setDevelopmentType(type);
applyRecommendedReleaseDate(type, productDesignCompleted);
};
const handleProductDesignCompletedChange = (completed: boolean) => {
setProductDesignCompleted(completed);
if (developmentType) applyRecommendedReleaseDate(developmentType, completed);
};
const selectedDevelopmentType = developmentType ? getVersionDevelopmentTypeOption(developmentType) : undefined;
const recommendedWorkDays = selectedDevelopmentType
? getRecommendedWorkDays(selectedDevelopmentType, { productDesignCompleted })
: 0;
const canSubmit = canSubmitNewVersionForm({
productId,
projectId,
versionNumber,
developmentType,
expectedReleaseDate,
submitting,
});
const handleSubmit = async () => {
if (!canSubmit) return;
@@ -562,7 +604,7 @@ function NewVersionModal({ overview, onClose, onCreate, currentUserName }: NewVe
name: `${projectName}V${versionNumber}`,
status,
priority,
...(expectedReleaseDate ? { expectedReleaseDate } : {}),
expectedReleaseDate,
members: currentUserName ? [{ role: 'product', name: currentUserName }] : [],
});
onClose();
@@ -573,7 +615,7 @@ function NewVersionModal({ overview, onClose, onCreate, currentUserName }: NewVe
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onClose}>
<div className="w-full max-w-md rounded-2xl bg-[var(--bg-card)] p-6 shadow-[var(--shadow-md)]" onClick={(e) => e.stopPropagation()}>
<div className="max-h-[90vh] w-full max-w-md overflow-y-auto rounded-2xl bg-[var(--bg-card)] p-6 shadow-[var(--shadow-md)]" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between mb-5">
<h2 className="text-[15px] font-semibold text-[var(--ink)]"></h2>
<button onClick={onClose} className="rounded-lg p-1 text-[var(--ink-muted)] hover:bg-[var(--bg-hover)] transition-colors">
@@ -641,15 +683,71 @@ function NewVersionModal({ overview, onClose, onCreate, currentUserName }: NewVe
</div>
</div>
{/* 截止日期 */}
{/* 项目开发类型 */}
<div>
<label className="mb-1.5 block text-[12px] font-medium text-[var(--ink-soft)]"></label>
<label className="mb-1.5 block text-[12px] font-medium text-[var(--ink-soft)]"><span className="text-red-500">*</span></label>
<div className="grid grid-cols-2 gap-2">
{VERSION_DEVELOPMENT_TYPE_OPTIONS.map((opt) => (
<button
key={opt.key}
onClick={() => handleDevelopmentTypeChange(opt.key)}
type="button"
className={`rounded-lg border px-3 py-2 text-left transition-colors ${developmentType === opt.key ? 'border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--accent)]' : 'border-[var(--line)] bg-[var(--bg-card)] text-[var(--ink-soft)] hover:border-[var(--accent)]'}`}
>
<span className="block text-[12px] font-medium">{opt.label}</span>
<span className="mt-0.5 block text-[10px] text-[var(--ink-muted)]">{opt.description}</span>
</button>
))}
</div>
</div>
{/* 产品设计是否已完成 */}
<div>
<label className="mb-1.5 block text-[12px] font-medium text-[var(--ink-soft)]"></label>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => handleProductDesignCompletedChange(false)}
className={`h-9 rounded-lg border px-3 text-[12px] font-medium transition-colors ${!productDesignCompleted ? 'border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--accent)]' : 'border-[var(--line)] bg-[var(--bg-card)] text-[var(--ink-soft)] hover:border-[var(--accent)]'}`}
>
</button>
<button
type="button"
onClick={() => handleProductDesignCompletedChange(true)}
className={`h-9 rounded-lg border px-3 text-[12px] font-medium transition-colors ${productDesignCompleted ? 'border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--accent)]' : 'border-[var(--line)] bg-[var(--bg-card)] text-[var(--ink-soft)] hover:border-[var(--accent)]'}`}
>
</button>
</div>
</div>
{/* 期望发版日期 */}
<div>
<label className="mb-1.5 block text-[12px] font-medium text-[var(--ink-soft)]"><span className="text-red-500">*</span></label>
<input
type="date"
value={expectedReleaseDate}
onChange={(e) => setExpectedReleaseDate(e.target.value)}
required
className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] text-[var(--ink)] focus:border-[var(--accent)] focus:outline-none focus:ring-2 focus:ring-[var(--accent-ring)]"
/>
{selectedDevelopmentType && (
<p className="mt-1.5 text-[11px] leading-5 text-[var(--ink-muted)]">
{recommendedWorkDays}
{selectedDevelopmentType.stages.map((stage, index) => {
const skipped = productDesignCompleted && stage.key === 'product_design';
return (
<span key={stage.key}>
{index > 0 && ' + '}
<span className={skipped ? 'text-[var(--ink-muted)] line-through' : ''}>
{stage.label}{stage.workDays}
</span>
</span>
);
})}
</p>
)}
</div>
</div>