Initial commit

This commit is contained in:
史悦
2026-02-02 18:30:58 +08:00
commit ae1ca8bfaa
40 changed files with 10900 additions and 0 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
node_modules
dist
.git
.gitignore
*.md
.vscode
.venv
.env
.env.local
.env.*.local

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
node_modules/
dist/
.venv/
*.log
.DS_Store
.env
.env.local
*.pdf
pdf_images/

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

429
docs/需求设计文档.md Normal file
View File

@@ -0,0 +1,429 @@
# ITTOView - PMP项目管理ITTO可视化学习平台
## 需求设计文档 v1.0
---
## 1. 项目概述
### 1.1 项目背景
PMPProject Management Professional认证考试中ITTOInput-Tools & Techniques-Output是核心知识点。PMBOK第6版包含
- **10大知识领域**:整合、范围、进度、成本、质量、资源、沟通、风险、采购、相关方管理
- **5大过程组**:启动、规划、执行、监控、收尾
- **49个项目管理过程**:每个过程都有对应的输入、工具与技术、输出
传统的ITTO学习方式PDF/表格)存在以下痛点:
1. **信息孤立**:难以看到过程之间的数据流向关系
2. **记忆困难**:大量术语和对应关系难以记忆
3. **缺乏互动**:无法进行自测和针对性复习
4. **查找低效**:无法快速定位特定输入/输出的使用场景
### 1.2 项目目标
构建一个**精美、交互式**的ITTO可视化学习平台帮助PMP考生
1. **直观理解**通过流程图可视化ITTO关系
2. **高效记忆**:基于间隔重复的记忆卡片系统
3. **精准评估**:完整的测评、错题、统计功能
4. **快速查询**:强大的搜索和关联分析能力
### 1.3 目标用户
- PMP/CAPM认证备考人员
- 项目管理学习者
- 企业项目管理培训机构
---
## 2. 功能需求
### 2.1 核心功能模块
#### 2.1.1 知识浏览模块
| 功能 | 描述 | 优先级 |
|------|------|--------|
| 知识领域视图 | 按10大知识领域分类展示所有过程 | P0 |
| 过程组视图 | 按5大过程组分类展示所有过程 | P0 |
| 过程详情页 | 展示单个过程的完整ITTO信息 | P0 |
| 表格视图 | 传统表格形式展示,支持排序筛选 | P1 |
| 卡片视图 | 卡片形式展示,适合快速浏览 | P1 |
#### 2.1.2 可视化模块
| 功能 | 描述 | 优先级 |
|------|------|--------|
| ITTO流程图 | 输入→工具技术→输出的流程图展示 | P0 |
| 数据流向图 | 展示输出如何流向其他过程作为输入 | P0 |
| 知识领域全景图 | 单个知识领域内所有过程的关系图 | P1 |
| 全局关系图谱 | 49个过程的完整关系网络 | P2 |
#### 2.1.3 搜索与筛选模块
| 功能 | 描述 | 优先级 |
|------|------|--------|
| 全局搜索 | 搜索过程名、输入、工具、输出 | P0 |
| 分类筛选 | 按知识领域/过程组筛选 | P0 |
| 类型筛选 | 仅搜索输入/工具/输出 | P1 |
| 关联查询 | 查看某项作为输入/输出的所有过程 | P1 |
#### 2.1.4 学习模块
| 功能 | 描述 | 优先级 |
|------|------|--------|
| 记忆卡片 | 基于间隔重复算法的闪卡系统 | P0 |
| 自测模式 | 多种题型(选择、填空、连线) | P0 |
| 错题本 | 自动收集错题,支持针对性复习 | P0 |
| 掌握度标记 | 熟悉/模糊/陌生三级标记 | P1 |
| 学习统计 | 学习时长、正确率、进度可视化 | P1 |
| 复习计划 | 基于艾宾浩斯遗忘曲线的复习提醒 | P2 |
#### 2.1.5 用户体验模块
| 功能 | 描述 | 优先级 |
|------|------|--------|
| 响应式布局 | 适配PC和平板设备 | P0 |
| 深色模式 | 支持明暗主题切换 | P1 |
| 键盘导航 | 支持快捷键操作 | P2 |
| 数据导出 | 导出学习记录为JSON/CSV | P2 |
### 2.2 页面结构
```
ITTOView/
├── 首页 (Dashboard)
│ ├── 学习进度概览
│ ├── 今日复习任务
│ └── 快速入口
├── 知识浏览
│ ├── 知识领域视图
│ ├── 过程组视图
│ └── 过程详情页
├── 可视化
│ ├── ITTO流程图
│ ├── 数据流向图
│ └── 全局关系图谱
├── 学习中心
│ ├── 记忆卡片
│ ├── 自测模式
│ ├── 错题本
│ └── 学习统计
└── 设置
├── 主题设置
└── 数据管理
```
---
## 3. 技术架构
### 3.1 技术选型
| 类别 | 技术方案 | 选型理由 |
|------|----------|----------|
| 框架 | React 18 + TypeScript | 生态成熟、类型安全、组件化开发 |
| 构建工具 | Vite | 快速冷启动、HMR、现代化构建 |
| UI组件库 | Radix UI + Tailwind CSS | 无样式组件+原子化CSS高度可定制 |
| 可视化 | React Flow + ELK.js | 流程图交互完善 + 自动布局算法 |
| 状态管理 | Zustand | 轻量、简洁、TypeScript友好 |
| 搜索引擎 | FlexSearch | 高性能全文搜索,支持中文 |
| 路由 | React Router v6 | 官方路由方案,功能完善 |
| 动画 | Framer Motion | 声明式动画,流畅交互 |
| 图表 | Recharts | 基于React的图表库学习统计使用 |
| 持久化 | localStorage + IndexedDB | 本地存储学习进度和用户数据 |
### 3.2 项目结构
```
src/
├── assets/ # 静态资源
│ ├── icons/
│ └── images/
├── components/ # 通用组件
│ ├── ui/ # 基础UI组件
│ ├── layout/ # 布局组件
│ └── shared/ # 共享业务组件
├── features/ # 功能模块
│ ├── browse/ # 知识浏览
│ ├── visualize/ # 可视化
│ ├── learn/ # 学习中心
│ └── search/ # 搜索功能
├── data/ # 数据层
│ ├── itto.json # ITTO核心数据
│ ├── models/ # 数据模型定义
│ └── index/ # 搜索索引
├── stores/ # 状态管理
│ ├── useAppStore.ts
│ ├── useLearnStore.ts
│ └── useSearchStore.ts
├── hooks/ # 自定义Hooks
├── utils/ # 工具函数
├── styles/ # 全局样式
├── types/ # TypeScript类型
└── App.tsx
```
---
## 4. 数据模型
### 4.1 核心数据结构
#### 4.1.1 知识领域 (KnowledgeArea)
```typescript
interface KnowledgeArea {
id: string; // 如 "KA01"
name: string; // 如 "项目整合管理"
nameEn: string; // 如 "Project Integration Management"
order: number; // 排序序号 1-10
color: string; // 主题色 "#3B82F6"
description: string; // 简要描述
}
```
#### 4.1.2 过程组 (ProcessGroup)
```typescript
interface ProcessGroup {
id: string; // 如 "PG01"
name: string; // 如 "启动过程组"
nameEn: string; // 如 "Initiating Process Group"
order: number; // 排序序号 1-5
color: string; // 主题色
}
```
#### 4.1.3 过程 (Process)
```typescript
interface Process {
id: string; // 如 "P001"
name: string; // 如 "制定项目章程"
nameEn: string; // 如 "Develop Project Charter"
knowledgeAreaId: string; // 所属知识领域
processGroupId: string; // 所属过程组
order: number; // 在知识领域内的序号
inputs: ArtifactRef[]; // 输入列表
tools: ToolRef[]; // 工具与技术列表
outputs: ArtifactRef[]; // 输出列表
}
```
#### 4.1.4 工件/文档 (Artifact)
```typescript
interface Artifact {
id: string; // 如 "A0001"
name: string; // 如 "项目章程"
nameEn: string; // 如 "Project Charter"
category: ArtifactCategory;
description?: string;
}
type ArtifactCategory =
| 'document' // 文档
| 'plan' // 计划
| 'baseline' // 基准
| 'report' // 报告
| 'register' // 登记册
| 'log' // 日志
| 'deliverable' // 可交付成果
| 'other'; // 其他
```
#### 4.1.5 工具与技术 (ToolTechnique)
```typescript
interface ToolTechnique {
id: string; // 如 "TT0001"
name: string; // 如 "专家判断"
nameEn: string; // 如 "Expert Judgment"
type: ToolType;
description?: string;
}
type ToolType =
| 'tool' // 工具
| 'technique' // 技术
| 'method' // 方法
| 'skill' // 技能
| 'meeting'; // 会议
```
### 4.2 学习数据结构
```typescript
// 学习记录
interface LearningRecord {
id: string;
userId: string;
processId: string;
masteryLevel: 'familiar' | 'fuzzy' | 'unfamiliar';
reviewCount: number;
correctCount: number;
lastReviewAt: Date;
nextReviewAt: Date; // 基于间隔重复算法计算
}
// 错题记录
interface WrongAnswer {
id: string;
questionId: string;
questionType: 'choice' | 'fill' | 'match';
userAnswer: string;
correctAnswer: string;
createdAt: Date;
reviewed: boolean;
}
// 学习统计
interface LearningStats {
totalStudyTime: number; // 总学习时长(分钟)
totalQuestions: number; // 总答题数
correctRate: number; // 正确率
masteredCount: number; // 已掌握过程数
streakDays: number; // 连续学习天数
lastStudyDate: Date;
}
```
---
## 5. 实施计划
### 5.1 里程碑规划
| 阶段 | 名称 | 主要产出 | 验收标准 |
|------|------|----------|----------|
| M0 | 数据准备 | ITTO数据JSON、数据校验 | 49个过程数据完整可用 |
| M1 | 基础架构 | 项目骨架、路由、布局 | 可浏览10大领域和5大过程组 |
| M2 | 核心浏览 | 过程详情页、ITTO表格、搜索 | 可搜索并查看任意过程详情 |
| M3 | 可视化 | ITTO流程图、数据流向图 | 图谱支持缩放/高亮/筛选 |
| M4 | 学习系统 | 卡片、测评、错题、统计 | 完整学习闭环可用 |
| M5 | 体验优化 | 动效、响应式、性能优化 | 流畅度达标、视觉统一 |
### 5.2 详细任务分解
#### M0: 数据准备
- [ ] 从PDF提取49个过程的ITTO数据
- [ ] 设计并实现数据JSON结构
- [ ] 建立输入/输出的关联关系
- [ ] 数据校验与完整性检查
#### M1: 基础架构
- [ ] 初始化Vite + React + TypeScript项目
- [ ] 配置Tailwind CSS和Radix UI
- [ ] 实现基础布局组件Header、Sidebar、Content
- [ ] 配置React Router路由
- [ ] 实现知识领域列表页
- [ ] 实现过程组列表页
#### M2: 核心浏览
- [ ] 实现过程详情页
- [ ] 实现ITTO表格组件
- [ ] 集成FlexSearch搜索引擎
- [ ] 实现全局搜索功能
- [ ] 实现分类筛选功能
#### M3: 可视化
- [ ] 集成React Flow
- [ ] 实现ITTO流程图组件
- [ ] 实现数据流向分析
- [ ] 实现知识领域全景图
- [ ] 优化图谱交互体验
#### M4: 学习系统
- [ ] 实现记忆卡片组件
- [ ] 实现间隔重复算法
- [ ] 实现自测模式(多种题型)
- [ ] 实现错题本功能
- [ ] 实现学习统计页面
- [ ] 实现数据持久化
#### M5: 体验优化
- [ ] 添加页面过渡动画
- [ ] 实现深色模式
- [ ] 响应式布局适配
- [ ] 性能优化(懒加载、虚拟化)
- [ ] 用户体验细节打磨
---
## 6. UI设计规范
### 6.1 色彩系统
#### 知识领域主题色
| 知识领域 | 主色 | 用途 |
|----------|------|------|
| 整合管理 | `#6366F1` (Indigo) | 统领全局 |
| 范围管理 | `#8B5CF6` (Violet) | 边界定义 |
| 进度管理 | `#EC4899` (Pink) | 时间紧迫 |
| 成本管理 | `#10B981` (Emerald) | 财务绿色 |
| 质量管理 | `#F59E0B` (Amber) | 金牌品质 |
| 资源管理 | `#3B82F6` (Blue) | 人力资源 |
| 沟通管理 | `#06B6D4` (Cyan) | 信息流动 |
| 风险管理 | `#EF4444` (Red) | 风险警示 |
| 采购管理 | `#84CC16` (Lime) | 采购绿通 |
| 相关方管理 | `#F97316` (Orange) | 人际温暖 |
#### 过程组主题色
| 过程组 | 主色 | 含义 |
|--------|------|------|
| 启动 | `#22C55E` | 绿灯启动 |
| 规划 | `#3B82F6` | 蓝图规划 |
| 执行 | `#F59E0B` | 黄金执行 |
| 监控 | `#8B5CF6` | 紫色监控 |
| 收尾 | `#6B7280` | 灰色收尾 |
### 6.2 组件设计原则
1. **卡片化设计**:信息以卡片形式呈现,层次分明
2. **色彩编码**:通过颜色快速识别知识领域和过程组
3. **渐进式披露**:先展示概要,点击展开详情
4. **一致性交互**:相同操作保持一致的交互反馈
5. **无障碍设计**:确保足够的对比度和可访问性
---
## 7. 附录
### 7.1 10大知识领域
1. **项目整合管理** - 7个过程
2. **项目范围管理** - 6个过程
3. **项目进度管理** - 6个过程
4. **项目成本管理** - 4个过程
5. **项目质量管理** - 3个过程
6. **项目资源管理** - 6个过程
7. **项目沟通管理** - 3个过程
8. **项目风险管理** - 7个过程
9. **项目采购管理** - 3个过程
10. **项目相关方管理** - 4个过程
### 7.2 5大过程组
1. **启动过程组** - 2个过程
2. **规划过程组** - 24个过程
3. **执行过程组** - 10个过程
4. **监控过程组** - 12个过程
5. **收尾过程组** - 1个过程
### 7.3 参考资料
- PMBOK第6版指南
- ITTO输入输出工具手册本项目源PDF
- React Flow官方文档
- Tailwind CSS设计系统
---
**文档版本**: v1.0
**创建日期**: 2026-02-02
**最后更新**: 2026-02-02

