feat: add ITTO collections view
This commit is contained in:
@@ -17,6 +17,7 @@ import PerformanceDomainPracticePage from './pages/PerformanceDomainPracticePage
|
|||||||
import KnowledgeAreasTailoringPage from './pages/KnowledgeAreasTailoringPage'
|
import KnowledgeAreasTailoringPage from './pages/KnowledgeAreasTailoringPage'
|
||||||
import { LearningMapsPage } from './pages/LearningMapsPage'
|
import { LearningMapsPage } from './pages/LearningMapsPage'
|
||||||
import { ApiDocPage } from './pages/ApiDocPage'
|
import { ApiDocPage } from './pages/ApiDocPage'
|
||||||
|
import { IttoCollectionsPage } from './pages/IttoCollectionsPage'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -30,6 +31,7 @@ function App() {
|
|||||||
<Route path="/process-groups/:id" element={<ProcessGroupsPage />} />
|
<Route path="/process-groups/:id" element={<ProcessGroupsPage />} />
|
||||||
<Route path="/process/:id" element={<ProcessDetailPage />} />
|
<Route path="/process/:id" element={<ProcessDetailPage />} />
|
||||||
<Route path="/process-matrix" element={<ProcessMatrixPage />} />
|
<Route path="/process-matrix" element={<ProcessMatrixPage />} />
|
||||||
|
<Route path="/itto-collections" element={<IttoCollectionsPage />} />
|
||||||
<Route path="/process-graph" element={<ProcessGraphPage />} />
|
<Route path="/process-graph" element={<ProcessGraphPage />} />
|
||||||
<Route path="/process-practice" element={<ProcessPracticePage />} />
|
<Route path="/process-practice" element={<ProcessPracticePage />} />
|
||||||
<Route path="/process-purpose-practice" element={<ProcessPurposePracticePage />} />
|
<Route path="/process-purpose-practice" element={<ProcessPurposePracticePage />} />
|
||||||
|
|||||||
197
src/pages/IttoCollectionsPage.tsx
Normal file
197
src/pages/IttoCollectionsPage.tsx
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { FileOutput, FileText, Wrench } from 'lucide-react'
|
||||||
|
import {
|
||||||
|
artifactMap,
|
||||||
|
knowledgeAreas,
|
||||||
|
processGroups,
|
||||||
|
processes,
|
||||||
|
toolMap,
|
||||||
|
normalizeProcessRef,
|
||||||
|
} from '@/data'
|
||||||
|
import type { Process, ProcessRef } from '@/types/itto'
|
||||||
|
|
||||||
|
type ViewKey = 'inputs' | 'tools' | 'outputs'
|
||||||
|
|
||||||
|
type CollectionRow = {
|
||||||
|
processGroupId: string
|
||||||
|
processGroupName: string
|
||||||
|
color: string
|
||||||
|
items: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollectionArea = {
|
||||||
|
id: string
|
||||||
|
order: number
|
||||||
|
name: string
|
||||||
|
nameEn: string
|
||||||
|
color: string
|
||||||
|
rows: CollectionRow[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs: Array<{ key: ViewKey; label: string; icon: typeof FileText }> = [
|
||||||
|
{ key: 'inputs', label: '输入', icon: FileText },
|
||||||
|
{ key: 'tools', label: '工具', icon: Wrench },
|
||||||
|
{ key: 'outputs', label: '输出', icon: FileOutput },
|
||||||
|
]
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: { opacity: 1, transition: { staggerChildren: 0.03 } },
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 10 },
|
||||||
|
visible: { opacity: 1, y: 0 },
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRef(ref: ProcessRef, viewKey: ViewKey) {
|
||||||
|
const normalized = normalizeProcessRef(ref)
|
||||||
|
const entity = viewKey === 'tools' ? toolMap.get(normalized.id) : artifactMap.get(normalized.id)
|
||||||
|
const name = entity?.name ?? normalized.id
|
||||||
|
const detail = normalized.detail?.map((item) => item.label).filter(Boolean) ?? []
|
||||||
|
|
||||||
|
if (!detail.length) return name
|
||||||
|
return `${name}(${detail.join('、')})`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRefsByView(process: Process, viewKey: ViewKey) {
|
||||||
|
if (viewKey === 'inputs') return process.inputs
|
||||||
|
if (viewKey === 'tools') return process.tools
|
||||||
|
return process.outputs
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueItems(items: string[]) {
|
||||||
|
return Array.from(new Set(items))
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCollection(viewKey: ViewKey): CollectionArea[] {
|
||||||
|
return knowledgeAreas.map((area) => {
|
||||||
|
const areaProcesses = processes.filter((process) => process.knowledgeAreaId === area.id)
|
||||||
|
const rows = processGroups.map((group) => {
|
||||||
|
const groupProcesses = areaProcesses.filter((process) => process.processGroupId === group.id)
|
||||||
|
const items = uniqueItems(
|
||||||
|
groupProcesses.flatMap((process) =>
|
||||||
|
getRefsByView(process, viewKey).map((ref) => formatRef(ref, viewKey))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
processGroupId: group.id,
|
||||||
|
processGroupName: group.name,
|
||||||
|
color: group.color,
|
||||||
|
items,
|
||||||
|
}
|
||||||
|
}).filter((row) => row.items.length > 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: area.id,
|
||||||
|
order: area.order,
|
||||||
|
name: area.name,
|
||||||
|
nameEn: area.nameEn,
|
||||||
|
color: area.color,
|
||||||
|
rows,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IttoCollectionsPage() {
|
||||||
|
const [activeTab, setActiveTab] = useState<ViewKey>('inputs')
|
||||||
|
const collection = useMemo(() => buildCollection(activeTab), [activeTab])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-bold text-gray-900 dark:text-white">输入 · 工具 · 输出</h1>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">按知识领域与过程组汇总</p>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex rounded-xl bg-white p-1 shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const Icon = tab.icon
|
||||||
|
const isActive = activeTab === tab.key
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.key}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveTab(tab.key)}
|
||||||
|
className={`inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
|
||||||
|
isActive
|
||||||
|
? 'bg-indigo-600 text-white shadow-sm'
|
||||||
|
: 'text-gray-600 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon size={16} />
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
key={activeTab}
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
{collection.map((area) => (
|
||||||
|
<motion.section
|
||||||
|
key={area.id}
|
||||||
|
variants={itemVariants}
|
||||||
|
className="overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-3 border-b border-gray-100 p-4 dark:border-gray-700"
|
||||||
|
style={{ backgroundColor: `${area.color}10` }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-sm font-bold text-white"
|
||||||
|
style={{ backgroundColor: area.color }}
|
||||||
|
>
|
||||||
|
{area.order}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h2 className="text-base font-semibold text-gray-900 dark:text-white">{area.name}</h2>
|
||||||
|
<p className="truncate text-xs text-gray-500 dark:text-gray-400">{area.nameEn}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
|
<thead className="bg-gray-50 dark:bg-gray-900/40">
|
||||||
|
<tr>
|
||||||
|
<th className="w-36 px-4 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400">
|
||||||
|
过程组
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400">
|
||||||
|
对应集合
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
|
{area.rows.map((row) => (
|
||||||
|
<tr key={row.processGroupId} className="align-top">
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span
|
||||||
|
className="inline-flex rounded-full px-2.5 py-1 text-xs font-medium text-white"
|
||||||
|
style={{ backgroundColor: row.color }}
|
||||||
|
>
|
||||||
|
{row.processGroupName}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm leading-6 text-gray-700 dark:text-gray-300">
|
||||||
|
{row.items.join(';')}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -795,7 +795,7 @@ export function ProcessDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
{showAnswer && practiceMode === 'proficient' && (
|
{showAnswer && practiceMode === 'proficient' && (
|
||||||
<div className="text-indigo-600 dark:text-indigo-300">
|
<div className="text-indigo-600 dark:text-indigo-300">
|
||||||
当前显示:{SECTION_LABELS[currentSection]}分组未完成答案({currentSectionRemainingItems.length} 项)
|
{SECTION_LABELS[currentSection]}分组未完成答案({currentSectionRemainingItems.length} 项)
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showAnswer && practiceMode === 'standard' && currentPracticeItem && (
|
{showAnswer && practiceMode === 'standard' && currentPracticeItem && (
|
||||||
@@ -1021,7 +1021,7 @@ function PracticeList({
|
|||||||
{'_'.repeat(item.name.length)}
|
{'_'.repeat(item.name.length)}
|
||||||
</span>
|
</span>
|
||||||
{mode === 'proficient' && isCurrentSection && (
|
{mode === 'proficient' && isCurrentSection && (
|
||||||
<span className="text-[11px] text-indigo-300 dark:text-indigo-500">待输入</span>
|
<span className="text-[11px] text-indigo-300 dark:text-indigo-500">待作答</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export function ToolDetailPage() {
|
|||||||
|
|
||||||
{usedByProcesses.length === 0 && (
|
{usedByProcesses.length === 0 && (
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-sm border border-gray-100 dark:border-gray-700 text-center text-gray-500 dark:text-gray-400">
|
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-sm border border-gray-100 dark:border-gray-700 text-center text-gray-500 dark:text-gray-400">
|
||||||
暂无使用此工具的过程记录
|
未找到相关过程
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ export { SettingsPage } from './SettingsPage'
|
|||||||
|
|
||||||
export { PerformanceDomainsPage } from './PerformanceDomainsPage'
|
export { PerformanceDomainsPage } from './PerformanceDomainsPage'
|
||||||
export { LearningMapsPage } from './LearningMapsPage'
|
export { LearningMapsPage } from './LearningMapsPage'
|
||||||
|
export { IttoCollectionsPage } from './IttoCollectionsPage'
|
||||||
|
|||||||
Reference in New Issue
Block a user