feat(api-docs): render protected Scalar documentation

This commit is contained in:
2026-08-12 09:05:03 +08:00
parent 1a5489a361
commit ae29e7b2fc
6 changed files with 643 additions and 3359 deletions

View File

@ -18,7 +18,6 @@
},
"dependencies": {
"@hookform/resolvers": "^5.4.0",
"@scalar/api-reference-react": "^0.9.61",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-devtools": "latest",
"@tanstack/react-query": "^5.101.2",

3160
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -41,17 +41,6 @@ export interface NavGroup {
}
export const NAV_GROUPS: NavGroup[] = [
{
label: '开发工具',
items: [
{
label: 'API 文档',
to: '/api-docs',
icon: FileJson,
permission: 'auth:api-docs:read',
},
],
},
{
label: '分类管理',
items: [
@ -112,6 +101,17 @@ export const NAV_GROUPS: NavGroup[] = [
{ label: '机器人会话', to: '/robots', icon: Inbox, permission: 'auth:robot-token:issue' },
],
},
{
label: '开发工具',
items: [
{
label: 'API 文档',
to: '/api-docs',
icon: FileJson,
permission: 'auth:api-docs:read',
},
],
},
]
export function getFirstAccessibleRoute(): string | null {

View File

@ -9,73 +9,68 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login'
import { Route as AuthedRouteImport } from './routes/_authed'
import { Route as ApiDocsRouteImport } from './routes/api-docs'
import { Route as LoginRouteImport } from './routes/login'
import { Route as AuthedIndexRouteImport } from './routes/_authed/index'
import { Route as AuthedSidRouteImport } from './routes/_authed/sid'
import { Route as AuthedRolesRouteImport } from './routes/_authed/roles'
import { Route as AuthedRobotsRouteImport } from './routes/_authed/robots'
import { Route as AuthedLocalesRouteImport } from './routes/_authed/locales'
import { Route as AuthedEventReportsRouteImport } from './routes/_authed/event-reports'
import { Route as AuthedDimzouTranslationsRouteImport } from './routes/_authed/dimzou-translations'
import { Route as AuthedDevicesRouteImport } from './routes/_authed/devices'
import { Route as AuthedCommentReportsRouteImport } from './routes/_authed/comment-reports'
import { Route as AuthedApiDocsRouteImport } from './routes/_authed/api-docs'
import { Route as AuthedAdminUsersRouteImport } from './routes/_authed/admin-users'
import { Route as AuthedUsersIndexRouteImport } from './routes/_authed/users/index'
import { Route as AuthedCommentReportsRouteImport } from './routes/_authed/comment-reports'
import { Route as AuthedDevicesRouteImport } from './routes/_authed/devices'
import { Route as AuthedDimzouTranslationsRouteImport } from './routes/_authed/dimzou-translations'
import { Route as AuthedEventReportsRouteImport } from './routes/_authed/event-reports'
import { Route as AuthedLocalesRouteImport } from './routes/_authed/locales'
import { Route as AuthedRobotsRouteImport } from './routes/_authed/robots'
import { Route as AuthedRolesRouteImport } from './routes/_authed/roles'
import { Route as AuthedSidRouteImport } from './routes/_authed/sid'
import { Route as AuthedCategoriesIndexRouteImport } from './routes/_authed/categories/index'
import { Route as AuthedStudioSubtopicsRouteImport } from './routes/_authed/studio/subtopics'
import { Route as AuthedStudioStylePresetsRouteImport } from './routes/_authed/studio/style-presets'
import { Route as AuthedStudioSeriesRouteImport } from './routes/_authed/studio/series'
import { Route as AuthedStudioRobotAuthorsRouteImport } from './routes/_authed/studio/robot-authors'
import { Route as AuthedStudioCategoryBriefsRouteImport } from './routes/_authed/studio/category-briefs'
import { Route as AuthedStudioArticlesRouteImport } from './routes/_authed/studio/articles'
import { Route as AuthedStudioAgentTokensRouteImport } from './routes/_authed/studio/agent-tokens'
import { Route as AuthedExcProvidersRouteImport } from './routes/_authed/exc/providers'
import { Route as AuthedExcOrdersRouteImport } from './routes/_authed/exc/orders'
import { Route as AuthedExcDispatchesRouteImport } from './routes/_authed/exc/dispatches'
import { Route as AuthedExcDemandsRouteImport } from './routes/_authed/exc/demands'
import { Route as AuthedDimzouPublicationsRouteImport } from './routes/_authed/dimzou/publications'
import { Route as AuthedDimzouDocumentsRouteImport } from './routes/_authed/dimzou/documents'
import { Route as AuthedCategoriesPendingRouteImport } from './routes/_authed/categories/pending'
import { Route as AuthedDimzouDocumentsRouteImport } from './routes/_authed/dimzou/documents'
import { Route as AuthedDimzouPublicationsRouteImport } from './routes/_authed/dimzou/publications'
import { Route as AuthedExcDemandsRouteImport } from './routes/_authed/exc/demands'
import { Route as AuthedExcDispatchesRouteImport } from './routes/_authed/exc/dispatches'
import { Route as AuthedExcOrdersRouteImport } from './routes/_authed/exc/orders'
import { Route as AuthedExcProvidersRouteImport } from './routes/_authed/exc/providers'
import { Route as AuthedStudioAgentTokensRouteImport } from './routes/_authed/studio/agent-tokens'
import { Route as AuthedStudioArticlesRouteImport } from './routes/_authed/studio/articles'
import { Route as AuthedStudioCategoryBriefsRouteImport } from './routes/_authed/studio/category-briefs'
import { Route as AuthedStudioRobotAuthorsRouteImport } from './routes/_authed/studio/robot-authors'
import { Route as AuthedStudioSeriesRouteImport } from './routes/_authed/studio/series'
import { Route as AuthedStudioStylePresetsRouteImport } from './routes/_authed/studio/style-presets'
import { Route as AuthedStudioSubtopicsRouteImport } from './routes/_authed/studio/subtopics'
import { Route as AuthedUsersIndexRouteImport } from './routes/_authed/users/index'
const AuthedRoute = AuthedRouteImport.update({
id: '/_authed',
getParentRoute: () => rootRouteImport,
} as any)
const ApiDocsRoute = ApiDocsRouteImport.update({
id: '/api-docs',
path: '/api-docs',
getParentRoute: () => rootRouteImport,
} as any)
const LoginRoute = LoginRouteImport.update({
id: '/login',
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
const AuthedRoute = AuthedRouteImport.update({
id: '/_authed',
getParentRoute: () => rootRouteImport,
} as any)
const AuthedIndexRoute = AuthedIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedSidRoute = AuthedSidRouteImport.update({
id: '/sid',
path: '/sid',
const AuthedAdminUsersRoute = AuthedAdminUsersRouteImport.update({
id: '/admin-users',
path: '/admin-users',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedRolesRoute = AuthedRolesRouteImport.update({
id: '/roles',
path: '/roles',
const AuthedCommentReportsRoute = AuthedCommentReportsRouteImport.update({
id: '/comment-reports',
path: '/comment-reports',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedRobotsRoute = AuthedRobotsRouteImport.update({
id: '/robots',
path: '/robots',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedLocalesRoute = AuthedLocalesRouteImport.update({
id: '/locales',
path: '/locales',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedEventReportsRoute = AuthedEventReportsRouteImport.update({
id: '/event-reports',
path: '/event-reports',
const AuthedDevicesRoute = AuthedDevicesRouteImport.update({
id: '/devices',
path: '/devices',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedDimzouTranslationsRoute =
@ -84,29 +79,29 @@ const AuthedDimzouTranslationsRoute =
path: '/dimzou-translations',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedDevicesRoute = AuthedDevicesRouteImport.update({
id: '/devices',
path: '/devices',
const AuthedEventReportsRoute = AuthedEventReportsRouteImport.update({
id: '/event-reports',
path: '/event-reports',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedCommentReportsRoute = AuthedCommentReportsRouteImport.update({
id: '/comment-reports',
path: '/comment-reports',
const AuthedLocalesRoute = AuthedLocalesRouteImport.update({
id: '/locales',
path: '/locales',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedApiDocsRoute = AuthedApiDocsRouteImport.update({
id: '/api-docs',
path: '/api-docs',
const AuthedRobotsRoute = AuthedRobotsRouteImport.update({
id: '/robots',
path: '/robots',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedAdminUsersRoute = AuthedAdminUsersRouteImport.update({
id: '/admin-users',
path: '/admin-users',
const AuthedRolesRoute = AuthedRolesRouteImport.update({
id: '/roles',
path: '/roles',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedUsersIndexRoute = AuthedUsersIndexRouteImport.update({
id: '/users/',
path: '/users/',
const AuthedSidRoute = AuthedSidRouteImport.update({
id: '/sid',
path: '/sid',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedCategoriesIndexRoute = AuthedCategoriesIndexRouteImport.update({
@ -114,62 +109,14 @@ const AuthedCategoriesIndexRoute = AuthedCategoriesIndexRouteImport.update({
path: '/categories/',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedStudioSubtopicsRoute = AuthedStudioSubtopicsRouteImport.update({
id: '/studio/subtopics',
path: '/studio/subtopics',
const AuthedCategoriesPendingRoute = AuthedCategoriesPendingRouteImport.update({
id: '/categories/pending',
path: '/categories/pending',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedStudioStylePresetsRoute =
AuthedStudioStylePresetsRouteImport.update({
id: '/studio/style-presets',
path: '/studio/style-presets',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedStudioSeriesRoute = AuthedStudioSeriesRouteImport.update({
id: '/studio/series',
path: '/studio/series',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedStudioRobotAuthorsRoute =
AuthedStudioRobotAuthorsRouteImport.update({
id: '/studio/robot-authors',
path: '/studio/robot-authors',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedStudioCategoryBriefsRoute =
AuthedStudioCategoryBriefsRouteImport.update({
id: '/studio/category-briefs',
path: '/studio/category-briefs',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedStudioArticlesRoute = AuthedStudioArticlesRouteImport.update({
id: '/studio/articles',
path: '/studio/articles',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedStudioAgentTokensRoute = AuthedStudioAgentTokensRouteImport.update({
id: '/studio/agent-tokens',
path: '/studio/agent-tokens',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedExcProvidersRoute = AuthedExcProvidersRouteImport.update({
id: '/exc/providers',
path: '/exc/providers',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedExcOrdersRoute = AuthedExcOrdersRouteImport.update({
id: '/exc/orders',
path: '/exc/orders',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedExcDispatchesRoute = AuthedExcDispatchesRouteImport.update({
id: '/exc/dispatches',
path: '/exc/dispatches',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedExcDemandsRoute = AuthedExcDemandsRouteImport.update({
id: '/exc/demands',
path: '/exc/demands',
const AuthedDimzouDocumentsRoute = AuthedDimzouDocumentsRouteImport.update({
id: '/dimzou/documents',
path: '/dimzou/documents',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedDimzouPublicationsRoute =
@ -178,22 +125,75 @@ const AuthedDimzouPublicationsRoute =
path: '/dimzou/publications',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedDimzouDocumentsRoute = AuthedDimzouDocumentsRouteImport.update({
id: '/dimzou/documents',
path: '/dimzou/documents',
const AuthedExcDemandsRoute = AuthedExcDemandsRouteImport.update({
id: '/exc/demands',
path: '/exc/demands',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedCategoriesPendingRoute = AuthedCategoriesPendingRouteImport.update({
id: '/categories/pending',
path: '/categories/pending',
const AuthedExcDispatchesRoute = AuthedExcDispatchesRouteImport.update({
id: '/exc/dispatches',
path: '/exc/dispatches',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedExcOrdersRoute = AuthedExcOrdersRouteImport.update({
id: '/exc/orders',
path: '/exc/orders',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedExcProvidersRoute = AuthedExcProvidersRouteImport.update({
id: '/exc/providers',
path: '/exc/providers',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedStudioAgentTokensRoute = AuthedStudioAgentTokensRouteImport.update({
id: '/studio/agent-tokens',
path: '/studio/agent-tokens',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedStudioArticlesRoute = AuthedStudioArticlesRouteImport.update({
id: '/studio/articles',
path: '/studio/articles',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedStudioCategoryBriefsRoute =
AuthedStudioCategoryBriefsRouteImport.update({
id: '/studio/category-briefs',
path: '/studio/category-briefs',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedStudioRobotAuthorsRoute =
AuthedStudioRobotAuthorsRouteImport.update({
id: '/studio/robot-authors',
path: '/studio/robot-authors',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedStudioSeriesRoute = AuthedStudioSeriesRouteImport.update({
id: '/studio/series',
path: '/studio/series',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedStudioStylePresetsRoute =
AuthedStudioStylePresetsRouteImport.update({
id: '/studio/style-presets',
path: '/studio/style-presets',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedStudioSubtopicsRoute = AuthedStudioSubtopicsRouteImport.update({
id: '/studio/subtopics',
path: '/studio/subtopics',
getParentRoute: () => AuthedRoute,
} as any)
const AuthedUsersIndexRoute = AuthedUsersIndexRouteImport.update({
id: '/users/',
path: '/users/',
getParentRoute: () => AuthedRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof AuthedIndexRoute
'/api-docs': typeof ApiDocsRoute
'/login': typeof LoginRoute
'/admin-users': typeof AuthedAdminUsersRoute
'/api-docs': typeof AuthedApiDocsRoute
'/comment-reports': typeof AuthedCommentReportsRoute
'/devices': typeof AuthedDevicesRoute
'/dimzou-translations': typeof AuthedDimzouTranslationsRoute
@ -220,9 +220,9 @@ export interface FileRoutesByFullPath {
'/users/': typeof AuthedUsersIndexRoute
}
export interface FileRoutesByTo {
'/api-docs': typeof ApiDocsRoute
'/login': typeof LoginRoute
'/admin-users': typeof AuthedAdminUsersRoute
'/api-docs': typeof AuthedApiDocsRoute
'/comment-reports': typeof AuthedCommentReportsRoute
'/devices': typeof AuthedDevicesRoute
'/dimzou-translations': typeof AuthedDimzouTranslationsRoute
@ -252,9 +252,9 @@ export interface FileRoutesByTo {
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/_authed': typeof AuthedRouteWithChildren
'/api-docs': typeof ApiDocsRoute
'/login': typeof LoginRoute
'/_authed/admin-users': typeof AuthedAdminUsersRoute
'/_authed/api-docs': typeof AuthedApiDocsRoute
'/_authed/comment-reports': typeof AuthedCommentReportsRoute
'/_authed/devices': typeof AuthedDevicesRoute
'/_authed/dimzou-translations': typeof AuthedDimzouTranslationsRoute
@ -285,9 +285,9 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/api-docs'
| '/login'
| '/admin-users'
| '/api-docs'
| '/comment-reports'
| '/devices'
| '/dimzou-translations'
@ -314,9 +314,9 @@ export interface FileRouteTypes {
| '/users/'
fileRoutesByTo: FileRoutesByTo
to:
| '/api-docs'
| '/login'
| '/admin-users'
| '/api-docs'
| '/comment-reports'
| '/devices'
| '/dimzou-translations'
@ -345,9 +345,9 @@ export interface FileRouteTypes {
id:
| '__root__'
| '/_authed'
| '/api-docs'
| '/login'
| '/_authed/admin-users'
| '/_authed/api-docs'
| '/_authed/comment-reports'
| '/_authed/devices'
| '/_authed/dimzou-translations'
@ -377,18 +377,12 @@ export interface FileRouteTypes {
}
export interface RootRouteChildren {
AuthedRoute: typeof AuthedRouteWithChildren
ApiDocsRoute: typeof ApiDocsRoute
LoginRoute: typeof LoginRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
'/_authed': {
id: '/_authed'
path: ''
@ -396,6 +390,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthedRouteImport
parentRoute: typeof rootRouteImport
}
'/api-docs': {
id: '/api-docs'
path: '/api-docs'
fullPath: '/api-docs'
preLoaderRoute: typeof ApiDocsRouteImport
parentRoute: typeof rootRouteImport
}
'/login': {
id: '/login'
path: '/login'
fullPath: '/login'
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
'/_authed/': {
id: '/_authed/'
path: '/'
@ -403,53 +411,11 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthedIndexRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/sid': {
id: '/_authed/sid'
path: '/sid'
fullPath: '/sid'
preLoaderRoute: typeof AuthedSidRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/roles': {
id: '/_authed/roles'
path: '/roles'
fullPath: '/roles'
preLoaderRoute: typeof AuthedRolesRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/robots': {
id: '/_authed/robots'
path: '/robots'
fullPath: '/robots'
preLoaderRoute: typeof AuthedRobotsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/locales': {
id: '/_authed/locales'
path: '/locales'
fullPath: '/locales'
preLoaderRoute: typeof AuthedLocalesRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/event-reports': {
id: '/_authed/event-reports'
path: '/event-reports'
fullPath: '/event-reports'
preLoaderRoute: typeof AuthedEventReportsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/dimzou-translations': {
id: '/_authed/dimzou-translations'
path: '/dimzou-translations'
fullPath: '/dimzou-translations'
preLoaderRoute: typeof AuthedDimzouTranslationsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/devices': {
id: '/_authed/devices'
path: '/devices'
fullPath: '/devices'
preLoaderRoute: typeof AuthedDevicesRouteImport
'/_authed/admin-users': {
id: '/_authed/admin-users'
path: '/admin-users'
fullPath: '/admin-users'
preLoaderRoute: typeof AuthedAdminUsersRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/comment-reports': {
@ -459,25 +425,53 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthedCommentReportsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/api-docs': {
id: '/_authed/api-docs'
path: '/api-docs'
fullPath: '/api-docs'
preLoaderRoute: typeof AuthedApiDocsRouteImport
'/_authed/devices': {
id: '/_authed/devices'
path: '/devices'
fullPath: '/devices'
preLoaderRoute: typeof AuthedDevicesRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/admin-users': {
id: '/_authed/admin-users'
path: '/admin-users'
fullPath: '/admin-users'
preLoaderRoute: typeof AuthedAdminUsersRouteImport
'/_authed/dimzou-translations': {
id: '/_authed/dimzou-translations'
path: '/dimzou-translations'
fullPath: '/dimzou-translations'
preLoaderRoute: typeof AuthedDimzouTranslationsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/users/': {
id: '/_authed/users/'
path: '/users'
fullPath: '/users/'
preLoaderRoute: typeof AuthedUsersIndexRouteImport
'/_authed/event-reports': {
id: '/_authed/event-reports'
path: '/event-reports'
fullPath: '/event-reports'
preLoaderRoute: typeof AuthedEventReportsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/locales': {
id: '/_authed/locales'
path: '/locales'
fullPath: '/locales'
preLoaderRoute: typeof AuthedLocalesRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/robots': {
id: '/_authed/robots'
path: '/robots'
fullPath: '/robots'
preLoaderRoute: typeof AuthedRobotsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/roles': {
id: '/_authed/roles'
path: '/roles'
fullPath: '/roles'
preLoaderRoute: typeof AuthedRolesRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/sid': {
id: '/_authed/sid'
path: '/sid'
fullPath: '/sid'
preLoaderRoute: typeof AuthedSidRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/categories/': {
@ -487,88 +481,11 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthedCategoriesIndexRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/studio/subtopics': {
id: '/_authed/studio/subtopics'
path: '/studio/subtopics'
fullPath: '/studio/subtopics'
preLoaderRoute: typeof AuthedStudioSubtopicsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/studio/style-presets': {
id: '/_authed/studio/style-presets'
path: '/studio/style-presets'
fullPath: '/studio/style-presets'
preLoaderRoute: typeof AuthedStudioStylePresetsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/studio/series': {
id: '/_authed/studio/series'
path: '/studio/series'
fullPath: '/studio/series'
preLoaderRoute: typeof AuthedStudioSeriesRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/studio/robot-authors': {
id: '/_authed/studio/robot-authors'
path: '/studio/robot-authors'
fullPath: '/studio/robot-authors'
preLoaderRoute: typeof AuthedStudioRobotAuthorsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/studio/category-briefs': {
id: '/_authed/studio/category-briefs'
path: '/studio/category-briefs'
fullPath: '/studio/category-briefs'
preLoaderRoute: typeof AuthedStudioCategoryBriefsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/studio/articles': {
id: '/_authed/studio/articles'
path: '/studio/articles'
fullPath: '/studio/articles'
preLoaderRoute: typeof AuthedStudioArticlesRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/studio/agent-tokens': {
id: '/_authed/studio/agent-tokens'
path: '/studio/agent-tokens'
fullPath: '/studio/agent-tokens'
preLoaderRoute: typeof AuthedStudioAgentTokensRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/exc/providers': {
id: '/_authed/exc/providers'
path: '/exc/providers'
fullPath: '/exc/providers'
preLoaderRoute: typeof AuthedExcProvidersRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/exc/orders': {
id: '/_authed/exc/orders'
path: '/exc/orders'
fullPath: '/exc/orders'
preLoaderRoute: typeof AuthedExcOrdersRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/exc/dispatches': {
id: '/_authed/exc/dispatches'
path: '/exc/dispatches'
fullPath: '/exc/dispatches'
preLoaderRoute: typeof AuthedExcDispatchesRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/exc/demands': {
id: '/_authed/exc/demands'
path: '/exc/demands'
fullPath: '/exc/demands'
preLoaderRoute: typeof AuthedExcDemandsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/dimzou/publications': {
id: '/_authed/dimzou/publications'
path: '/dimzou/publications'
fullPath: '/dimzou/publications'
preLoaderRoute: typeof AuthedDimzouPublicationsRouteImport
'/_authed/categories/pending': {
id: '/_authed/categories/pending'
path: '/categories/pending'
fullPath: '/categories/pending'
preLoaderRoute: typeof AuthedCategoriesPendingRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/dimzou/documents': {
@ -578,11 +495,95 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthedDimzouDocumentsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/categories/pending': {
id: '/_authed/categories/pending'
path: '/categories/pending'
fullPath: '/categories/pending'
preLoaderRoute: typeof AuthedCategoriesPendingRouteImport
'/_authed/dimzou/publications': {
id: '/_authed/dimzou/publications'
path: '/dimzou/publications'
fullPath: '/dimzou/publications'
preLoaderRoute: typeof AuthedDimzouPublicationsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/exc/demands': {
id: '/_authed/exc/demands'
path: '/exc/demands'
fullPath: '/exc/demands'
preLoaderRoute: typeof AuthedExcDemandsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/exc/dispatches': {
id: '/_authed/exc/dispatches'
path: '/exc/dispatches'
fullPath: '/exc/dispatches'
preLoaderRoute: typeof AuthedExcDispatchesRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/exc/orders': {
id: '/_authed/exc/orders'
path: '/exc/orders'
fullPath: '/exc/orders'
preLoaderRoute: typeof AuthedExcOrdersRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/exc/providers': {
id: '/_authed/exc/providers'
path: '/exc/providers'
fullPath: '/exc/providers'
preLoaderRoute: typeof AuthedExcProvidersRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/studio/agent-tokens': {
id: '/_authed/studio/agent-tokens'
path: '/studio/agent-tokens'
fullPath: '/studio/agent-tokens'
preLoaderRoute: typeof AuthedStudioAgentTokensRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/studio/articles': {
id: '/_authed/studio/articles'
path: '/studio/articles'
fullPath: '/studio/articles'
preLoaderRoute: typeof AuthedStudioArticlesRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/studio/category-briefs': {
id: '/_authed/studio/category-briefs'
path: '/studio/category-briefs'
fullPath: '/studio/category-briefs'
preLoaderRoute: typeof AuthedStudioCategoryBriefsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/studio/robot-authors': {
id: '/_authed/studio/robot-authors'
path: '/studio/robot-authors'
fullPath: '/studio/robot-authors'
preLoaderRoute: typeof AuthedStudioRobotAuthorsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/studio/series': {
id: '/_authed/studio/series'
path: '/studio/series'
fullPath: '/studio/series'
preLoaderRoute: typeof AuthedStudioSeriesRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/studio/style-presets': {
id: '/_authed/studio/style-presets'
path: '/studio/style-presets'
fullPath: '/studio/style-presets'
preLoaderRoute: typeof AuthedStudioStylePresetsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/studio/subtopics': {
id: '/_authed/studio/subtopics'
path: '/studio/subtopics'
fullPath: '/studio/subtopics'
preLoaderRoute: typeof AuthedStudioSubtopicsRouteImport
parentRoute: typeof AuthedRoute
}
'/_authed/users/': {
id: '/_authed/users/'
path: '/users'
fullPath: '/users/'
preLoaderRoute: typeof AuthedUsersIndexRouteImport
parentRoute: typeof AuthedRoute
}
}
@ -590,7 +591,6 @@ declare module '@tanstack/react-router' {
interface AuthedRouteChildren {
AuthedAdminUsersRoute: typeof AuthedAdminUsersRoute
AuthedApiDocsRoute: typeof AuthedApiDocsRoute
AuthedCommentReportsRoute: typeof AuthedCommentReportsRoute
AuthedDevicesRoute: typeof AuthedDevicesRoute
AuthedDimzouTranslationsRoute: typeof AuthedDimzouTranslationsRoute
@ -620,7 +620,6 @@ interface AuthedRouteChildren {
const AuthedRouteChildren: AuthedRouteChildren = {
AuthedAdminUsersRoute: AuthedAdminUsersRoute,
AuthedApiDocsRoute: AuthedApiDocsRoute,
AuthedCommentReportsRoute: AuthedCommentReportsRoute,
AuthedDevicesRoute: AuthedDevicesRoute,
AuthedDimzouTranslationsRoute: AuthedDimzouTranslationsRoute,
@ -653,6 +652,7 @@ const AuthedRouteWithChildren =
const rootRouteChildren: RootRouteChildren = {
AuthedRoute: AuthedRouteWithChildren,
ApiDocsRoute: ApiDocsRoute,
LoginRoute: LoginRoute,
}
export const routeTree = rootRouteImport

View File

@ -1,91 +0,0 @@
import { ApiReferenceReact } from '@scalar/api-reference-react'
import '@scalar/api-reference-react/style.css'
import { useQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
import { useState } from 'react'
import { ApiError } from '@/api/client'
import { fetchApiDocument, type ApiDocument } from '@/api/modules/api-docs'
import { PERM, requirePermission } from '@/auth/permissions'
import { PageHeader } from '@/components/page-header'
import { Button } from '@/components/ui/button'
export const Route = createFileRoute('/_authed/api-docs')({
beforeLoad: () => requirePermission(PERM.API_DOCS_READ),
component: ApiDocumentationPage,
})
const DOCUMENTS: Array<{ value: ApiDocument; label: string }> = [
{ value: 'public', label: '公共 API' },
{ value: 'admin', label: '管理端 API' },
]
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 (
<div>
<PageHeader
title="API 文档"
description="仅向拥有开发者文档权限的后台账号开放"
actions={
<div className="flex gap-2">
{DOCUMENTS.map((item) => (
<Button
key={item.value}
variant={document === item.value ? 'default' : 'outline'}
onClick={() => setDocument(item.value)}
>
{item.label}
</Button>
))}
</div>
}
/>
{specification.isPending && (
<div className="rounded-lg border p-8 text-center text-sm text-muted-foreground">
API
</div>
)}
{notGenerated && (
<div className="rounded-lg border border-dashed p-8 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>
)}
{specification.isError && !notGenerated && (
<div className="rounded-lg border border-destructive/40 p-8 text-center">
<p className="font-medium text-destructive">API </p>
<Button className="mt-4" variant="outline" onClick={() => void specification.refetch()}>
</Button>
</div>
)}
{specification.data && (
<div className="overflow-hidden rounded-lg border">
<ApiReferenceReact
key={document}
configuration={{
content: specification.data,
agent: { disabled: true },
hideModels: false,
}}
/>
</div>
)}
</div>
)
}

156
src/routes/api-docs.tsx Normal file
View File

@ -0,0 +1,156 @@
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'
export const Route = createFileRoute('/api-docs')({
beforeLoad: async () => {
if (typeof window !== 'undefined' && !isAuthenticated()) {
throw redirect({ to: '/login' })
}
const me = await fetchMe()
const roles = me.meta?.roles ?? []
const permissions = me.meta?.permissions ?? []
useAuthStore.getState().setSession(me.data, roles, permissions)
requirePermission(PERM.API_DOCS_READ)
},
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>
)
}