feat: P1 full implementation - 8 modules + frontend pages + feishu WS
P1 Backend (8 modules, 50+ new API endpoints): - P1-1: Stakeholder management (power-interest matrix) - P1-2: WBS task decomposition (tree structure + AI split) - P1-3: Risk management (register + matrix + AI identify) - P1-4: Requirement pool (MoSCoW + coverage + conflict detect) - P1-5: Change management (submit → evaluate → approve → implement) - P1-6: Health report (6 dimensions + red/yellow/green) - P1-7: Retrospective & knowledge base - P1-8: Multi-model support (6 models + strategy selection) Frontend (9 pages with sidebar navigation): - Project wizard, Kanban, Stakeholders, WBS, Risks, Requirements, Changes, Health, Retrospective Feishu integration: - WebSocket long-connection for receiving messages - Card button callback with decision tracking - 11 chat commands for P0+P1 features Tech: TypeScript, React 18, Arco Design, Hono, @larksuiteoapi/node-sdk
This commit is contained in:
98
src/pages/ChangePage.tsx
Normal file
98
src/pages/ChangePage.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Button, Modal, Form, Input, Select, Tag, Space, Message, Typography } from '@arco-design/web-react';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Title } = Typography;
|
||||
|
||||
interface ChangeRequest {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
type: 'scope' | 'schedule' | 'cost' | 'quality' | 'resource';
|
||||
impactLevel?: string;
|
||||
status: 'pending_approval' | 'approved' | 'rejected' | 'implemented';
|
||||
submitter?: string;
|
||||
}
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = { pending_approval: 'orange', approved: 'green', rejected: 'red', implemented: 'blue' };
|
||||
const STATUS_LABEL: Record<string, string> = { pending_approval: '待审批', approved: '已批准', rejected: '已驳回', implemented: '已执行' };
|
||||
const TYPE_OPTIONS = ['scope', 'schedule', 'cost', 'quality', 'resource'].map(v => ({ label: v, value: v }));
|
||||
|
||||
export default function ChangePage() {
|
||||
const [projectId] = useState('pmp-demo');
|
||||
const [data, setData] = useState<ChangeRequest[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const api = (path: string, opts?: RequestInit) => fetch(`/api/projects/${projectId}${path}`, opts);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try { const res = await api('/changes'); if (res.ok) { const d = await res.json(); setData(d?.changes || (Array.isArray(d) ? d : [])) }; } catch { Message.error('获取变更失败'); }
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { fetchData(); }, []);
|
||||
|
||||
const handleAdd = async () => {
|
||||
const values = form.getFieldsValue();
|
||||
try {
|
||||
const res = await api('/changes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values) });
|
||||
if (res.ok) { Message.success('添加成功'); setModalVisible(false); form.resetFields(); fetchData(); }
|
||||
} catch { Message.error('添加失败'); }
|
||||
};
|
||||
|
||||
const handleAction = async (cid: string, action: string) => {
|
||||
try {
|
||||
const res = await api(`/changes/${cid}/${action}`, { method: 'POST' });
|
||||
if (res.ok) { Message.success('操作成功'); fetchData(); }
|
||||
} catch { Message.error('操作失败'); }
|
||||
};
|
||||
|
||||
const handleStats = async () => {
|
||||
try {
|
||||
const res = await api('/changes/stats');
|
||||
if (res.ok) Modal.info({ title: '变更统计', content: JSON.stringify(await res.json(), null, 2) });
|
||||
} catch { Message.error('获取统计失败'); }
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '标题', dataIndex: 'title' },
|
||||
{ title: '类型', dataIndex: 'type' },
|
||||
{ title: '影响等级', dataIndex: 'impactLevel' },
|
||||
{ title: '状态', dataIndex: 'status', render: (v: string) => <Tag color={STATUS_COLOR[v]}>{STATUS_LABEL[v]}</Tag> },
|
||||
{ title: '提交人', dataIndex: 'submitter' },
|
||||
{
|
||||
title: '操作', render: (_: unknown, row: ChangeRequest) => (
|
||||
<Space>
|
||||
{row.status === 'pending_approval' && (
|
||||
<>
|
||||
<Button size="small" type="primary" onClick={() => handleAction(row.id, 'approve')}>批准</Button>
|
||||
<Button size="small" status="danger" onClick={() => handleAction(row.id, 'reject')}>驳回</Button>
|
||||
</>
|
||||
)}
|
||||
{row.status === 'approved' && <Button size="small" type="primary" onClick={() => handleAction(row.id, 'implement')}>执行</Button>}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<Title heading={4}>变更管理</Title>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Button type="primary" onClick={() => setModalVisible(true)}>添加变更</Button>
|
||||
<Button onClick={handleStats}>统计</Button>
|
||||
</Space>
|
||||
<Table rowKey="id" columns={columns} data={data} loading={loading} />
|
||||
<Modal title="添加变更" visible={modalVisible} onOk={handleAdd} onCancel={() => setModalVisible(false)}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="标题" field="title" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item label="描述" field="description"><TextArea /></Form.Item>
|
||||
<Form.Item label="类型" field="type" rules={[{ required: true }]}><Select options={TYPE_OPTIONS} /></Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
100
src/pages/HealthPage.tsx
Normal file
100
src/pages/HealthPage.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Button, Modal, Form, InputNumber, Space, Message, Typography, Table } from '@arco-design/web-react';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface DimensionScore {
|
||||
name: string;
|
||||
score: number;
|
||||
trend: 'up' | 'down' | 'stable';
|
||||
}
|
||||
|
||||
interface HealthReport {
|
||||
id: string;
|
||||
overallScore: number;
|
||||
dimensions: DimensionScore[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const DIMENSION_NAMES = ['任务进度', '风险管理', '需求覆盖', '阻塞状况', '变更控制', '里程碑'];
|
||||
const DIMENSION_KEYS = ['taskProgress', 'riskManagement', 'requirementCoverage', 'blockers', 'changeControl', 'milestones'];
|
||||
|
||||
function scoreColor(s: number) { return s >= 70 ? '#00b42a' : s >= 40 ? '#ff7d00' : '#f53f3f'; }
|
||||
function trendIcon(t: string) { return t === 'up' ? '↑' : t === 'down' ? '↓' : '→'; }
|
||||
|
||||
export default function HealthPage() {
|
||||
const [projectId] = useState('pmp-demo');
|
||||
const [reports, setReports] = useState<HealthReport[]>([]);
|
||||
const [current, setCurrent] = useState<HealthReport | null>(null);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const api = (path: string, opts?: RequestInit) => fetch(`/api/projects/${projectId}${path}`, opts);
|
||||
|
||||
const fetchReports = async () => {
|
||||
try { const res = await api('/health-reports'); if (res.ok) { const d = await res.json(); setReports(d?.reports || (Array.isArray(d) ? d : [])) }; } catch { /* ignore */ }
|
||||
};
|
||||
|
||||
useEffect(() => { fetchReports(); }, []);
|
||||
|
||||
const handleGenerate = async () => {
|
||||
const values = form.getFieldsValue();
|
||||
try {
|
||||
const res = await api('/health-report', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values) });
|
||||
if (res.ok) { const d = await res.json(); setCurrent(d); Message.success('报告已生成'); setModalVisible(false); fetchReports(); }
|
||||
} catch { Message.error('生成失败'); }
|
||||
};
|
||||
|
||||
const overall = current?.overallScore ?? 0;
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<Title heading={4}>项目健康度</Title>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Button type="primary" onClick={() => setModalVisible(true)}>生成报告</Button>
|
||||
</Space>
|
||||
|
||||
{current && (
|
||||
<>
|
||||
<Card style={{ textAlign: 'center', marginBottom: 16, background: scoreColor(overall), color: '#fff' }}>
|
||||
<div style={{ fontSize: 48, fontWeight: 700 }}>{overall}</div>
|
||||
<Text style={{ color: '#fff' }}>综合评分</Text>
|
||||
</Card>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12, marginBottom: 24 }}>
|
||||
{current.dimensions?.map((d, i) => (
|
||||
<Card key={i} size="small">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Text>{d.name || DIMENSION_NAMES[i]}</Text>
|
||||
<Space>
|
||||
<span style={{ width: 10, height: 10, borderRadius: '50%', background: scoreColor(d.score), display: 'inline-block' }} />
|
||||
<span style={{ color: scoreColor(d.score), fontWeight: 600 }}>{d.score}</span>
|
||||
<span>{trendIcon(d.trend)}</span>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Title heading={5}>历史报告</Title>
|
||||
<Table rowKey="id" columns={[
|
||||
{ title: 'ID', dataIndex: 'id' },
|
||||
{ title: '综合评分', dataIndex: 'overallScore' },
|
||||
{ title: '创建时间', dataIndex: 'createdAt' },
|
||||
]} data={reports} />
|
||||
|
||||
<Modal title="生成健康报告" visible={modalVisible} onOk={handleGenerate} onCancel={() => setModalVisible(false)}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="总任务数" field="totalTasks"><InputNumber min={0} /></Form.Item>
|
||||
<Form.Item label="已完成任务" field="completedTasks"><InputNumber min={0} /></Form.Item>
|
||||
<Form.Item label="总需求数" field="totalRequirements"><InputNumber min={0} /></Form.Item>
|
||||
<Form.Item label="已覆盖需求" field="coveredRequirements"><InputNumber min={0} /></Form.Item>
|
||||
<Form.Item label="风险数" field="totalRisks"><InputNumber min={0} /></Form.Item>
|
||||
<Form.Item label="已缓解风险" field="mitigatedRisks"><InputNumber min={0} /></Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
8
src/pages/KanbanPage.tsx
Normal file
8
src/pages/KanbanPage.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import KanbanBoard from '../components/KanbanBoard';
|
||||
|
||||
const KanbanPage: React.FC = () => {
|
||||
return <KanbanBoard />;
|
||||
};
|
||||
|
||||
export default KanbanPage;
|
||||
94
src/pages/RequirementPage.tsx
Normal file
94
src/pages/RequirementPage.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Button, Modal, Form, Input, Select, Tag, Space, Message, Typography } from '@arco-design/web-react';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Title } = Typography;
|
||||
|
||||
interface Requirement {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
priority: 'must' | 'should' | 'could' | 'wont';
|
||||
category: string;
|
||||
status: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
const PRIORITY_COLOR: Record<string, string> = { must: 'red', should: 'orange', could: 'blue', wont: 'grey' };
|
||||
const PRIORITY_LABEL: Record<string, string> = { must: 'Must', should: 'Should', could: 'Could', wont: "Won't" };
|
||||
|
||||
export default function RequirementPage() {
|
||||
const [projectId] = useState('pmp-demo');
|
||||
const [data, setData] = useState<Requirement[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const api = (path: string, opts?: RequestInit) => fetch(`/api/projects/${projectId}${path}`, opts);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await api('/requirements');
|
||||
if (res.ok) { const d = await res.json(); setData(d?.requirements || (Array.isArray(d) ? d : [])) };
|
||||
} catch { Message.error('获取需求失败'); }
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { fetchData(); }, []);
|
||||
|
||||
const handleAdd = async () => {
|
||||
const values = form.getFieldsValue();
|
||||
try {
|
||||
const res = await api('/requirements', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values) });
|
||||
if (res.ok) { Message.success('添加成功'); setModalVisible(false); form.resetFields(); fetchData(); }
|
||||
else Message.error('添加失败');
|
||||
} catch { Message.error('添加失败'); }
|
||||
};
|
||||
|
||||
const handleCoverage = async () => {
|
||||
try {
|
||||
const res = await api('/requirements/coverage');
|
||||
if (res.ok) { const d = await res.json(); Modal.info({ title: '需求覆盖率', content: JSON.stringify(d, null, 2) }); }
|
||||
} catch { Message.error('获取覆盖率失败'); }
|
||||
};
|
||||
|
||||
const handleConflict = async () => {
|
||||
try {
|
||||
const res = await api('/requirements/detect-conflicts', { method: 'POST' });
|
||||
if (res.ok) { const d = await res.json(); Modal.info({ title: '冲突检测结果', content: JSON.stringify(d, null, 2) }); }
|
||||
} catch { Message.error('冲突检测失败'); }
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '标题', dataIndex: 'title' },
|
||||
{ title: '优先级', dataIndex: 'priority', render: (v: string) => <Tag color={PRIORITY_COLOR[v]}>{PRIORITY_LABEL[v]}</Tag> },
|
||||
{ title: '分类', dataIndex: 'category' },
|
||||
{ title: '状态', dataIndex: 'status' },
|
||||
{ title: '来源', dataIndex: 'source' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<Title heading={4}>需求池管理</Title>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Button type="primary" onClick={() => setModalVisible(true)}>添加需求</Button>
|
||||
<Button onClick={handleCoverage}>覆盖率</Button>
|
||||
<Button onClick={handleConflict}>冲突检测</Button>
|
||||
</Space>
|
||||
<Table rowKey="id" columns={columns} data={data} loading={loading} />
|
||||
<Modal title="添加需求" visible={modalVisible} onOk={handleAdd} onCancel={() => setModalVisible(false)}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="标题" field="title" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item label="描述" field="description"><TextArea /></Form.Item>
|
||||
<Form.Item label="优先级" field="priority" rules={[{ required: true }]}>
|
||||
<Select options={['must', 'should', 'could', 'wont'].map(v => ({ label: PRIORITY_LABEL[v], value: v }))} />
|
||||
</Form.Item>
|
||||
<Form.Item label="分类" field="category">
|
||||
<Select options={['功能', '性能', '安全', '体验', '其他'].map(v => ({ label: v, value: v }))} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
140
src/pages/RetrospectivePage.tsx
Normal file
140
src/pages/RetrospectivePage.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Button, Modal, Form, Input, InputNumber, Space, Message, Typography, Tabs } from '@arco-design/web-react';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Title, Text } = Typography;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
interface RetroInput {
|
||||
projectName: string;
|
||||
goal: string;
|
||||
period: string;
|
||||
teamSize: number;
|
||||
taskCompletionRate: number;
|
||||
highlights?: string;
|
||||
challenges?: string;
|
||||
}
|
||||
|
||||
interface RetroResult {
|
||||
id: string;
|
||||
score: number;
|
||||
wentWell: string[];
|
||||
toImprove: string[];
|
||||
lessons: string[];
|
||||
aiInsights?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface KnowledgeItem {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export default function RetrospectivePage() {
|
||||
const [projectId] = useState('pmp-demo');
|
||||
const [result, setResult] = useState<RetroResult | null>(null);
|
||||
const [history, setHistory] = useState<RetroResult[]>([]);
|
||||
const [knowledge, setKnowledge] = useState<KnowledgeItem[]>([]);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const api = (path: string, opts?: RequestInit) => fetch(`/api/projects/${projectId}${path}`, opts);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const [r1, r2] = await Promise.all([api('/retrospectives'), api('/knowledge')]);
|
||||
if (r1.ok) setHistory(await r1.json());
|
||||
if (r2.ok) setKnowledge(await r2.json());
|
||||
} catch { /* ignore */ }
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const handleStart = async () => {
|
||||
const values = form.getFieldsValue();
|
||||
try {
|
||||
const res = await api('/retrospective', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(values) });
|
||||
if (res.ok) { const d = await res.json(); setResult(d); Message.success('复盘完成'); setModalVisible(false); form.resetFields(); }
|
||||
} catch { Message.error('复盘失败'); }
|
||||
};
|
||||
|
||||
const renderList = (items: string[], color: string) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 8 }}>
|
||||
{items.map((item, i) => <Card key={i} size="small" style={{ borderLeft: `3px solid ${color}` }}>{item}</Card>)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<Title heading={4}>项目复盘</Title>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Button type="primary" onClick={() => setModalVisible(true)}>开始复盘</Button>
|
||||
</Space>
|
||||
|
||||
{result && (
|
||||
<Tabs defaultActiveTab="result">
|
||||
<TabPane key="result" title="复盘结果">
|
||||
<Card style={{ textAlign: 'center', marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 56, fontWeight: 700, color: result.score >= 70 ? '#00b42a' : result.score >= 40 ? '#ff7d00' : '#f53f3f' }}>
|
||||
{result.score}
|
||||
</div>
|
||||
<Text>综合评分</Text>
|
||||
</Card>
|
||||
<Title heading={6}>✅ 做得好的</Title>
|
||||
{renderList(result.wentWell || [], '#00b42a')}
|
||||
<Title heading={6} style={{ marginTop: 16 }}>⚠️ 待改进</Title>
|
||||
{renderList(result.toImprove || [], '#ff7d00')}
|
||||
<Title heading={6} style={{ marginTop: 16 }}>💡 经验教训</Title>
|
||||
{renderList(result.lessons || [], '#165DFF')}
|
||||
{result.aiInsights && (
|
||||
<>
|
||||
<Title heading={6} style={{ marginTop: 16 }}>🤖 AI 洞察</Title>
|
||||
<Card size="small" style={{ background: '#f7f8fa' }}>{result.aiInsights}</Card>
|
||||
</>
|
||||
)}
|
||||
</TabPane>
|
||||
<TabPane key="knowledge" title="知识库">
|
||||
{knowledge.length === 0 ? <Text>暂无知识条目</Text> : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 8 }}>
|
||||
{knowledge.map(item => (
|
||||
<Card key={item.id} size="small" title={item.title}>
|
||||
<Text>{item.content}</Text>
|
||||
{item.tags && <div style={{ marginTop: 4 }}>{item.tags.map(t => <Text key={t} style={{ marginRight: 8, color: '#165DFF' }}>#{t}</Text>)}</div>}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
{!result && history.length > 0 && (
|
||||
<>
|
||||
<Title heading={5}>历史复盘</Title>
|
||||
{history.map(h => (
|
||||
<Card key={h.id} size="small" style={{ marginBottom: 8 }}>
|
||||
<Space>
|
||||
<Text bold>评分: {h.score}</Text>
|
||||
<Text type="secondary">{h.createdAt}</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Modal title="开始复盘" visible={modalVisible} onOk={handleStart} onCancel={() => setModalVisible(false)} style={{ width: 520 }}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="项目名称" field="projectName" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item label="项目目标" field="goal"><TextArea /></Form.Item>
|
||||
<Form.Item label="项目周期" field="period"><Input placeholder="如: 2024-Q1" /></Form.Item>
|
||||
<Form.Item label="团队规模" field="teamSize"><InputNumber min={1} /></Form.Item>
|
||||
<Form.Item label="任务完成率(%)" field="taskCompletionRate"><InputNumber min={0} max={100} /></Form.Item>
|
||||
<Form.Item label="亮点" field="highlights"><TextArea placeholder="做得好的方面..." /></Form.Item>
|
||||
<Form.Item label="挑战" field="challenges"><TextArea placeholder="遇到的困难..." /></Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
154
src/pages/RiskPage.tsx
Normal file
154
src/pages/RiskPage.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Table, Button, Modal, Form, Input, Select, Tag, Space, Message, InputNumber, Typography } from '@arco-design/web-react';
|
||||
import { IconPlus, IconApps, IconSearch } from '@arco-design/web-react/icon';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface Risk {
|
||||
id: string;
|
||||
title: string;
|
||||
category: string;
|
||||
probability: number;
|
||||
impact: number;
|
||||
score: number;
|
||||
level: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const LEVEL_MAP: Record<string, { label: string; color: string }> = {
|
||||
critical: { label: '极高', color: 'red' },
|
||||
high: { label: '高', color: 'orange' },
|
||||
medium: { label: '中', color: 'gold' },
|
||||
low: { label: '低', color: 'green' },
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '标题', dataIndex: 'title' },
|
||||
{ title: '类别', dataIndex: 'category' },
|
||||
{ title: '概率', dataIndex: 'probability' },
|
||||
{ title: '影响', dataIndex: 'impact' },
|
||||
{ title: '评分', dataIndex: 'score' },
|
||||
{
|
||||
title: '等级', dataIndex: 'level',
|
||||
render: (v: string) => { const l = LEVEL_MAP[v]; return l ? <Tag color={l.color}>{l.label}</Tag> : v; },
|
||||
},
|
||||
{ title: '状态', dataIndex: 'status' },
|
||||
];
|
||||
|
||||
export default function RiskPage() {
|
||||
const [projectId, setProjectId] = useState('pmp-demo');
|
||||
const [list, setList] = useState<Risk[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [identifyModal, setIdentifyModal] = useState(false);
|
||||
const [matrix, setMatrix] = useState<string | null>(null);
|
||||
const [projectDesc, setProjectDesc] = useState('');
|
||||
const [form] = Form.useForm();
|
||||
const base = `/api/projects/${projectId}/risks`;
|
||||
|
||||
const fetchList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(base);
|
||||
const data = await res.json();
|
||||
setList(data?.risks || data?.data || (Array.isArray(data) ? data : []));
|
||||
} catch { Message.error('获取风险失败'); }
|
||||
setLoading(false);
|
||||
}, [base]);
|
||||
|
||||
useEffect(() => { fetchList(); }, [fetchList]);
|
||||
|
||||
const handleAdd = async () => {
|
||||
try {
|
||||
await form.validate();
|
||||
const values = form.getFieldsValue();
|
||||
values.score = values.probability * values.impact;
|
||||
const res = await fetch(base, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
Message.success('添加成功');
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
fetchList();
|
||||
} catch { Message.error('添加失败'); }
|
||||
};
|
||||
|
||||
const handleIdentify = async () => {
|
||||
try {
|
||||
const res = await fetch(`${base}/identify`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ description: projectDesc }),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
Message.success('AI 识别完成');
|
||||
setIdentifyModal(false);
|
||||
setProjectDesc('');
|
||||
fetchList();
|
||||
} catch { Message.error('识别失败'); }
|
||||
};
|
||||
|
||||
const handleMatrix = async () => {
|
||||
try {
|
||||
const res = await fetch(`${base}/matrix`);
|
||||
const data = await res.json();
|
||||
setMatrix(JSON.stringify(data?.data || data, null, 2));
|
||||
} catch { Message.error('获取矩阵失败'); }
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="风险管理" style={{ margin: 16 }}
|
||||
extra={
|
||||
<Space>
|
||||
<Input value={projectId} onChange={setProjectId} style={{ width: 160 }} placeholder="项目ID" />
|
||||
<Button icon={<IconPlus />} type="primary" onClick={() => setModalVisible(true)}>添加</Button>
|
||||
<Button icon={<IconSearch />} onClick={() => setIdentifyModal(true)}>AI识别风险</Button>
|
||||
<Button icon={<IconApps />} onClick={handleMatrix}>风险矩阵</Button>
|
||||
</Space>
|
||||
}>
|
||||
<Table columns={columns} data={list} loading={loading} rowKey="id" />
|
||||
|
||||
{matrix && (
|
||||
<Card title="风险矩阵" style={{ marginTop: 16 }} extra={<Button size="small" onClick={() => setMatrix(null)}>关闭</Button>}>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', fontSize: 13 }}>{matrix}</pre>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Modal title="添加风险" visible={modalVisible} onOk={handleAdd} onCancel={() => { setModalVisible(false); form.resetFields(); }}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="标题" field="title" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item label="类别" field="category" rules={[{ required: true }]}>
|
||||
<Select options={[
|
||||
{ label: '技术', value: 'technical' },
|
||||
{ label: '管理', value: 'management' },
|
||||
{ label: '商业', value: 'business' },
|
||||
{ label: '外部', value: 'external' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
<Form.Item label="概率 (1-5)" field="probability" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={5} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="影响 (1-5)" field="impact" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={5} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" field="status" initialValue="open">
|
||||
<Select options={[
|
||||
{ label: '开放', value: 'open' },
|
||||
{ label: '缓解中', value: 'mitigating' },
|
||||
{ label: '已关闭', value: 'closed' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal title="AI 识别风险" visible={identifyModal} onOk={handleIdentify} onCancel={() => { setIdentifyModal(false); setProjectDesc(''); }}>
|
||||
<Text>输入项目描述,AI 将识别潜在风险:</Text>
|
||||
<TextArea rows={4} value={projectDesc} onChange={setProjectDesc} placeholder="描述项目范围、技术栈、团队情况..." style={{ marginTop: 8 }} />
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
123
src/pages/StakeholderPage.tsx
Normal file
123
src/pages/StakeholderPage.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Table, Button, Modal, Form, Input, Select, Tag, Space, Message, InputNumber } from '@arco-design/web-react';
|
||||
import { IconPlus, IconSearch } from '@arco-design/web-react/icon';
|
||||
|
||||
interface Stakeholder {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
organization: string;
|
||||
contact: string;
|
||||
power: number;
|
||||
interest: number;
|
||||
category: string;
|
||||
strategy: string;
|
||||
}
|
||||
|
||||
const CATEGORY_MAP: Record<string, { label: string; color: string }> = {
|
||||
manage_closely: { label: '重点管理', color: 'red' },
|
||||
keep_satisfied: { label: '保持满意', color: 'orange' },
|
||||
keep_informed: { label: '保持知情', color: 'arcoblue' },
|
||||
monitor: { label: '监督', color: 'green' },
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '姓名', dataIndex: 'name' },
|
||||
{ title: '角色', dataIndex: 'role' },
|
||||
{ title: '权力', dataIndex: 'power' },
|
||||
{ title: '利益', dataIndex: 'interest' },
|
||||
{
|
||||
title: '分类', dataIndex: 'category',
|
||||
render: (v: string) => {
|
||||
const c = CATEGORY_MAP[v];
|
||||
return c ? <Tag color={c.color}>{c.label}</Tag> : '-';
|
||||
},
|
||||
},
|
||||
{ title: '策略', dataIndex: 'strategy' },
|
||||
];
|
||||
|
||||
export default function StakeholderPage() {
|
||||
const [projectId, setProjectId] = useState('pmp-demo');
|
||||
const [list, setList] = useState<Stakeholder[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [analysis, setAnalysis] = useState<string | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const base = `/api/projects/${projectId}/stakeholders`;
|
||||
|
||||
const fetchList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(base);
|
||||
const data = await res.json();
|
||||
setList(data?.stakeholders || data?.data || (Array.isArray(data) ? data : []));
|
||||
} catch { Message.error('获取干系人失败'); }
|
||||
setLoading(false);
|
||||
}, [base]);
|
||||
|
||||
useEffect(() => { fetchList(); }, [fetchList]);
|
||||
|
||||
const handleAdd = async () => {
|
||||
try {
|
||||
await form.validate();
|
||||
const values = form.getFieldsValue();
|
||||
const res = await fetch(base, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
Message.success('添加成功');
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
fetchList();
|
||||
} catch { Message.error('添加失败'); }
|
||||
};
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
try {
|
||||
const res = await fetch(`${base}/analyze`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
setAnalysis(JSON.stringify(data?.data || data, null, 2));
|
||||
} catch { Message.error('分析失败'); }
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="干系人管理" style={{ margin: 16 }}
|
||||
extra={
|
||||
<Space>
|
||||
<Input value={projectId} onChange={setProjectId} style={{ width: 160 }} placeholder="项目ID" />
|
||||
<Button icon={<IconPlus />} type="primary" onClick={() => setModalVisible(true)}>添加</Button>
|
||||
<Button icon={<IconSearch />} onClick={handleAnalyze}>分析</Button>
|
||||
</Space>
|
||||
}>
|
||||
<Table columns={columns} data={list} loading={loading} rowKey="id" />
|
||||
|
||||
{analysis && (
|
||||
<Card title="分析结果" style={{ marginTop: 16 }} extra={<Button size="small" onClick={() => setAnalysis(null)}>关闭</Button>}>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', fontSize: 13 }}>{analysis}</pre>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Modal title="添加干系人" visible={modalVisible} onOk={handleAdd} onCancel={() => { setModalVisible(false); form.resetFields(); }}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="姓名" field="name" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="角色" field="role" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="组织" field="organization"><Input /></Form.Item>
|
||||
<Form.Item label="联系方式" field="contact"><Input /></Form.Item>
|
||||
<Form.Item label="权力 (1-5)" field="power" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={5} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="利益 (1-5)" field="interest" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={5} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
139
src/pages/WBSPage.tsx
Normal file
139
src/pages/WBSPage.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Button, Modal, Form, Input, Select, Tag, Space, Message, InputNumber, Typography } from '@arco-design/web-react';
|
||||
import { IconPlus, IconSearch } from '@arco-design/web-react/icon';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface WBSNode {
|
||||
id: string;
|
||||
wbsCode: string;
|
||||
name: string;
|
||||
description: string;
|
||||
level: number;
|
||||
status: string;
|
||||
estimatedHours: number;
|
||||
assignee: string;
|
||||
priority: string;
|
||||
children?: WBSNode[];
|
||||
}
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: '待开始', color: 'grey' },
|
||||
in_progress: { label: '进行中', color: 'blue' },
|
||||
done: { label: '已完成', color: 'green' },
|
||||
};
|
||||
|
||||
const PRIORITY_MAP: Record<string, { label: string; color: string }> = {
|
||||
must: { label: '必须', color: 'red' },
|
||||
should: { label: '应该', color: 'orange' },
|
||||
could: { label: '可以', color: 'blue' },
|
||||
};
|
||||
|
||||
const WBSRow: React.FC<{ node: WBSNode; projectId: string; onRefresh: () => void }> = ({ node, projectId, onRefresh }) => {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
|
||||
const handleDecompose = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/projects/${projectId}/wbs/${node.id}/decompose`, { method: 'POST' });
|
||||
if (!res.ok) throw new Error();
|
||||
Message.success('AI 拆解已提交');
|
||||
onRefresh();
|
||||
} catch { Message.error('拆解失败'); }
|
||||
};
|
||||
|
||||
const s = STATUS_MAP[node.status] || { label: node.status, color: 'grey' };
|
||||
const p = PRIORITY_MAP[node.priority] || { label: node.priority, color: 'grey' };
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', padding: '6px 0', borderBottom: '1px solid var(--color-border-1)' }}>
|
||||
<div style={{ width: 24 * node.level }}>
|
||||
{node.children?.length ? (
|
||||
<Text onClick={() => setExpanded(!expanded)} style={{ cursor: 'pointer' }}>{expanded ? '▼' : '▶'}</Text>
|
||||
) : <span style={{ marginLeft: 12 }} />}
|
||||
</div>
|
||||
<Text style={{ width: 80, flexShrink: 0 }}>{node.wbsCode}</Text>
|
||||
<Text style={{ flex: 1 }}>{node.name}</Text>
|
||||
<Tag color={s.color} size="small">{s.label}</Tag>
|
||||
<Text style={{ margin: '0 12px', flexShrink: 0 }}>{node.estimatedHours}h</Text>
|
||||
<Text style={{ width: 80, flexShrink: 0 }}>{node.assignee || '-'}</Text>
|
||||
<Tag color={p.color} size="small" style={{ margin: '0 8px' }}>{p.label}</Tag>
|
||||
<Button size="mini" icon={<IconSearch />} onClick={handleDecompose}>AI拆解</Button>
|
||||
</div>
|
||||
{expanded && node.children?.map(child => (
|
||||
<WBSRow key={child.id} node={child} projectId={projectId} onRefresh={onRefresh} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function WBSPage() {
|
||||
const [projectId, setProjectId] = useState('pmp-demo');
|
||||
const [nodes, setNodes] = useState<WBSNode[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const base = `/api/projects/${projectId}/wbs`;
|
||||
|
||||
const fetchNodes = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(base);
|
||||
const data = await res.json();
|
||||
setNodes(data?.tree || data?.data || (Array.isArray(data) ? data : []));
|
||||
} catch { Message.error('获取 WBS 失败'); }
|
||||
setLoading(false);
|
||||
}, [base]);
|
||||
|
||||
useEffect(() => { fetchNodes(); }, [fetchNodes]);
|
||||
|
||||
const handleAdd = async () => {
|
||||
try {
|
||||
await form.validate();
|
||||
const res = await fetch(base, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(form.getFieldsValue()),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
Message.success('添加成功');
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
fetchNodes();
|
||||
} catch { Message.error('添加失败'); }
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="WBS 任务分解" style={{ margin: 16 }}
|
||||
extra={
|
||||
<Space>
|
||||
<Input value={projectId} onChange={setProjectId} style={{ width: 160 }} placeholder="项目ID" />
|
||||
<Button icon={<IconPlus />} type="primary" onClick={() => setModalVisible(true)}>添加节点</Button>
|
||||
</Space>
|
||||
}>
|
||||
{loading ? <Text>加载中...</Text> : nodes.map(n => (
|
||||
<WBSRow key={n.id} node={n} projectId={projectId} onRefresh={fetchNodes} />
|
||||
))}
|
||||
|
||||
<Modal title="添加 WBS 节点" visible={modalVisible} onOk={handleAdd} onCancel={() => { setModalVisible(false); form.resetFields(); }}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="名称" field="name" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item label="描述" field="description"><Input /></Form.Item>
|
||||
<Form.Item label="层级" field="level" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={10} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="预估工时" field="estimatedHours">
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="优先级" field="priority">
|
||||
<Select options={[
|
||||
{ label: '必须', value: 'must' },
|
||||
{ label: '应该', value: 'should' },
|
||||
{ label: '可以', value: 'could' },
|
||||
]} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user