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

@@ -5,6 +5,7 @@
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Eye, EyeOff } from 'lucide-react'
import { import {
knowledgeAreas, knowledgeAreas,
processGroups, processGroups,
@@ -14,9 +15,20 @@ import {
interface ProcessMatrixProps { interface ProcessMatrixProps {
className?: string className?: string
isFullScreen?: boolean isFullScreen?: boolean
hiddenKnowledgeAreaIds?: Set<string>
hiddenProcessGroupIds?: Set<string>
onToggleKnowledgeArea?: (id: string) => void
onToggleProcessGroup?: (id: string) => void
} }
export function ProcessMatrix({ className, isFullScreen = false }: ProcessMatrixProps) { export function ProcessMatrix({
className,
isFullScreen = false,
hiddenKnowledgeAreaIds = new Set(),
hiddenProcessGroupIds = new Set(),
onToggleKnowledgeArea,
onToggleProcessGroup,
}: ProcessMatrixProps) {
// 构建矩阵数据knowledgeAreaId -> processGroupId -> Process[] // 构建矩阵数据knowledgeAreaId -> processGroupId -> Process[]
const matrix = new Map<string, Map<string, typeof processes>>() const matrix = new Map<string, Map<string, typeof processes>>()
@@ -47,62 +59,105 @@ export function ProcessMatrix({ className, isFullScreen = false }: ProcessMatrix
<th className="sticky left-0 z-10 bg-gray-100 dark:bg-gray-800 p-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-700 min-w-[160px]"> <th className="sticky left-0 z-10 bg-gray-100 dark:bg-gray-800 p-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-700 min-w-[160px]">
/ /
</th> </th>
{processGroups.map(pg => ( {processGroups.map(pg => {
<th const isHidden = hiddenProcessGroupIds.has(pg.id)
key={pg.id} return (
className="p-3 text-center text-sm font-semibold text-white border border-gray-200 dark:border-gray-700 min-w-[140px]" <th
style={{ backgroundColor: pg.color }} key={pg.id}
> className="p-3 text-center text-sm font-semibold text-white border border-gray-200 dark:border-gray-700 min-w-[140px]"
<div>{pg.name}</div> style={{ backgroundColor: pg.color }}
<div className="text-xs opacity-80 font-normal mt-1"> >
{pg.processCount} <div className="flex flex-col items-center gap-2">
</div> <div className={clsx("transition-opacity duration-150", isHidden && "opacity-30")}>
</th> <div>{pg.name}</div>
))} <div className="text-xs opacity-80 font-normal mt-1">
{pg.processCount}
</div>
</div>
{onToggleProcessGroup && (
<button
type="button"
onClick={() => onToggleProcessGroup(pg.id)}
aria-label={isHidden ? `显示${pg.name}` : `隐藏${pg.name}`}
aria-pressed={!isHidden}
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-white/20 hover:bg-white/30 transition-colors"
>
{isHidden ? <Eye size={12} /> : <EyeOff size={12} />}
<span>{isHidden ? '显示' : '隐藏'}</span>
</button>
)}
</div>
</th>
)
})}
</tr> </tr>
</thead> </thead>
{/* 表体:知识领域 × 过程 */} {/* 表体:知识领域 × 过程 */}
<tbody> <tbody>
{knowledgeAreas.map((ka, kaIndex) => ( {knowledgeAreas.map((ka, kaIndex) => {
<motion.tr const isKaHidden = hiddenKnowledgeAreaIds.has(ka.id)
key={ka.id} return (
initial={{ opacity: 0, y: 10 }} <motion.tr
animate={{ opacity: 1, y: 0 }} key={ka.id}
transition={{ delay: kaIndex * 0.05 }} initial={{ opacity: 0, y: 10 }}
> animate={{ opacity: 1, y: 0 }}
{/* 知识领域名称 */} transition={{ delay: kaIndex * 0.05 }}
<td
className="sticky left-0 z-10 p-3 border border-gray-200 dark:border-gray-700 font-medium"
style={{
backgroundColor: `${ka.color}15`,
borderLeftWidth: 4,
borderLeftColor: ka.color,
}}
> >
<Link {/* 知识领域名称 */}
to={`/knowledge-areas/${ka.id}`} <td
className="hover:underline text-gray-900 dark:text-white" className="sticky left-0 z-10 p-3 border border-gray-200 dark:border-gray-700 font-medium"
style={{
backgroundColor: `${ka.color}15`,
borderLeftWidth: 4,
borderLeftColor: ka.color,
}}
> >
<span className="font-bold mr-2" style={{ color: ka.color }}> <div className="flex items-center justify-between gap-2">
{ka.order} <Link
</span> to={`/knowledge-areas/${ka.id}`}
{ka.name} className={clsx(
</Link> "hover:underline text-gray-900 dark:text-white transition-opacity duration-150",
</td> isKaHidden && "opacity-30"
)}
>
<span className="font-bold mr-2" style={{ color: ka.color }}>
{ka.order}
</span>
{ka.name}
</Link>
{onToggleKnowledgeArea && (
<button
type="button"
onClick={() => onToggleKnowledgeArea(ka.id)}
aria-label={isKaHidden ? `显示${ka.name}` : `隐藏${ka.name}`}
aria-pressed={!isKaHidden}
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors shrink-0"
style={{ color: ka.color }}
>
{isKaHidden ? <Eye size={12} /> : <EyeOff size={12} />}
</button>
)}
</div>
</td>
{/* 每个过程组的单元格 */} {/* 每个过程组的单元格 */}
{processGroups.map(pg => { {processGroups.map(pg => {
const cellProcesses = matrix.get(ka.id)?.get(pg.id) || [] const cellProcesses = matrix.get(ka.id)?.get(pg.id) || []
const isCellHidden = isKaHidden || hiddenProcessGroupIds.has(pg.id)
return ( return (
<td <td
key={pg.id} key={pg.id}
className="p-2 border border-gray-200 dark:border-gray-700 align-top bg-white dark:bg-gray-800" className="p-2 border border-gray-200 dark:border-gray-700 align-top bg-white dark:bg-gray-800"
> >
<div className={clsx( <div
"gap-1", className={clsx(
isFullScreen ? "grid grid-cols-2" : "space-y-1" "gap-1 transition-opacity duration-150",
)}> isFullScreen ? "grid grid-cols-2" : "space-y-1",
isCellHidden && "opacity-0 pointer-events-none"
)}
style={isCellHidden ? { visibility: 'hidden' } : undefined}
>
{cellProcesses.map(p => ( {cellProcesses.map(p => (
<Link <Link
key={p.id} key={p.id}
@@ -142,7 +197,7 @@ export function ProcessMatrix({ className, isFullScreen = false }: ProcessMatrix
) )
})} })}
</motion.tr> </motion.tr>
))} )})}
</tbody> </tbody>
{/* 表尾:统计 */} {/* 表尾:统计 */}

View File

@@ -1,7 +1,8 @@
import { useParams, Link, useLocation, useNavigate } from 'react-router-dom' import { useParams, Link, useLocation, useNavigate } from 'react-router-dom'
import { motion } from 'framer-motion' import { motion, AnimatePresence } from 'framer-motion'
import { ArrowLeft, ArrowRight, FileText, Wrench, FileOutput, LayoutGrid, Workflow, User, Target, Clock, MapPin, HelpCircle, Cog } from 'lucide-react' 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 { getProcessDetail, processes, artifactMap, toolMap } from '@/data'
import { useState, useEffect } from 'react'
// 5W1H图标和标签配置 // 5W1H图标和标签配置
const w5h1Config = { const w5h1Config = {
@@ -13,12 +14,54 @@ const w5h1Config = {
how: { icon: Cog, label: 'How', title: '如何做', color: 'text-indigo-600 dark:text-indigo-400' }, 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() { export function ProcessDetailPage() {
const { id } = useParams() const { id } = useParams()
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const processDetail = id ? getProcessDetail(id) : null 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 fromMatrix = location.state?.from === 'matrix'
const fromRoadmap = location.state?.from === 'roadmap' const fromRoadmap = location.state?.from === 'roadmap'
@@ -140,6 +183,25 @@ export function ProcessDetailPage() {
</motion.div> </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表格 - 更紧凑 */} {/* ITTO表格 - 更紧凑 */}
<motion.div <motion.div
initial={{ opacity: 0, y: 10 }} 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="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"> <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">
<FileText size={16} className="text-blue-600 dark:text-blue-400" /> <div className="flex items-center gap-2">
<h3 className="font-semibold text-blue-900 dark:text-blue-100 text-sm"> ({processDetail.inputs.length})</h3> <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> </div>
<ul className="divide-y divide-gray-100 dark:divide-gray-700"> <AnimatePresence mode="wait" initial={false}>
{processDetail.inputs.map((inputId) => { {visible.inputs ? (
const artifact = artifactMap.get(inputId) <motion.ul
return ( key="inputs-list"
<li key={inputId} className="px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"> initial={{ opacity: 0, y: 4 }}
<div className="font-medium text-gray-900 dark:text-white text-sm">{artifact?.name || inputId}</div> animate={{ opacity: 1, y: 0 }}
{artifact && <div className="text-xs text-gray-500 dark:text-gray-400">{artifact.nameEn}</div>} exit={{ opacity: 0, y: -4 }}
</li> transition={{ duration: 0.18 }}
) className="divide-y divide-gray-100 dark:divide-gray-700"
})} >
</ul> {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>
{/* 工具与技术 */} {/* 工具与技术 */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden"> <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"> <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">
<Wrench size={16} className="text-amber-600 dark:text-amber-400" /> <div className="flex items-center gap-2">
<h3 className="font-semibold text-amber-900 dark:text-amber-100 text-sm"> ({processDetail.tools.length})</h3> <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> </div>
<ul className="divide-y divide-gray-100 dark:divide-gray-700"> <AnimatePresence mode="wait" initial={false}>
{processDetail.tools.map((toolId) => { {visible.tools ? (
const tool = toolMap.get(toolId) <motion.ul
return ( key="tools-list"
<li key={toolId} className="px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"> initial={{ opacity: 0, y: 4 }}
<div className="font-medium text-gray-900 dark:text-white text-sm">{tool?.name || toolId}</div> animate={{ opacity: 1, y: 0 }}
{tool && <div className="text-xs text-gray-500 dark:text-gray-400">{tool.nameEn}</div>} exit={{ opacity: 0, y: -4 }}
</li> transition={{ duration: 0.18 }}
) className="divide-y divide-gray-100 dark:divide-gray-700"
})} >
</ul> {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>
{/* 输出 */} {/* 输出 */}
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden"> <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"> <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">
<FileOutput size={16} className="text-emerald-600 dark:text-emerald-400" /> <div className="flex items-center gap-2">
<h3 className="font-semibold text-emerald-900 dark:text-emerald-100 text-sm"> ({processDetail.outputs.length})</h3> <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> </div>
<ul className="divide-y divide-gray-100 dark:divide-gray-700"> <AnimatePresence mode="wait" initial={false}>
{processDetail.outputs.map((outputId) => { {visible.outputs ? (
const artifact = artifactMap.get(outputId) <motion.ul
return ( key="outputs-list"
<li key={outputId} className="px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"> initial={{ opacity: 0, y: 4 }}
<div className="font-medium text-gray-900 dark:text-white text-sm">{artifact?.name || outputId}</div> animate={{ opacity: 1, y: 0 }}
{artifact && <div className="text-xs text-gray-500 dark:text-gray-400">{artifact.nameEn}</div>} exit={{ opacity: 0, y: -4 }}
</li> transition={{ duration: 0.18 }}
) className="divide-y divide-gray-100 dark:divide-gray-700"
})} >
</ul> {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> </div>
</motion.div> </motion.div>

View File

@@ -1,17 +1,102 @@
/** /**
* 49过程矩阵页面 * 49过程矩阵页面
*/ */
import { useEffect } from 'react' import { useEffect, useState } from 'react'
import { ProcessMatrix } from '@/components/visualize' import { ProcessMatrix } from '@/components/visualize'
import { Maximize2, Minimize2 } from 'lucide-react' import { Maximize2, Minimize2, Eye } from 'lucide-react'
import { useAppStore } from '@/stores/useAppStore' import { useAppStore } from '@/stores/useAppStore'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { knowledgeAreas, processGroups } from '@/data'
const STORAGE_KEY = 'ittoview:process-matrix:hidden-items'
interface HiddenIds {
knowledgeAreas: Set<string>
processGroups: Set<string>
}
// 从 localStorage 加载并清洗无效 ID
function loadHiddenIds(): HiddenIds {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return { knowledgeAreas: new Set<string>(), processGroups: new Set<string>() }
const parsed = JSON.parse(raw)
if (typeof parsed !== 'object' || !parsed) {
return { knowledgeAreas: new Set<string>(), processGroups: new Set<string>() }
}
// 清洗无效 ID
const validKaIds = new Set(knowledgeAreas.map(ka => ka.id))
const validPgIds = new Set(processGroups.map(pg => pg.id))
const kaIds = Array.isArray(parsed.knowledgeAreas)
? parsed.knowledgeAreas.filter((id: string) => validKaIds.has(id))
: []
const pgIds = Array.isArray(parsed.processGroups)
? parsed.processGroups.filter((id: string) => validPgIds.has(id))
: []
return {
knowledgeAreas: new Set(kaIds),
processGroups: new Set(pgIds),
}
} catch {
return { knowledgeAreas: new Set<string>(), processGroups: new Set<string>() }
}
}
export function ProcessMatrixPage() { export function ProcessMatrixPage() {
const isFullScreen = useAppStore((s) => s.matrixFullScreen) const isFullScreen = useAppStore((s) => s.matrixFullScreen)
const setMatrixFullScreen = useAppStore((s) => s.setMatrixFullScreen) const setMatrixFullScreen = useAppStore((s) => s.setMatrixFullScreen)
const setSidebarOpen = useAppStore((s) => s.setSidebarOpen) const setSidebarOpen = useAppStore((s) => s.setSidebarOpen)
// 显示/隐藏状态管理
const [hiddenIds, setHiddenIds] = useState(() => loadHiddenIds())
const hiddenKnowledgeAreaIds = hiddenIds.knowledgeAreas
const hiddenProcessGroupIds = hiddenIds.processGroups
// 持久化状态
useEffect(() => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
knowledgeAreas: Array.from(hiddenKnowledgeAreaIds),
processGroups: Array.from(hiddenProcessGroupIds),
}))
} catch (error) {
console.warn('无法保存矩阵显示状态:', error)
}
}, [hiddenKnowledgeAreaIds, hiddenProcessGroupIds])
const toggleKnowledgeArea = (id: string) => {
setHiddenIds(prev => {
const next = new Set(prev.knowledgeAreas)
if (next.has(id)) {
next.delete(id)
} else {
next.add(id)
}
return { ...prev, knowledgeAreas: next }
})
}
const toggleProcessGroup = (id: string) => {
setHiddenIds(prev => {
const next = new Set(prev.processGroups)
if (next.has(id)) {
next.delete(id)
} else {
next.add(id)
}
return { ...prev, processGroups: next }
})
}
const showAll = () => {
setHiddenIds({ knowledgeAreas: new Set(), processGroups: new Set() })
}
const hasHidden = hiddenKnowledgeAreaIds.size > 0 || hiddenProcessGroupIds.size > 0
const toggleFullScreen = () => { const toggleFullScreen = () => {
if (!isFullScreen) { if (!isFullScreen) {
setSidebarOpen(false) setSidebarOpen(false)
@@ -46,20 +131,34 @@ export function ProcessMatrixPage() {
)} )}
{!isFullScreen && ( {!isFullScreen && (
<div className="flex justify-between items-end"> <div className="flex justify-between items-end gap-4">
<div> <div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">49</h1> <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 className="text-gray-500 dark:text-gray-400 mt-1">
× ×
</p> </p>
</div> </div>
<button <div className="flex items-center gap-2">
onClick={toggleFullScreen} {hasHidden && (
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors shadow-sm" <button
> onClick={showAll}
<Maximize2 className="w-4 h-4" /> className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-200 dark:border-indigo-800 rounded-lg hover:bg-indigo-100 dark:hover:bg-indigo-900/50 transition-colors"
>
</button> <Eye className="w-4 h-4" />
<span className="text-xs opacity-75">
( {hiddenKnowledgeAreaIds.size} / {hiddenProcessGroupIds.size} )
</span>
</button>
)}
<button
onClick={toggleFullScreen}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors shadow-sm"
>
<Maximize2 className="w-4 h-4" />
</button>
</div>
</div> </div>
)} )}
@@ -71,13 +170,27 @@ export function ProcessMatrixPage() {
{isFullScreen && ( {isFullScreen && (
<div className="flex justify-between items-center px-4 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 shrink-0"> <div className="flex justify-between items-center px-4 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 shrink-0">
<span className="font-medium text-gray-900 dark:text-white">49</span> <span className="font-medium text-gray-900 dark:text-white">49</span>
<button <div className="flex items-center gap-2">
onClick={toggleFullScreen} {hasHidden && (
className="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors" <button
> onClick={showAll}
<Minimize2 className="w-4 h-4" /> className="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-200 dark:border-indigo-800 rounded-md hover:bg-indigo-100 dark:hover:bg-indigo-900/50 transition-colors"
退 >
</button> <Eye className="w-4 h-4" />
<span className="text-xs opacity-75">
({hiddenKnowledgeAreaIds.size} / {hiddenProcessGroupIds.size} )
</span>
</button>
)}
<button
onClick={toggleFullScreen}
className="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
>
<Minimize2 className="w-4 h-4" />
退
</button>
</div>
</div> </div>
)} )}
@@ -85,6 +198,10 @@ export function ProcessMatrixPage() {
<ProcessMatrix <ProcessMatrix
className={clsx(isFullScreen && "h-full w-full overflow-auto no-scrollbar")} className={clsx(isFullScreen && "h-full w-full overflow-auto no-scrollbar")}
isFullScreen={isFullScreen} isFullScreen={isFullScreen}
hiddenKnowledgeAreaIds={hiddenKnowledgeAreaIds}
hiddenProcessGroupIds={hiddenProcessGroupIds}
onToggleKnowledgeArea={toggleKnowledgeArea}
onToggleProcessGroup={toggleProcessGroup}
/> />
</div> </div>
</div> </div>