- /admin 切换为 AdminNext:分组管理(多层树、格式、增删改)、内容(选分组新增/编辑/删除条目) - 侧边栏按分组树勾选,存入 settings.sidebar.tree_hidden - 首页卡片:新建卡片、勾选分组(可混合格式)、每组条数 - 新增 manage.ts 客户端;旧 AdminPage 暂留作回退 - 测试 59 passed;Playwright 跑通 建根/子分组 → 加条目 → 侧栏勾选 → 混合卡片
182 lines
5.5 KiB
TypeScript
182 lines
5.5 KiB
TypeScript
/**
|
||
* 原子模型的后台接口:分组树 + 卡片。
|
||
*
|
||
* 分组是唯一容器,卡片只是「勾选分组」的展示组合;两者都通过业务键关联。
|
||
*/
|
||
export type GroupFormat = 'apps' | 'posters' | 'articles'
|
||
|
||
export type GroupEntry = {
|
||
id: string
|
||
name?: string
|
||
title?: string
|
||
}
|
||
|
||
export type GroupNode = {
|
||
key: string
|
||
parentKey?: string | null
|
||
title: string
|
||
format: GroupFormat
|
||
icon: string
|
||
barColor: string
|
||
previewLimit?: number | null
|
||
sort: number
|
||
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
|
||
}): 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 },
|
||
): 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 }[] {
|
||
return groups.flatMap((group) => [
|
||
{ key: group.key, title: group.title, depth, format: group.format },
|
||
...flattenGroups(group.children, depth + 1),
|
||
])
|
||
}
|