14
index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ITTOView - PMP项目管理ITTO可视化学习平台</title>
<meta name="description" content="PMP认证考试ITTO可视化学习平台帮助您高效掌握49个项目管理过程" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6557
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
package.json Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "ittoview",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"@antv/g6": "^4.8.25",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"clsx": "^2.1.0",
"elkjs": "^0.9.2",
"flexsearch": "^0.7.43",
"framer-motion": "^11.0.3",
"lucide-react": "^0.344.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.22.0",
"reactflow": "^11.10.4",
"recharts": "^2.12.0",
"zustand": "^4.5.0"
},
"devDependencies": {
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.1.0"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

33
src/App.tsx Normal file
View File

@@ -0,0 +1,33 @@
import { Routes, Route } from 'react-router-dom'
import { Layout } from './components/layout/Layout'
import { HomePage } from './pages/HomePage'
import { KnowledgeAreasPage } from './pages/KnowledgeAreasPage'
import { ProcessGroupsPage } from './pages/ProcessGroupsPage'
import { ProcessDetailPage } from './pages/ProcessDetailPage'
import { ProcessMatrixPage } from './pages/ProcessMatrixPage'
import { ProcessGraphPage } from './pages/ProcessGraphPage'
import { ArtifactDetailPage } from './pages/ArtifactDetailPage'
import { ToolDetailPage } from './pages/ToolDetailPage'
import { SettingsPage } from './pages/SettingsPage'
function App() {
return (
<Layout>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/knowledge-areas" element={<KnowledgeAreasPage />} />
<Route path="/knowledge-areas/:id" element={<KnowledgeAreasPage />} />
<Route path="/process-groups" element={<ProcessGroupsPage />} />
<Route path="/process-groups/:id" element={<ProcessGroupsPage />} />
<Route path="/process/:id" element={<ProcessDetailPage />} />
<Route path="/process-matrix" element={<ProcessMatrixPage />} />
<Route path="/process-graph" element={<ProcessGraphPage />} />
<Route path="/artifact/:id" element={<ArtifactDetailPage />} />
<Route path="/tool/:id" element={<ToolDetailPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Layout>
)
}
export default App

View File

@@ -0,0 +1,255 @@
import { useAppStore } from '@/stores/useAppStore'
import { Menu, Search, Sun, Moon, X } from 'lucide-react'
import { useState, useMemo, useRef, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { processes, artifacts, tools, knowledgeAreaMap } from '@/data'
interface SearchResult {
type: 'process' | 'artifact' | 'tool'
id: string
name: string
nameEn: string
extra?: string
link: string
}
export function Header() {
const sidebarOpen = useAppStore((s) => s.sidebarOpen)
const toggleSidebar = useAppStore((s) => s.toggleSidebar)
const darkMode = useAppStore((s) => s.darkMode)
const toggleDarkMode = useAppStore((s) => s.toggleDarkMode)
const searchQuery = useAppStore((s) => s.searchQuery)
const setSearchQuery = useAppStore((s) => s.setSearchQuery)
const [isSearchOpen, setIsSearchOpen] = useState(false)
const searchRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const navigate = useNavigate()
// 搜索结果
const searchResults = useMemo<SearchResult[]>(() => {
if (!searchQuery.trim()) return []
const query = searchQuery.toLowerCase()
const results: SearchResult[] = []
// 搜索过程
processes.forEach((p) => {
if (
p.name.toLowerCase().includes(query) ||
p.nameEn.toLowerCase().includes(query) ||
p.code.toLowerCase().includes(query)
) {
const ka = knowledgeAreaMap.get(p.knowledgeAreaId)
results.push({
type: 'process',
id: p.id,
name: `${p.code} ${p.name}`,
nameEn: p.nameEn,
extra: ka?.name,
link: `/process/${p.id}`,
})
}
})
// 搜索工件/文档
artifacts.forEach((a) => {
if (
a.name.toLowerCase().includes(query) ||
a.nameEn.toLowerCase().includes(query)
) {
results.push({
type: 'artifact',
id: a.id,
name: a.name,
nameEn: a.nameEn,
extra: '工件/文档',
link: `/artifact/${a.id}`,
})
}
})
// 搜索工具与技术
tools.forEach((t) => {
if (
t.name.toLowerCase().includes(query) ||
t.nameEn.toLowerCase().includes(query)
) {
results.push({
type: 'tool',
id: t.id,
name: t.name,
nameEn: t.nameEn,
extra: '工具与技术',
link: `/tool/${t.id}`,
})
}
})
return results.slice(0, 10) // 限制结果数量
}, [searchQuery])
// 点击外部关闭搜索
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
setIsSearchOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
// 键盘快捷键
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
event.preventDefault()
setIsSearchOpen(true)
inputRef.current?.focus()
}
if (event.key === 'Escape') {
setIsSearchOpen(false)
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [])
const handleResultClick = (result: SearchResult) => {
navigate(result.link)
setIsSearchOpen(false)
}
const getTypeColor = (type: string) => {
switch (type) {
case 'process':
return 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:text-indigo-300'
case 'artifact':
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300'
case 'tool':
return 'bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300'
default:
return 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
}
}
const getTypeLabel = (type: string) => {
switch (type) {
case 'process':
return '过程'
case 'artifact':
return '工件'
case 'tool':
return '工具'
default:
return type
}
}
return (
<header className={`
sticky top-0 z-10 flex h-16 items-center justify-between gap-4
border-b border-gray-200 dark:border-gray-700
bg-white dark:bg-gray-800 px-4
transition-all duration-300
${sidebarOpen ? 'lg:ml-64' : 'lg:ml-20'}
`}>
{/* 左侧:菜单按钮和搜索 */}
<div className="flex items-center gap-4 flex-1">
<button
onClick={toggleSidebar}
className="lg:hidden flex h-10 w-10 items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500"
aria-label="切换菜单"
>
<Menu size={20} />
</button>
{/* 搜索框 */}
<div ref={searchRef} className="relative flex-1 max-w-lg">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
<input
ref={inputRef}
type="text"
placeholder="搜索过程、输入、工具、输出..."
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value)
setIsSearchOpen(true)
}}
onFocus={() => setIsSearchOpen(true)}
onClick={() => setIsSearchOpen(true)}
className="w-full h-10 pl-10 pr-20 rounded-lg border border-gray-200 dark:border-gray-600
bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white
placeholder-gray-400 dark:placeholder-gray-500
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent
transition-colors"
/>
{searchQuery && (
<button
onClick={() => {
setSearchQuery('')
inputRef.current?.focus()
}}
className="absolute right-12 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<X size={16} />
</button>
)}
<kbd className="absolute right-3 top-1/2 -translate-y-1/2 hidden sm:inline-flex
h-5 items-center gap-1 rounded border border-gray-200 dark:border-gray-600
bg-gray-100 dark:bg-gray-600 px-1.5 text-xs text-gray-500 dark:text-gray-400">
K
</kbd>
{/* 搜索结果下拉 */}
{isSearchOpen && searchQuery && (
<div className="absolute top-full left-0 right-0 mt-2 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden z-50">
{searchResults.length > 0 ? (
<ul className="max-h-96 overflow-y-auto">
{searchResults.map((result) => (
<li key={`${result.type}-${result.id}`}>
<button
onClick={() => handleResultClick(result)}
className="w-full px-4 py-3 flex items-center gap-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 text-left transition-colors"
>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${getTypeColor(result.type)}`}>
{getTypeLabel(result.type)}
</span>
<div className="flex-1 min-w-0">
<div className="font-medium text-gray-900 dark:text-white truncate">
{result.name}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 truncate">
{result.nameEn}
{result.extra && ` · ${result.extra}`}
</div>
</div>
</button>
</li>
))}
</ul>
) : (
<div className="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
</div>
)}
</div>
)}
</div>
</div>
{/* 右侧:操作按钮 */}
<div className="flex items-center gap-2">
{/* 主题切换 */}
<button
onClick={toggleDarkMode}
className="flex h-10 w-10 items-center justify-center rounded-lg
hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400"
aria-label="切换主题"
>
{darkMode ? <Sun size={20} /> : <Moon size={20} />}
</button>
</div>
</header>
)
}

View File

@@ -0,0 +1,41 @@
import { ReactNode } from 'react'
import { Header } from './Header'
import { Sidebar } from './Sidebar'
import { useAppStore } from '@/stores/useAppStore'
import { clsx } from 'clsx'
interface LayoutProps {
children: ReactNode
}
export function Layout({ children }: LayoutProps) {
const sidebarOpen = useAppStore((s) => s.sidebarOpen)
const darkMode = useAppStore((s) => s.darkMode)
return (
<div className={clsx('min-h-screen', darkMode && 'dark')}>
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
{/* 侧边栏 */}
<Sidebar />
{/* 主内容区 */}
<div className="flex flex-1 flex-col overflow-hidden">
{/* 顶部导航 */}
<Header />
{/* 页面内容 */}
<main
className={clsx(
'flex-1 overflow-y-auto p-6 transition-all duration-300',
sidebarOpen ? 'lg:ml-64' : 'lg:ml-20'
)}
>
<div className="mx-auto max-w-7xl">
{children}
</div>
</main>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,111 @@
import { Link, useLocation } from 'react-router-dom'
import { clsx } from 'clsx'
import { useAppStore } from '@/stores/useAppStore'
import {
Home,
BookOpen,
Layers,
LayoutGrid,
Share2,
Settings,
ChevronLeft,
ChevronRight,
} from 'lucide-react'
const navItems = [
{ path: '/', label: '首页', icon: Home },
{ path: '/knowledge-areas', label: '知识领域', icon: BookOpen },
{ path: '/process-groups', label: '过程组', icon: Layers },
{ path: '/process-matrix', label: '49过程矩阵', icon: LayoutGrid },
{ path: '/process-graph', label: '过程关系图', icon: Share2 },
{ path: '/settings', label: '设置', icon: Settings },
]
export function Sidebar() {
const location = useLocation()
const sidebarOpen = useAppStore((s) => s.sidebarOpen)
const toggleSidebar = useAppStore((s) => s.toggleSidebar)
return (
<>
{/* 移动端遮罩 */}
{sidebarOpen && (
<div
className="fixed inset-0 z-20 bg-black/50 lg:hidden"
onClick={toggleSidebar}
/>
)}
{/* 侧边栏 */}
<aside
className={clsx(
'fixed left-0 top-0 z-30 h-full bg-white dark:bg-gray-800 shadow-lg transition-all duration-300',
sidebarOpen ? 'w-64' : 'w-20',
'lg:translate-x-0',
sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
)}
>
{/* Logo */}
<div className="flex h-16 items-center justify-between px-4 border-b border-gray-200 dark:border-gray-700">
<Link to="/" className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 text-white font-bold text-lg">
IT
</div>
{sidebarOpen && (
<span className="text-lg font-semibold text-gray-900 dark:text-white">
ITTOView
</span>
)}
</Link>
<button
onClick={toggleSidebar}
className="hidden lg:flex h-8 w-8 items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500"
aria-label="切换侧边栏"
>
{sidebarOpen ? <ChevronLeft size={18} /> : <ChevronRight size={18} />}
</button>
</div>
{/* 导航菜单 */}
<nav className="mt-4 px-3">
<ul className="space-y-1">
{navItems.map((item) => {
const isActive = location.pathname === item.path ||
(item.path !== '/' && location.pathname.startsWith(item.path))
const Icon = item.icon
return (
<li key={item.path}>
<Link
to={item.path}
className={clsx(
'flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors',
isActive
? 'bg-indigo-50 text-indigo-600 dark:bg-indigo-900/50 dark:text-indigo-400'
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'
)}
>
<Icon size={20} />
{sidebarOpen && <span>{item.label}</span>}
</Link>
</li>
)
})}
</ul>
</nav>
{/* 底部信息 */}
{sidebarOpen && (
<div className="absolute bottom-4 left-4 right-4">
<div className="rounded-lg bg-gray-50 dark:bg-gray-700/50 p-4">
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">PMBOK 6</div>
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">
49 · 10
</div>
</div>
</div>
)}
</aside>
</>
)
}

View File

@@ -0,0 +1,3 @@
export { Layout } from './Layout'
export { Header } from './Header'
export { Sidebar } from './Sidebar'

View File

@@ -0,0 +1,148 @@
/**
* 49过程矩阵图组件
* 展示知识领域(行) × 过程组(列) 的矩阵视图
*/
import { Link } from 'react-router-dom'
import { motion } from 'framer-motion'
import {
knowledgeAreas,
processGroups,
processes,
} from '@/data'
export function ProcessMatrix() {
// 构建矩阵数据knowledgeAreaId -> processGroupId -> Process[]
const matrix = new Map<string, Map<string, typeof processes>>()
knowledgeAreas.forEach(ka => {
const row = new Map<string, typeof processes>()
processGroups.forEach(pg => {
row.set(pg.id, [])
})
matrix.set(ka.id, row)
})
processes.forEach(p => {
const row = matrix.get(p.knowledgeAreaId)
if (row) {
const cell = row.get(p.processGroupId)
if (cell) {
cell.push(p)
}
}
})
return (
<div className="overflow-x-auto">
<table className="w-full border-collapse min-w-[900px]">
{/* 表头:过程组 */}
<thead>
<tr>
<th className="sticky left-0 z-10 bg-gray-100 dark:bg-gray-800 p-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-700 min-w-[160px]">
/
</th>
{processGroups.map(pg => (
<th
key={pg.id}
className="p-3 text-center text-sm font-semibold text-white border border-gray-200 dark:border-gray-700 min-w-[140px]"
style={{ backgroundColor: pg.color }}
>
<div>{pg.name}</div>
<div className="text-xs opacity-80 font-normal mt-1">
{pg.processCount}
</div>
</th>
))}
</tr>
</thead>
{/* 表体:知识领域 × 过程 */}
<tbody>
{knowledgeAreas.map((ka, kaIndex) => (
<motion.tr
key={ka.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: kaIndex * 0.05 }}
>
{/* 知识领域名称 */}
<td
className="sticky left-0 z-10 p-3 border border-gray-200 dark:border-gray-700 font-medium"
style={{
backgroundColor: `${ka.color}15`,
borderLeftWidth: 4,
borderLeftColor: ka.color,
}}
>
<Link
to={`/knowledge-areas/${ka.id}`}
className="hover:underline text-gray-900 dark:text-white"
>
<span className="font-bold mr-2" style={{ color: ka.color }}>
{ka.order}
</span>
{ka.name}
</Link>
</td>
{/* 每个过程组的单元格 */}
{processGroups.map(pg => {
const cellProcesses = matrix.get(ka.id)?.get(pg.id) || []
return (
<td
key={pg.id}
className="p-2 border border-gray-200 dark:border-gray-700 align-top bg-white dark:bg-gray-800"
>
<div className="space-y-1">
{cellProcesses.map(p => (
<Link
key={p.id}
to={`/process/${p.id}`}
state={{ from: 'matrix' }}
className="block px-2 py-1.5 rounded text-xs hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<span
className="inline-block px-1.5 py-0.5 rounded text-white font-medium mr-1"
style={{ backgroundColor: ka.color, fontSize: '10px' }}
>
{p.code}
</span>
<span className="text-gray-700 dark:text-gray-300">
{p.name}
</span>
</Link>
))}
{cellProcesses.length === 0 && (
<div className="text-gray-300 dark:text-gray-600 text-xs text-center py-2">
-
</div>
)}
</div>
</td>
)
})}
</motion.tr>
))}
</tbody>
{/* 表尾:统计 */}
<tfoot>
<tr className="bg-gray-50 dark:bg-gray-800/50">
<td className="sticky left-0 z-10 p-3 border border-gray-200 dark:border-gray-700 font-semibold text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800/50">
{processes.length}
</td>
{processGroups.map(pg => (
<td
key={pg.id}
className="p-3 border border-gray-200 dark:border-gray-700 text-center font-semibold"
style={{ color: pg.color }}
>
{pg.processCount}
</td>
))}
</tr>
</tfoot>
</table>
</div>
)
}

View File

@@ -0,0 +1 @@
export { ProcessMatrix } from './ProcessMatrix'

95
src/data/artifacts.json Normal file
View File

@@ -0,0 +1,95 @@
{
"artifacts": [
{ "id": "A001", "name": "项目章程", "nameEn": "Project Charter", "category": "document" },
{ "id": "A002", "name": "商业论证", "nameEn": "Business Case", "category": "document" },
{ "id": "A003", "name": "效益管理计划", "nameEn": "Benefits Management Plan", "category": "plan" },
{ "id": "A004", "name": "协议", "nameEn": "Agreements", "category": "document" },
{ "id": "A005", "name": "事业环境因素", "nameEn": "Enterprise Environmental Factors", "category": "other" },
{ "id": "A006", "name": "组织过程资产", "nameEn": "Organizational Process Assets", "category": "other" },
{ "id": "A007", "name": "假设日志", "nameEn": "Assumption Log", "category": "log" },
{ "id": "A008", "name": "项目管理计划", "nameEn": "Project Management Plan", "category": "plan" },
{ "id": "A009", "name": "范围管理计划", "nameEn": "Scope Management Plan", "category": "plan" },
{ "id": "A010", "name": "需求管理计划", "nameEn": "Requirements Management Plan", "category": "plan" },
{ "id": "A011", "name": "进度管理计划", "nameEn": "Schedule Management Plan", "category": "plan" },
{ "id": "A012", "name": "成本管理计划", "nameEn": "Cost Management Plan", "category": "plan" },
{ "id": "A013", "name": "质量管理计划", "nameEn": "Quality Management Plan", "category": "plan" },
{ "id": "A014", "name": "资源管理计划", "nameEn": "Resource Management Plan", "category": "plan" },
{ "id": "A015", "name": "沟通管理计划", "nameEn": "Communications Management Plan", "category": "plan" },
{ "id": "A016", "name": "风险管理计划", "nameEn": "Risk Management Plan", "category": "plan" },
{ "id": "A017", "name": "采购管理计划", "nameEn": "Procurement Management Plan", "category": "plan" },
{ "id": "A018", "name": "相关方参与计划", "nameEn": "Stakeholder Engagement Plan", "category": "plan" },
{ "id": "A019", "name": "变更管理计划", "nameEn": "Change Management Plan", "category": "plan" },
{ "id": "A020", "name": "配置管理计划", "nameEn": "Configuration Management Plan", "category": "plan" },
{ "id": "A021", "name": "范围基准", "nameEn": "Scope Baseline", "category": "baseline" },
{ "id": "A022", "name": "进度基准", "nameEn": "Schedule Baseline", "category": "baseline" },
{ "id": "A023", "name": "成本基准", "nameEn": "Cost Baseline", "category": "baseline" },
{ "id": "A024", "name": "绩效测量基准", "nameEn": "Performance Measurement Baseline", "category": "baseline" },
{ "id": "A025", "name": "项目生命周期描述", "nameEn": "Project Life Cycle Description", "category": "document" },
{ "id": "A026", "name": "开发方法", "nameEn": "Development Approach", "category": "document" },
{ "id": "A027", "name": "项目范围说明书", "nameEn": "Project Scope Statement", "category": "document" },
{ "id": "A028", "name": "需求文件", "nameEn": "Requirements Documentation", "category": "document" },
{ "id": "A029", "name": "需求跟踪矩阵", "nameEn": "Requirements Traceability Matrix", "category": "document" },
{ "id": "A030", "name": "工作分解结构(WBS)", "nameEn": "Work Breakdown Structure", "category": "document" },
{ "id": "A031", "name": "WBS词典", "nameEn": "WBS Dictionary", "category": "document" },
{ "id": "A032", "name": "活动清单", "nameEn": "Activity List", "category": "document" },
{ "id": "A033", "name": "活动属性", "nameEn": "Activity Attributes", "category": "document" },
{ "id": "A034", "name": "里程碑清单", "nameEn": "Milestone List", "category": "document" },
{ "id": "A035", "name": "项目进度网络图", "nameEn": "Project Schedule Network Diagram", "category": "document" },
{ "id": "A036", "name": "活动资源需求", "nameEn": "Activity Resource Requirements", "category": "document" },
{ "id": "A037", "name": "资源分解结构", "nameEn": "Resource Breakdown Structure", "category": "document" },
{ "id": "A038", "name": "持续时间估算", "nameEn": "Duration Estimates", "category": "document" },
{ "id": "A039", "name": "估算依据", "nameEn": "Basis of Estimates", "category": "document" },
{ "id": "A040", "name": "项目进度计划", "nameEn": "Project Schedule", "category": "document" },
{ "id": "A041", "name": "进度数据", "nameEn": "Schedule Data", "category": "document" },
{ "id": "A042", "name": "项目日历", "nameEn": "Project Calendars", "category": "document" },
{ "id": "A043", "name": "成本估算", "nameEn": "Cost Estimates", "category": "document" },
{ "id": "A044", "name": "项目资金需求", "nameEn": "Project Funding Requirements", "category": "document" },
{ "id": "A045", "name": "质量测量指标", "nameEn": "Quality Metrics", "category": "document" },
{ "id": "A046", "name": "质量报告", "nameEn": "Quality Reports", "category": "report" },
{ "id": "A047", "name": "测试与评估文件", "nameEn": "Test and Evaluation Documents", "category": "document" },
{ "id": "A048", "name": "团队章程", "nameEn": "Team Charter", "category": "document" },
{ "id": "A049", "name": "资源日历", "nameEn": "Resource Calendars", "category": "document" },
{ "id": "A050", "name": "项目团队派工单", "nameEn": "Project Team Assignments", "category": "document" },
{ "id": "A051", "name": "物质资源分配单", "nameEn": "Physical Resource Assignments", "category": "document" },
{ "id": "A052", "name": "团队绩效评价", "nameEn": "Team Performance Assessments", "category": "document" },
{ "id": "A053", "name": "变更请求", "nameEn": "Change Requests", "category": "document" },
{ "id": "A054", "name": "项目沟通记录", "nameEn": "Project Communications", "category": "document" },
{ "id": "A055", "name": "风险登记册", "nameEn": "Risk Register", "category": "register" },
{ "id": "A056", "name": "风险报告", "nameEn": "Risk Report", "category": "report" },
{ "id": "A057", "name": "采购文件", "nameEn": "Procurement Documentation", "category": "document" },
{ "id": "A058", "name": "采购工作说明书", "nameEn": "Procurement Statement of Work", "category": "document" },
{ "id": "A059", "name": "招标文件", "nameEn": "Bid Documents", "category": "document" },
{ "id": "A060", "name": "供方选择标准", "nameEn": "Source Selection Criteria", "category": "document" },
{ "id": "A061", "name": "自制或外购决策", "nameEn": "Make-or-Buy Decisions", "category": "document" },
{ "id": "A062", "name": "独立成本估算", "nameEn": "Independent Cost Estimates", "category": "document" },
{ "id": "A063", "name": "选定的卖方", "nameEn": "Selected Sellers", "category": "document" },
{ "id": "A064", "name": "相关方登记册", "nameEn": "Stakeholder Register", "category": "register" },
{ "id": "A065", "name": "问题日志", "nameEn": "Issue Log", "category": "log" },
{ "id": "A066", "name": "经验教训登记册", "nameEn": "Lessons Learned Register", "category": "register" },
{ "id": "A067", "name": "工作绩效数据", "nameEn": "Work Performance Data", "category": "document" },
{ "id": "A068", "name": "工作绩效信息", "nameEn": "Work Performance Information", "category": "document" },
{ "id": "A069", "name": "工作绩效报告", "nameEn": "Work Performance Reports", "category": "report" },
{ "id": "A070", "name": "可交付成果", "nameEn": "Deliverables", "category": "deliverable" },
{ "id": "A071", "name": "核实的可交付成果", "nameEn": "Verified Deliverables", "category": "deliverable" },
{ "id": "A072", "name": "验收的可交付成果", "nameEn": "Accepted Deliverables", "category": "deliverable" },
{ "id": "A073", "name": "最终产品、服务或成果移交", "nameEn": "Final Product, Service, or Result Transition", "category": "deliverable" },
{ "id": "A074", "name": "最终报告", "nameEn": "Final Report", "category": "report" },
{ "id": "A075", "name": "组织过程资产更新", "nameEn": "Organizational Process Assets Updates", "category": "other" },
{ "id": "A076", "name": "项目文件", "nameEn": "Project Documents", "category": "document" },
{ "id": "A077", "name": "项目文件更新", "nameEn": "Project Documents Updates", "category": "document" },
{ "id": "A078", "name": "项目管理计划更新", "nameEn": "Project Management Plan Updates", "category": "plan" },
{ "id": "A079", "name": "批准的变更请求", "nameEn": "Approved Change Requests", "category": "document" },
{ "id": "A080", "name": "变更日志", "nameEn": "Change Log", "category": "log" },
{ "id": "A081", "name": "卖方建议书", "nameEn": "Seller Proposals", "category": "document" },
{ "id": "A082", "name": "资源需求", "nameEn": "Resource Requirements", "category": "document" },
{ "id": "A083", "name": "活动成本估算", "nameEn": "Activity Cost Estimates", "category": "document" },
{ "id": "A084", "name": "质量控制测量结果", "nameEn": "Quality Control Measurements", "category": "document" },
{ "id": "A085", "name": "预测", "nameEn": "Forecasts", "category": "document" },
{ "id": "A086", "name": "成本预测", "nameEn": "Cost Forecasts", "category": "document" },
{ "id": "A087", "name": "进度预测", "nameEn": "Schedule Forecasts", "category": "document" },
{ "id": "A088", "name": "关闭的采购", "nameEn": "Closed Procurements", "category": "document" },
{ "id": "A089", "name": "卖方绩效评价文档", "nameEn": "Seller Performance Evaluation Documentation", "category": "document" },
{ "id": "A090", "name": "采购策略", "nameEn": "Procurement Strategy", "category": "document" },
{ "id": "A091", "name": "采购文档更新", "nameEn": "Procurement Documentation Updates", "category": "document" }
]
}

126
src/data/index.ts Normal file
View File

@@ -0,0 +1,126 @@
/**
* ITTO数据索引
* 统一导出所有ITTO核心数据
*/
import knowledgeAreasData from './knowledge-areas.json';
import processGroupsData from './process-groups.json';
import processesData from './processes.json';
import artifactsData from './artifacts.json';
import toolsData from './tools.json';
import type {
KnowledgeArea,
ProcessGroup,
Process,
Artifact,
ToolTechnique,
DataFlow,
} from '../types/itto';
// 导出原始数据
export const knowledgeAreas: KnowledgeArea[] = knowledgeAreasData.knowledgeAreas as KnowledgeArea[];
export const processGroups: ProcessGroup[] = processGroupsData.processGroups as ProcessGroup[];
export const processes: Process[] = processesData.processes as Process[];
export const artifacts: Artifact[] = artifactsData.artifacts as Artifact[];
export const tools: ToolTechnique[] = toolsData.tools as ToolTechnique[];
// 创建查找映射表
export const knowledgeAreaMap = new Map<string, KnowledgeArea>(
knowledgeAreas.map(ka => [ka.id, ka])
);
export const processGroupMap = new Map<string, ProcessGroup>(
processGroups.map(pg => [pg.id, pg])
);
export const processMap = new Map<string, Process>(
processes.map(p => [p.id, p])
);
export const artifactMap = new Map<string, Artifact>(
artifacts.map(a => [a.id, a])
);
export const toolMap = new Map<string, ToolTechnique>(
tools.map(t => [t.id, t])
);
// 按知识领域分组的过程
export const processesByKnowledgeArea = new Map<string, Process[]>();
knowledgeAreas.forEach(ka => {
processesByKnowledgeArea.set(
ka.id,
processes.filter(p => p.knowledgeAreaId === ka.id).sort((a, b) => a.order - b.order)
);
});
// 按过程组分组的过程
export const processesByProcessGroup = new Map<string, Process[]>();
processGroups.forEach(pg => {
processesByProcessGroup.set(
pg.id,
processes.filter(p => p.processGroupId === pg.id)
);
});
// 计算数据流向关系
export function computeDataFlows(): DataFlow[] {
const flows: DataFlow[] = [];
processes.forEach(sourceProcess => {
sourceProcess.outputs.forEach(outputId => {
processes.forEach(targetProcess => {
if (targetProcess.id !== sourceProcess.id && targetProcess.inputs.includes(outputId)) {
flows.push({
sourceProcessId: sourceProcess.id,
targetProcessId: targetProcess.id,
artifactId: outputId,
});
}
});
});
});
return flows;
}
// 获取工件的使用情况
export function getArtifactUsage(artifactId: string): {
asInput: Process[];
asOutput: Process[];
} {
return {
asInput: processes.filter(p => p.inputs.includes(artifactId)),
asOutput: processes.filter(p => p.outputs.includes(artifactId)),
};
}
// 获取工具的使用情况
export function getToolUsage(toolId: string): Process[] {
return processes.filter(p => p.tools.includes(toolId));
}
// 获取过程的完整信息(包含关联数据)
export function getProcessDetail(processId: string) {
const process = processMap.get(processId);
if (!process) return null;
return {
...process,
knowledgeArea: knowledgeAreaMap.get(process.knowledgeAreaId),
processGroup: processGroupMap.get(process.processGroupId),
inputDetails: process.inputs.map(id => artifactMap.get(id)).filter(Boolean),
toolDetails: process.tools.map(id => toolMap.get(id)).filter(Boolean),
outputDetails: process.outputs.map(id => artifactMap.get(id)).filter(Boolean),
};
}
// 统计信息
export const stats = {
knowledgeAreaCount: knowledgeAreas.length,
processGroupCount: processGroups.length,
processCount: processes.length,
artifactCount: artifacts.length,
toolCount: tools.length,
};

View File

@@ -0,0 +1,104 @@
{
"knowledgeAreas": [
{
"id": "KA01",
"name": "项目整合管理",
"nameEn": "Project Integration Management",
"chapter": 4,
"order": 1,
"color": "#6366F1",
"description": "包括识别、定义、组合、统一和协调各项目管理过程组的各种过程和活动",
"processCount": 7
},
{
"id": "KA02",
"name": "项目范围管理",
"nameEn": "Project Scope Management",
"chapter": 5,
"order": 2,
"color": "#8B5CF6",
"description": "确保项目包含且只包含成功完成项目所需的全部工作",
"processCount": 6
},
{
"id": "KA03",
"name": "项目进度管理",
"nameEn": "Project Schedule Management",
"chapter": 6,
"order": 3,
"color": "#EC4899",
"description": "管理项目按时完成所需的各个过程",
"processCount": 6
},
{
"id": "KA04",
"name": "项目成本管理",
"nameEn": "Project Cost Management",
"chapter": 7,
"order": 4,
"color": "#10B981",
"description": "规划、估算、预算、融资、筹资、管理和控制成本的各过程",
"processCount": 4
},
{
"id": "KA05",
"name": "项目质量管理",
"nameEn": "Project Quality Management",
"chapter": 8,
"order": 5,
"color": "#F59E0B",
"description": "把组织的质量政策应用于规划、管理、控制项目和产品质量要求",
"processCount": 3
},
{
"id": "KA06",
"name": "项目资源管理",
"nameEn": "Project Resource Management",
"chapter": 9,
"order": 6,
"color": "#3B82F6",
"description": "识别、获取和管理所需资源以成功完成项目",
"processCount": 6
},
{
"id": "KA07",
"name": "项目沟通管理",
"nameEn": "Project Communications Management",
"chapter": 10,
"order": 7,
"color": "#06B6D4",
"description": "确保项目信息及时且恰当地规划、收集、生成、发布、存储、检索、管理、控制、监督和最终处置",
"processCount": 3
},
{
"id": "KA08",
"name": "项目风险管理",
"nameEn": "Project Risk Management",
"chapter": 11,
"order": 8,
"color": "#EF4444",
"description": "规划风险管理、识别风险、开展风险分析、规划风险应对、实施风险应对和监督风险的各个过程",
"processCount": 7
},
{
"id": "KA09",
"name": "项目采购管理",
"nameEn": "Project Procurement Management",
"chapter": 12,
"order": 9,
"color": "#84CC16",
"description": "从项目团队外部采购或获取所需产品、服务或成果",
"processCount": 3
},
{
"id": "KA10",
"name": "项目相关方管理",
"nameEn": "Project Stakeholder Management",
"chapter": 13,
"order": 10,
"color": "#F97316",
"description": "识别影响或受项目影响的人员、团体或组织,分析其期望和影响,制定管理策略",
"processCount": 4
}
]
}

View File

@@ -0,0 +1,49 @@
{
"processGroups": [
{
"id": "PG01",
"name": "启动过程组",
"nameEn": "Initiating Process Group",
"order": 1,
"color": "#22C55E",
"description": "定义一个新项目或现有项目的一个新阶段,授权开始该项目或阶段",
"processCount": 2
},
{
"id": "PG02",
"name": "规划过程组",
"nameEn": "Planning Process Group",
"order": 2,
"color": "#3B82F6",
"description": "明确项目范围,优化目标,为实现目标制定行动方案",
"processCount": 24
},
{
"id": "PG03",
"name": "执行过程组",
"nameEn": "Executing Process Group",
"order": 3,
"color": "#F59E0B",
"description": "完成项目管理计划中确定的工作,以满足项目要求",
"processCount": 10
},
{
"id": "PG04",
"name": "监控过程组",
"nameEn": "Monitoring and Controlling Process Group",
"order": 4,
"color": "#8B5CF6",
"description": "跟踪、审查和调整项目进展与绩效,识别必要的计划变更并启动相应变更",
"processCount": 12
},
{
"id": "PG05",
"name": "收尾过程组",
"nameEn": "Closing Process Group",
"order": 5,
"color": "#6B7280",
"description": "正式完成或关闭项目、阶段或合同",
"processCount": 1
}
]
}

593
src/data/processes.json Normal file
View File

@@ -0,0 +1,593 @@
{
"processes": [
{
"id": "P1.1",
"code": "1.1",
"name": "制定项目章程",
"nameEn": "Develop Project Charter",
"knowledgeAreaId": "KA01",
"processGroupId": "PG01",
"order": 1,
"inputs": ["A002", "A004", "A003", "A005", "A006"],
"tools": ["TT001", "TT002", "TT022", "TT032"],
"outputs": ["A001", "A007"]
},
{
"id": "P1.2",
"code": "1.2",
"name": "制定项目管理计划",
"nameEn": "Develop Project Management Plan",
"knowledgeAreaId": "KA01",
"processGroupId": "PG02",
"order": 2,
"inputs": ["A001", "A076", "A005", "A006"],
"tools": ["TT001", "TT002", "TT022", "TT032"],
"outputs": ["A008"]
},
{
"id": "P1.3",
"code": "1.3",
"name": "指导与管理项目工作",
"nameEn": "Direct and Manage Project Work",
"knowledgeAreaId": "KA01",
"processGroupId": "PG03",
"order": 3,
"inputs": ["A008", "A076", "A079", "A005", "A006"],
"tools": ["TT001", "TT033", "TT032"],
"outputs": ["A070", "A067", "A065", "A053", "A078", "A077", "A075"]
},
{
"id": "P1.4",
"code": "1.4",
"name": "管理项目知识",
"nameEn": "Manage Project Knowledge",
"knowledgeAreaId": "KA01",
"processGroupId": "PG03",
"order": 4,
"inputs": ["A008", "A076", "A070", "A005", "A006"],
"tools": ["TT001", "TT110", "TT111", "TT022"],
"outputs": ["A066", "A078", "A075"]
},
{
"id": "P1.5",
"code": "1.5",
"name": "监控项目工作",
"nameEn": "Monitor and Control Project Work",
"knowledgeAreaId": "KA01",
"processGroupId": "PG04",
"order": 5,
"inputs": ["A008", "A076", "A068", "A004", "A005", "A006"],
"tools": ["TT001", "TT008", "TT018", "TT032"],
"outputs": ["A069", "A053", "A078", "A077"]
},
{
"id": "P1.6",
"code": "1.6",
"name": "实施整体变更控制",
"nameEn": "Perform Integrated Change Control",
"knowledgeAreaId": "KA01",
"processGroupId": "PG04",
"order": 6,
"inputs": ["A008", "A076", "A069", "A053", "A005", "A006"],
"tools": ["TT001", "TT108", "TT008", "TT018", "TT032"],
"outputs": ["A079", "A080", "A078", "A077"]
},
{
"id": "P1.7",
"code": "1.7",
"name": "结束项目或阶段",
"nameEn": "Close Project or Phase",
"knowledgeAreaId": "KA01",
"processGroupId": "PG05",
"order": 7,
"inputs": ["A001", "A008", "A076", "A072", "A005", "A006"],
"tools": ["TT001", "TT008", "TT032"],
"outputs": ["A078", "A073", "A074", "A075"]
},
{
"id": "P2.1",
"code": "2.1",
"name": "规划范围管理",
"nameEn": "Plan Scope Management",
"knowledgeAreaId": "KA02",
"processGroupId": "PG02",
"order": 1,
"inputs": ["A001", "A008", "A005", "A006"],
"tools": ["TT001", "TT008", "TT032"],
"outputs": ["A009", "A010"]
},
{
"id": "P2.2",
"code": "2.2",
"name": "收集需求",
"nameEn": "Collect Requirements",
"knowledgeAreaId": "KA02",
"processGroupId": "PG02",
"order": 2,
"inputs": ["A001", "A008", "A076", "A004", "A005", "A006"],
"tools": ["TT001", "TT002", "TT008", "TT018", "TT034", "TT022", "TT115"],
"outputs": ["A028", "A029"]
},
{
"id": "P2.3",
"code": "2.3",
"name": "定义范围",
"nameEn": "Define Scope",
"knowledgeAreaId": "KA02",
"processGroupId": "PG02",
"order": 3,
"inputs": ["A001", "A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT008", "TT018", "TT022", "TT114"],
"outputs": ["A027", "A077"]
},
{
"id": "P2.4",
"code": "2.4",
"name": "创建WBS",
"nameEn": "Create WBS",
"knowledgeAreaId": "KA02",
"processGroupId": "PG02",
"order": 4,
"inputs": ["A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT059"],
"outputs": ["A021", "A077"]
},
{
"id": "P2.5",
"code": "2.5",
"name": "确认范围",
"nameEn": "Validate Scope",
"knowledgeAreaId": "KA02",
"processGroupId": "PG04",
"order": 5,
"inputs": ["A008", "A076", "A071", "A067", "A005", "A006"],
"tools": ["TT065", "TT018"],
"outputs": ["A072", "A068", "A053", "A077"]
},
{
"id": "P2.6",
"code": "2.6",
"name": "控制范围",
"nameEn": "Control Scope",
"knowledgeAreaId": "KA02",
"processGroupId": "PG04",
"order": 6,
"inputs": ["A008", "A076", "A067", "A005", "A006"],
"tools": ["TT008"],
"outputs": ["A068", "A053", "A078", "A077"]
},
{
"id": "P3.1",
"code": "3.1",
"name": "规划进度管理",
"nameEn": "Plan Schedule Management",
"knowledgeAreaId": "KA03",
"processGroupId": "PG02",
"order": 1,
"inputs": ["A001", "A008", "A005", "A006"],
"tools": ["TT001", "TT008", "TT032"],
"outputs": ["A011"]
},
{
"id": "P3.2",
"code": "3.2",
"name": "定义活动",
"nameEn": "Define Activities",
"knowledgeAreaId": "KA03",
"processGroupId": "PG02",
"order": 2,
"inputs": ["A008", "A005", "A006"],
"tools": ["TT001", "TT059", "TT060", "TT032"],
"outputs": ["A032", "A033", "A034", "A053", "A078"]
},
{
"id": "P3.3",
"code": "3.3",
"name": "排列活动顺序",
"nameEn": "Sequence Activities",
"knowledgeAreaId": "KA03",
"processGroupId": "PG02",
"order": 3,
"inputs": ["A008", "A076", "A005", "A006"],
"tools": ["TT061", "TT062", "TT050", "TT033"],
"outputs": ["A035", "A077"]
},
{
"id": "P3.4",
"code": "3.4",
"name": "估算活动持续时间",
"nameEn": "Estimate Activity Durations",
"knowledgeAreaId": "KA03",
"processGroupId": "PG02",
"order": 4,
"inputs": ["A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT055", "TT056", "TT057", "TT058", "TT018", "TT032"],
"outputs": ["A038", "A039", "A077"]
},
{
"id": "P3.5",
"code": "3.5",
"name": "制定进度计划",
"nameEn": "Develop Schedule",
"knowledgeAreaId": "KA03",
"processGroupId": "PG02",
"order": 5,
"inputs": ["A008", "A076", "A004", "A005", "A006"],
"tools": ["TT045", "TT046", "TT047", "TT008", "TT050", "TT051", "TT033", "TT063"],
"outputs": ["A022", "A040", "A041", "A042", "A053", "A078", "A077"]
},
{
"id": "P3.6",
"code": "3.6",
"name": "控制进度",
"nameEn": "Control Schedule",
"knowledgeAreaId": "KA03",
"processGroupId": "PG04",
"order": 6,
"inputs": ["A008", "A076", "A067", "A005", "A006"],
"tools": ["TT008", "TT046", "TT047", "TT050", "TT051", "TT033"],
"outputs": ["A068", "A087", "A053", "A078", "A077"]
},
{
"id": "P4.1",
"code": "4.1",
"name": "规划成本管理",
"nameEn": "Plan Cost Management",
"knowledgeAreaId": "KA04",
"processGroupId": "PG02",
"order": 1,
"inputs": ["A001", "A008", "A005", "A006"],
"tools": ["TT001", "TT008", "TT032"],
"outputs": ["A012"]
},
{
"id": "P4.2",
"code": "4.2",
"name": "估算成本",
"nameEn": "Estimate Costs",
"knowledgeAreaId": "KA04",
"processGroupId": "PG02",
"order": 2,
"inputs": ["A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT055", "TT056", "TT057", "TT058", "TT008", "TT018", "TT033"],
"outputs": ["A043", "A039", "A077"]
},
{
"id": "P4.3",
"code": "4.3",
"name": "制定预算",
"nameEn": "Determine Budget",
"knowledgeAreaId": "KA04",
"processGroupId": "PG02",
"order": 3,
"inputs": ["A008", "A076", "A004", "A005", "A006"],
"tools": ["TT001", "TT010", "TT008", "TT016", "TT044"],
"outputs": ["A023", "A044", "A077"]
},
{
"id": "P4.4",
"code": "4.4",
"name": "控制成本",
"nameEn": "Control Costs",
"knowledgeAreaId": "KA04",
"processGroupId": "PG04",
"order": 4,
"inputs": ["A008", "A076", "A044", "A067", "A005", "A006"],
"tools": ["TT001", "TT008", "TT011", "TT013", "TT014", "TT016", "TT033"],
"outputs": ["A068", "A086", "A053", "A078", "A077"]
}
,
{
"id": "P5.1",
"code": "5.1",
"name": "规划质量管理",
"nameEn": "Plan Quality Management",
"knowledgeAreaId": "KA05",
"processGroupId": "PG02",
"order": 1,
"inputs": ["A001", "A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT002", "TT008", "TT018", "TT034", "TT066", "TT064", "TT032"],
"outputs": ["A013", "A045", "A078", "A077"]
},
{
"id": "P5.2",
"code": "5.2",
"name": "管理质量",
"nameEn": "Manage Quality",
"knowledgeAreaId": "KA05",
"processGroupId": "PG03",
"order": 2,
"inputs": ["A008", "A076", "A005", "A006"],
"tools": ["TT002", "TT008", "TT018", "TT034", "TT067", "TT068", "TT069", "TT070"],
"outputs": ["A046", "A047", "A053", "A078", "A077"]
},
{
"id": "P5.3",
"code": "5.3",
"name": "控制质量",
"nameEn": "Control Quality",
"knowledgeAreaId": "KA05",
"processGroupId": "PG04",
"order": 3,
"inputs": ["A008", "A076", "A079", "A070", "A067", "A005", "A006"],
"tools": ["TT002", "TT008", "TT065", "TT066", "TT034"],
"outputs": ["A084", "A071", "A068", "A053", "A078", "A077"]
},
{
"id": "P6.1",
"code": "6.1",
"name": "规划资源管理",
"nameEn": "Plan Resource Management",
"knowledgeAreaId": "KA06",
"processGroupId": "PG02",
"order": 1,
"inputs": ["A001", "A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT034", "TT112", "TT032"],
"outputs": ["A014", "A048", "A077"]
},
{
"id": "P6.2",
"code": "6.2",
"name": "估算活动资源",
"nameEn": "Estimate Activity Resources",
"knowledgeAreaId": "KA06",
"processGroupId": "PG02",
"order": 2,
"inputs": ["A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT057", "TT055", "TT056", "TT008", "TT033", "TT032"],
"outputs": ["A082", "A039", "A037", "A077"]
},
{
"id": "P6.3",
"code": "6.3",
"name": "获取资源",
"nameEn": "Acquire Resources",
"knowledgeAreaId": "KA06",
"processGroupId": "PG03",
"order": 3,
"inputs": ["A008", "A076", "A005", "A006"],
"tools": ["TT018", "TT022", "TT107", "TT102"],
"outputs": ["A051", "A050", "A049", "A053", "A078", "A077", "A005"]
},
{
"id": "P6.4",
"code": "6.4",
"name": "建设团队",
"nameEn": "Develop Team",
"knowledgeAreaId": "KA06",
"processGroupId": "PG03",
"order": 4,
"inputs": ["A008", "A076", "A005", "A006"],
"tools": ["TT101", "TT102", "TT086", "TT022", "TT103", "TT104", "TT105", "TT032"],
"outputs": ["A052", "A053", "A078", "A077", "A005"]
},
{
"id": "P6.5",
"code": "6.5",
"name": "管理团队",
"nameEn": "Manage Team",
"knowledgeAreaId": "KA06",
"processGroupId": "PG03",
"order": 5,
"inputs": ["A008", "A076", "A052", "A067", "A005", "A006"],
"tools": ["TT022", "TT033"],
"outputs": ["A053", "A078", "A077", "A005"]
},
{
"id": "P6.6",
"code": "6.6",
"name": "控制资源",
"nameEn": "Control Resources",
"knowledgeAreaId": "KA06",
"processGroupId": "PG04",
"order": 6,
"inputs": ["A008", "A076", "A067", "A004", "A005", "A006"],
"tools": ["TT008", "TT069", "TT022", "TT033"],
"outputs": ["A068", "A053", "A078", "A077"]
},
{
"id": "P7.1",
"code": "7.1",
"name": "规划沟通管理",
"nameEn": "Plan Communications Management",
"knowledgeAreaId": "KA07",
"processGroupId": "PG02",
"order": 1,
"inputs": ["A001", "A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT085", "TT086", "TT087", "TT008", "TT022", "TT034", "TT032"],
"outputs": ["A015", "A078", "A077"]
},
{
"id": "P7.2",
"code": "7.2",
"name": "管理沟通",
"nameEn": "Manage Communications",
"knowledgeAreaId": "KA07",
"processGroupId": "PG03",
"order": 2,
"inputs": ["A008", "A076", "A069", "A005", "A006"],
"tools": ["TT086", "TT085", "TT088", "TT033", "TT090", "TT032"],
"outputs": ["A054", "A078", "A077", "A075"]
},
{
"id": "P7.3",
"code": "7.3",
"name": "监督沟通",
"nameEn": "Monitor Communications",
"knowledgeAreaId": "KA07",
"processGroupId": "PG04",
"order": 3,
"inputs": ["A008", "A076", "A067", "A005", "A006"],
"tools": ["TT001", "TT033", "TT022", "TT032"],
"outputs": ["A068", "A053", "A078", "A077"]
},
{
"id": "P8.1",
"code": "8.1",
"name": "规划风险管理",
"nameEn": "Plan Risk Management",
"knowledgeAreaId": "KA08",
"processGroupId": "PG02",
"order": 1,
"inputs": ["A001", "A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT008", "TT032"],
"outputs": ["A016"]
},
{
"id": "P8.2",
"code": "8.2",
"name": "识别风险",
"nameEn": "Identify Risks",
"knowledgeAreaId": "KA08",
"processGroupId": "PG02",
"order": 2,
"inputs": ["A008", "A076", "A004", "A057", "A005", "A006"],
"tools": ["TT001", "TT002", "TT008", "TT022", "TT120", "TT032"],
"outputs": ["A055", "A056", "A077"]
},
{
"id": "P8.3",
"code": "8.3",
"name": "实施定性风险分析",
"nameEn": "Perform Qualitative Risk Analysis",
"knowledgeAreaId": "KA08",
"processGroupId": "PG02",
"order": 3,
"inputs": ["A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT002", "TT008", "TT022", "TT071", "TT044", "TT072", "TT034", "TT032"],
"outputs": ["A077"]
},
{
"id": "P8.4",
"code": "8.4",
"name": "实施定量风险分析",
"nameEn": "Perform Quantitative Risk Analysis",
"knowledgeAreaId": "KA08",
"processGroupId": "PG02",
"order": 4,
"inputs": ["A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT002", "TT022", "TT034", "TT075", "TT079", "TT080", "TT077"],
"outputs": ["A077"]
},
{
"id": "P8.5",
"code": "8.5",
"name": "规划风险应对",
"nameEn": "Plan Risk Responses",
"knowledgeAreaId": "KA08",
"processGroupId": "PG02",
"order": 5,
"inputs": ["A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT002", "TT022", "TT081", "TT082", "TT083", "TT084", "TT008", "TT018"],
"outputs": ["A053", "A078", "A077"]
},
{
"id": "P8.6",
"code": "8.6",
"name": "实施风险应对",
"nameEn": "Implement Risk Responses",
"knowledgeAreaId": "KA08",
"processGroupId": "PG03",
"order": 6,
"inputs": ["A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT022", "TT033"],
"outputs": ["A053", "A077"]
},
{
"id": "P8.7",
"code": "8.7",
"name": "监督风险",
"nameEn": "Monitor Risks",
"knowledgeAreaId": "KA08",
"processGroupId": "PG04",
"order": 7,
"inputs": ["A008", "A076", "A067", "A069", "A005", "A006"],
"tools": ["TT008", "TT067", "TT032"],
"outputs": ["A068", "A053", "A078", "A077", "A075"]
},
{
"id": "P9.1",
"code": "9.1",
"name": "规划采购管理",
"nameEn": "Plan Procurement Management",
"knowledgeAreaId": "KA09",
"processGroupId": "PG02",
"order": 1,
"inputs": ["A001", "A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT002", "TT092", "TT008", "TT032"],
"outputs": ["A017", "A091", "A059", "A058", "A060", "A061", "A062", "A053", "A077", "A075"]
},
{
"id": "P9.2",
"code": "9.2",
"name": "实施采购",
"nameEn": "Conduct Procurements",
"knowledgeAreaId": "KA09",
"processGroupId": "PG03",
"order": 2,
"inputs": ["A008", "A076", "A057", "A081", "A005", "A006"],
"tools": ["TT001", "TT093", "TT008", "TT022", "TT094"],
"outputs": ["A063", "A004", "A053", "A078", "A077", "A075"]
},
{
"id": "P9.3",
"code": "9.3",
"name": "控制采购",
"nameEn": "Control Procurements",
"knowledgeAreaId": "KA09",
"processGroupId": "PG04",
"order": 3,
"inputs": ["A008", "A076", "A057", "A004", "A079", "A067", "A005", "A006"],
"tools": ["TT001", "TT095", "TT008", "TT065", "TT067"],
"outputs": ["A088", "A068", "A057", "A053", "A078", "A077", "A075"]
},
{
"id": "P10.1",
"code": "10.1",
"name": "识别相关方",
"nameEn": "Identify Stakeholders",
"knowledgeAreaId": "KA10",
"processGroupId": "PG01",
"order": 1,
"inputs": ["A001", "A008", "A076", "A004", "A005", "A006"],
"tools": ["TT001", "TT002", "TT008", "TT034", "TT032"],
"outputs": ["A064", "A053", "A078", "A077"]
},
{
"id": "P10.2",
"code": "10.2",
"name": "规划相关方参与",
"nameEn": "Plan Stakeholder Engagement",
"knowledgeAreaId": "KA10",
"processGroupId": "PG02",
"order": 2,
"inputs": ["A001", "A008", "A076", "A004", "A005", "A006"],
"tools": ["TT001", "TT002", "TT018", "TT034", "TT032"],
"outputs": ["A018"]
},
{
"id": "P10.3",
"code": "10.3",
"name": "管理相关方参与",
"nameEn": "Manage Stakeholder Engagement",
"knowledgeAreaId": "KA10",
"processGroupId": "PG03",
"order": 3,
"inputs": ["A008", "A076", "A005", "A006"],
"tools": ["TT001", "TT088", "TT022", "TT100", "TT032"],
"outputs": ["A053", "A078", "A077"]
},
{
"id": "P10.4",
"code": "10.4",
"name": "监督相关方参与",
"nameEn": "Monitor Stakeholder Engagement",
"knowledgeAreaId": "KA10",
"processGroupId": "PG04",
"order": 4,
"inputs": ["A008", "A076", "A067", "A005", "A006"],
"tools": ["TT008", "TT018", "TT034", "TT088", "TT032"],
"outputs": ["A068", "A053", "A078", "A077"]
}
]
}

View File

124
src/data/tools.json Normal file
View File

@@ -0,0 +1,124 @@
{
"tools": [
{ "id": "TT001", "name": "专家判断", "nameEn": "Expert Judgment", "type": "technique", "category": "general" },
{ "id": "TT002", "name": "数据收集", "nameEn": "Data Gathering", "type": "technique", "category": "general" },
{ "id": "TT003", "name": "头脑风暴", "nameEn": "Brainstorming", "type": "technique", "category": "data-gathering" },
{ "id": "TT004", "name": "焦点小组", "nameEn": "Focus Groups", "type": "technique", "category": "data-gathering" },
{ "id": "TT005", "name": "访谈", "nameEn": "Interviews", "type": "technique", "category": "data-gathering" },
{ "id": "TT006", "name": "问卷调查", "nameEn": "Questionnaires and Surveys", "type": "technique", "category": "data-gathering" },
{ "id": "TT007", "name": "标杆对照", "nameEn": "Benchmarking", "type": "technique", "category": "data-gathering" },
{ "id": "TT008", "name": "数据分析", "nameEn": "Data Analysis", "type": "technique", "category": "general" },
{ "id": "TT009", "name": "备选方案分析", "nameEn": "Alternatives Analysis", "type": "technique", "category": "data-analysis" },
{ "id": "TT010", "name": "成本效益分析", "nameEn": "Cost-Benefit Analysis", "type": "technique", "category": "data-analysis" },
{ "id": "TT011", "name": "挣值分析", "nameEn": "Earned Value Analysis", "type": "technique", "category": "data-analysis" },
{ "id": "TT012", "name": "根本原因分析", "nameEn": "Root Cause Analysis", "type": "technique", "category": "data-analysis" },
{ "id": "TT013", "name": "趋势分析", "nameEn": "Trend Analysis", "type": "technique", "category": "data-analysis" },
{ "id": "TT014", "name": "偏差分析", "nameEn": "Variance Analysis", "type": "technique", "category": "data-analysis" },
{ "id": "TT015", "name": "假设情景分析", "nameEn": "What-If Scenario Analysis", "type": "technique", "category": "data-analysis" },
{ "id": "TT016", "name": "储备分析", "nameEn": "Reserve Analysis", "type": "technique", "category": "data-analysis" },
{ "id": "TT017", "name": "文件分析", "nameEn": "Document Analysis", "type": "technique", "category": "data-analysis" },
{ "id": "TT018", "name": "决策", "nameEn": "Decision Making", "type": "technique", "category": "general" },
{ "id": "TT019", "name": "投票", "nameEn": "Voting", "type": "technique", "category": "decision-making" },
{ "id": "TT020", "name": "多标准决策分析", "nameEn": "Multicriteria Decision Analysis", "type": "technique", "category": "decision-making" },
{ "id": "TT021", "name": "独裁型决策", "nameEn": "Autocratic Decision Making", "type": "technique", "category": "decision-making" },
{ "id": "TT022", "name": "人际关系与团队技能", "nameEn": "Interpersonal and Team Skills", "type": "skill", "category": "general" },
{ "id": "TT023", "name": "冲突管理", "nameEn": "Conflict Management", "type": "skill", "category": "interpersonal" },
{ "id": "TT024", "name": "引导", "nameEn": "Facilitation", "type": "skill", "category": "interpersonal" },
{ "id": "TT025", "name": "会议管理", "nameEn": "Meeting Management", "type": "skill", "category": "interpersonal" },
{ "id": "TT026", "name": "谈判", "nameEn": "Negotiation", "type": "skill", "category": "interpersonal" },
{ "id": "TT027", "name": "影响力", "nameEn": "Influencing", "type": "skill", "category": "interpersonal" },
{ "id": "TT028", "name": "积极倾听", "nameEn": "Active Listening", "type": "skill", "category": "interpersonal" },
{ "id": "TT029", "name": "领导力", "nameEn": "Leadership", "type": "skill", "category": "interpersonal" },
{ "id": "TT030", "name": "激励", "nameEn": "Motivation", "type": "skill", "category": "interpersonal" },
{ "id": "TT031", "name": "团队建设", "nameEn": "Team Building", "type": "skill", "category": "interpersonal" },
{ "id": "TT032", "name": "会议", "nameEn": "Meetings", "type": "meeting", "category": "general" },
{ "id": "TT033", "name": "项目管理信息系统", "nameEn": "Project Management Information System", "type": "tool", "category": "general" },
{ "id": "TT034", "name": "数据表现", "nameEn": "Data Representation", "type": "technique", "category": "general" },
{ "id": "TT035", "name": "亲和图", "nameEn": "Affinity Diagrams", "type": "technique", "category": "data-representation" },
{ "id": "TT036", "name": "因果图", "nameEn": "Cause-and-Effect Diagrams", "type": "technique", "category": "data-representation" },
{ "id": "TT037", "name": "流程图", "nameEn": "Flowcharts", "type": "technique", "category": "data-representation" },
{ "id": "TT038", "name": "直方图", "nameEn": "Histograms", "type": "technique", "category": "data-representation" },
{ "id": "TT039", "name": "矩阵图", "nameEn": "Matrix Diagrams", "type": "technique", "category": "data-representation" },
{ "id": "TT040", "name": "思维导图", "nameEn": "Mind Mapping", "type": "technique", "category": "data-representation" },
{ "id": "TT041", "name": "散点图", "nameEn": "Scatter Diagrams", "type": "technique", "category": "data-representation" },
{ "id": "TT042", "name": "层级图", "nameEn": "Hierarchical Charts", "type": "technique", "category": "data-representation" },
{ "id": "TT043", "name": "责任分配矩阵", "nameEn": "Responsibility Assignment Matrix", "type": "technique", "category": "data-representation" },
{ "id": "TT044", "name": "概率和影响矩阵", "nameEn": "Probability and Impact Matrix", "type": "technique", "category": "data-representation" },
{ "id": "TT045", "name": "进度网络分析", "nameEn": "Schedule Network Analysis", "type": "technique", "category": "scheduling" },
{ "id": "TT046", "name": "关键路径法", "nameEn": "Critical Path Method", "type": "technique", "category": "scheduling" },
{ "id": "TT047", "name": "资源优化", "nameEn": "Resource Optimization", "type": "technique", "category": "scheduling" },
{ "id": "TT048", "name": "资源平衡", "nameEn": "Resource Leveling", "type": "technique", "category": "scheduling" },
{ "id": "TT049", "name": "资源平滑", "nameEn": "Resource Smoothing", "type": "technique", "category": "scheduling" },
{ "id": "TT050", "name": "提前量和滞后量", "nameEn": "Leads and Lags", "type": "technique", "category": "scheduling" },
{ "id": "TT051", "name": "进度压缩", "nameEn": "Schedule Compression", "type": "technique", "category": "scheduling" },
{ "id": "TT052", "name": "赶工", "nameEn": "Crashing", "type": "technique", "category": "scheduling" },
{ "id": "TT053", "name": "快速跟进", "nameEn": "Fast Tracking", "type": "technique", "category": "scheduling" },
{ "id": "TT054", "name": "估算", "nameEn": "Estimating", "type": "technique", "category": "general" },
{ "id": "TT055", "name": "类比估算", "nameEn": "Analogous Estimating", "type": "technique", "category": "estimating" },
{ "id": "TT056", "name": "参数估算", "nameEn": "Parametric Estimating", "type": "technique", "category": "estimating" },
{ "id": "TT057", "name": "自下而上估算", "nameEn": "Bottom-Up Estimating", "type": "technique", "category": "estimating" },
{ "id": "TT058", "name": "三点估算", "nameEn": "Three-Point Estimating", "type": "technique", "category": "estimating" },
{ "id": "TT059", "name": "分解", "nameEn": "Decomposition", "type": "technique", "category": "general" },
{ "id": "TT060", "name": "滚动式规划", "nameEn": "Rolling Wave Planning", "type": "technique", "category": "general" },
{ "id": "TT061", "name": "紧前关系绘图法", "nameEn": "Precedence Diagramming Method", "type": "technique", "category": "scheduling" },
{ "id": "TT062", "name": "确定和整合依赖关系", "nameEn": "Dependency Determination and Integration", "type": "technique", "category": "scheduling" },
{ "id": "TT063", "name": "进度计划编制工具", "nameEn": "Scheduling Tool", "type": "tool", "category": "scheduling" },
{ "id": "TT064", "name": "质量成本", "nameEn": "Cost of Quality", "type": "technique", "category": "quality" },
{ "id": "TT065", "name": "检查", "nameEn": "Inspection", "type": "technique", "category": "quality" },
{ "id": "TT066", "name": "测试/产品评估", "nameEn": "Testing/Product Evaluations", "type": "technique", "category": "quality" },
{ "id": "TT067", "name": "审计", "nameEn": "Audits", "type": "technique", "category": "quality" },
{ "id": "TT068", "name": "面向X的设计", "nameEn": "Design for X", "type": "technique", "category": "quality" },
{ "id": "TT069", "name": "问题解决", "nameEn": "Problem Solving", "type": "technique", "category": "quality" },
{ "id": "TT070", "name": "质量改进方法", "nameEn": "Quality Improvement Methods", "type": "technique", "category": "quality" },
{ "id": "TT071", "name": "风险概率和影响评估", "nameEn": "Risk Probability and Impact Assessment", "type": "technique", "category": "risk" },
{ "id": "TT072", "name": "风险分类", "nameEn": "Risk Categorization", "type": "technique", "category": "risk" },
{ "id": "TT073", "name": "风险数据质量评估", "nameEn": "Risk Data Quality Assessment", "type": "technique", "category": "risk" },
{ "id": "TT074", "name": "风险紧迫性评估", "nameEn": "Risk Urgency Assessment", "type": "technique", "category": "risk" },
{ "id": "TT075", "name": "敏感性分析", "nameEn": "Sensitivity Analysis", "type": "technique", "category": "risk" },
{ "id": "TT076", "name": "预期货币价值分析", "nameEn": "Expected Monetary Value Analysis", "type": "technique", "category": "risk" },
{ "id": "TT077", "name": "模拟", "nameEn": "Simulations", "type": "technique", "category": "risk" },
{ "id": "TT078", "name": "蒙特卡洛模拟", "nameEn": "Monte Carlo Simulation", "type": "technique", "category": "risk" },
{ "id": "TT079", "name": "决策树分析", "nameEn": "Decision Tree Analysis", "type": "technique", "category": "risk" },
{ "id": "TT080", "name": "影响图", "nameEn": "Influence Diagrams", "type": "technique", "category": "risk" },
{ "id": "TT081", "name": "威胁应对策略", "nameEn": "Strategies for Threats", "type": "technique", "category": "risk" },
{ "id": "TT082", "name": "机会应对策略", "nameEn": "Strategies for Opportunities", "type": "technique", "category": "risk" },
{ "id": "TT083", "name": "应急应对策略", "nameEn": "Contingent Response Strategies", "type": "technique", "category": "risk" },
{ "id": "TT084", "name": "整体项目风险应对策略", "nameEn": "Strategies for Overall Project Risk", "type": "technique", "category": "risk" },
{ "id": "TT085", "name": "沟通方法", "nameEn": "Communication Methods", "type": "technique", "category": "communication" },
{ "id": "TT086", "name": "沟通技术", "nameEn": "Communication Technology", "type": "tool", "category": "communication" },
{ "id": "TT087", "name": "沟通模型", "nameEn": "Communication Models", "type": "technique", "category": "communication" },
{ "id": "TT088", "name": "沟通技能", "nameEn": "Communication Skills", "type": "skill", "category": "communication" },
{ "id": "TT089", "name": "反馈", "nameEn": "Feedback", "type": "skill", "category": "communication" },
{ "id": "TT090", "name": "演示", "nameEn": "Presentations", "type": "skill", "category": "communication" },
{ "id": "TT091", "name": "采购策略", "nameEn": "Procurement Strategy", "type": "technique", "category": "procurement" },
{ "id": "TT092", "name": "供方选择分析", "nameEn": "Source Selection Analysis", "type": "technique", "category": "procurement" },
{ "id": "TT093", "name": "投标人会议", "nameEn": "Bidder Conferences", "type": "meeting", "category": "procurement" },
{ "id": "TT094", "name": "建议书评价", "nameEn": "Proposal Evaluation", "type": "technique", "category": "procurement" },
{ "id": "TT095", "name": "索赔管理", "nameEn": "Claims Administration", "type": "technique", "category": "procurement" },
{ "id": "TT096", "name": "相关方分析", "nameEn": "Stakeholder Analysis", "type": "technique", "category": "stakeholder" },
{ "id": "TT097", "name": "相关方映射分析/表现", "nameEn": "Stakeholder Mapping/Representation", "type": "technique", "category": "stakeholder" },
{ "id": "TT098", "name": "权力利益方格", "nameEn": "Power/Interest Grid", "type": "technique", "category": "stakeholder" },
{ "id": "TT099", "name": "凸显模型", "nameEn": "Salience Model", "type": "technique", "category": "stakeholder" },
{ "id": "TT100", "name": "基本规则", "nameEn": "Ground Rules", "type": "technique", "category": "interpersonal" },
{ "id": "TT101", "name": "集中办公", "nameEn": "Colocation", "type": "technique", "category": "resource" },
{ "id": "TT102", "name": "虚拟团队", "nameEn": "Virtual Teams", "type": "technique", "category": "resource" },
{ "id": "TT103", "name": "认可与奖励", "nameEn": "Recognition and Rewards", "type": "technique", "category": "resource" },
{ "id": "TT104", "name": "培训", "nameEn": "Training", "type": "technique", "category": "resource" },
{ "id": "TT105", "name": "个人和团队评估", "nameEn": "Individual and Team Assessments", "type": "technique", "category": "resource" },
{ "id": "TT106", "name": "资源日历", "nameEn": "Resource Calendars", "type": "tool", "category": "resource" },
{ "id": "TT107", "name": "预分派", "nameEn": "Pre-Assignment", "type": "technique", "category": "resource" },
{ "id": "TT108", "name": "变更控制工具", "nameEn": "Change Control Tools", "type": "tool", "category": "integration" },
{ "id": "TT109", "name": "配置管理系统", "nameEn": "Configuration Management System", "type": "tool", "category": "integration" },
{ "id": "TT110", "name": "知识管理", "nameEn": "Knowledge Management", "type": "technique", "category": "integration" },
{ "id": "TT111", "name": "信息管理", "nameEn": "Information Management", "type": "technique", "category": "integration" },
{ "id": "TT112", "name": "组织理论", "nameEn": "Organizational Theory", "type": "technique", "category": "resource" },
{ "id": "TT113", "name": "需求跟踪矩阵", "nameEn": "Requirements Traceability Matrix", "type": "tool", "category": "scope" },
{ "id": "TT114", "name": "产品分析", "nameEn": "Product Analysis", "type": "technique", "category": "scope" },
{ "id": "TT115", "name": "原型法", "nameEn": "Prototypes", "type": "technique", "category": "scope" },
{ "id": "TT116", "name": "观察/交谈", "nameEn": "Observation/Conversation", "type": "technique", "category": "data-gathering" },
{ "id": "TT117", "name": "引导式研讨会", "nameEn": "Facilitated Workshops", "type": "technique", "category": "data-gathering" },
{ "id": "TT118", "name": "群体创新技术", "nameEn": "Group Creativity Techniques", "type": "technique", "category": "data-gathering" },
{ "id": "TT119", "name": "群体决策技术", "nameEn": "Group Decision-Making Techniques", "type": "technique", "category": "decision-making" },
{ "id": "TT120", "name": "核对单", "nameEn": "Checklists", "type": "tool", "category": "general" }
]
}

72
src/index.css Normal file
View File

@@ -0,0 +1,72 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--color-bg: #ffffff;
--color-bg-secondary: #f9fafb;
--color-text: #111827;
--color-text-secondary: #6b7280;
--color-border: #e5e7eb;
}
.dark {
--color-bg: #111827;
--color-bg-secondary: #1f2937;
--color-text: #f9fafb;
--color-text-secondary: #9ca3af;
--color-border: #374151;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--color-bg);
color: var(--color-text);
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}
/* 过渡动画 */
.page-transition-enter {
opacity: 0;
transform: translateY(10px);
}
.page-transition-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 200ms ease-out, transform 200ms ease-out;
}
.page-transition-exit {
opacity: 1;
}
.page-transition-exit-active {
opacity: 0;
transition: opacity 150ms ease-in;
}

13
src/main.tsx Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
)

View File

@@ -0,0 +1,163 @@
/**
* 工件详情页面
* 显示工件被哪些过程使用(作为输入或输出)
*/
import { useParams, Link } from 'react-router-dom'
import { motion } from 'framer-motion'
import { ArrowLeft, FileInput, FileOutput, ArrowRight } from 'lucide-react'
import { artifactMap, getArtifactUsage, knowledgeAreaMap } from '@/data'
export function ArtifactDetailPage() {
const { id } = useParams()
const artifact = id ? artifactMap.get(id) : null
const usage = id ? getArtifactUsage(id) : { asInput: [], asOutput: [] }
if (!artifact) {
return (
<div className="flex flex-col items-center justify-center py-20">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
</h1>
<Link to="/" className="text-indigo-600 dark:text-indigo-400 hover:underline">
</Link>
</div>
)
}
return (
<div className="space-y-6">
{/* 返回按钮 */}
<Link
to="/"
className="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400"
>
<ArrowLeft size={16} />
</Link>
{/* 工件信息 */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm border border-gray-100 dark:border-gray-700"
>
<div className="flex items-center gap-4">
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-blue-500 text-white">
<FileInput size={24} />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{artifact.name}
</h1>
<p className="text-gray-500 dark:text-gray-400">{artifact.nameEn}</p>
</div>
</div>
{artifact.description && (
<p className="mt-4 text-gray-600 dark:text-gray-300">{artifact.description}</p>
)}
<div className="mt-4">
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300">
{artifact.category}
</span>
</div>
</motion.div>
{/* 作为输出的过程 */}
{usage.asOutput.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden"
>
<div className="flex items-center gap-3 px-6 py-4 border-b border-gray-100 dark:border-gray-700 bg-emerald-50 dark:bg-emerald-900/20">
<FileOutput size={20} className="text-emerald-600 dark:text-emerald-400" />
<h2 className="font-semibold text-gray-900 dark:text-white">
{usage.asOutput.length}
</h2>
</div>
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
{usage.asOutput.map((process) => {
const ka = knowledgeAreaMap.get(process.knowledgeAreaId)
return (
<li key={process.id}>
<Link
to={`/process/${process.id}`}
className="flex items-center justify-between px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
<div className="flex items-center gap-4">
<div
className="flex h-10 w-10 items-center justify-center rounded-lg text-white font-medium"
style={{ backgroundColor: ka?.color }}
>
{process.code}
</div>
<div>
<div className="font-medium text-gray-900 dark:text-white">
{process.name}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{ka?.name}
</div>
</div>
</div>
<ArrowRight size={18} className="text-gray-400" />
</Link>
</li>
)
})}
</ul>
</motion.div>
)}
{/* 作为输入的过程 */}
{usage.asInput.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden"
>
<div className="flex items-center gap-3 px-6 py-4 border-b border-gray-100 dark:border-gray-700 bg-blue-50 dark:bg-blue-900/20">
<FileInput size={20} className="text-blue-600 dark:text-blue-400" />
<h2 className="font-semibold text-gray-900 dark:text-white">
使{usage.asInput.length}
</h2>
</div>
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
{usage.asInput.map((process) => {
const ka = knowledgeAreaMap.get(process.knowledgeAreaId)
return (
<li key={process.id}>
<Link
to={`/process/${process.id}`}
className="flex items-center justify-between px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
<div className="flex items-center gap-4">
<div
className="flex h-10 w-10 items-center justify-center rounded-lg text-white font-medium"
style={{ backgroundColor: ka?.color }}
>
{process.code}
</div>
<div>
<div className="font-medium text-gray-900 dark:text-white">
{process.name}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{ka?.name}
</div>
</div>
</div>
<ArrowRight size={18} className="text-gray-400" />
</Link>
</li>
)
})}
</ul>
</motion.div>
)}
</div>
)
}

163
src/pages/HomePage.tsx Normal file
View File

@@ -0,0 +1,163 @@
import { Link } from 'react-router-dom'
import { motion } from 'framer-motion'
import { BookOpen, Layers, GitBranch, ArrowRight } from 'lucide-react'
import { stats } from '@/data'
const features = [
{
icon: BookOpen,
title: '知识领域',
description: '按10大知识领域浏览49个项目管理过程',
link: '/knowledge-areas',
color: 'from-indigo-500 to-purple-500',
count: stats.knowledgeAreaCount,
},
{
icon: Layers,
title: '过程组',
description: '按5大过程组分类查看项目管理流程',
link: '/process-groups',
color: 'from-blue-500 to-cyan-500',
count: stats.processGroupCount,
},
{
icon: GitBranch,
title: '可视化',
description: 'ITTO流程图和数据流向可视化分析',
link: '/visualize',
color: 'from-emerald-500 to-teal-500',
count: stats.processCount,
},
]
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
}
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
},
},
}
export function HomePage() {
return (
<div className="space-y-8">
{/* 欢迎区域 */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500 p-8 text-white"
>
<div className="relative z-10">
<h1 className="text-3xl font-bold mb-2">使 ITTOView</h1>
<p className="text-lg text-white/80 mb-6">
PMP项目管理ITTO可视化学习平台49
</p>
<div className="flex flex-wrap gap-4">
<Link
to="/knowledge-areas"
className="inline-flex items-center gap-2 px-6 py-3 bg-white text-indigo-600 rounded-lg font-medium hover:bg-white/90 transition-colors"
>
<ArrowRight size={18} />
</Link>
<Link
to="/visualize"
className="inline-flex items-center gap-2 px-6 py-3 bg-white/20 text-white rounded-lg font-medium hover:bg-white/30 transition-colors"
>
</Link>
</div>
</div>
{/* 装饰背景 */}
<div className="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full -translate-y-1/2 translate-x-1/2" />
<div className="absolute bottom-0 left-0 w-48 h-48 bg-white/10 rounded-full translate-y-1/2 -translate-x-1/2" />
</motion.div>
{/* 统计卡片 */}
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid grid-cols-2 md:grid-cols-4 gap-4"
>
{[
{ label: '知识领域', value: stats.knowledgeAreaCount, color: 'text-indigo-600' },
{ label: '过程组', value: stats.processGroupCount, color: 'text-blue-600' },
{ label: '项目过程', value: stats.processCount, color: 'text-emerald-600' },
{ label: '工具技术', value: stats.toolCount, color: 'text-amber-600' },
].map((stat) => (
<motion.div
key={stat.label}
variants={itemVariants}
className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm border border-gray-100 dark:border-gray-700"
>
<div className={`text-3xl font-bold ${stat.color}`}>{stat.value}</div>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">{stat.label}</div>
</motion.div>
))}
</motion.div>
{/* 功能入口 */}
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid md:grid-cols-3 gap-6"
>
{features.map((feature) => {
const Icon = feature.icon
return (
<motion.div key={feature.title} variants={itemVariants}>
<Link
to={feature.link}
className="group block bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm border border-gray-100 dark:border-gray-700 hover:shadow-md hover:border-gray-200 dark:hover:border-gray-600 transition-all h-full"
>
<div className="flex items-start gap-4">
<div className={`flex h-12 w-12 items-center justify-center rounded-lg bg-gradient-to-br ${feature.color} text-white`}>
<Icon size={24} />
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{feature.title}
</h3>
{feature.count !== null && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{feature.count}
</span>
)}
</div>
<p className="text-gray-500 dark:text-gray-400 mt-1">
{feature.description}
</p>
</div>
</div>
<div className="mt-4 flex items-center text-indigo-600 dark:text-indigo-400 text-sm font-medium">
<ArrowRight
size={16}
className="ml-1 group-hover:translate-x-1 transition-transform"
/>
</div>
</Link>
</motion.div>
)
})}
</motion.div>
</div>
)
}

View File

@@ -0,0 +1,190 @@
import { Link, useParams } from 'react-router-dom'
import { motion } from 'framer-motion'
import { ArrowRight, FileText, Wrench, FileOutput } from 'lucide-react'
import { knowledgeAreas, processesByKnowledgeArea, knowledgeAreaMap, processGroupMap } from '@/data'
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.05 },
},
}
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}
export function KnowledgeAreasPage() {
const { id } = useParams()
const selectedKA = id ? knowledgeAreaMap.get(id) : null
const processes = id ? processesByKnowledgeArea.get(id) || [] : []
if (selectedKA) {
// 显示知识领域详情
return (
<div className="space-y-6">
{/* 面包屑 */}
<nav className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<Link to="/knowledge-areas" className="hover:text-indigo-600 dark:hover:text-indigo-400">
</Link>
<span>/</span>
<span className="text-gray-900 dark:text-white">{selectedKA.name}</span>
</nav>
{/* 知识领域标题 */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="rounded-xl p-6"
style={{ backgroundColor: `${selectedKA.color}15` }}
>
<div className="flex items-center gap-4">
<div
className="flex h-14 w-14 items-center justify-center rounded-xl text-white font-bold text-xl"
style={{ backgroundColor: selectedKA.color }}
>
{selectedKA.order}
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{selectedKA.name}
</h1>
<p className="text-gray-500 dark:text-gray-400">{selectedKA.nameEn}</p>
</div>
</div>
<p className="mt-4 text-gray-600 dark:text-gray-300">{selectedKA.description}</p>
</motion.div>
{/* 过程列表 */}
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-4"
>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
{processes.length}
</h2>
{processes.map((process) => {
const pg = processGroupMap.get(process.processGroupId)
return (
<motion.div key={process.id} variants={itemVariants}>
<Link
to={`/process/${process.id}`}
className="group block bg-white dark:bg-gray-800 rounded-xl p-5 shadow-sm border border-gray-100 dark:border-gray-700 hover:shadow-md hover:border-gray-200 dark:hover:border-gray-600 transition-all"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div
className="flex h-10 w-10 items-center justify-center rounded-lg text-white font-medium"
style={{ backgroundColor: selectedKA.color }}
>
{process.code}
</div>
<div>
<h3 className="font-semibold text-gray-900 dark:text-white">
{process.name}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
{process.nameEn}
</p>
</div>
</div>
<div className="flex items-center gap-4">
{pg && (
<span
className="px-3 py-1 rounded-full text-xs font-medium text-white"
style={{ backgroundColor: pg.color }}
>
{pg.name}
</span>
)}
<ArrowRight
size={20}
className="text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300 group-hover:translate-x-1 transition-all"
/>
</div>
</div>
{/* ITTO统计 */}
<div className="mt-4 flex items-center gap-6 text-sm text-gray-500 dark:text-gray-400">
<span className="flex items-center gap-1">
<FileText size={14} />
{process.inputs.length}
</span>
<span className="flex items-center gap-1">
<Wrench size={14} />
{process.tools.length}
</span>
<span className="flex items-center gap-1">
<FileOutput size={14} />
{process.outputs.length}
</span>
</div>
</Link>
</motion.div>
)
})}
</motion.div>
</div>
)
}
// 显示知识领域列表
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white"></h1>
<p className="text-gray-500 dark:text-gray-400 mt-1">
PMBOK第6版定义的10大项目管理知识领域
</p>
</div>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid md:grid-cols-2 gap-4"
>
{knowledgeAreas.map((ka) => (
<motion.div key={ka.id} variants={itemVariants}>
<Link
to={`/knowledge-areas/${ka.id}`}
className="group block bg-white dark:bg-gray-800 rounded-xl p-5 shadow-sm border border-gray-100 dark:border-gray-700 hover:shadow-md transition-all"
style={{ borderLeftWidth: 4, borderLeftColor: ka.color }}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div
className="flex h-12 w-12 items-center justify-center rounded-lg text-white font-bold text-lg"
style={{ backgroundColor: ka.color }}
>
{ka.order}
</div>
<div>
<h3 className="font-semibold text-gray-900 dark:text-white">{ka.name}</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">{ka.nameEn}</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500 dark:text-gray-400">
{ka.processCount}
</span>
<ArrowRight
size={20}
className="text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300 group-hover:translate-x-1 transition-all"
/>
</div>
</div>
<p className="mt-3 text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
{ka.description}
</p>
</Link>
</motion.div>
))}
</motion.div>
</div>
)
}

View File

@@ -0,0 +1,239 @@
import { useParams, Link, useLocation, useNavigate } from 'react-router-dom'
import { motion } from 'framer-motion'
import { ArrowLeft, ArrowRight, FileText, Wrench, FileOutput, LayoutGrid } from 'lucide-react'
import { getProcessDetail, processes, artifactMap, toolMap } from '@/data'
export function ProcessDetailPage() {
const { id } = useParams()
const location = useLocation()
const navigate = useNavigate()
const processDetail = id ? getProcessDetail(id) : null
// 检查是否从矩阵页面来
const fromMatrix = location.state?.from === 'matrix'
if (!processDetail) {
return (
<div className="flex flex-col items-center justify-center py-20">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
</h2>
<Link
to="/knowledge-areas"
className="text-indigo-600 dark:text-indigo-400 hover:underline"
>
</Link>
</div>
)
}
const ka = processDetail.knowledgeArea
const pg = processDetail.processGroup
// 获取前后过程
const currentIndex = processes.findIndex(p => p.id === id)
const prevProcess = currentIndex > 0 ? processes[currentIndex - 1] : null
const nextProcess = currentIndex < processes.length - 1 ? processes[currentIndex + 1] : null
return (
<div className="space-y-6">
{/* 返回按钮 + 面包屑 */}
<div className="flex items-center gap-4">
{fromMatrix && (
<button
onClick={() => navigate('/process-matrix')}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/50 transition-colors text-sm font-medium"
>
<LayoutGrid size={16} />
</button>
)}
<nav className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<Link to="/knowledge-areas" className="hover:text-indigo-600 dark:hover:text-indigo-400">
</Link>
<span>/</span>
{ka && (
<>
<Link
to={`/knowledge-areas/${ka.id}`}
className="hover:text-indigo-600 dark:hover:text-indigo-400"
>
{ka.name}
</Link>
<span>/</span>
</>
)}
<span className="text-gray-900 dark:text-white">{processDetail.name}</span>
</nav>
</div>
{/* 过程标题 */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="rounded-xl p-6 bg-white dark:bg-gray-800 shadow-sm border border-gray-100 dark:border-gray-700"
>
<div className="flex items-start gap-4">
<div
className="flex h-16 w-16 items-center justify-center rounded-xl text-white font-bold text-xl shrink-0"
style={{ backgroundColor: ka?.color || '#6366F1' }}
>
{processDetail.code}
</div>
<div className="flex-1">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{processDetail.name}
</h1>
<p className="text-gray-500 dark:text-gray-400">{processDetail.nameEn}</p>
<div className="flex items-center gap-3 mt-3">
{ka && (
<span
className="px-3 py-1 rounded-full text-xs font-medium text-white"
style={{ backgroundColor: ka.color }}
>
{ka.name}
</span>
)}
{pg && (
<span
className="px-3 py-1 rounded-full text-xs font-medium text-white"
style={{ backgroundColor: pg.color }}
>
{pg.name}
</span>
)}
</div>
</div>
</div>
</motion.div>
{/* ITTO表格 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="grid lg:grid-cols-3 gap-6"
>
{/* 输入 */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden">
<div className="flex items-center gap-2 px-4 py-3 bg-blue-50 dark:bg-blue-900/30 border-b border-blue-100 dark:border-blue-800">
<FileText size={18} className="text-blue-600 dark:text-blue-400" />
<h3 className="font-semibold text-blue-900 dark:text-blue-100">
({processDetail.inputs.length})
</h3>
</div>
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
{processDetail.inputs.map((inputId) => {
const artifact = artifactMap.get(inputId)
return (
<li key={inputId} className="px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white">
{artifact?.name || inputId}
</div>
{artifact && (
<div className="text-sm text-gray-500 dark:text-gray-400">
{artifact.nameEn}
</div>
)}
</li>
)
})}
</ul>
</div>
{/* 工具与技术 */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden">
<div className="flex items-center gap-2 px-4 py-3 bg-amber-50 dark:bg-amber-900/30 border-b border-amber-100 dark:border-amber-800">
<Wrench size={18} className="text-amber-600 dark:text-amber-400" />
<h3 className="font-semibold text-amber-900 dark:text-amber-100">
({processDetail.tools.length})
</h3>
</div>
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
{processDetail.tools.map((toolId) => {
const tool = toolMap.get(toolId)
return (
<li key={toolId} className="px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white">
{tool?.name || toolId}
</div>
{tool && (
<div className="text-sm text-gray-500 dark:text-gray-400">
{tool.nameEn}
</div>
)}
</li>
)
})}
</ul>
</div>
{/* 输出 */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden">
<div className="flex items-center gap-2 px-4 py-3 bg-emerald-50 dark:bg-emerald-900/30 border-b border-emerald-100 dark:border-emerald-800">
<FileOutput size={18} className="text-emerald-600 dark:text-emerald-400" />
<h3 className="font-semibold text-emerald-900 dark:text-emerald-100">
({processDetail.outputs.length})
</h3>
</div>
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
{processDetail.outputs.map((outputId) => {
const artifact = artifactMap.get(outputId)
return (
<li key={outputId} className="px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white">
{artifact?.name || outputId}
</div>
{artifact && (
<div className="text-sm text-gray-500 dark:text-gray-400">
{artifact.nameEn}
</div>
)}
</li>
)
})}
</ul>
</div>
</motion.div>
{/* 前后导航 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="flex items-center justify-between pt-6 border-t border-gray-200 dark:border-gray-700"
>
{prevProcess ? (
<Link
to={`/process/${prevProcess.id}`}
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
<ArrowLeft size={18} />
<div>
<div className="text-xs text-gray-400"></div>
<div className="font-medium">{prevProcess.code} {prevProcess.name}</div>
</div>
</Link>
) : (
<div />
)}
{nextProcess ? (
<Link
to={`/process/${nextProcess.id}`}
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors text-right"
>
<div>
<div className="text-xs text-gray-400"></div>
<div className="font-medium">{nextProcess.code} {nextProcess.name}</div>
</div>
<ArrowRight size={18} />
</Link>
) : (
<div />
)}
</motion.div>
</div>
)
}

View File

@@ -0,0 +1,576 @@
/**
* 过程关系图页面 (Force Graph)
* 展示所有实体(过程、工件、工具)及其相互关系
*/
import { useEffect, useRef, useState, useMemo } from 'react'
import G6, { Graph, INode } from '@antv/g6'
import { motion } from 'framer-motion'
import {
ZoomIn, ZoomOut, Maximize2,
Activity, FileText, Wrench,
ChevronRight, ChevronLeft
} from 'lucide-react'
import {
processes,
artifacts,
tools,
knowledgeAreaMap,
getProcessDetail,
getArtifactUsage,
getToolUsage,
} from '@/data'
export function ProcessGraphPage() {
const containerRef = useRef<HTMLDivElement>(null)
const graphRef = useRef<Graph | null>(null)
const [selectedNode, setSelectedNode] = useState<any>(null)
const [details, setDetails] = useState<any>(null)
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false)
// 更新详情数据
useEffect(() => {
if (!selectedNode) {
setDetails(null)
setIsSidebarCollapsed(false)
return
}
// 选中新节点时自动展开
setIsSidebarCollapsed(false)
if (selectedNode.type === 'process') {
setDetails(getProcessDetail(selectedNode.id))
} else if (selectedNode.type === 'artifact') {
setDetails({
...selectedNode,
usage: getArtifactUsage(selectedNode.id)
})
} else if (selectedNode.type === 'tool') {
setDetails({
...selectedNode,
usage: getToolUsage(selectedNode.id)
})
}
}, [selectedNode])
// 构建全量数据
const graphData = useMemo(() => {
const nodes: any[] = []
const edges: any[] = []
const addedNodeIds = new Set<string>()
// 1. 添加过程节点
processes.forEach(p => {
const ka = knowledgeAreaMap.get(p.knowledgeAreaId)
nodes.push({
id: p.id,
label: `${p.code}\n${p.name}`, // 添加编号
type: 'circle',
size: 50, // 过程节点最大
style: {
fill: ka?.color || '#6366F1',
stroke: '#fff',
lineWidth: 3,
shadowColor: 'rgba(0,0,0,0.2)',
shadowBlur: 5,
},
labelCfg: {
position: 'center', // 文字居中
style: {
fontSize: 12,
fill: '#fff', // 白色文字
fontWeight: 700,
stroke: '#000', // 黑色描边
lineWidth: 2,
}
},
data: { ...p, type: 'process' },
})
addedNodeIds.add(p.id)
})
// 2. 添加工件节点和关系
const usedArtifacts = new Set<string>()
processes.forEach(p => {
p.inputs.forEach(id => usedArtifacts.add(id))
p.outputs.forEach(id => usedArtifacts.add(id))
})
artifacts.forEach(a => {
if (usedArtifacts.has(a.id)) {
nodes.push({
id: a.id,
label: a.name,
type: 'rect', // 工件使用矩形
size: [80, 30],
style: {
fill: '#10B981', // Green
stroke: '#fff',
lineWidth: 1,
radius: 4,
},
labelCfg: {
position: 'center',
style: {
fontSize: 11,
fill: '#fff',
stroke: '#000',
lineWidth: 2,
}
},
data: { ...a, type: 'artifact' },
})
addedNodeIds.add(a.id)
}
})
// 3. 添加工具节点和关系
const usedTools = new Set<string>()
processes.forEach(p => {
p.tools.forEach(id => usedTools.add(id))
})
tools.forEach(t => {
if (usedTools.has(t.id)) {
nodes.push({
id: t.id,
label: t.name,
type: 'diamond', // 工具使用菱形
size: [80, 40],
style: {
fill: '#8B5CF6', // Purple
stroke: '#fff',
lineWidth: 1,
},
labelCfg: {
position: 'center',
style: {
fontSize: 11,
fill: '#fff',
stroke: '#000',
lineWidth: 2,
}
},
data: { ...t, type: 'tool' },
})
addedNodeIds.add(t.id)
}
})
// 4. 构建边
processes.forEach(p => {
// 输入关系: Artifact -> Process
p.inputs.forEach(inputId => {
if (addedNodeIds.has(inputId)) {
edges.push({
source: inputId,
target: p.id,
type: 'line',
style: {
stroke: '#94a3b8',
opacity: 0.3,
endArrow: true,
}
})
}
})
// 输出关系: Process -> Artifact
p.outputs.forEach(outputId => {
if (addedNodeIds.has(outputId)) {
edges.push({
source: p.id,
target: outputId,
type: 'line',
style: {
stroke: '#94a3b8',
opacity: 0.3,
endArrow: true,
}
})
}
})
// 工具关系: Tool -> Process
p.tools.forEach(toolId => {
if (addedNodeIds.has(toolId)) {
edges.push({
source: toolId,
target: p.id,
type: 'line',
style: {
stroke: '#a78bfa',
opacity: 0.3,
lineDash: [4, 2],
}
})
}
})
})
return { nodes, edges }
}, [])
useEffect(() => {
if (!containerRef.current) return
if (graphRef.current) {
graphRef.current.destroy()
}
const width = containerRef.current.offsetWidth
const height = containerRef.current.offsetHeight || 800
const graph = new G6.Graph({
container: containerRef.current,
width,
height,
fitView: true,
modes: {
default: ['drag-canvas', 'zoom-canvas', 'drag-node'], // 移除 activate-relations改为手动控制
},
layout: {
type: 'gForce',
preventOverlap: true,
nodeSize: 50,
linkDistance: () => {
// 过程节点之间的距离远一点,工件和工具近一点
return 140
},
nodeStrength: 1200,
edgeStrength: 200,
gpuEnabled: true,
},
defaultNode: {
size: 30,
style: {
lineWidth: 2,
stroke: '#5B8FF9',
fill: '#C6E5FF',
cursor: 'pointer',
},
labelCfg: {
style: {
fontSize: 11,
fontWeight: 600,
fill: '#fff',
stroke: '#000',
lineWidth: 2, // 文字描边,增加对比度
}
}
},
defaultEdge: {
size: 1,
color: '#e2e2e2',
style: {
cursor: 'pointer',
}
},
nodeStateStyles: {
active: {
opacity: 1,
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 10,
},
inactive: {
opacity: 0.1,
},
selected: {
lineWidth: 4,
stroke: '#F59E0B', // 选中时金色边框
shadowColor: '#F59E0B',
shadowBlur: 15,
opacity: 1,
}
},
edgeStateStyles: {
active: {
opacity: 1,
stroke: '#333',
lineWidth: 2,
},
inactive: {
opacity: 0.05,
},
},
})
graph.data(graphData)
graph.render()
// 监听节点点击
graph.on('node:click', (e) => {
const item = e.item as INode
const model = item.getModel()
// 更新选中状态
setSelectedNode({
...model.data as any,
type: (model.data as any).type
})
// 高亮逻辑
const nodes = graph.getNodes()
const edges = graph.getEdges()
// 1. 重置所有状态
nodes.forEach(n => {
graph.clearItemStates(n, ['active', 'inactive', 'selected'])
graph.setItemState(n, 'inactive', true) // 默认全部变暗
})
edges.forEach(e => {
graph.clearItemStates(e, ['active', 'inactive'])
graph.setItemState(e, 'inactive', true) // 默认全部变暗
})
// 2. 高亮当前节点
graph.setItemState(item, 'inactive', false)
graph.setItemState(item, 'selected', true)
// 3. 高亮邻居节点和边
item.getNeighbors().forEach(neighbor => {
graph.setItemState(neighbor, 'inactive', false)
graph.setItemState(neighbor, 'active', true)
})
item.getEdges().forEach(edge => {
graph.setItemState(edge, 'inactive', false)
graph.setItemState(edge, 'active', true)
})
})
// 画布点击清除选中
graph.on('canvas:click', () => {
setSelectedNode(null)
// 恢复所有节点和边
const nodes = graph.getNodes()
const edges = graph.getEdges()
nodes.forEach(n => {
graph.clearItemStates(n, ['active', 'inactive', 'selected'])
})
edges.forEach(e => {
graph.clearItemStates(e, ['active', 'inactive'])
})
})
graphRef.current = graph
const handleResize = () => {
if (containerRef.current && graphRef.current) {
graphRef.current.changeSize(
containerRef.current.offsetWidth,
containerRef.current.offsetHeight
)
}
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
if (graphRef.current) {
graphRef.current.destroy()
}
}
}, [graphData])
return (
<div className="flex h-[calc(100vh-100px)] bg-gray-50 dark:bg-gray-900 rounded-xl overflow-hidden border border-gray-200 dark:border-gray-700 relative">
{/* 图例 */}
<div className="absolute top-4 left-4 bg-white/90 dark:bg-gray-800/90 p-3 rounded-lg shadow-md border border-gray-100 dark:border-gray-700 z-10 flex flex-col gap-2">
<div className="flex items-center gap-2 text-xs font-medium">
<div className="w-3 h-3 rounded-full bg-indigo-500"></div>
<span> (Process) - </span>
</div>
<div className="flex items-center gap-2 text-xs font-medium">
<div className="w-3 h-3 rounded bg-emerald-500"></div>
<span> (Artifact) - </span>
</div>
<div className="flex items-center gap-2 text-xs font-medium">
<div className="w-3 h-3 rotate-45 bg-purple-500"></div>
<span> (Tool) - </span>
</div>
</div>
{/* 画布 */}
<div ref={containerRef} className="w-full h-full" />
{/* 缩放控制 */}
<div className="absolute bottom-4 left-4 flex flex-col gap-2 bg-white dark:bg-gray-800 p-1 rounded-lg shadow-md border border-gray-200 dark:border-gray-700">
<button onClick={() => graphRef.current?.zoom(1.2)} className="p-2 hover:bg-gray-100 rounded"><ZoomIn size={18}/></button>
<button onClick={() => graphRef.current?.zoom(0.8)} className="p-2 hover:bg-gray-100 rounded"><ZoomOut size={18}/></button>
<button onClick={() => graphRef.current?.fitView()} className="p-2 hover:bg-gray-100 rounded"><Maximize2 size={18}/></button>
</div>
{/* 右侧详情面板 */}
{selectedNode && details && (
<>
{/* 折叠/展开按钮 */}
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
className={`absolute top-1/2 -translate-y-1/2 z-30 p-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-lg rounded-l-lg text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-all ${
isSidebarCollapsed ? 'right-0' : 'right-96'
}`}
>
{isSidebarCollapsed ? <ChevronLeft size={20} /> : <ChevronRight size={20} />}
</motion.button>
<motion.div
initial={{ x: '100%' }}
animate={{ x: isSidebarCollapsed ? '100%' : 0 }}
exit={{ x: '100%' }}
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
className="absolute right-0 top-0 bottom-0 w-96 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 shadow-2xl z-20 flex flex-col"
>
{/* Header */}
<div className="p-6 border-b border-gray-100 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-800/50">
<div className="flex items-center gap-2 mb-3">
{selectedNode.type === 'process' && <Activity className="text-indigo-600 dark:text-indigo-400" size={20} />}
{selectedNode.type === 'artifact' && <FileText className="text-emerald-600 dark:text-emerald-400" size={20} />}
{selectedNode.type === 'tool' && <Wrench className="text-purple-600 dark:text-purple-400" size={20} />}
<span className="text-xs font-bold uppercase text-gray-500 dark:text-gray-400 tracking-wider">
{selectedNode.type === 'process' ? '过程 Process' : selectedNode.type === 'artifact' ? '工件 Artifact' : '工具 Tool'}
</span>
</div>
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-1 leading-tight">{selectedNode.name}</h2>
<div className="text-sm text-gray-500 dark:text-gray-400 font-medium">{selectedNode.nameEn}</div>
{selectedNode.type === 'process' && (
<div className="mt-3 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300">
{selectedNode.code}
</div>
)}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{/* Process Details */}
{selectedNode.type === 'process' && details.inputDetails && (
<>
<div>
<h3 className="text-sm font-bold text-gray-900 dark:text-white uppercase tracking-wider mb-3 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
(Inputs)
</h3>
<div className="space-y-2">
{details.inputDetails.length > 0 ? (
details.inputDetails.map((item: any) => (
<div key={item.id} className="p-3 bg-gray-50 dark:bg-gray-700/30 rounded-lg border border-gray-100 dark:border-gray-700 hover:border-emerald-200 dark:hover:border-emerald-800 transition-colors">
<div className="text-sm font-medium text-gray-800 dark:text-gray-200">{item.name}</div>
</div>
))
) : <div className="text-sm text-gray-400 italic"></div>}
</div>
</div>
<div>
<h3 className="text-sm font-bold text-gray-900 dark:text-white uppercase tracking-wider mb-3 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-500"></span>
(Tools)
</h3>
<div className="space-y-2">
{details.toolDetails.length > 0 ? (
details.toolDetails.map((item: any) => (
<div key={item.id} className="p-3 bg-gray-50 dark:bg-gray-700/30 rounded-lg border border-gray-100 dark:border-gray-700 hover:border-purple-200 dark:hover:border-purple-800 transition-colors">
<div className="text-sm font-medium text-gray-800 dark:text-gray-200">{item.name}</div>
</div>
))
) : <div className="text-sm text-gray-400 italic"></div>}
</div>
</div>
<div>
<h3 className="text-sm font-bold text-gray-900 dark:text-white uppercase tracking-wider mb-3 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-500"></span>
(Outputs)
</h3>
<div className="space-y-2">
{details.outputDetails.length > 0 ? (
details.outputDetails.map((item: any) => (
<div key={item.id} className="p-3 bg-gray-50 dark:bg-gray-700/30 rounded-lg border border-gray-100 dark:border-gray-700 hover:border-blue-200 dark:hover:border-blue-800 transition-colors">
<div className="text-sm font-medium text-gray-800 dark:text-gray-200">{item.name}</div>
</div>
))
) : <div className="text-sm text-gray-400 italic"></div>}
</div>
</div>
</>
)}
{/* Artifact Details */}
{selectedNode.type === 'artifact' && details.usage && (
<>
<div>
<h3 className="text-sm font-bold text-gray-900 dark:text-white uppercase tracking-wider mb-3 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-indigo-500"></span>
(Used By)
</h3>
<div className="space-y-2">
{details.usage.asInput.length > 0 ? (
details.usage.asInput.map((p: any) => (
<div key={p.id} className="p-3 bg-gray-50 dark:bg-gray-700/30 rounded-lg border border-gray-100 dark:border-gray-700">
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-bold text-indigo-600 dark:text-indigo-400">{p.code}</span>
</div>
<div className="text-sm font-medium text-gray-800 dark:text-gray-200">{p.name}</div>
</div>
))
) : <div className="text-sm text-gray-400 italic"></div>}
</div>
</div>
<div>
<h3 className="text-sm font-bold text-gray-900 dark:text-white uppercase tracking-wider mb-3 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-500"></span>
(Produced By)
</h3>
<div className="space-y-2">
{details.usage.asOutput.length > 0 ? (
details.usage.asOutput.map((p: any) => (
<div key={p.id} className="p-3 bg-gray-50 dark:bg-gray-700/30 rounded-lg border border-gray-100 dark:border-gray-700">
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-bold text-blue-600 dark:text-blue-400">{p.code}</span>
</div>
<div className="text-sm font-medium text-gray-800 dark:text-gray-200">{p.name}</div>
</div>
))
) : <div className="text-sm text-gray-400 italic"></div>}
</div>
</div>
</>
)}
{/* Tool Details */}
{selectedNode.type === 'tool' && details.usage && (
<>
<div>
<h3 className="text-sm font-bold text-gray-900 dark:text-white uppercase tracking-wider mb-3 flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-indigo-500"></span>
使 (Used In)
</h3>
<div className="space-y-2">
{details.usage.length > 0 ? (
details.usage.map((p: any) => (
<div key={p.id} className="p-3 bg-gray-50 dark:bg-gray-700/30 rounded-lg border border-gray-100 dark:border-gray-700">
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-bold text-indigo-600 dark:text-indigo-400">{p.code}</span>
</div>
<div className="text-sm font-medium text-gray-800 dark:text-gray-200">{p.name}</div>
</div>
))
) : <div className="text-sm text-gray-400 italic">使</div>}
</div>
</div>
</>
)}
</div>
</motion.div>
</>
)}
</div>
)
}

View File

@@ -0,0 +1,193 @@
import { Link, useParams } from 'react-router-dom'
import { motion } from 'framer-motion'
import { ArrowRight, FileText, Wrench, FileOutput } from 'lucide-react'
import { processGroups, processesByProcessGroup, processGroupMap, knowledgeAreaMap } from '@/data'
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.05 },
},
}
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}
export function ProcessGroupsPage() {
const { id } = useParams()
const selectedPG = id ? processGroupMap.get(id) : null
const processes = id ? processesByProcessGroup.get(id) || [] : []
if (selectedPG) {
// 显示过程组详情
return (
<div className="space-y-6">
{/* 面包屑 */}
<nav className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<Link to="/process-groups" className="hover:text-indigo-600 dark:hover:text-indigo-400">
</Link>
<span>/</span>
<span className="text-gray-900 dark:text-white">{selectedPG.name}</span>
</nav>
{/* 过程组标题 */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="rounded-xl p-6"
style={{ backgroundColor: `${selectedPG.color}15` }}
>
<div className="flex items-center gap-4">
<div
className="flex h-14 w-14 items-center justify-center rounded-xl text-white font-bold text-xl"
style={{ backgroundColor: selectedPG.color }}
>
{selectedPG.order}
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{selectedPG.name}
</h1>
<p className="text-gray-500 dark:text-gray-400">{selectedPG.nameEn}</p>
</div>
</div>
<p className="mt-4 text-gray-600 dark:text-gray-300">{selectedPG.description}</p>
</motion.div>
{/* 过程列表 */}
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-4"
>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
{processes.length}
</h2>
{processes.map((process) => {
const ka = knowledgeAreaMap.get(process.knowledgeAreaId)
return (
<motion.div key={process.id} variants={itemVariants}>
<Link
to={`/process/${process.id}`}
className="group block bg-white dark:bg-gray-800 rounded-xl p-5 shadow-sm border border-gray-100 dark:border-gray-700 hover:shadow-md hover:border-gray-200 dark:hover:border-gray-600 transition-all"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div
className="flex h-10 w-10 items-center justify-center rounded-lg text-white font-medium"
style={{ backgroundColor: ka?.color || selectedPG.color }}
>
{process.code}
</div>
<div>
<h3 className="font-semibold text-gray-900 dark:text-white">
{process.name}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
{process.nameEn}
</p>
</div>
</div>
<div className="flex items-center gap-4">
{ka && (
<span
className="px-3 py-1 rounded-full text-xs font-medium text-white"
style={{ backgroundColor: ka.color }}
>
{ka.name}
</span>
)}
<ArrowRight
size={20}
className="text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300 group-hover:translate-x-1 transition-all"
/>
</div>
</div>
{/* ITTO统计 */}
<div className="mt-4 flex items-center gap-6 text-sm text-gray-500 dark:text-gray-400">
<span className="flex items-center gap-1">
<FileText size={14} />
{process.inputs.length}
</span>
<span className="flex items-center gap-1">
<Wrench size={14} />
{process.tools.length}
</span>
<span className="flex items-center gap-1">
<FileOutput size={14} />
{process.outputs.length}
</span>
</div>
</Link>
</motion.div>
)
})}
</motion.div>
</div>
)
}
// 显示过程组列表
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white"></h1>
<p className="text-gray-500 dark:text-gray-400 mt-1">
PMBOK第6版定义的5大项目管理过程组
</p>
</div>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-4"
>
{processGroups.map((pg) => (
<motion.div key={pg.id} variants={itemVariants}>
<Link
to={`/process-groups/${pg.id}`}
className="group block bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm border border-gray-100 dark:border-gray-700 hover:shadow-md transition-all"
style={{ borderLeftWidth: 4, borderLeftColor: pg.color }}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div
className="flex h-14 w-14 items-center justify-center rounded-xl text-white font-bold text-xl"
style={{ backgroundColor: pg.color }}
>
{pg.order}
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{pg.name}</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">{pg.nameEn}</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{pg.processCount}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400"></div>
</div>
<ArrowRight
size={24}
className="text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300 group-hover:translate-x-1 transition-all"
/>
</div>
</div>
<p className="mt-4 text-gray-500 dark:text-gray-400">
{pg.description}
</p>
</Link>
</motion.div>
))}
</motion.div>
</div>
)
}

View File

@@ -0,0 +1,20 @@
/**
* 49过程矩阵页面
*/
import { ProcessMatrix } from '@/components/visualize'
export function ProcessMatrixPage() {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">49</h1>
<p className="text-gray-500 dark:text-gray-400 mt-1">
×
</p>
</div>
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-4 overflow-hidden">
<ProcessMatrix />
</div>
</div>
)
}

View File

@@ -0,0 +1,87 @@
import { motion } from 'framer-motion'
import { Sun, Moon, Palette } from 'lucide-react'
import { useAppStore } from '@/stores/useAppStore'
export function SettingsPage() {
const { darkMode, setDarkMode } = useAppStore()
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white"></h1>
<p className="text-gray-500 dark:text-gray-400 mt-1">
使
</p>
</div>
{/* 主题设置 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden"
>
<div className="flex items-center gap-3 px-6 py-4 border-b border-gray-100 dark:border-gray-700">
<Palette size={20} className="text-indigo-600 dark:text-indigo-400" />
<h2 className="font-semibold text-gray-900 dark:text-white"></h2>
</div>
<div className="p-6">
<div className="mb-4">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
</label>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
</p>
</div>
<div className="grid grid-cols-2 gap-4 max-w-xs">
{[
{ value: false, label: '浅色', icon: Sun },
{ value: true, label: '深色', icon: Moon },
].map((option) => {
const Icon = option.icon
const isSelected = darkMode === option.value
return (
<button
key={option.label}
onClick={() => setDarkMode(option.value)}
className={`flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-all ${
isSelected
? 'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/30'
: 'border-gray-200 dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-500'
}`}
>
<Icon
size={24}
className={isSelected ? 'text-indigo-600 dark:text-indigo-400' : 'text-gray-500'}
/>
<span
className={`text-sm font-medium ${
isSelected ? 'text-indigo-600 dark:text-indigo-400' : 'text-gray-700 dark:text-gray-300'
}`}
>
{option.label}
</span>
</button>
)
})}
</div>
</div>
</motion.div>
{/* 关于 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6"
>
<h2 className="font-semibold text-gray-900 dark:text-white mb-4"> ITTOView</h2>
<div className="space-y-2 text-sm text-gray-500 dark:text-gray-400">
<p>1.0.0</p>
<p> PMBOK 6</p>
<p> 49 ITTO </p>
</div>
</motion.div>
</div>
)
}

View File

@@ -0,0 +1,124 @@
/**
* 工具与技术详情页面
* 显示工具被哪些过程使用
*/
import { useParams, Link } from 'react-router-dom'
import { motion } from 'framer-motion'
import { ArrowLeft, Wrench, ArrowRight } from 'lucide-react'
import { toolMap, getToolUsage, knowledgeAreaMap } from '@/data'
export function ToolDetailPage() {
const { id } = useParams()
const tool = id ? toolMap.get(id) : null
const usedByProcesses = id ? getToolUsage(id) : []
if (!tool) {
return (
<div className="flex flex-col items-center justify-center py-20">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
</h1>
<Link to="/" className="text-indigo-600 dark:text-indigo-400 hover:underline">
</Link>
</div>
)
}
return (
<div className="space-y-6">
{/* 返回按钮 */}
<Link
to="/"
className="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400"
>
<ArrowLeft size={16} />
</Link>
{/* 工具信息 */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm border border-gray-100 dark:border-gray-700"
>
<div className="flex items-center gap-4">
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-amber-500 text-white">
<Wrench size={24} />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{tool.name}
</h1>
<p className="text-gray-500 dark:text-gray-400">{tool.nameEn}</p>
</div>
</div>
{tool.description && (
<p className="mt-4 text-gray-600 dark:text-gray-300">{tool.description}</p>
)}
<div className="mt-4 flex gap-2">
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300">
{tool.type}
</span>
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300">
{tool.category}
</span>
</div>
</motion.div>
{/* 使用此工具的过程 */}
{usedByProcesses.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden"
>
<div className="flex items-center gap-3 px-6 py-4 border-b border-gray-100 dark:border-gray-700 bg-amber-50 dark:bg-amber-900/20">
<Wrench size={20} className="text-amber-600 dark:text-amber-400" />
<h2 className="font-semibold text-gray-900 dark:text-white">
使{usedByProcesses.length}
</h2>
</div>
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
{usedByProcesses.map((process) => {
const ka = knowledgeAreaMap.get(process.knowledgeAreaId)
return (
<li key={process.id}>
<Link
to={`/process/${process.id}`}
className="flex items-center justify-between px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
<div className="flex items-center gap-4">
<div
className="flex h-10 w-10 items-center justify-center rounded-lg text-white font-medium"
style={{ backgroundColor: ka?.color }}
>
{process.code}
</div>
<div>
<div className="font-medium text-gray-900 dark:text-white">
{process.name}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{ka?.name}
</div>
</div>
</div>
<ArrowRight size={18} className="text-gray-400" />
</Link>
</li>
)
})}
</ul>
</motion.div>
)}
{usedByProcesses.length === 0 && (
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-sm border border-gray-100 dark:border-gray-700 text-center text-gray-500 dark:text-gray-400">
使
</div>
)}
</div>
)
}

9
src/pages/index.ts Normal file
View File

@@ -0,0 +1,9 @@
export { HomePage } from './HomePage'
export { KnowledgeAreasPage } from './KnowledgeAreasPage'
export { ProcessGroupsPage } from './ProcessGroupsPage'
export { ProcessDetailPage } from './ProcessDetailPage'
export { ProcessMatrixPage } from './ProcessMatrixPage'
export { ProcessGraphPage } from './ProcessGraphPage'
export { ArtifactDetailPage } from './ArtifactDetailPage'
export { ToolDetailPage } from './ToolDetailPage'
export { SettingsPage } from './SettingsPage'

42
src/stores/useAppStore.ts Normal file
View File

@@ -0,0 +1,42 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface AppState {
// UI状态
sidebarOpen: boolean
darkMode: boolean
searchQuery: string
// 操作
toggleSidebar: () => void
setSidebarOpen: (open: boolean) => void
toggleDarkMode: () => void
setDarkMode: (dark: boolean) => void
setSearchQuery: (query: string) => void
}
export const useAppStore = create<AppState>()(
persist(
(set) => ({
// 初始状态
sidebarOpen: true,
darkMode: false,
searchQuery: '',
// 操作方法
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setSidebarOpen: (open) => set({ sidebarOpen: open }),
toggleDarkMode: () => set((state) => ({ darkMode: !state.darkMode })),
setDarkMode: (dark) => set({ darkMode: dark }),
setSearchQuery: (query) => set({ searchQuery: query }),
}),
{
name: 'ittoview-app-storage',
partialize: (state) => ({
sidebarOpen: state.sidebarOpen,
darkMode: state.darkMode,
// searchQuery 不持久化到 localStorage刷新后重置
}),
}
)
)

124
src/types/itto.ts Normal file
View File

@@ -0,0 +1,124 @@
/**
* ITTO数据类型定义
* PMP项目管理ITTO可视化学习平台
*/
// 知识领域
export interface KnowledgeArea {
id: string; // 如 "KA01"
name: string; // 如 "项目整合管理"
nameEn: string; // 如 "Project Integration Management"
chapter: number; // PMBOK章节号 4-13
order: number; // 排序序号 1-10
color: string; // 主题色
description: string; // 简要描述
processCount: number; // 包含的过程数量
}
// 过程组
export interface ProcessGroup {
id: string; // 如 "PG01"
name: string; // 如 "启动过程组"
nameEn: string; // 如 "Initiating Process Group"
order: number; // 排序序号 1-5
color: string; // 主题色
description: string; // 简要描述
processCount: number; // 包含的过程数量
}
// 过程
export interface Process {
id: string; // 如 "P4.1"
code: string; // 如 "4.1"
name: string; // 如 "制定项目章程"
nameEn: string; // 如 "Develop Project Charter"
knowledgeAreaId: string; // 所属知识领域ID
processGroupId: string; // 所属过程组ID
order: number; // 在知识领域内的序号
inputs: string[]; // 输入工件ID列表
tools: string[]; // 工具与技术ID列表
outputs: string[]; // 输出工件ID列表
}
// 工件类别
export type ArtifactCategory =
| 'document' // 文档
| 'plan' // 计划
| 'baseline' // 基准
| 'report' // 报告
| 'register' // 登记册
| 'log' // 日志
| 'deliverable' // 可交付成果
| 'other'; // 其他
// 工件/文档
export interface Artifact {
id: string; // 如 "A001"
name: string; // 如 "项目章程"
nameEn: string; // 如 "Project Charter"
category: ArtifactCategory;
description?: string;
}
// 工具类型
export type ToolType =
| 'tool' // 工具
| 'technique' // 技术
| 'method' // 方法
| 'skill' // 技能
| 'meeting'; // 会议
// 工具与技术
export interface ToolTechnique {
id: string; // 如 "TT001"
name: string; // 如 "专家判断"
nameEn: string; // 如 "Expert Judgment"
type: ToolType;
category: string; // 分类
description?: string;
}
// 学习掌握程度
export type MasteryLevel = 'familiar' | 'fuzzy' | 'unfamiliar';
// 学习记录
export interface LearningRecord {
id: string;
processId: string;
masteryLevel: MasteryLevel;
reviewCount: number;
correctCount: number;
lastReviewAt: Date;
nextReviewAt: Date; // 基于间隔重复算法计算
}
// 题目类型
export type QuestionType = 'choice' | 'fill' | 'match';
// 错题记录
export interface WrongAnswer {
id: string;
questionId: string;
questionType: QuestionType;
userAnswer: string;
correctAnswer: string;
createdAt: Date;
reviewed: boolean;
}
// 学习统计
export interface LearningStats {
totalStudyTime: number; // 总学习时长(分钟)
totalQuestions: number; // 总答题数
correctRate: number; // 正确率
masteredCount: number; // 已掌握过程数
streakDays: number; // 连续学习天数
lastStudyDate: Date;
}
// 数据流向关系
export interface DataFlow {
sourceProcessId: string; // 源过程ID
targetProcessId: string; // 目标过程ID
artifactId: string; // 流转的工件ID
}

52
tailwind.config.js Normal file
View File

@@ -0,0 +1,52 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
darkMode: 'class',
theme: {
extend: {
colors: {
// 知识领域主题色
ka: {
integration: '#6366F1', // 整合管理 - Indigo
scope: '#8B5CF6', // 范围管理 - Violet
schedule: '#EC4899', // 进度管理 - Pink
cost: '#10B981', // 成本管理 - Emerald
quality: '#F59E0B', // 质量管理 - Amber
resource: '#3B82F6', // 资源管理 - Blue
communication: '#06B6D4', // 沟通管理 - Cyan
risk: '#EF4444', // 风险管理 - Red
procurement: '#84CC16', // 采购管理 - Lime
stakeholder: '#F97316', // 相关方管理 - Orange
},
// 过程组主题色
pg: {
initiating: '#22C55E', // 启动 - Green
planning: '#3B82F6', // 规划 - Blue
executing: '#F59E0B', // 执行 - Amber
monitoring: '#8B5CF6', // 监控 - Violet
closing: '#6B7280', // 收尾 - Gray
},
},
fontFamily: {
sans: [
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'"Helvetica Neue"',
'Arial',
'"Noto Sans"',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
'"Noto Color Emoji"',
],
},
},
},
plugins: [],
}

31
tsconfig.json Normal file
View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
tsconfig.node.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

18
vite.config.ts Normal file
View File

@@ -0,0 +1,18 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
base: './',
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3000,
open: true,
},
})