feat(admin): 跳转规范化、侧栏子分类与归类管理
- 条目保存时规范化 href(裸域名补 https,拒绝 javascript:/data: 等协议),详情按钮按解析结果置灰/跳转
- settings.sidebar 增加 hidden_groups / shown_groups / group_order,侧栏 Tab 支持分组显隐与排序
- 新增 PATCH /api/categories/{id} 与「归类」Tab:编辑类型/标题/图标/色条/条数;改类型自动迁移条目与首页卡片
- 测试 54 passed(href 规范化 3 例、分类更新 3 例)
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
* 接口是公开读、登录后写;写的时候带 `version` 做乐观锁,
|
||||
* 冲突(多标签页同时改)时返回 409,页面提示刷新。
|
||||
*/
|
||||
import type { Category } from './data/types'
|
||||
|
||||
export type SettingsTone = 'plain' | 'primary' | 'success' | 'neutral'
|
||||
|
||||
export type HeaderItem = {
|
||||
@@ -39,6 +41,12 @@ export type SiteSettings = {
|
||||
sidebar: {
|
||||
hidden: string[]
|
||||
order: string[]
|
||||
/** 隐藏的分组:`分区id/分组id` */
|
||||
hidden_groups: string[]
|
||||
/** 强制显示的分组(覆盖数据库 show_in_sidebar=0) */
|
||||
shown_groups: string[]
|
||||
/** 每个分区下的分组顺序:分区 id → 分组 id 列表 */
|
||||
group_order: Record<string, string[]>
|
||||
}
|
||||
sections: SectionSettings
|
||||
profile: ProfileSettings
|
||||
@@ -71,7 +79,13 @@ export function defaultSettings(): SiteSettings {
|
||||
{ text: '{status}', tag: '状态', tone: 'success' },
|
||||
],
|
||||
},
|
||||
sidebar: { hidden: [], order: [] },
|
||||
sidebar: {
|
||||
hidden: [],
|
||||
order: [],
|
||||
hidden_groups: [],
|
||||
shown_groups: [],
|
||||
group_order: {},
|
||||
},
|
||||
sections: { hidden: [], order: [], titles: {}, limits: {} },
|
||||
profile: {
|
||||
name: '本地管理员',
|
||||
@@ -151,6 +165,42 @@ export function applySectionSettings<T extends { id: string }>(
|
||||
return orderAndFilter(categories, sections)
|
||||
}
|
||||
|
||||
/**
|
||||
* 侧栏子分类(分组):按设置过滤、排序。
|
||||
*
|
||||
* 数据库里的 `show_in_sidebar` 是默认值;`shown_groups` 可以把它强行打开,
|
||||
* `hidden_groups` 则再隐藏一层。
|
||||
*/
|
||||
export function applySidebarGroups(
|
||||
categories: Category[],
|
||||
sidebar: SiteSettings['sidebar'],
|
||||
): Category[] {
|
||||
const hidden = new Set(sidebar.hidden_groups)
|
||||
const shown = new Set(sidebar.shown_groups)
|
||||
|
||||
return categories.map((category) => {
|
||||
if (category.kind !== 'apps') return category
|
||||
|
||||
const ranks = new Map(
|
||||
(sidebar.group_order[category.id] ?? []).map((id, index) => [id, index] as const),
|
||||
)
|
||||
const groups = category.groups
|
||||
.filter((group) => {
|
||||
const key = `${category.id}/${group.id}`
|
||||
return !hidden.has(key) && (group.showInSidebar !== false || shown.has(key))
|
||||
})
|
||||
.map((group, index) => ({
|
||||
group,
|
||||
index,
|
||||
rank: ranks.get(group.id) ?? Number.MAX_SAFE_INTEGER,
|
||||
}))
|
||||
.sort((a, b) => (a.rank === b.rank ? a.index - b.index : a.rank - b.rank))
|
||||
.map((entry) => entry.group)
|
||||
|
||||
return { ...category, groups }
|
||||
})
|
||||
}
|
||||
|
||||
/** 卡片标题:设置里的覆盖值优先 */
|
||||
export function sectionTitle(
|
||||
id: string,
|
||||
@@ -223,3 +273,32 @@ export async function deleteCategory(id: string): Promise<SimpleResult> {
|
||||
return { ok: false, error: error instanceof Error ? error.message : '网络错误' }
|
||||
}
|
||||
}
|
||||
|
||||
/** 修改分区(归类、标题、图标、色条、预览条数) */
|
||||
export type UpdateCategoryInput = {
|
||||
title?: string
|
||||
icon?: string
|
||||
barColor?: string
|
||||
kind?: 'apps' | 'posters' | 'articles'
|
||||
previewLimit?: number
|
||||
emptyText?: string
|
||||
}
|
||||
|
||||
export async function updateCategory(
|
||||
id: string,
|
||||
patch: UpdateCategoryInput,
|
||||
): Promise<SimpleResult> {
|
||||
try {
|
||||
const response = await fetch(`/api/categories/${encodeURIComponent(id)}`, {
|
||||
method: 'PATCH',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(patch),
|
||||
})
|
||||
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 : '网络错误' }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user