import { useParams, Link, useLocation, useNavigate } from 'react-router-dom' import { motion } from 'framer-motion' import { ArrowLeft, ArrowRight, FileText, Wrench, FileOutput, LayoutGrid, Eye, EyeOff, Info, Check } from 'lucide-react' import { getProcessDetail, processes } from '@/data' import { useState, useEffect, useMemo, useCallback, useRef } from 'react' import { InputArea } from '@/components/practice/InputArea' import { normalizeAnswer } from '@/utils/practice' type IttoSection = 'inputs' | 'tools' | 'outputs' type CharStatus = 'pending' | 'correct' | 'error' interface PracticeItem { id: string section: IttoSection name: string normalizedAnswer: string } 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>(() => { 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 practiceItems = useMemo(() => { if (!processDetail) return [] const buildItems = (details: any[] | undefined, section: IttoSection): PracticeItem[] => { if (!details) return [] return details.map((d: any, i: number) => { const name: string = d?.name || d?.id || `项${i + 1}` return { id: `${section}-${d?.id || i}`, section, name, normalizedAnswer: normalizeAnswer(name, false), } }) } return [ ...buildItems(processDetail.inputDetails, 'inputs'), ...buildItems(processDetail.toolDetails, 'tools'), ...buildItems(processDetail.outputDetails, 'outputs'), ] }, [processDetail]) // ── 练习模式状态 ────────────────────────────────────────────────────────── const [isPracticeMode, setIsPracticeMode] = useState(false) const [answeredItems, setAnsweredItems] = useState>(new Set()) const [currentPracticeId, setCurrentPracticeId] = useState(null) const [userInput, setUserInput] = useState([]) const [charStatuses, setCharStatuses] = useState([]) const [isComposing, setIsComposing] = useState(false) const isComposingRef = useRef(false) const latestInputRef = useRef([]) const [inputLocked, setInputLocked] = useState(false) const [lastErrorTimestamp, setLastErrorTimestamp] = useState(null) const [showAnswer, setShowAnswer] = useState(false) const longPressTimerRef = useRef(null) const autoAdvanceTimerRef = useRef(null) const currentPracticeItem = useMemo( () => practiceItems.find((item) => item.id === currentPracticeId) ?? null, [practiceItems, currentPracticeId] ) const currentPracticeIndex = practiceItems.findIndex((item) => item.id === currentPracticeId) // 清理定时器 const clearLongPressTimer = useCallback(() => { if (longPressTimerRef.current !== null) { window.clearTimeout(longPressTimerRef.current) longPressTimerRef.current = null } }, []) const clearAutoAdvanceTimer = useCallback(() => { if (autoAdvanceTimerRef.current !== null) { window.clearTimeout(autoAdvanceTimerRef.current) autoAdvanceTimerRef.current = null } }, []) useEffect(() => { return () => { clearLongPressTimer(); clearAutoAdvanceTimer() } }, [clearLongPressTimer, clearAutoAdvanceTimer]) // 切换到某个练习项时重置输入状态 useEffect(() => { if (!isPracticeMode || !currentPracticeItem) return setUserInput(new Array(currentPracticeItem.name.length).fill('')) setCharStatuses(new Array(currentPracticeItem.name.length).fill('pending')) setLastErrorTimestamp(null) setShowAnswer(false) setInputLocked(false) }, [isPracticeMode, currentPracticeItem]) useEffect(() => { latestInputRef.current = userInput }, [userInput]) // 切换过程时自动退出练习(避免定时器跨过程触发) useEffect(() => { exitPractice() // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]) // ── 验证逻辑 ────────────────────────────────────────────────────────────── const validateInput = useCallback( (input: string[]) => { if (!currentPracticeItem) return const originalAnswer = currentPracticeItem.name const normalizedInput = normalizeAnswer(input.join(''), false) const normalizedAns = currentPracticeItem.normalizedAnswer const normalizeChar = (c: string) => normalizeAnswer(c, false) || c const newStatuses = input.map((char, i) => { if (!char) return 'pending' const expected = originalAnswer[i] ?? '' if (!expected) return 'error' return normalizeChar(char) === normalizeChar(expected) ? 'correct' : 'error' }) setCharStatuses(newStatuses) const isComplete = input.every((c) => c !== '') && input.length === originalAnswer.length const isCorrect = isComplete && normalizedInput === normalizedAns if (isCorrect) { setAnsweredItems((prev) => new Set(prev).add(currentPracticeItem.id)) clearAutoAdvanceTimer() if (currentPracticeIndex === practiceItems.length - 1) { autoAdvanceTimerRef.current = window.setTimeout(() => exitPractice(), 500) } else { autoAdvanceTimerRef.current = window.setTimeout(() => { setCurrentPracticeId(practiceItems[currentPracticeIndex + 1].id) }, 300) } } else if (isComplete) { setLastErrorTimestamp(Date.now()) } }, [currentPracticeItem, currentPracticeIndex, practiceItems, clearAutoAdvanceTimer] ) const handleInputChange = useCallback( (newInput: string[]) => { latestInputRef.current = newInput setUserInput(newInput) if (isComposingRef.current) return validateInput(newInput) }, [validateInput] ) const handleCompositionStart = useCallback((_index: number) => { isComposingRef.current = true setIsComposing(true) }, []) const handleCompositionEnd = useCallback( (index: number, value: string) => { isComposingRef.current = false setIsComposing(false) requestAnimationFrame(() => { const cur = latestInputRef.current const newInput = [...cur] if (value) { value.split('').forEach((ch, i) => { if (index + i < newInput.length) newInput[index + i] = ch }) } else { newInput[index] = '' } latestInputRef.current = newInput setUserInput(newInput) validateInput(newInput) }) }, [validateInput] ) const handlePaste = useCallback( (e: React.ClipboardEvent) => { if (!currentPracticeItem) return e.preventDefault() const text = e.clipboardData.getData('text') const targetLen = currentPracticeItem.name.length const newInput = new Array(targetLen).fill('') text.split('').forEach((ch, i) => { if (i < targetLen) newInput[i] = ch }) handleInputChange(newInput) }, [currentPracticeItem, handleInputChange] ) // ── 长按显示答案 ────────────────────────────────────────────────────────── const handleLongPressStart = useCallback(() => { clearLongPressTimer() longPressTimerRef.current = window.setTimeout(() => { setShowAnswer(true) setInputLocked(true) }, 300) }, [clearLongPressTimer]) const handleLongPressEnd = useCallback(() => { clearLongPressTimer() setShowAnswer(false) setInputLocked(false) }, [clearLongPressTimer]) // ── 开始 / 退出练习 ─────────────────────────────────────────────────────── const startPractice = useCallback(() => { if (practiceItems.length === 0) return clearAutoAdvanceTimer() setIsPracticeMode(true) setAnsweredItems(new Set()) setCurrentPracticeId(practiceItems[0].id) }, [practiceItems, clearAutoAdvanceTimer]) const exitPractice = useCallback(() => { clearAutoAdvanceTimer() clearLongPressTimer() setIsPracticeMode(false) setAnsweredItems(new Set()) setCurrentPracticeId(null) setUserInput([]) setCharStatuses([]) setIsComposing(false) isComposingRef.current = false setLastErrorTimestamp(null) setShowAnswer(false) setInputLocked(false) }, [clearAutoAdvanceTimer, clearLongPressTimer]) if (!processDetail) { return (

未找到该过程

返回知识领域
) } const ka = processDetail.knowledgeArea const pg = processDetail.processGroup const purpose = (processDetail as any).purpose 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 const fromMatrix = location.state?.from === 'matrix' return (
{/* 返回按钮 + 面包屑 */}
{fromMatrix && ( )}
{/* 过程标题 - 更紧凑 */}
{processDetail.code}

{processDetail.name}

{processDetail.nameEn}

{ka && ( {ka.name} )} {pg && ( {pg.name} )}
{/* 主要作用 */} {purpose && (

主要作用

{purpose}

)} {/* 全局控制按钮 */} {!isPracticeMode && ( )} {/* ITTO表格 - 更紧凑 */} {/* 输入 */}

输入 ({processDetail.inputs.length})

{!isPracticeMode && ( )}
{isPracticeMode ? ( it.section === 'inputs')} answeredItems={answeredItems} currentPracticeId={currentPracticeId} showAnswer={showAnswer} onLongPressStart={handleLongPressStart} onLongPressEnd={handleLongPressEnd} /> ) : visible.inputs ? (
    {processDetail.inputDetails?.map((inputDetail: any) => { const hasDetail = inputDetail.detail && inputDetail.detail.length > 0 return (
  • {inputDetail.name || inputDetail.id}
    {inputDetail.nameEn &&
    {inputDetail.nameEn}
    } {hasDetail && (
    {inputDetail.detail.map((item: any, idx: number) => ( {item.label} {idx < inputDetail.detail.length - 1 && '、'} ))}
    )} {inputDetail.note && (
    💡 {inputDetail.note}
    )}
  • ) })}
) : (
)}
{/* 工具与技术 */}

