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

@@ -10,6 +10,8 @@ import {
calcPlanProgress,
sortPlansNewestFirst,
getPlanLogsForPlans,
getResearchDirectionPresetOptions,
getResearchDirectionProgressSummary,
getRequirementCoverageSummary,
PRODUCT_PLAN_KIND_LABEL,
PRODUCT_PLAN_REVIEW_FAILURE_OPTIONS,
@@ -20,7 +22,7 @@ import { FieldError } from '@/components/FieldError';
import { FilterSelect } from '@/components/FilterSelect';
import { WorkDateTimePicker } from '@/components/WorkDateTimePicker';
import { AiDecomposeButton } from './AiDecomposeButton';
import { PlanLogTimeline, PlanRequirementCoveragePanel } from './PlanRequirementCoveragePanel';
import { PlanLinkedRequirementReferenceList, PlanLogTimeline, PlanRequirementCoveragePanel, PlanResearchDirectionPanel } from './PlanRequirementCoveragePanel';
import type { VersionWithContext } from '@/lib/derive';
import type { Requirement } from '@/lib/requirement';
import { mergeSelectedRequirementOptions } from '@/lib/requirement-selector';
@@ -80,8 +82,8 @@ export function PlanTab({ plans, versionId, version, versionDeadline, currentUse
const totalDuration = calcTotalDuration(typePlans);
return (
<div className={planType === 'research' ? 'space-y-4' : '-m-5 h-[calc(100vh-98px)] min-h-[520px]'}>
{planType === 'research' && (
<div className="-m-5 h-[calc(100vh-98px)] min-h-[520px]">
{false && planType === 'research' && (
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-[12px] text-[var(--ink-muted)]">{typePlans.length} {TYPE_LABEL[planType]}</span>
@@ -95,11 +97,11 @@ export function PlanTab({ plans, versionId, version, versionDeadline, currentUse
</div>
)}
{typePlans.length === 0 && planType === 'research' ? (
{false && typePlans.length === 0 && planType === 'research' ? (
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-12 text-center text-[13px] text-[var(--ink-muted)]">
{TYPE_LABEL[planType]}
</div>
) : planType === 'research' ? (
) : false && planType === 'research' ? (
<div className="space-y-3">
{typePlans.map((plan) => {
const now = new Date().toISOString();
@@ -402,7 +404,7 @@ function ProductUiPlanWorkspace({
onCreatePlan,
}: {
typePlans: VersionPlan[];
planType: 'product' | 'ui';
planType: VersionPlan['type'];
totalDuration: string;
versionDeadline?: string;
version?: VersionWithContext;
@@ -461,7 +463,9 @@ function ProductUiPlanWorkspace({
<div className="min-h-0 flex-1 space-y-1 overflow-y-auto p-2">
{typePlans.map((plan) => {
const { effectiveStatus } = getPlanRuntime(plan);
const summary = getRequirementCoverageSummary(plan);
const summary = plan.type === 'research'
? getResearchDirectionProgressSummary(plan)
: getRequirementCoverageSummary(plan);
const isSelected = plan.id === selectedPlanIdOrFirst;
return (
<button
@@ -561,7 +565,7 @@ function ProductUiPlanDetail({
onOpenComplete,
}: {
plan: VersionPlan;
planType: 'product' | 'ui';
planType: VersionPlan['type'];
version?: VersionWithContext;
currentUserName: string;
versionMembers: { role: string; name: string }[];
@@ -691,15 +695,27 @@ function ProductUiPlanDetail({
</div>
)}
{selectedRequirements.length > 0 && (
<PlanRequirementCoveragePanel
{plan.type === 'research' && (
<PlanResearchDirectionPanel
plan={plan}
requirements={selectedRequirements}
canEdit={canEditCoverage}
currentUserName={currentUserName}
onUpdate={onUpdate}
/>
)}
{selectedRequirements.length > 0 && (
plan.type === 'research' ? (
<PlanLinkedRequirementReferenceList requirements={selectedRequirements} />
) : (
<PlanRequirementCoveragePanel
plan={plan}
requirements={selectedRequirements}
canEdit={canEditCoverage}
currentUserName={currentUserName}
onUpdate={onUpdate}
/>
)
)}
{plan.status === 'in_progress' && !completionState.canSubmitResult && (
<p className="mt-3 text-[11px] text-[var(--ink-muted)]">{completionState.missingReasons.join('、')}</p>
)}
@@ -756,6 +772,7 @@ function PlanFormModal({ initial, planType, versionId, versionDeadline, currentU
() => mergeSelectedRequirementOptions(linkedRequirements ?? [], allRequirements ?? [], Array.from(selectedReqs)),
[linkedRequirements, allRequirements, selectedReqs],
);
const researchDirectionPresetOptions = useMemo(() => getResearchDirectionPresetOptions(tasks), [tasks]);
const isOverdue = !!(versionDeadline && endTime && new Date(endTime) > new Date(versionDeadline));
const handleSubmit = (e: React.FormEvent) => {
@@ -908,7 +925,23 @@ function PlanFormModal({ initial, planType, versionId, versionDeadline, currentU
<span className="text-[10px] text-[var(--ink-muted)] ml-1"></span>
</label>
{/* 预设选项 */}
{tasks.length === 0 && (
<div className="flex flex-wrap gap-1.5 mb-2">
{researchDirectionPresetOptions.map((option) => (
<button
key={option.title}
type="button"
disabled={option.disabled}
onClick={() => {
if (option.disabled) return;
setTasks([...tasks, { id: `task-${Date.now()}-${Math.random().toString(36).slice(2, 5)}`, title: option.title, status: 'pending' }]);
}}
className={`h-6 px-2.5 rounded-md text-[11px] border transition-colors ${option.disabled ? 'cursor-not-allowed border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--accent)] opacity-70' : 'border-dashed border-[var(--line)] text-[var(--ink-soft)] hover:border-[var(--accent)] hover:text-[var(--accent)]'}`}
>
+ {option.title}
</button>
))}
</div>
{false && tasks.length === 0 && (
<div className="flex flex-wrap gap-1.5 mb-2">
{['竞品分析', '用户访谈', '数据调研', '技术可行性分析', '市场调研', '需求分析'].map((preset) => (
<button