feat: add knowledge API docs
This commit is contained in:
@@ -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
361
src/pages/ApiDocPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user