feat: add knowledge API docs

This commit is contained in:
ittoview
2026-05-09 16:52:07 +01:00
parent 1b9f6da480
commit 1e38167f15
372 changed files with 18581 additions and 2 deletions

View File

@@ -14,6 +14,7 @@ import PrinciplesPage from './pages/PrinciplesPage'
import { PerformanceDomainsPage } from './pages/PerformanceDomainsPage'
import KnowledgeAreasTailoringPage from './pages/KnowledgeAreasTailoringPage'
import { LearningMapsPage } from './pages/LearningMapsPage'
import { ApiDocPage } from './pages/ApiDocPage'
function App() {
return (
@@ -36,6 +37,7 @@ function App() {
<Route path="/artifact/:id" element={<ArtifactDetailPage />} />
<Route path="/tool/:id" element={<ToolDetailPage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/doc" element={<ApiDocPage />} />
</Routes>
</Layout>
)

361
src/pages/ApiDocPage.tsx Normal file
View File

@@ -0,0 +1,361 @@
import { useMemo, useState } from 'react'
import { motion } from 'framer-motion'
import { CheckCircle2, Copy, Play, Server, TerminalSquare } from 'lucide-react'
type ApiEndpoint = {
id: string
name: string
method: 'GET'
path: string
samplePath: string
description: string
fields: string[]
}
const endpoints: ApiEndpoint[] = [
{
id: 'process-groups',
name: '过程组列表',
method: 'GET',
path: '/api/process-groups.json',
samplePath: '/api/process-groups.json',
description: '返回五大过程组基础信息。',
fields: ['id过程组 ID', 'name过程组名称'],
},
{
id: 'knowledge-areas',
name: '知识领域列表',
method: 'GET',
path: '/api/knowledge-areas.json',
samplePath: '/api/knowledge-areas.json',
description: '返回十大知识领域及其裁剪因素。',
fields: ['id知识领域 ID', 'name知识领域名称', 'tailoringFactors裁剪因素数组', 'title裁剪因素标题', 'description裁剪因素说明'],
},
{
id: 'knowledge-area-tailoring',
name: '知识领域裁剪因素',
method: 'GET',
path: '/api/knowledge-areas/{id}/tailoring-factors.json',
samplePath: '/api/knowledge-areas/KA01/tailoring-factors.json',
description: '返回指定知识领域的裁剪因素。',
fields: ['title裁剪因素标题', 'description裁剪因素说明'],
},
{
id: 'knowledge-area-processes',
name: '知识领域过程',
method: 'GET',
path: '/api/knowledge-areas/{id}/processes.json',
samplePath: '/api/knowledge-areas/KA01/processes.json',
description: '返回指定知识领域下的过程。',
fields: ['id过程 ID', 'name过程名称', 'processGroupId过程组 ID', 'processGroupName过程组名称', 'purpose主要作用'],
},
{
id: 'process-group-processes',
name: '过程组过程',
method: 'GET',
path: '/api/process-groups/{id}/processes.json',
samplePath: '/api/process-groups/PG02/processes.json',
description: '返回指定过程组下的过程。',
fields: ['id过程 ID', 'name过程名称', 'knowledgeAreaId知识领域 ID', 'knowledgeAreaName知识领域名称', 'purpose主要作用'],
},
{
id: 'processes',
name: '过程列表',
method: 'GET',
path: '/api/processes.json',
samplePath: '/api/processes.json',
description: '返回 49 个项目管理过程。',
fields: ['id过程 ID', 'name过程名称', 'knowledgeAreaId知识领域 ID', 'knowledgeAreaName知识领域名称', 'processGroupId过程组 ID', 'processGroupName过程组名称', 'purpose主要作用'],
},
{
id: 'process-detail',
name: '过程详情',
method: 'GET',
path: '/api/processes/{id}.json',
samplePath: '/api/processes/P1.1.json',
description: '返回指定过程基础信息。',
fields: ['id过程 ID', 'name过程名称', 'knowledgeAreaId知识领域 ID', 'knowledgeAreaName知识领域名称', 'processGroupId过程组 ID', 'processGroupName过程组名称', 'purpose主要作用'],
},
{
id: 'process-itto',
name: '过程 ITTO',
method: 'GET',
path: '/api/processes/{id}/itto.json',
samplePath: '/api/processes/P1.1/itto.json',
description: '返回指定过程的输入、工具与技术、输出。',
fields: ['id过程 ID', 'name过程名称', 'inputs输入数组', 'tools工具数组', 'outputs输出数组', 'details明细项数组', 'note过程语境备注'],
},
{
id: 'performance-domains',
name: '绩效域列表',
method: 'GET',
path: '/api/performance-domains.json',
samplePath: '/api/performance-domains.json',
description: '返回八大项目绩效域。',
fields: ['id绩效域 ID', 'name绩效域名称'],
},
{
id: 'performance-domain-detail',
name: '绩效域详情',
method: 'GET',
path: '/api/performance-domains/{id}.json',
samplePath: '/api/performance-domains/PD01.json',
description: '返回指定绩效域的目标、要点、交互与检查项。',
fields: ['id绩效域 ID', 'name绩效域名称', 'expectedGoals预期目标', 'keyPoints绩效要点', 'interactions相互作用', 'checks检查方法', 'goal检查目标', 'indicators检查指标'],
},
{
id: 'artifact-usage',
name: '工件使用情况',
method: 'GET',
path: '/api/artifacts/{id}/usage.json',
samplePath: '/api/artifacts/A001/usage.json',
description: '返回指定工件作为输入或输出的过程。',
fields: ['id工件 ID', 'name工件名称', 'asInput作为输入被哪些过程使用', 'asOutput由哪些过程输出'],
},
{
id: 'tool-usage',
name: '工具与技术使用情况',
method: 'GET',
path: '/api/tools/{id}/usage.json',
samplePath: '/api/tools/TT001/usage.json',
description: '返回指定工具与技术出现的过程。',
fields: ['id工具 ID', 'name工具名称', 'usedIn使用该工具的过程数组'],
},
]
const fieldGroups = [
{
title: '通用字段',
items: ['id稳定编号', 'name中文名称', 'purpose主要作用'],
},
{
title: '过程字段',
items: ['knowledgeAreaId所属知识领域 ID', 'knowledgeAreaName所属知识领域名称', 'processGroupId所属过程组 ID', 'processGroupName所属过程组名称'],
},
{
title: '引用字段',
items: ['inputs输入数组', 'tools工具数组', 'outputs输出数组', 'details明细项数组', 'note补充说明'],
},
]
function formatJson(value: unknown) {
return JSON.stringify(value, null, 2)
}
export function ApiDocPage() {
const [selectedId, setSelectedId] = useState(endpoints[0].id)
const selectedEndpoint = useMemo(
() => endpoints.find((endpoint) => endpoint.id === selectedId) ?? endpoints[0],
[selectedId]
)
const [requestPath, setRequestPath] = useState(selectedEndpoint.samplePath)
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<string>('')
const [status, setStatus] = useState<string>('')
function handleEndpointChange(endpointId: string) {
const endpoint = endpoints.find((item) => item.id === endpointId) ?? endpoints[0]
setSelectedId(endpoint.id)
setRequestPath(endpoint.samplePath)
setStatus('')
setResult('')
}
async function handleFetchTest() {
setLoading(true)
setStatus('请求中')
setResult('')
try {
const response = await fetch(requestPath, { headers: { Accept: 'application/json' } })
const contentType = response.headers.get('content-type') ?? ''
const body = contentType.includes('application/json') ? await response.json() : await response.text()
setStatus(`${response.status} ${response.statusText || 'OK'}`)
setResult(typeof body === 'string' ? body : formatJson(body))
} catch (error) {
setStatus('请求失败')
setResult(error instanceof Error ? error.message : '无法完成请求')
} finally {
setLoading(false)
}
}
async function copyPath() {
await navigator.clipboard?.writeText(requestPath)
}
return (
<div className="mx-auto max-w-7xl space-y-6">
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
className="rounded-2xl bg-white p-6 shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700"
>
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<div className="mb-3 inline-flex items-center gap-2 rounded-full bg-indigo-50 px-3 py-1 text-xs font-medium text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300">
<Server size={14} />
</div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">API </h1>
<p className="mt-2 max-w-2xl text-sm leading-6 text-gray-500 dark:text-gray-400">
JSON GET /api
</p>
</div>
<div className="grid grid-cols-3 gap-3 text-center">
<div className="rounded-xl bg-gray-50 px-4 py-3 dark:bg-gray-900/50">
<div className="text-lg font-bold text-gray-900 dark:text-white">{endpoints.length}</div>
<div className="text-xs text-gray-500 dark:text-gray-400"></div>
</div>
<div className="rounded-xl bg-gray-50 px-4 py-3 dark:bg-gray-900/50">
<div className="text-lg font-bold text-gray-900 dark:text-white">GET</div>
<div className="text-xs text-gray-500 dark:text-gray-400"></div>
</div>
<div className="rounded-xl bg-gray-50 px-4 py-3 dark:bg-gray-900/50">
<div className="text-lg font-bold text-gray-900 dark:text-white">JSON</div>
<div className="text-xs text-gray-500 dark:text-gray-400"></div>
</div>
</div>
</div>
</motion.div>
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_420px]">
<motion.section
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.04 }}
className="rounded-2xl bg-white shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700"
>
<div className="border-b border-gray-100 px-5 py-4 dark:border-gray-700">
<h2 className="font-semibold text-gray-900 dark:text-white"></h2>
</div>
<div className="divide-y divide-gray-100 dark:divide-gray-700">
{endpoints.map((endpoint) => (
<button
key={endpoint.id}
type="button"
onClick={() => handleEndpointChange(endpoint.id)}
className={`w-full px-5 py-4 text-left transition-colors hover:bg-gray-50 dark:hover:bg-gray-700/40 ${
selectedEndpoint.id === endpoint.id ? 'bg-indigo-50/70 dark:bg-indigo-900/20' : ''
}`}
>
<div className="flex flex-col gap-2 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="rounded-md bg-emerald-50 px-2 py-0.5 text-xs font-bold text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300">
{endpoint.method}
</span>
<h3 className="font-medium text-gray-900 dark:text-white">{endpoint.name}</h3>
</div>
<p className="mt-1 font-mono text-xs text-indigo-600 dark:text-indigo-300">{endpoint.path}</p>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">{endpoint.description}</p>
</div>
{selectedEndpoint.id === endpoint.id && <CheckCircle2 className="h-5 w-5 text-indigo-500" />}
</div>
</button>
))}
</div>
</motion.section>
<motion.aside
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.08 }}
className="space-y-6"
>
<section className="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700">
<div className="mb-4 flex items-center gap-2">
<TerminalSquare className="h-5 w-5 text-indigo-500" />
<h2 className="font-semibold text-gray-900 dark:text-white"></h2>
</div>
<label className="text-sm font-medium text-gray-700 dark:text-gray-300" htmlFor="api-endpoint">
</label>
<select
id="api-endpoint"
value={selectedId}
onChange={(event) => handleEndpointChange(event.target.value)}
className="mt-2 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-900 outline-none transition focus:border-indigo-500 focus:ring-2 focus:ring-indigo-100 dark:border-gray-600 dark:bg-gray-900 dark:text-white dark:focus:ring-indigo-900/40"
>
{endpoints.map((endpoint) => (
<option key={endpoint.id} value={endpoint.id}>{endpoint.name}</option>
))}
</select>
<label className="mt-4 block text-sm font-medium text-gray-700 dark:text-gray-300" htmlFor="api-path">
</label>
<div className="mt-2 flex gap-2">
<input
id="api-path"
value={requestPath}
onChange={(event) => setRequestPath(event.target.value)}
className="min-w-0 flex-1 rounded-lg border border-gray-200 bg-white px-3 py-2 font-mono text-sm text-gray-900 outline-none transition focus:border-indigo-500 focus:ring-2 focus:ring-indigo-100 dark:border-gray-600 dark:bg-gray-900 dark:text-white dark:focus:ring-indigo-900/40"
/>
<button
type="button"
onClick={copyPath}
className="rounded-lg border border-gray-200 px-3 text-gray-600 transition hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
aria-label="复制请求路径"
>
<Copy size={17} />
</button>
</div>
<button
type="button"
onClick={handleFetchTest}
disabled={loading || !requestPath.trim()}
className="mt-4 inline-flex w-full items-center justify-center gap-2 rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-60"
>
<Play size={16} />
{loading ? '请求中' : '发送请求'}
</button>
{status && (
<div className="mt-4 rounded-lg bg-gray-50 p-3 dark:bg-gray-900/60">
<div className="mb-2 text-xs font-medium text-gray-500 dark:text-gray-400"></div>
<div className="mb-3 text-sm font-semibold text-gray-900 dark:text-white">{status}</div>
<pre className="max-h-96 overflow-auto whitespace-pre-wrap break-words rounded-lg bg-gray-950 p-3 text-xs leading-5 text-gray-100">
{result}
</pre>
</div>
)}
</section>
<section className="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700">
<h2 className="font-semibold text-gray-900 dark:text-white"></h2>
<div className="mt-4 space-y-4">
{fieldGroups.map((group) => (
<div key={group.title}>
<h3 className="text-sm font-medium text-gray-800 dark:text-gray-200">{group.title}</h3>
<ul className="mt-2 space-y-1 text-sm leading-6 text-gray-500 dark:text-gray-400">
{group.items.map((item) => <li key={item}> {item}</li>)}
</ul>
</div>
))}
</div>
</section>
</motion.aside>
</div>
<motion.section
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.12 }}
className="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-gray-100 dark:bg-gray-800 dark:ring-gray-700"
>
<h2 className="font-semibold text-gray-900 dark:text-white">{selectedEndpoint.name}</h2>
<div className="mt-3 grid gap-2 md:grid-cols-2 xl:grid-cols-3">
{selectedEndpoint.fields.map((field) => (
<div key={field} className="rounded-lg bg-gray-50 px-3 py-2 text-sm text-gray-600 dark:bg-gray-900/50 dark:text-gray-300">
{field}
</div>
))}
</div>
</motion.section>
</div>
)
}