feat(版本): 完善研发计划与预警已读
关键改动: - 增加版本表单、发布校验和调研方向进度规则 - 扩展小宝预警已读状态、风险签名和今日证据 - 补充组长权限、加班查看范围、活动记录与相关测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user