工具与技术 ({processDetail.tools.length})

{!isPracticeMode && ( )}
{isPracticeMode ? ( it.section === 'tools')} answeredItems={answeredItems} currentPracticeId={currentPracticeId} showAnswer={showAnswer} onLongPressStart={handleLongPressStart} onLongPressEnd={handleLongPressEnd} /> ) : visible.tools ? (
    {processDetail.toolDetails?.map((toolDetail: any) => { const hasDetail = toolDetail.detail && toolDetail.detail.length > 0 return (
  • {toolDetail.name || toolDetail.id}
    {toolDetail.nameEn &&
    {toolDetail.nameEn}
    } {hasDetail && (
    {toolDetail.detail.map((item: any, idx: number) => ( {item.label} {idx < toolDetail.detail.length - 1 && '、'} ))}
    )} {toolDetail.note && (
    💡 {toolDetail.note}
    )}
  • ) })}
) : (
)}
{/* 输出 */}

输出 ({processDetail.outputs.length})

{!isPracticeMode && ( )}
{isPracticeMode ? ( it.section === 'outputs')} answeredItems={answeredItems} currentPracticeId={currentPracticeId} showAnswer={showAnswer} onLongPressStart={handleLongPressStart} onLongPressEnd={handleLongPressEnd} /> ) : visible.outputs ? (
    {processDetail.outputDetails?.map((outputDetail: any) => { const hasDetail = outputDetail.detail && outputDetail.detail.length > 0 return (
  • {outputDetail.name || outputDetail.id}
    {outputDetail.nameEn &&
    {outputDetail.nameEn}
    } {hasDetail && (
    {outputDetail.detail.map((item: any, idx: number) => ( {item.label} {idx < outputDetail.detail.length - 1 && '、'} ))}
    )} {outputDetail.note && (
    💡 {outputDetail.note}
    )}
  • ) })}
) : (
)}
{/* 练习模式底部输入区域 */} {isPracticeMode && currentPracticeItem && (
第 {currentPracticeIndex + 1} 项 / 共 {practiceItems.length} 项 {showAnswer && ( 答案:{currentPracticeItem.name} )}
)} {/* 前后导航 - 更紧凑 */} {prevProcess ? (
上一个
{prevProcess.code} {prevProcess.name}
) :
} {nextProcess ? (
下一个
{nextProcess.code} {nextProcess.name}
) :
}
) } // ── 练习列表子组件 ────────────────────────────────────────────────────────── interface PracticeListProps { items: PracticeItem[] answeredItems: Set currentPracticeId: string | null showAnswer: boolean onLongPressStart: () => void onLongPressEnd: () => void } function PracticeList({ items, answeredItems, currentPracticeId, showAnswer, onLongPressStart, onLongPressEnd, }: PracticeListProps) { if (items.length === 0) return null return (
    {items.map((item) => { const isAnswered = answeredItems.has(item.id) const isCurrent = item.id === currentPracticeId return (
  • { e.preventDefault(); onLongPressStart() } : undefined} onPointerUp={isCurrent ? onLongPressEnd : undefined} onPointerLeave={isCurrent ? onLongPressEnd : undefined} onPointerCancel={isCurrent ? onLongPressEnd : undefined} > {isAnswered ? (
    {item.name}
    ) : isCurrent && showAnswer ? (
    {item.name} 答案
    ) : isCurrent ? ( {'_'.repeat(item.name.length)} ) : ( {'_'.repeat(item.name.length)} )}
  • ) })}
) }