mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-21 14:30:59 +08:00
Compare commits
11 Commits
feature/us
...
feature/ap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f36bcb74ca | ||
|
|
2263fd97db | ||
|
|
9234d1099e | ||
|
|
373dece19d | ||
|
|
b09828bcc2 | ||
|
|
8751a7707c | ||
|
|
f91b240802 | ||
|
|
062b289f7a | ||
|
|
c1dc77f6db | ||
|
|
cea60175c2 | ||
|
|
2bd3630512 |
65
docs/components/api-overview.tsx
Normal file
65
docs/components/api-overview.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { getOpenAPIOperations } from "@/lib/openapi-operations";
|
||||||
|
|
||||||
|
const methodColors: Record<string, string> = {
|
||||||
|
GET: "bg-emerald-100 text-emerald-700",
|
||||||
|
POST: "bg-blue-100 text-blue-700",
|
||||||
|
PUT: "bg-amber-100 text-amber-700",
|
||||||
|
PATCH: "bg-purple-100 text-purple-700",
|
||||||
|
DELETE: "bg-rose-100 text-rose-700",
|
||||||
|
};
|
||||||
|
|
||||||
|
function MethodBadge({ method }: { method: string }) {
|
||||||
|
const color = methodColors[method] ?? "bg-slate-100 text-slate-700";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`font-semibold uppercase tracking-wide text-xs px-2 py-1 rounded ${color}`}
|
||||||
|
>
|
||||||
|
{method}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function APIOverviewTable() {
|
||||||
|
const operations = getOpenAPIOperations();
|
||||||
|
|
||||||
|
if (operations.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="not-prose mt-6 overflow-x-auto">
|
||||||
|
<table className="w-full border-separate border-spacing-y-2 text-sm">
|
||||||
|
<thead className="text-left text-muted-foreground">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-2 font-medium">路径</th>
|
||||||
|
<th className="px-3 py-2 font-medium">方法</th>
|
||||||
|
<th className="px-3 py-2 font-medium">摘要</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{operations.map((operation) => (
|
||||||
|
<tr
|
||||||
|
key={`${operation.method}-${operation.route}`}
|
||||||
|
className="bg-muted/30"
|
||||||
|
>
|
||||||
|
<td className="px-3 py-2 align-top font-mono">
|
||||||
|
<Link className="hover:underline" href={operation.href}>
|
||||||
|
{operation.route}
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 align-top">
|
||||||
|
<MethodBadge method={operation.method} />
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 align-top text-muted-foreground">
|
||||||
|
{operation.summary || "—"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,3 +2,11 @@
|
|||||||
title: API 概览
|
title: API 概览
|
||||||
description: Open API 接口文档
|
description: Open API 接口文档
|
||||||
---
|
---
|
||||||
|
|
||||||
|
import { APIOverviewTable } from "@/components/api-overview";
|
||||||
|
|
||||||
|
# 接口列表
|
||||||
|
|
||||||
|
以下列表聚合了所有已生成的接口页面,展示对应的路径、请求方法以及摘要,便于快速检索和跳转。
|
||||||
|
|
||||||
|
<APIOverviewTable />
|
||||||
|
|||||||
68
docs/lib/openapi-operations.ts
Normal file
68
docs/lib/openapi-operations.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import matter from "gray-matter";
|
||||||
|
|
||||||
|
import { source } from "@/lib/source";
|
||||||
|
|
||||||
|
interface OperationFrontmatter {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
_openapi?: {
|
||||||
|
method?: string;
|
||||||
|
route?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenAPIOperation {
|
||||||
|
href: string;
|
||||||
|
method: string;
|
||||||
|
route: string;
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFrontmatter(content: string): OperationFrontmatter {
|
||||||
|
const result = matter(content);
|
||||||
|
|
||||||
|
return result.data as OperationFrontmatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSummary(frontmatter: OperationFrontmatter): string {
|
||||||
|
return frontmatter.title ?? frontmatter.description ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOpenAPIOperations(): OpenAPIOperation[] {
|
||||||
|
return source
|
||||||
|
.getPages()
|
||||||
|
.filter((page) =>
|
||||||
|
page.url.startsWith("/openapi/") && page.url !== "/openapi"
|
||||||
|
)
|
||||||
|
.map((page) => {
|
||||||
|
if (typeof page.data.content !== "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontmatter = parseFrontmatter(page.data.content);
|
||||||
|
|
||||||
|
const method = frontmatter._openapi?.method?.toUpperCase();
|
||||||
|
const route = frontmatter._openapi?.route;
|
||||||
|
const summary = normalizeSummary(frontmatter);
|
||||||
|
|
||||||
|
if (!method || !route) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
href: page.url,
|
||||||
|
method,
|
||||||
|
route,
|
||||||
|
summary,
|
||||||
|
} satisfies OpenAPIOperation;
|
||||||
|
})
|
||||||
|
.filter((operation): operation is OpenAPIOperation => Boolean(operation))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const routeCompare = a.route.localeCompare(b.route);
|
||||||
|
if (routeCompare !== 0) {
|
||||||
|
return routeCompare;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.method.localeCompare(b.method);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
|
import { rmSync } from "node:fs";
|
||||||
|
|
||||||
import { generateFiles } from "fumadocs-openapi";
|
import { generateFiles } from "fumadocs-openapi";
|
||||||
import { openapi } from "@/lib/openapi";
|
import { openapi } from "@/lib/openapi";
|
||||||
|
|
||||||
|
const outputDir = "./content/docs/openapi/(generated)";
|
||||||
|
|
||||||
|
rmSync(outputDir, { recursive: true, force: true });
|
||||||
|
|
||||||
void generateFiles({
|
void generateFiles({
|
||||||
input: openapi,
|
input: openapi,
|
||||||
output: "./content/docs/openapi/(generated)",
|
output: outputDir,
|
||||||
// we recommend to enable it
|
// we recommend to enable it
|
||||||
// make sure your endpoint description doesn't break MDX syntax.
|
// make sure your endpoint description doesn't break MDX syntax.
|
||||||
includeDescription: true,
|
includeDescription: true,
|
||||||
|
per: "operation",
|
||||||
|
groupBy: "route",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -121,6 +121,19 @@ body {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-icon {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
/* .vditor {
|
/* .vditor {
|
||||||
--textarea-background-color: transparent;
|
--textarea-background-color: transparent;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="timeline" :class="{ 'hover-enabled': hover }">
|
<div class="timeline">
|
||||||
<div class="timeline-item" v-for="(item, idx) in items" :key="idx">
|
<div class="timeline-item" v-for="(item, idx) in items" :key="idx">
|
||||||
<div
|
<div
|
||||||
class="timeline-icon"
|
class="timeline-icon"
|
||||||
@@ -26,7 +26,6 @@ export default {
|
|||||||
name: 'BaseTimeline',
|
name: 'BaseTimeline',
|
||||||
props: {
|
props: {
|
||||||
items: { type: Array, default: () => [] },
|
items: { type: Array, default: () => [] },
|
||||||
hover: { type: Boolean, default: false },
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -46,12 +45,6 @@ export default {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover-enabled .timeline-item:hover {
|
|
||||||
background-color: var(--menu-selected-background-color);
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-icon {
|
.timeline-icon {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
发布评论
|
发布评论
|
||||||
<span class="shortcut-icon" v-if="!isMobile"> {{ isMac ? '⌘' : 'Ctrl' }} ⏎ </span>
|
<span class="shortcut-icon" v-if="!isMobile"> {{ isMac ? '⌘' : 'Ctrl' }} ⏎ </span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else> <loading-four /> 发布中... </template>
|
<template v-else> <loading-four class="loading-icon" /> 发布中... </template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
发送
|
发送
|
||||||
<span class="shortcut-icon" v-if="!isMobile"> {{ isMac ? '⌘' : 'Ctrl' }} ⏎ </span>
|
<span class="shortcut-icon" v-if="!isMobile"> {{ isMac ? '⌘' : 'Ctrl' }} ⏎ </span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else> <loading-four /> 发送中... </template>
|
<template v-else> <loading-four class="loading-icon" /> 发送中... </template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="about-api-title">API文档和调试入口</div>
|
<div class="about-api-title">API文档和调试入口</div>
|
||||||
<div class="about-api-link">API Playground <share /></div>
|
<a href="http://docs.open-isle.com" target="_blank" rel="noopener" class="about-api-link">
|
||||||
|
API 文档与 Playground <share />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -233,6 +235,7 @@ export default {
|
|||||||
.about-api-link {
|
.about-api-link {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-api-link:hover {
|
.about-api-link:hover {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<div v-else class="login-page-button-primary disabled">
|
<div v-else class="login-page-button-primary disabled">
|
||||||
<div class="login-page-button-text">
|
<div class="login-page-button-text">
|
||||||
<loading-four />
|
<loading-four class="loading-icon" />
|
||||||
登录中...
|
登录中...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,9 @@
|
|||||||
>
|
>
|
||||||
发布
|
发布
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="post-submit-loading"><loading-four /> 发布中...</div>
|
<div v-else class="post-submit-loading">
|
||||||
|
<loading-four class="loading-icon" /> 发布中...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LotteryForm v-if="postType === 'LOTTERY'" :data="lottery" />
|
<LotteryForm v-if="postType === 'LOTTERY'" :data="lottery" />
|
||||||
|
|||||||
@@ -26,7 +26,9 @@
|
|||||||
>
|
>
|
||||||
更新
|
更新
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="post-submit-loading"><loading-four /> 更新中...</div>
|
<div v-else class="post-submit-loading">
|
||||||
|
<loading-four class="loading-icon" /> 更新中...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="signup-page-button-primary disabled">
|
<div v-else class="signup-page-button-primary disabled">
|
||||||
<div class="signup-page-button-text">
|
<div class="signup-page-button-text">
|
||||||
<loading-four />
|
<loading-four class="loading-icon" />
|
||||||
发送中...
|
发送中...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="signup-page-button-primary disabled">
|
<div v-else class="signup-page-button-primary disabled">
|
||||||
<div class="signup-page-button-text">
|
<div class="signup-page-button-text">
|
||||||
<loading-four />
|
<loading-four class="loading-icon" />
|
||||||
验证中...
|
验证中...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user