fix: 修复步骤3白屏问题 - 将useState从render函数提取为独立子组件(StakeholderStep/MilestoneStep)
Some checks failed
CI / lint-and-typecheck (push) Failing after 1m33s
CI / test (push) Has been skipped
CI / build (push) Has been skipped

This commit is contained in:
2026-04-11 19:19:24 +08:00
parent 49f5c6d5aa
commit 4b4362ca84

View File

@@ -10,7 +10,6 @@ import {
Card, Card,
Table, Table,
Select, Select,
DatePicker,
Tag, Tag,
} from '@arco-design/web-react'; } from '@arco-design/web-react';
import '@arco-design/web-react/dist/css/arco.css'; import '@arco-design/web-react/dist/css/arco.css';
@@ -18,7 +17,6 @@ import '@arco-design/web-react/dist/css/arco.css';
const { Title, Text } = Typography; const { Title, Text } = Typography;
const { TextArea } = Input; const { TextArea } = Input;
// Step definitions
const STEPS = [ const STEPS = [
{ title: '基本信息', description: '项目名称和目标' }, { title: '基本信息', description: '项目名称和目标' },
{ title: '章程信息', description: '范围与约束' }, { title: '章程信息', description: '范围与约束' },
@@ -27,7 +25,6 @@ const STEPS = [
{ title: '完成', description: '确认创建' }, { title: '完成', description: '确认创建' },
]; ];
// Types
interface Stakeholder { interface Stakeholder {
key: string; key: string;
name: string; name: string;
@@ -66,6 +63,123 @@ const createEmptyProject = (): ProjectData => ({
milestones: [], milestones: [],
}); });
// --- Extracted sub-components (hooks rules compliant) ---
const StakeholderStep: React.FC<{
stakeholders: Stakeholder[];
onChange: (s: Stakeholder[]) => void;
}> = ({ stakeholders, onChange }) => {
const [newName, setNewName] = useState('');
const [newRole, setNewRole] = useState('');
const [newPower, setNewPower] = useState<'高' | '中' | '低'>('中');
const [newInterest, setNewInterest] = useState<'高' | '中' | '低'>('中');
const addStakeholder = () => {
if (!newName || !newRole) return;
const strategy =
newPower === '高' && newInterest === '高'
? '重点管理'
: newPower === '低' && newInterest === '高'
? '保持满意'
: newPower === '高' && newInterest === '低'
? '随时告知'
: '监督';
onChange([
...stakeholders,
{ key: Date.now().toString(), name: newName, role: newRole, power: newPower, interest: newInterest, strategy },
]);
setNewName('');
setNewRole('');
};
const columns = [
{ title: '姓名', dataIndex: 'name', key: 'name' },
{ title: '角色', dataIndex: 'role', key: 'role' },
{
title: '权力',
dataIndex: 'power',
key: 'power',
render: (val: string) => (
<Tag color={val === '高' ? 'red' : val === '中' ? 'orange' : 'green'}>{val}</Tag>
),
},
{
title: '利益/关注度',
dataIndex: 'interest',
key: 'interest',
render: (val: string) => (
<Tag color={val === '高' ? 'red' : val === '中' ? 'orange' : 'green'}>{val}</Tag>
),
},
{ title: '参与策略', dataIndex: 'strategy', key: 'strategy' },
];
return (
<div>
<Space style={{ marginBottom: 16 }}>
<Input placeholder="姓名" value={newName} onChange={setNewName} style={{ width: 120 }} />
<Input placeholder="角色" value={newRole} onChange={setNewRole} style={{ width: 120 }} />
<Select value={newPower} onChange={setNewPower} style={{ width: 80 }}>
<Select.Option value="高"></Select.Option>
<Select.Option value="中"></Select.Option>
<Select.Option value="低"></Select.Option>
</Select>
<Select value={newInterest} onChange={setNewInterest} style={{ width: 80 }}>
<Select.Option value="高"></Select.Option>
<Select.Option value="中"></Select.Option>
<Select.Option value="低"></Select.Option>
</Select>
<Button type="primary" onClick={addStakeholder}>
</Button>
</Space>
<Table columns={columns} data={stakeholders} pagination={false} size="small" />
</div>
);
};
const MilestoneStep: React.FC<{
milestones: Milestone[];
onChange: (m: Milestone[]) => void;
}> = ({ milestones, onChange }) => {
const [newName, setNewName] = useState('');
const [newDate, setNewDate] = useState('');
const [newDeliverable, setNewDeliverable] = useState('');
const addMilestone = () => {
if (!newName) return;
onChange([
...milestones,
{ key: Date.now().toString(), name: newName, targetDate: newDate, deliverable: newDeliverable },
]);
setNewName('');
setNewDate('');
setNewDeliverable('');
};
const columns = [
{ title: '里程碑', dataIndex: 'name', key: 'name' },
{ title: '目标日期', dataIndex: 'targetDate', key: 'targetDate' },
{ title: '交付物', dataIndex: 'deliverable', key: 'deliverable' },
];
return (
<div>
<Space style={{ marginBottom: 16 }}>
<Input placeholder="里程碑名称" value={newName} onChange={setNewName} style={{ width: 160 }} />
<Input placeholder="目标日期" value={newDate} onChange={setNewDate} style={{ width: 120 }} />
<Input placeholder="交付物" value={newDeliverable} onChange={setNewDeliverable} style={{ width: 160 }} />
<Button type="primary" onClick={addMilestone}>
</Button>
</Space>
<Table columns={columns} data={milestones} pagination={false} size="small" />
</div>
);
};
// --- Main Wizard ---
const WizardPage: React.FC = () => { const WizardPage: React.FC = () => {
const [current, setCurrent] = useState(0); const [current, setCurrent] = useState(0);
const [project, setProject] = useState<ProjectData>(createEmptyProject()); const [project, setProject] = useState<ProjectData>(createEmptyProject());
@@ -80,211 +194,114 @@ const WizardPage: React.FC = () => {
console.log('Project created:', project); console.log('Project created:', project);
}; };
// ---- Step 1: Basic Info ---- const renderStep = () => {
const renderBasicInfo = () => ( switch (current) {
<Form layout="vertical" style={{ maxWidth: 600 }}> case 0:
<Form.Item label="项目名称" required> return (
<Input <Form layout="vertical" style={{ maxWidth: 600 }}>
placeholder="例如:客户管理系统" <Form.Item label="项目名称" required>
value={project.name} <Input
onChange={(val) => setProject({ ...project, name: val })} placeholder="例如:客户管理系统"
/> value={project.name}
</Form.Item> onChange={(val) => setProject({ ...project, name: val })}
<Form.Item label="项目目标SMART原则" required> />
<TextArea </Form.Item>
placeholder="具体、可衡量、可实现、相关、有时限的目标" <Form.Item label="项目目标SMART原则" required>
rows={4} <TextArea
value={project.goal} placeholder="具体、可衡量、可实现、相关、有时限的目标"
onChange={(val) => setProject({ ...project, goal: val })} rows={4}
/> value={project.goal}
</Form.Item> onChange={(val) => setProject({ ...project, goal: val })}
</Form> />
); </Form.Item>
</Form>
// ---- Step 2: Charter ---- );
const renderCharter = () => ( case 1:
<Form layout="vertical" style={{ maxWidth: 600 }}> return (
<Form.Item label="包含范围"> <Form layout="vertical" style={{ maxWidth: 600 }}>
<TextArea <Form.Item label="包含范围">
placeholder="本项目要做什么..." <TextArea
rows={3} placeholder="本项目要做什么..."
value={project.scopeIn} rows={3}
onChange={(val) => setProject({ ...project, scopeIn: val })} value={project.scopeIn}
/> onChange={(val) => setProject({ ...project, scopeIn: val })}
</Form.Item> />
<Form.Item label="不包含范围"> </Form.Item>
<TextArea <Form.Item label="不包含范围">
placeholder="本项目明确不做什么..." <TextArea
rows={3} placeholder="本项目明确不做什么..."
value={project.scopeOut} rows={3}
onChange={(val) => setProject({ ...project, scopeOut: val })} value={project.scopeOut}
/> onChange={(val) => setProject({ ...project, scopeOut: val })}
</Form.Item> />
<Form.Item label="约束条件"> </Form.Item>
<Input <Form.Item label="约束条件">
placeholder="例如必须在6月底前上线预算不超过10万" <Input
value={project.constraints} placeholder="例如必须在6月底前上线预算不超过10万"
onChange={(val) => setProject({ ...project, constraints: val })} value={project.constraints}
/> onChange={(val) => setProject({ ...project, constraints: val })}
</Form.Item> />
<Form.Item label="假设条件"> </Form.Item>
<Input <Form.Item label="假设条件">
placeholder="例如团队3人全职投入第三方API在5月就绪" <Input
value={project.assumptions} placeholder="例如团队3人全职投入第三方API在5月就绪"
onChange={(val) => setProject({ ...project, assumptions: val })} value={project.assumptions}
/> onChange={(val) => setProject({ ...project, assumptions: val })}
</Form.Item> />
</Form> </Form.Item>
); </Form>
);
// ---- Step 3: Stakeholders ---- case 2:
const renderStakeholders = () => { return (
const columns = [ <StakeholderStep
{ title: '姓名', dataIndex: 'name', key: 'name' }, stakeholders={project.stakeholders}
{ title: '角色', dataIndex: 'role', key: 'role' }, onChange={(s) => setProject({ ...project, stakeholders: s })}
{ />
title: '权力', );
dataIndex: 'power', case 3:
key: 'power', return (
render: (val: string) => ( <MilestoneStep
<Tag color={val === '高' ? 'red' : val === '中' ? 'orange' : 'green'}>{val}</Tag> milestones={project.milestones}
), onChange={(m) => setProject({ ...project, milestones: m })}
}, />
{ );
title: '利益/关注度', case 4:
dataIndex: 'interest', return (
key: 'interest', <Card title="项目创建确认">
render: (val: string) => ( <Space direction="vertical" size="medium" style={{ width: '100%' }}>
<Tag color={val === '高' ? 'red' : val === '中' ? 'orange' : 'green'}>{val}</Tag> <div>
), <Text bold></Text>
}, <Text>{project.name}</Text>
{ title: '参与策略', dataIndex: 'strategy', key: 'strategy' }, </div>
]; <div>
<Text bold></Text>
const [newName, setNewName] = useState(''); <Text>{project.goal}</Text>
const [newRole, setNewRole] = useState(''); </div>
const [newPower, setNewPower] = useState<'高' | '中' | '低'>('中'); <div>
const [newInterest, setNewInterest] = useState<'高' | '中' | '低'>('中'); <Text bold></Text>
<Text>{project.scopeIn}</Text>
const addStakeholder = () => { </div>
if (!newName || !newRole) return; <div>
const strategy = <Text bold></Text>
newPower === '高' && newInterest === '高' <Text>{project.stakeholders.length}</Text>
? '重点管理' </div>
: newPower === '低' && newInterest === '高' <div>
? '保持满意' <Text bold></Text>
: newPower === '高' && newInterest === '低' <Text>{project.milestones.length}</Text>
? '随时告知' </div>
: '监督'; {(!project.name || !project.goal) && (
setProject({ <div style={{ color: '#ff7d00', fontSize: 14, marginTop: 8 }}>
...project,
stakeholders: [ </div>
...project.stakeholders, )}
{ key: Date.now().toString(), name: newName, role: newRole, power: newPower, interest: newInterest, strategy }, </Space>
], </Card>
}); );
setNewName(''); default:
setNewRole(''); return null;
}; }
return (
<div>
<Space style={{ marginBottom: 16 }}>
<Input placeholder="姓名" value={newName} onChange={setNewName} style={{ width: 120 }} />
<Input placeholder="角色" value={newRole} onChange={setNewRole} style={{ width: 120 }} />
<Select value={newPower} onChange={setNewPower} style={{ width: 80 }}>
<Select.Option value="高"></Select.Option>
<Select.Option value="中"></Select.Option>
<Select.Option value="低"></Select.Option>
</Select>
<Select value={newInterest} onChange={setNewInterest} style={{ width: 80 }}>
<Select.Option value="高"></Select.Option>
<Select.Option value="中"></Select.Option>
<Select.Option value="低"></Select.Option>
</Select>
<Button type="primary" onClick={addStakeholder}>
</Button>
</Space>
<Table columns={columns} data={project.stakeholders} pagination={false} size="small" />
</div>
);
}; };
// ---- Step 4: Milestones ----
const renderMilestones = () => {
const columns = [
{ title: '里程碑', dataIndex: 'name', key: 'name' },
{ title: '目标日期', dataIndex: 'targetDate', key: 'targetDate' },
{ title: '交付物', dataIndex: 'deliverable', key: 'deliverable' },
];
const [newName, setNewName] = useState('');
const [newDate, setNewDate] = useState('');
const [newDeliverable, setNewDeliverable] = useState('');
const addMilestone = () => {
if (!newName) return;
setProject({
...project,
milestones: [
...project.milestones,
{ key: Date.now().toString(), name: newName, targetDate: newDate, deliverable: newDeliverable },
],
});
setNewName('');
setNewDate('');
setNewDeliverable('');
};
return (
<div>
<Space style={{ marginBottom: 16 }}>
<Input placeholder="里程碑名称" value={newName} onChange={setNewName} style={{ width: 160 }} />
<Input placeholder="目标日期" value={newDate} onChange={setNewDate} style={{ width: 120 }} />
<Input placeholder="交付物" value={newDeliverable} onChange={setNewDeliverable} style={{ width: 160 }} />
<Button type="primary" onClick={addMilestone}>
</Button>
</Space>
<Table columns={columns} data={project.milestones} pagination={false} size="small" />
</div>
);
};
// ---- Step 5: Confirm ----
const renderConfirm = () => (
<Card title="项目创建确认">
<Space direction="vertical" size="medium" style={{ width: '100%' }}>
<div>
<Text bold></Text>
<Text>{project.name}</Text>
</div>
<div>
<Text bold></Text>
<Text>{project.goal}</Text>
</div>
<div>
<Text bold></Text>
<Text>{project.scopeIn}</Text>
</div>
<div>
<Text bold></Text>
<Text>{project.stakeholders.length}</Text>
</div>
<div>
<Text bold></Text>
<Text>{project.milestones.length}</Text>
</div>
{(!project.name || !project.goal) && (
<div style={{ color: '#ff7d00', fontSize: 14, marginTop: 8 }}> </div>
)}
</Space>
</Card>
);
const stepRenderers = [renderBasicInfo, renderCharter, renderStakeholders, renderMilestones, renderConfirm];
return ( return (
<div style={{ maxWidth: 900, margin: '40px auto', padding: '0 24px' }}> <div style={{ maxWidth: 900, margin: '40px auto', padding: '0 24px' }}>
<Title heading={4} style={{ marginBottom: 24 }}> <Title heading={4} style={{ marginBottom: 24 }}>
@@ -296,7 +313,7 @@ const WizardPage: React.FC = () => {
))} ))}
</Steps> </Steps>
<Card style={{ minHeight: 300, marginBottom: 24 }}>{stepRenderers[current]()}</Card> <Card style={{ minHeight: 300, marginBottom: 24 }}>{renderStep()}</Card>
<Space style={{ display: 'flex', justifyContent: 'space-between' }}> <Space style={{ display: 'flex', justifyContent: 'space-between' }}>
<Button onClick={prev} disabled={current === 0}> <Button onClick={prev} disabled={current === 0}>