- entry 新增 format(apps/posters/articles),老数据按根分组 view_mode 回填;seed 按分区写入 - 分组管理移除格式字段/显示,根分组只作容器 - 内容弹窗新增逐条格式选择,列表增加格式列;ItemEditor 保存时保留原格式 - 前台按 entry.format 渲染:卡片/分组页支持同组混合格式,详情按条目格式选模板 - 迁移后 live 20 应用 / 13 海报;cargo test 59 passed;Playwright 验证混合展示
127 lines
3.7 KiB
TypeScript
127 lines
3.7 KiB
TypeScript
/**
|
||
* 条目编辑的前端接口。
|
||
*
|
||
* 读取是公开的(编辑表单要能预览),写入必须登录,并用 `updatedAt` 做乐观锁。
|
||
*/
|
||
export type EntryFormat = 'apps' | 'posters' | 'articles'
|
||
|
||
export type EntryRecord = {
|
||
key: string
|
||
format: EntryFormat
|
||
name: string
|
||
description: string
|
||
icon: string
|
||
href: string
|
||
cover: string
|
||
banner: string
|
||
badge: string
|
||
comment: string
|
||
summary: string
|
||
body: string
|
||
tags: string[]
|
||
updated_at: number
|
||
}
|
||
|
||
export type EntryInput = {
|
||
name: string
|
||
/** 条目的展示格式;不传由后端按 apps 处理 */
|
||
format?: EntryFormat
|
||
description: string
|
||
icon: string
|
||
tags: string[]
|
||
badge: string
|
||
comment: string
|
||
summary: string
|
||
body: string
|
||
cover: string
|
||
banner: string
|
||
href: string
|
||
/** 编辑时传目标分区/分组,可移动条目 */
|
||
nodeKey?: string
|
||
}
|
||
|
||
export type ItemResult =
|
||
| { ok: true; entry: EntryRecord }
|
||
| { ok: false; status: number; error: string }
|
||
|
||
export async function fetchItem(key: string): Promise<ItemResult> {
|
||
try {
|
||
const response = await fetch(`/api/items/${encodeURIComponent(key)}`, {
|
||
credentials: 'same-origin',
|
||
})
|
||
if (response.ok) {
|
||
return { ok: true, entry: (await response.json()) as EntryRecord }
|
||
}
|
||
const payload = (await response.json().catch(() => null)) as { error?: string } | null
|
||
return { ok: false, status: response.status, error: payload?.error ?? '读取失败' }
|
||
} catch (error) {
|
||
return { ok: false, status: 0, error: error instanceof Error ? error.message : '网络错误' }
|
||
}
|
||
}
|
||
|
||
export async function saveItem(
|
||
key: string,
|
||
updatedAt: number,
|
||
item: EntryInput,
|
||
): Promise<ItemResult> {
|
||
try {
|
||
const response = await fetch(`/api/items/${encodeURIComponent(key)}`, {
|
||
method: 'PATCH',
|
||
credentials: 'same-origin',
|
||
headers: { 'content-type': 'application/json' },
|
||
body: JSON.stringify({ updatedAt, item }),
|
||
})
|
||
|
||
if (response.ok) {
|
||
return { ok: true, entry: (await response.json()) as EntryRecord }
|
||
}
|
||
|
||
const payload = (await response.json().catch(() => null)) as { error?: string } | null
|
||
return {
|
||
ok: false,
|
||
status: response.status,
|
||
error: payload?.error ?? `保存失败(${response.status})`,
|
||
}
|
||
} catch (error) {
|
||
return { ok: false, status: 0, error: error instanceof Error ? error.message : '网络错误' }
|
||
}
|
||
}
|
||
|
||
/** 新建条目:指定分区 / 分组与字段 */
|
||
export async function createItem(input: {
|
||
key: string
|
||
nodeKey: string
|
||
item: EntryInput
|
||
}): Promise<SimpleResult> {
|
||
try {
|
||
const response = await fetch('/api/items', {
|
||
method: 'POST',
|
||
credentials: 'same-origin',
|
||
headers: { 'content-type': 'application/json' },
|
||
body: JSON.stringify(input),
|
||
})
|
||
if (response.ok) return { ok: true }
|
||
const payload = (await response.json().catch(() => null)) as { error?: string } | null
|
||
return { ok: false, error: payload?.error ?? `创建失败(${response.status})` }
|
||
} catch (error) {
|
||
return { ok: false, error: error instanceof Error ? error.message : '网络错误' }
|
||
}
|
||
}
|
||
|
||
/** 删除条目 */
|
||
export async function deleteItem(key: string): Promise<SimpleResult> {
|
||
try {
|
||
const response = await fetch(`/api/items/${encodeURIComponent(key)}`, {
|
||
method: 'DELETE',
|
||
credentials: 'same-origin',
|
||
})
|
||
if (response.ok) return { ok: true }
|
||
const payload = (await response.json().catch(() => null)) as { error?: string } | null
|
||
return { ok: false, error: payload?.error ?? `删除失败(${response.status})` }
|
||
} catch (error) {
|
||
return { ok: false, error: error instanceof Error ? error.message : '网络错误' }
|
||
}
|
||
}
|
||
|
||
export type SimpleResult = { ok: true } | { ok: false; error: string }
|