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

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'