211 lines
7.8 KiB
TypeScript
211 lines
7.8 KiB
TypeScript
/**
|
||
* 49过程矩阵页面
|
||
*/
|
||
import { useEffect, useState } from 'react'
|
||
import { ProcessMatrix } from '@/components/visualize'
|
||
import { Maximize2, Minimize2, Eye } from 'lucide-react'
|
||
import { useAppStore } from '@/stores/useAppStore'
|
||
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() {
|
||
const isFullScreen = useAppStore((s) => s.matrixFullScreen)
|
||
const setMatrixFullScreen = useAppStore((s) => s.setMatrixFullScreen)
|
||
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 = () => {
|
||
if (!isFullScreen) {
|
||
setSidebarOpen(false)
|
||
}
|
||
setMatrixFullScreen(!isFullScreen)
|
||
}
|
||
|
||
// Handle Escape key to exit full screen
|
||
useEffect(() => {
|
||
const handleEsc = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape' && isFullScreen) {
|
||
setMatrixFullScreen(false)
|
||
}
|
||
}
|
||
window.addEventListener('keydown', handleEsc)
|
||
return () => window.removeEventListener('keydown', handleEsc)
|
||
}, [isFullScreen, setMatrixFullScreen])
|
||
|
||
return (
|
||
<div className={clsx(isFullScreen ? "fixed inset-0 z-50 bg-white dark:bg-gray-900 p-0 m-0" : "space-y-6")}>
|
||
{/* 隐藏滚动条的样式 */}
|
||
{isFullScreen && (
|
||
<style>{`
|
||
.no-scrollbar::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
.no-scrollbar {
|
||
-ms-overflow-style: none;
|
||
scrollbar-width: none;
|
||
}
|
||
`}</style>
|
||
)}
|
||
|
||
{!isFullScreen && (
|
||
<div className="flex justify-between items-end gap-4">
|
||
<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="flex items-center gap-2">
|
||
{hasHidden && (
|
||
<button
|
||
onClick={showAll}
|
||
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"
|
||
>
|
||
<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 className={clsx(
|
||
"bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden transition-all duration-300",
|
||
!isFullScreen && "p-4",
|
||
isFullScreen && "h-full w-full border-0 rounded-none shadow-none flex flex-col"
|
||
)}>
|
||
{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">
|
||
<span className="font-medium text-gray-900 dark:text-white">49过程矩阵全景图</span>
|
||
<div className="flex items-center gap-2">
|
||
{hasHidden && (
|
||
<button
|
||
onClick={showAll}
|
||
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"
|
||
>
|
||
<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 className={clsx("relative", isFullScreen ? "flex-1 overflow-hidden" : "")}>
|
||
<ProcessMatrix
|
||
className={clsx(isFullScreen && "h-full w-full overflow-auto no-scrollbar")}
|
||
isFullScreen={isFullScreen}
|
||
hiddenKnowledgeAreaIds={hiddenKnowledgeAreaIds}
|
||
hiddenProcessGroupIds={hiddenProcessGroupIds}
|
||
onToggleKnowledgeArea={toggleKnowledgeArea}
|
||
onToggleProcessGroup={toggleProcessGroup}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|