feat(#1): 项目创建向导页 - 5步向导(基本信息/章程/干系人/里程碑/确认)
This commit is contained in:
47
PROGRESS.md
47
PROGRESS.md
@@ -4,50 +4,51 @@
|
||||
|
||||
## 当前状态
|
||||
|
||||
- **当前Issue:** 准备创建Gitea仓库并导入第一批Issues
|
||||
- **当前Issue:** P0-1 项目创建向导页(准备开始)
|
||||
- **状态:** 📋 规划完成,待启动开发
|
||||
- **上次更新:** 2026-04-11 17:30
|
||||
- **上次更新:** 2026-04-11 17:50
|
||||
- **PRD版本:** v0.2
|
||||
- **Gitea仓库:** http://192.168.120.110:4000/xiaohei/pmp-tool (已创建Issues 1-3)
|
||||
|
||||
## Issue流水线
|
||||
|
||||
| # | Issue | 描述 | 状态 | 断点备注 |
|
||||
|---|-------|------|------|---------|
|
||||
| 0 | 项目初始化 | 创建Gitea仓库、目录结构、基础配置 | 🔨 进行中 | 待创建仓库 |
|
||||
| 1 | 项目创建向导页 | React+Arco Design向导框架 | ⬜ | |
|
||||
| 2 | 章程模板填充 | 基于用户输入生成章程Markdown | ⬜ | |
|
||||
| 3 | 飞书消息发送 | Node.js+Hono调用飞书Open API | ⬜ | |
|
||||
| 4 | 看板基础组件 | 三列看板+拖拽(dnd-kit) | ⬜ | |
|
||||
| 5 | 任务数据模型 | PostgreSQL建表+CRUD | ⬜ | |
|
||||
| 6 | HR管理员原型 | 任务拆解逻辑 | ⬜ | |
|
||||
| 7 | 经验管理员原型 | 执行记录存储和查询 | ⬜ | |
|
||||
| 8 | 决策交互卡片 | 飞书消息卡片+回调 | ⬜ | |
|
||||
| 9 | 检查清单引擎 | PMBOK检查清单展示+勾选 | ⬜ | |
|
||||
| 10 | 执行记录API | REST接口查询Agent日志 | ⬜ | |
|
||||
| 11 | 创建流程串联 | 向导→章程→任务→看板→通知全流程 | ⬜ | |
|
||||
| 12 | 基础CI/CD | GitHub Actions lint+单测 | ⬜ | |
|
||||
| 13 | 内测反馈表单 | 飞书表单收集用户反馈 | ⬜ | |
|
||||
| 14 | 打包部署脚本 | Docker-compose一键启动 | ⬜ | |
|
||||
|---|-------|------|------|------|
|
||||
| 1 | P0-1: 项目创建向导页 | 用React+Arco Design实现向导页框架,包含步骤条和基本表单(项目名称、目标) | 🔨 进行中 - Claude Code 工作中
|
||||
| 2 | P0-2: 章程模板填充 | 基于用户输入生成项目章节Markdown(目标、范围、里程碑、干系人) | ⬜ 待开发 | |
|
||||
| 3 | P0-3: 飞书消息发送 | Node.js+Hono调用飞书Open API发送文本消息 | ⬜ 待开发 | |
|
||||
| 4 | P0-4: 看板基础组件 | 三列看板+拖拽(dnd-kit) | ⬜ 待开发 | |
|
||||
| 5 | P0-5: 任务数据模型 | PostgreSQL建表+CRUD | ⬜ 待开发 | |
|
||||
| 6 | P0-6: HR管理员原型 | 任务拆解逻辑 | ⬜ 待开发 | |
|
||||
| 7 | P0-7: 经验管理员原型 | 执行记录存储和查询 | ⬜ 待开发 | |
|
||||
| 8 | P0-8: 决策交互卡片 | 飞书消息卡片+回调 | ⬜ 待开发 | |
|
||||
| 9 | P0-9: 检查清单引擎 | PMBOK检查清单展示+勾选 | ⬜ 待开发 | |
|
||||
| 10 | P0-10: 执行记录API | REST接口查询Agent日志 | ⬜ 待开发 | |
|
||||
| 11 | P0-11: 创建流程串联 | 向导→章程→任务→看板→通知全流程 | ⬜ 待开发 | |
|
||||
| 12 | P0-12: 基础CI/CD | GitHub Actions lint+单测 | ⬜ 待开发 | |
|
||||
| 13 | P0-13: 内测反馈表单 | 飞书表单收集用户反馈 | ⬜ 待开发 | |
|
||||
| 14 | P0-14: 打包部署脚本 | Docker-compose一键启动 | ⬜ 待开发 | |
|
||||
|
||||
## 断点续传信息
|
||||
|
||||
(每次任务中断时更新此区域,新会话从这里恢复)
|
||||
|
||||
```
|
||||
当前任务: 项目初始化
|
||||
当前任务: P0-1 项目创建向导页
|
||||
Claude Code执行状态: 未启动
|
||||
已交付文件: poc/agent_poc.py, poc/README.md, PRD.md
|
||||
Git状态: poc目录已init,未push
|
||||
下一步: 创建Gitea仓库 → 导入Issues → 启动Issue #1
|
||||
已交付文件: PROGRESS.md, PRD.md, knowledge/DEV-KNOWLEDGE.md, HEARTBEAT-CHECK.md, poc/agent_poc.py
|
||||
Git状态: 本地main分支已提交并推送到origin
|
||||
下一步: 启动Claude Code开发P0-1(项目创建向导页)
|
||||
```
|
||||
|
||||
## 里程碑
|
||||
|
||||
| 里程碑 | 目标日期 | 状态 |
|
||||
|--------|---------|------|
|
||||
| 仓库建立+Issue导入 | 2026-04-12 | 🔨 |
|
||||
| 仓库建立+Issue导入 | 2026-04-11 | ✅ |
|
||||
| 前端框架+基础页面 | 2026-04-25 | ⬜ |
|
||||
| 后端API+数据库 | 2026-05-10 | ⬜ |
|
||||
| Agent编排核心 | 2026-05-25 | ⬜ |
|
||||
| 全流程串联 | 2026-06-10 | ⬜ |
|
||||
| 内测版发布 | 2026-06-30 | ⬜ |
|
||||
|
||||
|
||||
15
package.json
Normal file
15
package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "pmp-tool",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@arco-design/web-react": "^2.67.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
329
src/pages/WizardPage.tsx
Normal file
329
src/pages/WizardPage.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Steps,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Message,
|
||||
Typography,
|
||||
Space,
|
||||
Card,
|
||||
Table,
|
||||
Select,
|
||||
DatePicker,
|
||||
Tag,
|
||||
} from '@arco-design/web-react';
|
||||
import '@arco-design/web-react/dist/css/arco.css';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
// Step definitions
|
||||
const STEPS = [
|
||||
{ title: '基本信息', description: '项目名称和目标' },
|
||||
{ title: '章程信息', description: '范围与约束' },
|
||||
{ title: '干系人', description: '识别关键干系人' },
|
||||
{ title: '里程碑', description: '设定关键节点' },
|
||||
{ title: '完成', description: '确认创建' },
|
||||
];
|
||||
|
||||
// Types
|
||||
interface Stakeholder {
|
||||
key: string;
|
||||
name: string;
|
||||
role: string;
|
||||
power: '高' | '中' | '低';
|
||||
interest: '高' | '中' | '低';
|
||||
strategy: string;
|
||||
}
|
||||
|
||||
interface Milestone {
|
||||
key: string;
|
||||
name: string;
|
||||
targetDate: string;
|
||||
deliverable: string;
|
||||
}
|
||||
|
||||
interface ProjectData {
|
||||
name: string;
|
||||
goal: string;
|
||||
scopeIn: string;
|
||||
scopeOut: string;
|
||||
constraints: string;
|
||||
assumptions: string;
|
||||
stakeholders: Stakeholder[];
|
||||
milestones: Milestone[];
|
||||
}
|
||||
|
||||
const createEmptyProject = (): ProjectData => ({
|
||||
name: '',
|
||||
goal: '',
|
||||
scopeIn: '',
|
||||
scopeOut: '',
|
||||
constraints: '',
|
||||
assumptions: '',
|
||||
stakeholders: [],
|
||||
milestones: [],
|
||||
});
|
||||
|
||||
const WizardPage: React.FC = () => {
|
||||
const [current, setCurrent] = useState(0);
|
||||
const [project, setProject] = useState<ProjectData>(createEmptyProject());
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const next = () => setCurrent(Math.min(current + 1, STEPS.length - 1));
|
||||
const prev = () => setCurrent(Math.max(current - 1, 0));
|
||||
|
||||
const handleSubmit = () => {
|
||||
setSubmitted(true);
|
||||
Message.success(`项目「${project.name}」已创建!`);
|
||||
console.log('Project created:', project);
|
||||
};
|
||||
|
||||
// ---- Step 1: Basic Info ----
|
||||
const renderBasicInfo = () => (
|
||||
<Form layout="vertical" style={{ maxWidth: 600 }}>
|
||||
<Form.Item label="项目名称" required>
|
||||
<Input
|
||||
placeholder="例如:客户管理系统"
|
||||
value={project.name}
|
||||
onChange={(val) => setProject({ ...project, name: val })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="项目目标(SMART原则)" required>
|
||||
<TextArea
|
||||
placeholder="具体、可衡量、可实现、相关、有时限的目标"
|
||||
rows={4}
|
||||
value={project.goal}
|
||||
onChange={(val) => setProject({ ...project, goal: val })}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
|
||||
// ---- Step 2: Charter ----
|
||||
const renderCharter = () => (
|
||||
<Form layout="vertical" style={{ maxWidth: 600 }}>
|
||||
<Form.Item label="包含范围">
|
||||
<TextArea
|
||||
placeholder="本项目要做什么..."
|
||||
rows={3}
|
||||
value={project.scopeIn}
|
||||
onChange={(val) => setProject({ ...project, scopeIn: val })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="不包含范围">
|
||||
<TextArea
|
||||
placeholder="本项目明确不做什么..."
|
||||
rows={3}
|
||||
value={project.scopeOut}
|
||||
onChange={(val) => setProject({ ...project, scopeOut: val })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="约束条件">
|
||||
<Input
|
||||
placeholder="例如:必须在6月底前上线,预算不超过10万"
|
||||
value={project.constraints}
|
||||
onChange={(val) => setProject({ ...project, constraints: val })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="假设条件">
|
||||
<Input
|
||||
placeholder="例如:团队3人全职投入,第三方API在5月就绪"
|
||||
value={project.assumptions}
|
||||
onChange={(val) => setProject({ ...project, assumptions: val })}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
|
||||
// ---- Step 3: Stakeholders ----
|
||||
const renderStakeholders = () => {
|
||||
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' },
|
||||
];
|
||||
|
||||
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 === '低'
|
||||
? '随时告知'
|
||||
: '监督';
|
||||
setProject({
|
||||
...project,
|
||||
stakeholders: [
|
||||
...project.stakeholders,
|
||||
{ key: Date.now().toString(), name: newName, role: newRole, power: newPower, interest: newInterest, strategy },
|
||||
],
|
||||
});
|
||||
setNewName('');
|
||||
setNewRole('');
|
||||
};
|
||||
|
||||
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) && (
|
||||
<Message.warning>⚠️ 项目名称和目标为必填项</Message.warning>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const stepRenderers = [renderBasicInfo, renderCharter, renderStakeholders, renderMilestones, renderConfirm];
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 900, margin: '40px auto', padding: '0 24px' }}>
|
||||
<Title heading={4} style={{ marginBottom: 24 }}>
|
||||
🚀 创建新项目
|
||||
</Title>
|
||||
<Steps current={current} style={{ marginBottom: 32 }}>
|
||||
{STEPS.map((step, idx) => (
|
||||
<Steps.Step key={idx} title={step.title} description={step.description} />
|
||||
))}
|
||||
</Steps>
|
||||
|
||||
<Card style={{ minHeight: 300, marginBottom: 24 }}>{stepRenderers[current]()}</Card>
|
||||
|
||||
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Button onClick={prev} disabled={current === 0}>
|
||||
上一步
|
||||
</Button>
|
||||
{current < STEPS.length - 1 ? (
|
||||
<Button type="primary" onClick={next}>
|
||||
下一步
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={!project.name || !project.goal}
|
||||
>
|
||||
创建项目
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{submitted && (
|
||||
<Card style={{ marginTop: 24, background: '#e8ffea' }}>
|
||||
<Text>✅ 项目「{project.name}」创建成功!正在跳转到看板...</Text>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WizardPage;
|
||||
1
src/pages/index.ts
Normal file
1
src/pages/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as WizardPage } from './WizardPage';
|
||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user