feat(矩阵): 添加知识领域和过程组的显示/隐藏功能
feat(详情页): 为ITTO内容添加显示/隐藏控制功能 refactor: 优化状态管理使用localStorage持久化
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
import { clsx } from 'clsx'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import {
|
||||
knowledgeAreas,
|
||||
processGroups,
|
||||
@@ -14,9 +15,20 @@ import {
|
||||
interface ProcessMatrixProps {
|
||||
className?: string
|
||||
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[]
|
||||
const matrix = new Map<string, Map<string, typeof processes>>()
|
||||
|
||||
@@ -47,24 +59,45 @@ 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>
|
||||
{processGroups.map(pg => (
|
||||
{processGroups.map(pg => {
|
||||
const isHidden = hiddenProcessGroupIds.has(pg.id)
|
||||
return (
|
||||
<th
|
||||
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]"
|
||||
style={{ backgroundColor: pg.color }}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className={clsx("transition-opacity duration-150", isHidden && "opacity-30")}>
|
||||
<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>
|
||||
</thead>
|
||||
|
||||
{/* 表体:知识领域 × 过程 */}
|
||||
<tbody>
|
||||
{knowledgeAreas.map((ka, kaIndex) => (
|
||||
{knowledgeAreas.map((ka, kaIndex) => {
|
||||
const isKaHidden = hiddenKnowledgeAreaIds.has(ka.id)
|
||||
return (
|
||||
<motion.tr
|
||||
key={ka.id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
@@ -80,29 +113,51 @@ export function ProcessMatrix({ className, isFullScreen = false }: ProcessMatrix
|
||||
borderLeftColor: ka.color,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Link
|
||||
to={`/knowledge-areas/${ka.id}`}
|
||||
className="hover:underline text-gray-900 dark:text-white"
|
||||
className={clsx(
|
||||
"hover:underline text-gray-900 dark:text-white transition-opacity duration-150",
|
||||
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 => {
|
||||
const cellProcesses = matrix.get(ka.id)?.get(pg.id) || []
|
||||
const isCellHidden = isKaHidden || hiddenProcessGroupIds.has(pg.id)
|
||||
return (
|
||||
<td
|
||||
key={pg.id}
|
||||
className="p-2 border border-gray-200 dark:border-gray-700 align-top bg-white dark:bg-gray-800"
|
||||
>
|
||||
<div className={clsx(
|
||||
"gap-1",
|
||||
isFullScreen ? "grid grid-cols-2" : "space-y-1"
|
||||
)}>
|
||||
<div
|
||||
className={clsx(
|
||||
"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 => (
|
||||
<Link
|
||||
key={p.id}
|
||||
@@ -142,7 +197,7 @@ export function ProcessMatrix({ className, isFullScreen = false }: ProcessMatrix
|
||||
)
|
||||
})}
|
||||
</motion.tr>
|
||||
))}
|
||||
)})}
|
||||
</tbody>
|
||||
|
||||
{/* 表尾:统计 */}
|
||||
|
||||
@@ -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,11 +211,32 @@ 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">
|
||||
<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>
|
||||
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
<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>
|
||||
<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 (
|
||||
@@ -163,16 +246,50 @@ export function ProcessDetailPage() {
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</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">
|
||||
<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>
|
||||
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
<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>
|
||||
<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 (
|
||||
@@ -182,16 +299,50 @@ export function ProcessDetailPage() {
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</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">
|
||||
<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>
|
||||
<ul className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
<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>
|
||||
<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 (
|
||||
@@ -201,7 +352,20 @@ export function ProcessDetailPage() {
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -1,17 +1,102 @@
|
||||
/**
|
||||
* 49过程矩阵页面
|
||||
*/
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ProcessMatrix } from '@/components/visualize'
|
||||
import { Maximize2, Minimize2 } from 'lucide-react'
|
||||
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)
|
||||
@@ -46,13 +131,26 @@ export function ProcessMatrixPage() {
|
||||
)}
|
||||
|
||||
{!isFullScreen && (
|
||||
<div className="flex justify-between items-end">
|
||||
<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"
|
||||
@@ -61,6 +159,7 @@ export function ProcessMatrixPage() {
|
||||
全屏查看
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={clsx(
|
||||
@@ -71,6 +170,19 @@ export function ProcessMatrixPage() {
|
||||
{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"
|
||||
@@ -79,12 +191,17 @@ export function ProcessMatrixPage() {
|
||||
退出全屏
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user