From 27a28ab5838dee4bb57b03849f0096ac211a002b Mon Sep 17 00:00:00 2001 From: CNWei Date: Sun, 1 Nov 2026 20:53:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=B8=80=E8=A7=88=E9=A1=B5=20/interface=EF=BC=8C=E6=BB=9A?= =?UTF-8?q?=E5=8A=A8=E4=BF=A1=E6=81=AF=E5=8C=BA=E5=9F=9F=E6=94=B6=E7=AA=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新页面 /interface:按用途分组的接口表(方法 / 路径 / 说明 / 权限), 用库里的 Card + Table + Tag 排;服务端 SPA 路由加入该路径 - 侧边栏「查看接口」不再直接打开 /api/projects 的原始 JSON,改指到该页面 - 页头滚动信息宽度上限 720px(原来铺满,约减少 1/3) --- src/server.rs | 2 +- stash-web/src/App.css | 21 +++ stash-web/src/AppNext.tsx | 6 +- stash-web/src/components/api/ApiDocs.tsx | 131 ++++++++++++++++++ .../src/components/header/SiteHeader.tsx | 2 +- .../src/components/sidebar/ServicePanel.tsx | 2 +- 6 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 stash-web/src/components/api/ApiDocs.tsx diff --git a/src/server.rs b/src/server.rs index 2eac9f1..24a39f3 100644 --- a/src/server.rs +++ b/src/server.rs @@ -164,7 +164,7 @@ fn not_found_page(path: &str) -> Response { const SPA_PREFIXES: &[&str] = &["/c/", "/g/"]; /// 精确匹配的前端页面(没有实体文件,返回入口页) -const SPA_PAGES: &[&str] = &["/login", "/admin"]; +const SPA_PAGES: &[&str] = &["/login", "/admin", "/interface"]; /// 是否为前端路由路径(没有实体文件,应该返回入口页)。 fn is_spa_path(path: &str) -> bool { diff --git a/stash-web/src/App.css b/stash-web/src/App.css index 129fcd4..76bbbe1 100644 --- a/stash-web/src/App.css +++ b/stash-web/src/App.css @@ -133,6 +133,11 @@ white-space: nowrap; } +/* 滚动信息只占中间约 2/3:原来铺满整条,信息一长就把两边挤没了 */ +.header-ticker { + max-width: 720px; +} + .header-actions { margin-left: auto; } @@ -1031,6 +1036,22 @@ gap: var(--su-space-2); } +/* 接口一览页 */ +.api-docs { + display: flex; + flex-direction: column; + gap: var(--su-space-5); +} + +.api-path { + padding: 0 var(--su-space-2); + border-radius: var(--su-radius-xs); + background-color: var(--su-bg-subtle); + color: var(--su-text-sub); + font-family: var(--su-font-mono); + font-size: var(--su-font-size-sm); +} + /* 数据管理:分组下拉与「新建数据」紧挨着 */ .entry-toolbar { display: flex; diff --git a/stash-web/src/AppNext.tsx b/stash-web/src/AppNext.tsx index da4a78f..7b05919 100644 --- a/stash-web/src/AppNext.tsx +++ b/stash-web/src/AppNext.tsx @@ -18,6 +18,7 @@ import { ItemEditor } from './components/content/ItemEditor' import { PosterGrid } from './components/content/PosterGrid' import { SectionCard } from './components/content/SectionCard' import { SiteHeader } from './components/header/SiteHeader' +import { ApiDocs } from './components/api/ApiDocs' import { GroupMark } from './components/common/GroupMark' import { LoginDialog } from './components/auth/LoginDialog' import { ProfileCard } from './components/sidebar/ProfileCard' @@ -59,9 +60,11 @@ type Route = | { kind: 'home' } | { kind: 'group'; key: string } | { kind: 'item'; key: string; itemId: string } + | { kind: 'api' } function parseRoute(pathname: string): Route { const segments = pathname.split('/').filter(Boolean) + if (segments[0] === 'interface') return { kind: 'api' } if ( (segments[0] === 'g' || segments[0] === 'c') && segments[1] !== undefined @@ -358,7 +361,7 @@ export default function AppNext() { }, [manage.groups]) const activeKey = - route.kind === 'home' + route.kind === 'home' || route.kind === 'api' ? 'home' : route.kind === 'group' ? `g:${route.key}` @@ -740,6 +743,7 @@ export default function AppNext() { )} {route.kind === 'home' && renderHome()} + {route.kind === 'api' && } {route.kind === 'group' && renderGroupPage(route.key)} {route.kind === 'item' && renderItemPage(route.key, route.itemId)} diff --git a/stash-web/src/components/api/ApiDocs.tsx b/stash-web/src/components/api/ApiDocs.tsx new file mode 100644 index 0000000..168d7f2 --- /dev/null +++ b/stash-web/src/components/api/ApiDocs.tsx @@ -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 ( +
+ + 接口一览 + + 说明:这是 Stash 自带的 HTTP 接口清单,全部挂在同一个端口上。读接口公开,写接口需要登录会话并校验同源 + Origin;带 需登录 标记的即为写操作。 + + + + {GROUPS.map((group) => ( + + + + + {group.title} + + + {group.items.length} 个 + + + + {group.desc} + + ({ + id: `${item.method} ${item.path}`, + method: {item.method}, + path: {item.path}, + summary: item.summary, + auth: item.auth === true ? 需登录 : 公开, + }))} + /> + + + ))} + + ) +} diff --git a/stash-web/src/components/header/SiteHeader.tsx b/stash-web/src/components/header/SiteHeader.tsx index 7501928..8cf850c 100644 --- a/stash-web/src/components/header/SiteHeader.tsx +++ b/stash-web/src/components/header/SiteHeader.tsx @@ -77,7 +77,7 @@ export function SiteHeader({ /> {headerEnabled && ( - + {headerItems.map((item, index) => ( {item.tag !== undefined && item.tag !== null && item.tag !== '' && ( diff --git a/stash-web/src/components/sidebar/ServicePanel.tsx b/stash-web/src/components/sidebar/ServicePanel.tsx index 34ff8ac..7d4b7f5 100644 --- a/stash-web/src/components/sidebar/ServicePanel.tsx +++ b/stash-web/src/components/sidebar/ServicePanel.tsx @@ -56,7 +56,7 @@ export function ServicePanel({ projects, live, refreshing, onRefresh }: ServiceP 重新扫描 - + 查看接口