Test API documentation viewer
This commit is contained in:
+1
-137
@@ -1,12 +1,8 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ApiError } from '@/api/client'
|
||||
import { fetchApiDocument, type ApiDocument } from '@/api/modules/api-docs'
|
||||
import { fetchMe } from '@/api/modules/auth'
|
||||
import { PERM, requirePermission } from '@/auth/permissions'
|
||||
import { isAuthenticated, useAuthStore } from '@/auth/store'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ApiDocumentationPage } from '@/features/api-docs/page'
|
||||
|
||||
export const Route = createFileRoute('/api-docs')({
|
||||
beforeLoad: async () => {
|
||||
@@ -22,135 +18,3 @@ export const Route = createFileRoute('/api-docs')({
|
||||
},
|
||||
component: ApiDocumentationPage,
|
||||
})
|
||||
|
||||
interface ScalarApi {
|
||||
createApiReference: (
|
||||
element: HTMLElement,
|
||||
configuration: Record<string, unknown>,
|
||||
) => void
|
||||
}
|
||||
|
||||
const DOCUMENTS: Array<{ value: ApiDocument; label: string }> = [
|
||||
{ value: 'public', label: '公共 API' },
|
||||
{ value: 'admin', label: '管理端 API' },
|
||||
]
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Scalar?: ScalarApi
|
||||
}
|
||||
}
|
||||
|
||||
let scalarLoader: Promise<ScalarApi> | null = null
|
||||
|
||||
function loadScalar(): Promise<ScalarApi> {
|
||||
if (window.Scalar) return Promise.resolve(window.Scalar)
|
||||
if (scalarLoader) return scalarLoader
|
||||
|
||||
scalarLoader = new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/@scalar/api-reference'
|
||||
script.async = true
|
||||
script.onload = () =>
|
||||
window.Scalar ? resolve(window.Scalar) : reject(new Error('Scalar failed to initialize'))
|
||||
script.onerror = () => reject(new Error('Scalar failed to load'))
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
|
||||
return scalarLoader
|
||||
}
|
||||
|
||||
function ScalarReference({ content }: { content: string }) {
|
||||
const container = useRef<HTMLDivElement>(null)
|
||||
const [failed, setFailed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
setFailed(false)
|
||||
|
||||
void loadScalar()
|
||||
.then((scalar) => {
|
||||
if (!active || !container.current) return
|
||||
container.current.replaceChildren()
|
||||
scalar.createApiReference(container.current, { content })
|
||||
})
|
||||
.catch(() => {
|
||||
if (active) setFailed(true)
|
||||
})
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
container.current?.replaceChildren()
|
||||
}
|
||||
}, [content])
|
||||
|
||||
if (failed) {
|
||||
return <div className="p-8 text-center text-destructive">文档阅读器加载失败,请重试。</div>
|
||||
}
|
||||
|
||||
return <div ref={container} className="min-h-svh" />
|
||||
}
|
||||
|
||||
function ApiDocumentationPage() {
|
||||
const [document, setDocument] = useState<ApiDocument>('public')
|
||||
const specification = useQuery({
|
||||
queryKey: ['api-docs', document],
|
||||
queryFn: ({ signal }) => fetchApiDocument(document, signal),
|
||||
})
|
||||
|
||||
const notGenerated =
|
||||
specification.error instanceof ApiError &&
|
||||
specification.error.code === 'API_DOCUMENT_NOT_GENERATED'
|
||||
|
||||
return (
|
||||
<main className="min-h-svh bg-background">
|
||||
<div className="fixed bottom-4 right-4 z-[100] flex gap-1 rounded-lg border bg-background/90 p-1 shadow-lg backdrop-blur">
|
||||
{DOCUMENTS.map((item) => (
|
||||
<Button
|
||||
key={item.value}
|
||||
size="sm"
|
||||
variant={document === item.value ? 'default' : 'ghost'}
|
||||
onClick={() => setDocument(item.value)}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{specification.isPending && (
|
||||
<div className="grid min-h-svh place-items-center p-8">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
正在加载 API 文档…
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notGenerated && (
|
||||
<div className="grid min-h-svh place-items-center p-8">
|
||||
<div className="text-center">
|
||||
<p className="font-medium">文档尚未生成</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
请联系运维人员运行{' '}
|
||||
<code className="font-mono">php artisan api-docs:generate</code>。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{specification.isError && !notGenerated && (
|
||||
<div className="grid min-h-svh place-items-center p-8">
|
||||
<div className="text-center">
|
||||
<p className="font-medium text-destructive">API 文档加载失败</p>
|
||||
<Button className="mt-4" variant="outline" onClick={() => void specification.refetch()}>
|
||||
重试
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{specification.data && (
|
||||
<ScalarReference key={document} content={specification.data} />
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user