feat: 新增接口一览页 /interface,滚动信息区域收窄
- 新页面 /interface:按用途分组的接口表(方法 / 路径 / 说明 / 权限), 用库里的 Card + Table + Tag 排;服务端 SPA 路由加入该路径 - 侧边栏「查看接口」不再直接打开 /api/projects 的原始 JSON,改指到该页面 - 页头滚动信息宽度上限 720px(原来铺满,约减少 1/3)
This commit is contained in:
+1
-1
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' && <ApiDocs />}
|
||||
{route.kind === 'group' && renderGroupPage(route.key)}
|
||||
{route.kind === 'item' && renderItemPage(route.key, route.itemId)}
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user