Initial commit
This commit is contained in:
163
src/pages/ArtifactDetailPage.tsx
Normal file
163
src/pages/ArtifactDetailPage.tsx
Normal 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
163
src/pages/HomePage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
190
src/pages/KnowledgeAreasPage.tsx
Normal file
190
src/pages/KnowledgeAreasPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
239
src/pages/ProcessDetailPage.tsx
Normal file
239
src/pages/ProcessDetailPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
576
src/pages/ProcessGraphPage.tsx
Normal file
576
src/pages/ProcessGraphPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
193
src/pages/ProcessGroupsPage.tsx
Normal file
193
src/pages/ProcessGroupsPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
20
src/pages/ProcessMatrixPage.tsx
Normal file
20
src/pages/ProcessMatrixPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
87
src/pages/SettingsPage.tsx
Normal file
87
src/pages/SettingsPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
124
src/pages/ToolDetailPage.tsx
Normal file
124
src/pages/ToolDetailPage.tsx
Normal 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
9
src/pages/index.ts
Normal 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'
|
||||
Reference in New Issue
Block a user