feat(矩阵): 添加知识领域和过程组的显示/隐藏功能

feat(详情页): 为ITTO内容添加显示/隐藏控制功能

refactor: 优化状态管理使用localStorage持久化
This commit is contained in:
史悦
2026-02-14 00:42:45 +08:00
parent 033ae6b121
commit 6505f977d9
3 changed files with 439 additions and 103 deletions

View File

@@ -1,7 +1,8 @@
import { useParams, Link, useLocation, useNavigate } from 'react-router-dom'
import { motion } from 'framer-motion'
import { ArrowLeft, ArrowRight, FileText, Wrench, FileOutput, LayoutGrid, Workflow, User, Target, Clock, MapPin, HelpCircle, Cog } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
import { ArrowLeft, ArrowRight, FileText, Wrench, FileOutput, LayoutGrid, Workflow, User, Target, Clock, MapPin, HelpCircle, Cog, Eye, EyeOff } from 'lucide-react'
import { getProcessDetail, processes, artifactMap, toolMap } from '@/data'
import { useState, useEffect } from 'react'
// 5W1H图标和标签配置
const w5h1Config = {
@@ -13,12 +14,54 @@ const w5h1Config = {
how: { icon: Cog, label: 'How', title: '如何做', color: 'text-indigo-600 dark:text-indigo-400' },
}
type IttoSection = 'inputs' | 'tools' | 'outputs'
const STORAGE_KEY = 'ittoview:process-detail:itto-visibility'
export function ProcessDetailPage() {
const { id } = useParams()
const location = useLocation()
const navigate = useNavigate()
const processDetail = id ? getProcessDetail(id) : null
// ITTO 显示/隐藏状态管理
const [visible, setVisible] = useState<Record<IttoSection, boolean>>(() => {
const defaultVisible = { inputs: true, tools: true, outputs: true }
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return defaultVisible
const parsed = JSON.parse(raw)
// 数据校验:确保是对象且包含正确的键
if (typeof parsed !== 'object' || parsed === null) return defaultVisible
return {
inputs: typeof parsed.inputs === 'boolean' ? parsed.inputs : true,
tools: typeof parsed.tools === 'boolean' ? parsed.tools : true,
outputs: typeof parsed.outputs === 'boolean' ? parsed.outputs : true,
}
} catch {
return defaultVisible
}
})
// 持久化状态
useEffect(() => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(visible))
} catch (error) {
// 在隐私模式或受限环境下localStorage 可能不可用
console.warn('无法保存 ITTO 显示状态:', error)
}
}, [visible])
const toggleSection = (key: IttoSection) => {
setVisible(prev => ({ ...prev, [key]: !prev[key] }))
}
const allVisible = Object.values(visible).every(Boolean)
const toggleAll = () => {
const newState = !allVisible
setVisible({ inputs: newState, tools: newState, outputs: newState })
}
const fromMatrix = location.state?.from === 'matrix'
const fromRoadmap = location.state?.from === 'roadmap'
@@ -140,6 +183,25 @@ export function ProcessDetailPage() {
</motion.div>
)}
{/* 全局控制按钮 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.08 }}
className="flex justify-end"
>
<button
type="button"
onClick={toggleAll}
aria-label={allVisible ? '隐藏所有 ITTO 内容' : '显示所有 ITTO 内容'}
aria-pressed={allVisible}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/50 transition-colors text-sm font-medium"
>
{allVisible ? <EyeOff size={16} /> : <Eye size={16} />}
<span>{allVisible ? '全部隐藏' : '全部显示'}</span>
</button>
</motion.div>
{/* ITTO表格 - 更紧凑 */}
<motion.div
initial={{ opacity: 0, y: 10 }}
@@ -149,59 +211,161 @@ export function ProcessDetailPage() {
>
{/* 输入 */}
<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-3 py-2 bg-blue-50 dark:bg-blue-900/30 border-b border-blue-100 dark:border-blue-800">
<FileText size={16} className="text-blue-600 dark:text-blue-400" />
<h3 className="font-semibold text-blue-900 dark:text-blue-100 text-sm"> ({processDetail.inputs.length})</h3>
<div className="flex items-center justify-between px-3 py-2 bg-blue-50 dark:bg-blue-900/30 border-b border-blue-100 dark:border-blue-800">
<div className="flex items-center gap-2">
<FileText size={16} className="text-blue-600 dark:text-blue-400" />
<h3 className="font-semibold text-blue-900 dark:text-blue-100 text-sm"> ({processDetail.inputs.length})</h3>
</div>
<button
type="button"
onClick={() => toggleSection('inputs')}
aria-label={visible.inputs ? '隐藏输入' : '显示输入'}
aria-pressed={visible.inputs}
className="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/50 transition-colors"
>
{visible.inputs ? <EyeOff size={14} /> : <Eye size={14} />}
<span>{visible.inputs ? '隐藏' : '显示'}</span>
</button>
</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-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white text-sm">{artifact?.name || inputId}</div>
{artifact && <div className="text-xs text-gray-500 dark:text-gray-400">{artifact.nameEn}</div>}
</li>
)
})}
</ul>
<AnimatePresence mode="wait" initial={false}>
{visible.inputs ? (
<motion.ul
key="inputs-list"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.18 }}
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-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white text-sm">{artifact?.name || inputId}</div>
{artifact && <div className="text-xs text-gray-500 dark:text-gray-400">{artifact.nameEn}</div>}
</li>
)
})}
</motion.ul>
) : (
<motion.div
key="inputs-hidden"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.18 }}
className="px-3 py-6 text-center text-xs text-gray-500 dark:text-gray-400"
>
{processDetail.inputs.length}
</motion.div>
)}
</AnimatePresence>
</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-3 py-2 bg-amber-50 dark:bg-amber-900/30 border-b border-amber-100 dark:border-amber-800">
<Wrench size={16} className="text-amber-600 dark:text-amber-400" />
<h3 className="font-semibold text-amber-900 dark:text-amber-100 text-sm"> ({processDetail.tools.length})</h3>
<div className="flex items-center justify-between px-3 py-2 bg-amber-50 dark:bg-amber-900/30 border-b border-amber-100 dark:border-amber-800">
<div className="flex items-center gap-2">
<Wrench size={16} className="text-amber-600 dark:text-amber-400" />
<h3 className="font-semibold text-amber-900 dark:text-amber-100 text-sm"> ({processDetail.tools.length})</h3>
</div>
<button
type="button"
onClick={() => toggleSection('tools')}
aria-label={visible.tools ? '隐藏工具与技术' : '显示工具与技术'}
aria-pressed={visible.tools}
className="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/50 transition-colors"
>
{visible.tools ? <EyeOff size={14} /> : <Eye size={14} />}
<span>{visible.tools ? '隐藏' : '显示'}</span>
</button>
</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-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white text-sm">{tool?.name || toolId}</div>
{tool && <div className="text-xs text-gray-500 dark:text-gray-400">{tool.nameEn}</div>}
</li>
)
})}
</ul>
<AnimatePresence mode="wait" initial={false}>
{visible.tools ? (
<motion.ul
key="tools-list"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.18 }}
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-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white text-sm">{tool?.name || toolId}</div>
{tool && <div className="text-xs text-gray-500 dark:text-gray-400">{tool.nameEn}</div>}
</li>
)
})}
</motion.ul>
) : (
<motion.div
key="tools-hidden"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.18 }}
className="px-3 py-6 text-center text-xs text-gray-500 dark:text-gray-400"
>
{processDetail.tools.length}
</motion.div>
)}
</AnimatePresence>
</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-3 py-2 bg-emerald-50 dark:bg-emerald-900/30 border-b border-emerald-100 dark:border-emerald-800">
<FileOutput size={16} className="text-emerald-600 dark:text-emerald-400" />
<h3 className="font-semibold text-emerald-900 dark:text-emerald-100 text-sm"> ({processDetail.outputs.length})</h3>
<div className="flex items-center justify-between px-3 py-2 bg-emerald-50 dark:bg-emerald-900/30 border-b border-emerald-100 dark:border-emerald-800">
<div className="flex items-center gap-2">
<FileOutput size={16} className="text-emerald-600 dark:text-emerald-400" />
<h3 className="font-semibold text-emerald-900 dark:text-emerald-100 text-sm"> ({processDetail.outputs.length})</h3>
</div>
<button
type="button"
onClick={() => toggleSection('outputs')}
aria-label={visible.outputs ? '隐藏输出' : '显示输出'}
aria-pressed={visible.outputs}
className="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium text-emerald-700 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors"
>
{visible.outputs ? <EyeOff size={14} /> : <Eye size={14} />}
<span>{visible.outputs ? '隐藏' : '显示'}</span>
</button>
</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-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white text-sm">{artifact?.name || outputId}</div>
{artifact && <div className="text-xs text-gray-500 dark:text-gray-400">{artifact.nameEn}</div>}
</li>
)
})}
</ul>
<AnimatePresence mode="wait" initial={false}>
{visible.outputs ? (
<motion.ul
key="outputs-list"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.18 }}
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-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white text-sm">{artifact?.name || outputId}</div>
{artifact && <div className="text-xs text-gray-500 dark:text-gray-400">{artifact.nameEn}</div>}
</li>
)
})}
</motion.ul>
) : (
<motion.div
key="outputs-hidden"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.18 }}
className="px-3 py-6 text-center text-xs text-gray-500 dark:text-gray-400"
>
{processDetail.outputs.length}
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>