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
重新扫描
-
+
查看接口