前台 - 搜索框从内容列移到 Layout 的 toolbar 插槽,去掉 fixed + TOP_OFFSET + top-blur 三处补偿 - 左右栏滚动容器不变,左栏吸顶改用 CSS sticky(修回顶时侧栏抖动) - 只让分组导航滚动,资料卡与本地服务固定 - 页头操作支持配置:搜索改为刷新(重扫项目 + 重拉内容库) - 站点级设置:页面标题 / favicon / 品牌 logo / 品牌文字 - 停用的分组在侧边栏、首页卡片、分组页、条目页统一不再出现 后台 - 「侧边栏」「首页卡片」合并为「首页」Tab - 首页卡片表格新增状态列与顺序列(上下箭头即时保存) - 分组显隐只由「分组管理 → 状态」决定,去掉侧栏独立勾选 - 页头 Tab 增加站点信息、首页 Logo(带实时预览)、操作工具开关 - 后台页头与前台同构并吸顶 后端 - settings 新增 site(标题 / favicon / 品牌 / 操作工具)与 sidebar.show_nav - 写接口成功后广播内容库变更,SSE 改为「广播 + data_version 兜底」 - catalog SSE 事件名与前端监听对齐(catalog)
204 lines
5.8 KiB
TypeScript
204 lines
5.8 KiB
TypeScript
/**
|
||
* 原子模型的后台接口:分组树 + 卡片。
|
||
*
|
||
* 分组是唯一容器,卡片只是「勾选分组」的展示组合;两者都通过业务键关联。
|
||
*/
|
||
export type GroupFormat = 'apps' | 'posters' | 'articles'
|
||
|
||
export type GroupEntry = {
|
||
id: string
|
||
format?: GroupFormat
|
||
name?: string
|
||
title?: string
|
||
href?: string
|
||
description?: string
|
||
desc?: string
|
||
}
|
||
|
||
export type GroupNode = {
|
||
key: string
|
||
parentKey?: string | null
|
||
title: string
|
||
format: GroupFormat
|
||
icon: string
|
||
barColor: string
|
||
previewLimit?: number | null
|
||
sort: number
|
||
/** 是否启用;禁用后前台不展示 */
|
||
enabled: boolean
|
||
entries: GroupEntry[]
|
||
children: GroupNode[]
|
||
}
|
||
|
||
export type CardGroupRef = {
|
||
groupKey: string
|
||
previewLimit?: number | null
|
||
}
|
||
|
||
export type CardItem = {
|
||
id: number
|
||
key: string
|
||
title: string
|
||
sort: number
|
||
groups: CardGroupRef[]
|
||
}
|
||
|
||
export type ManageData = {
|
||
groups: GroupNode[]
|
||
cards: CardItem[]
|
||
}
|
||
|
||
export type SimpleResult = { ok: true } | { ok: false; error: string }
|
||
|
||
async function errorMessage(response: Response, fallback: string): Promise<string> {
|
||
const payload = (await response.json().catch(() => null)) as { error?: string } | null
|
||
return payload?.error ?? `${fallback}(${response.status})`
|
||
}
|
||
|
||
export async function fetchManage(): Promise<ManageData> {
|
||
try {
|
||
const response = await fetch('/api/catalog2', { credentials: 'same-origin' })
|
||
if (!response.ok) return { groups: [], cards: [] }
|
||
return (await response.json()) as ManageData
|
||
} catch {
|
||
return { groups: [], cards: [] }
|
||
}
|
||
}
|
||
|
||
export async function createNode(input: {
|
||
key: string
|
||
title: string
|
||
parentKey?: string
|
||
format?: GroupFormat
|
||
icon?: string
|
||
sort?: number
|
||
enabled?: boolean
|
||
}): Promise<SimpleResult> {
|
||
try {
|
||
const response = await fetch('/api/nodes', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: { 'content-type': 'application/json' },
|
||
body: JSON.stringify(input),
|
||
})
|
||
return response.ok ? { ok: true } : { ok: false, error: await errorMessage(response, '创建失败') }
|
||
} catch (error) {
|
||
return { ok: false, error: error instanceof Error ? error.message : '网络错误' }
|
||
}
|
||
}
|
||
|
||
export async function updateNode(
|
||
key: string,
|
||
patch: {
|
||
title?: string
|
||
parentKey?: string
|
||
sort?: number
|
||
format?: GroupFormat
|
||
icon?: string
|
||
enabled?: boolean
|
||
},
|
||
): Promise<SimpleResult> {
|
||
try {
|
||
const response = await fetch(`/api/nodes/${encodeURIComponent(key)}`, {
|
||
method: 'PATCH',
|
||
credentials: 'same-origin',
|
||
headers: { 'content-type': 'application/json' },
|
||
body: JSON.stringify(patch),
|
||
})
|
||
return response.ok ? { ok: true } : { ok: false, error: await errorMessage(response, '保存失败') }
|
||
} catch (error) {
|
||
return { ok: false, error: error instanceof Error ? error.message : '网络错误' }
|
||
}
|
||
}
|
||
|
||
export async function deleteNode(key: string): Promise<SimpleResult> {
|
||
try {
|
||
const response = await fetch(`/api/nodes/${encodeURIComponent(key)}`, {
|
||
method: 'DELETE',
|
||
credentials: 'same-origin',
|
||
})
|
||
return response.ok ? { ok: true } : { ok: false, error: await errorMessage(response, '删除失败') }
|
||
} catch (error) {
|
||
return { ok: false, error: error instanceof Error ? error.message : '网络错误' }
|
||
}
|
||
}
|
||
|
||
export async function createCard(title: string): Promise<{ ok: true; id: number } | { ok: false; error: string }> {
|
||
try {
|
||
const response = await fetch('/api/cards', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: { 'content-type': 'application/json' },
|
||
body: JSON.stringify({ title }),
|
||
})
|
||
if (!response.ok) return { ok: false, error: await errorMessage(response, '创建失败') }
|
||
const payload = (await response.json()) as { id: number }
|
||
return { ok: true, id: payload.id }
|
||
} catch (error) {
|
||
return { ok: false, error: error instanceof Error ? error.message : '网络错误' }
|
||
}
|
||
}
|
||
|
||
export async function updateCard(
|
||
id: number,
|
||
patch: { title?: string; sort?: number },
|
||
): Promise<SimpleResult> {
|
||
try {
|
||
const response = await fetch(`/api/cards/${id}`, {
|
||
method: 'PATCH',
|
||
credentials: 'same-origin',
|
||
headers: { 'content-type': 'application/json' },
|
||
body: JSON.stringify(patch),
|
||
})
|
||
return response.ok ? { ok: true } : { ok: false, error: await errorMessage(response, '保存失败') }
|
||
} catch (error) {
|
||
return { ok: false, error: error instanceof Error ? error.message : '网络错误' }
|
||
}
|
||
}
|
||
|
||
export async function deleteCard(id: number): Promise<SimpleResult> {
|
||
try {
|
||
const response = await fetch(`/api/cards/${id}`, {
|
||
method: 'DELETE',
|
||
credentials: 'same-origin',
|
||
})
|
||
return response.ok ? { ok: true } : { ok: false, error: await errorMessage(response, '删除失败') }
|
||
} catch (error) {
|
||
return { ok: false, error: error instanceof Error ? error.message : '网络错误' }
|
||
}
|
||
}
|
||
|
||
export async function setCardGroups(
|
||
id: number,
|
||
groups: CardGroupRef[],
|
||
): Promise<SimpleResult> {
|
||
try {
|
||
const response = await fetch(`/api/cards/${id}/groups`, {
|
||
method: 'PUT',
|
||
credentials: 'same-origin',
|
||
headers: { 'content-type': 'application/json' },
|
||
body: JSON.stringify(groups),
|
||
})
|
||
return response.ok ? { ok: true } : { ok: false, error: await errorMessage(response, '保存失败') }
|
||
} catch (error) {
|
||
return { ok: false, error: error instanceof Error ? error.message : '网络错误' }
|
||
}
|
||
}
|
||
|
||
/** 把分组树拍平成下拉选项 */
|
||
export function flattenGroups(
|
||
groups: GroupNode[],
|
||
depth = 0,
|
||
): { key: string; title: string; depth: number; format: GroupFormat; enabled: boolean }[] {
|
||
return groups.flatMap((group) => [
|
||
{
|
||
key: group.key,
|
||
title: group.title,
|
||
depth,
|
||
format: group.format,
|
||
enabled: group.enabled !== false,
|
||
},
|
||
...flattenGroups(group.children, depth + 1),
|
||
])
|
||
}
|