- v23 迁移删除 section_block 表,以及 node 的 view_mode / empty_text / show_in_sidebar / allow_groups
- 重建 node 表并保留 44 条内容、15 个分组、5 张卡片,外键检查通过
- 种子库直接生成 card / card_group,不再依赖 section_block
- 删除旧 /api/catalog、/api/categories、/api/groups/{id} 与 categories 模块
- 分组接口不再接收/返回 format,前端 GroupNode 同步移除
- README/HANDOFF 更新到 v23;管理页提示改为『格式由每条内容决定』
202 lines
5.7 KiB
TypeScript
202 lines
5.7 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
|
||
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; enabled: boolean }[] {
|
||
return groups.flatMap((group) => [
|
||
{
|
||
key: group.key,
|
||
title: group.title,
|
||
depth,
|
||
enabled: group.enabled !== false,
|
||
},
|
||
...flattenGroups(group.children, depth + 1),
|
||
])
|
||
}
|