feat: 新增接口一览页 /interface,滚动信息区域收窄

- 新页面 /interface:按用途分组的接口表(方法 / 路径 / 说明 / 权限),
  用库里的 Card + Table + Tag 排;服务端 SPA 路由加入该路径
- 侧边栏「查看接口」不再直接打开 /api/projects 的原始 JSON,改指到该页面
- 页头滚动信息宽度上限 720px(原来铺满,约减少 1/3)
This commit is contained in:
2026-11-01 20:53:55 +08:00
parent 036551ee09
commit 27a28ab583
6 changed files with 160 additions and 4 deletions
+131
View File
@@ -0,0 +1,131 @@
import { Card, Space, Table, Tag, Text, Title } from '@stash/ui'
/**
* 接口一览:把 `/api/*` 按用途分组列出来。
*
* 页面只做展示,不请求后端——清单跟 `src/server.rs` 的路由表人工对齐,
* 改接口时顺手改这里(文件顶部有分组与权限说明)。
*/
type Endpoint = {
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'
path: string
summary: string
/** 需要登录会话 */
auth?: boolean
}
const GROUPS: { title: string; desc: string; items: Endpoint[] }[] = [
{
title: '健康与项目',
desc: '托管目录的扫描结果与实时推送。',
items: [
{ method: 'GET', path: '/api/health', summary: '存活检查,返回服务名与状态' },
{ method: 'GET', path: '/api/projects', summary: '扫描托管目录(每次请求都重新扫描)' },
{ method: 'GET', path: '/api/projects/events', summary: 'SSE:目录变化时推送最新项目列表' },
],
},
{
title: '内容库',
desc: '分区 / 分组 / 条目 / 卡片。写操作都需要登录会话与同源 Origin。',
items: [
{ method: 'GET', path: '/api/catalog', summary: '旧版完整目录(兼容保留)' },
{ method: 'GET', path: '/api/catalog2', summary: '分组树 + 卡片,后台配置用' },
{ method: 'GET', path: '/api/catalog/events', summary: 'SSE:内容库变更时推送「该重拉」' },
{ method: 'POST', path: '/api/nodes', summary: '新建分区 / 分组', auth: true },
{ method: 'PATCH', path: '/api/nodes/{key}', summary: '改标题、所属、图标、排序、启停', auth: true },
{ method: 'DELETE', path: '/api/nodes/{key}', summary: '删除分区 / 分组(级联子节点)', auth: true },
{ method: 'GET', path: '/api/items/{key}', summary: '条目详情' },
{ method: 'POST', path: '/api/items', summary: '新建条目', auth: true },
{ method: 'PATCH', path: '/api/items/{key}', summary: '就地编辑(带 updatedAt 乐观锁)', auth: true },
{ method: 'DELETE', path: '/api/items/{key}', summary: '删除条目', auth: true },
{ method: 'POST', path: '/api/items/{key}/view', summary: '浏览数 +1' },
{ method: 'POST', path: '/api/items/{key}/like', summary: '点赞 / 取消点赞' },
{ method: 'POST', path: '/api/cards', summary: '新建首页卡片', auth: true },
{ method: 'PATCH', path: '/api/cards/{id}', summary: '改卡片标题 / 顺序', auth: true },
{ method: 'DELETE', path: '/api/cards/{id}', summary: '删除卡片', auth: true },
{ method: 'PUT', path: '/api/cards/{id}/groups', summary: '整组替换卡片包含的分组与顺序', auth: true },
{ method: 'POST', path: '/api/categories', summary: '旧版:新建分类', auth: true },
{ method: 'PATCH', path: '/api/categories/{id}', summary: '旧版:改分类', auth: true },
{ method: 'DELETE', path: '/api/categories/{id}', summary: '旧版:删分类', auth: true },
{ method: 'POST', path: '/api/categories/{id}/groups', summary: '旧版:新建分组', auth: true },
{ method: 'PATCH', path: '/api/groups/{id}', summary: '旧版:改分组', auth: true },
{ method: 'DELETE', path: '/api/groups/{id}', summary: '旧版:删分组', auth: true },
],
},
{
title: '站点设置',
desc: '页头、侧边栏、卡片、资料、站点信息等整份 JSON。',
items: [
{ method: 'GET', path: '/api/settings', summary: '读取设置(含 version 乐观锁)' },
{ method: 'PATCH', path: '/api/settings', summary: '整份保存设置', auth: true },
{ method: 'GET', path: '/api/backup', summary: '下载 SQLite 快照', auth: true },
],
},
{
title: '认证',
desc: '单管理员账号;会话 token 只存 sha256,Cookie 为 HttpOnly + SameSite=Strict。',
items: [
{ method: 'GET', path: '/api/auth/session', summary: '当前会话状态(是否登录、是否已设密码)' },
{ method: 'POST', path: '/api/auth/setup', summary: '首次设置管理员密码(仅空库可用)' },
{ method: 'POST', path: '/api/auth/login', summary: '登录;连续失败 5 次锁 60 秒' },
{ method: 'POST', path: '/api/auth/logout', summary: '退出并清掉会话' },
{ method: 'PATCH', path: '/api/auth/password', summary: '改密码(需当前密码)', auth: true },
],
},
]
const METHOD_TONE = {
GET: 'neutral',
POST: 'primary',
PATCH: 'warning',
PUT: 'warning',
DELETE: 'danger',
} as const
export function ApiDocs() {
return (
<div className="api-docs">
<Space direction="column" gap="sm" align="stretch">
<Title level={2}>接口一览</Title>
<Text size="sm" tone="muted">
说明:这是 Stash 自带的 HTTP 接口清单,全部挂在同一个端口上。读接口公开,写接口需要登录会话并校验同源
Origin;带 <Tag tone="primary">需登录</Tag> 标记的即为写操作。
</Text>
</Space>
{GROUPS.map((group) => (
<Card key={group.title} padding="lg">
<Space direction="column" gap="sm" align="stretch">
<Space gap="sm" align="center">
<Title level={4} as="h2">
{group.title}
</Title>
<Text size="sm" tone="muted">
{group.items.length} 个
</Text>
</Space>
<Text size="sm" tone="muted">
{group.desc}
</Text>
<Table
rowKey="id"
columns={[
{ key: 'method', title: '方法', width: 90 },
{ key: 'path', title: '路径', width: 280 },
{ key: 'summary', title: '说明' },
{ key: 'auth', title: '权限', width: 100 },
]}
rows={group.items.map((item) => ({
id: `${item.method} ${item.path}`,
method: <Tag tone={METHOD_TONE[item.method]}>{item.method}</Tag>,
path: <code className="api-path">{item.path}</code>,
summary: item.summary,
auth: item.auth === true ? <Tag tone="primary">需登录</Tag> : <Text tone="muted">公开</Text>,
}))}
/>
</Space>
</Card>
))}
</div>
)
}
@@ -77,7 +77,7 @@ export function SiteHeader({
/>
{headerEnabled && (
<Ticker speed={headerSpeed} gap={headerGap} label="运行状态">
<Ticker speed={headerSpeed} gap={headerGap} label="运行状态" className="header-ticker">
{headerItems.map((item, index) => (
<span className="header-ticker-item" key={`${index}-${item.text}`}>
{item.tag !== undefined && item.tag !== null && item.tag !== '' && (
@@ -56,7 +56,7 @@ export function ServicePanel({ projects, live, refreshing, onRefresh }: ServiceP
重新扫描
</Button>
<a className="link-button" href="/api/projects" target="_blank" rel="noreferrer">
<a className="link-button" href="/interface">
<Icon name="link_3_line" />
查看接口
</a>