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