diff --git a/src/App.tsx b/src/App.tsx
index 243a423..ed32875 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -12,6 +12,7 @@ import { SettingsPage } from './pages/SettingsPage'
import ProcessPracticePage from './pages/ProcessPracticePage'
import PrinciplesPage from './pages/PrinciplesPage'
import { PerformanceDomainsPage } from './pages/PerformanceDomainsPage'
+import KnowledgeAreasTailoringPage from './pages/KnowledgeAreasTailoringPage'
function App() {
return (
@@ -20,6 +21,7 @@ function App() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/src/pages/KnowledgeAreasTailoringPage.tsx b/src/pages/KnowledgeAreasTailoringPage.tsx
new file mode 100644
index 0000000..d6c6c01
--- /dev/null
+++ b/src/pages/KnowledgeAreasTailoringPage.tsx
@@ -0,0 +1,792 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import clsx from 'clsx'
+import { motion } from 'framer-motion'
+import { Lightbulb, Maximize2, Minimize2, RotateCcw } from 'lucide-react'
+import { knowledgeAreas } from '@/data'
+import { InputArea } from '@/components/practice/InputArea'
+import { CelebrationAnimation } from '@/components/practice/CelebrationAnimation'
+import { useLongPress } from '@/hooks/useLongPress'
+import { announceToScreenReader, normalizeAnswer } from '@/utils/practice'
+import {
+ generateTailoringPracticeItems,
+ type TailoringPracticeItem,
+} from '@/utils/tailoringPractice'
+import { useAppStore } from '@/stores/useAppStore'
+
+type CharStatus = 'pending' | 'correct' | 'error'
+
+const STORAGE_KEY = 'knowledge-areas-tailoring-practice-progress'
+
+interface AnswerOverlayState {
+ itemId: string
+ answer: string
+ expiresAt: number
+}
+
+interface FactorTitleCellProps {
+ item: TailoringPracticeItem
+ isPracticeMode: boolean
+ isAnswered: boolean
+ isCurrent: boolean
+ showAnswer: string | null
+ onLongPress: (itemId: string) => void
+ onLongPressEnd: () => void
+ onClick: (itemId: string) => void
+}
+
+function FactorTitleCell({
+ item,
+ isPracticeMode,
+ isAnswered,
+ isCurrent,
+ showAnswer,
+ onLongPress,
+ onLongPressEnd,
+ onClick,
+}: FactorTitleCellProps) {
+ const longPressHandlers = useLongPress(item.id, {
+ onLongPress,
+ onLongPressEnd,
+ })
+
+ if (!isPracticeMode) {
+ return (
+
+
+ {item.factorIndex + 1}
+
+ {item.title}
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+export default function KnowledgeAreasTailoringPage() {
+ const setSidebarOpen = useAppStore((s) => s.setSidebarOpen)
+ const [practiceItems] = useState(() => generateTailoringPracticeItems())
+ const practiceItemMap = useMemo(
+ () => new Map(practiceItems.map((item) => [item.id, item])),
+ [practiceItems]
+ )
+ const sortedKnowledgeAreas = useMemo(
+ () => [...knowledgeAreas].sort((a, b) => a.order - b.order),
+ []
+ )
+
+ const groupedKnowledgeAreas = useMemo(
+ () =>
+ sortedKnowledgeAreas.map((knowledgeArea) => ({
+ ...knowledgeArea,
+ items: practiceItems.filter((item) => item.knowledgeAreaId === knowledgeArea.id),
+ })),
+ [practiceItems, sortedKnowledgeAreas]
+ )
+
+ const loadProgress = useCallback(() => {
+ const validIds = new Set(practiceItems.map((item) => item.id))
+
+ try {
+ const saved = localStorage.getItem(STORAGE_KEY)
+ if (!saved) {
+ return {
+ answeredItems: new Map(),
+ currentItemId: practiceItems[0]?.id ?? null,
+ }
+ }
+
+ const parsed = JSON.parse(saved)
+ const answeredEntries = Array.isArray(parsed.answeredItems)
+ ? parsed.answeredItems.filter(
+ (entry: unknown) =>
+ Array.isArray(entry) && entry[1] === true && typeof entry[0] === 'string' && validIds.has(entry[0])
+ )
+ : []
+
+ return {
+ answeredItems: new Map(answeredEntries),
+ currentItemId:
+ typeof parsed.currentItemId === 'string' && validIds.has(parsed.currentItemId)
+ ? parsed.currentItemId
+ : practiceItems[0]?.id ?? null,
+ }
+ } catch (error) {
+ console.error('加载裁剪因素练习进度失败:', error)
+ return {
+ answeredItems: new Map(),
+ currentItemId: practiceItems[0]?.id ?? null,
+ }
+ }
+ }, [practiceItems])
+
+ const [isPracticeMode, setIsPracticeMode] = useState(false)
+ const [isFullScreen, setIsFullScreen] = useState(false)
+ const [answeredItems, setAnsweredItems] = useState