fix(admin): 子分组开关即时保存并立即显示分组管理
- 拨动开关即调用 PATCH allowGroups,成功后本地更新分类,无需再点保存 - 侧栏导航的分组列表同样跟随开关显示 - 失败时回滚开关状态
This commit is contained in:
+1
-1
@@ -100,7 +100,7 @@ README.md 运行、路由、库结构、登录说明
|
||||
| 页头滚动信息 | 启用开关、速度 8–120、条目增删/排序、标签与色调、占位符 `{count}` `{project}` `{status}` |
|
||||
| 首页卡片 | 显示/隐藏、标题覆盖、预览条数、顺序、**新增分类**、**删除分类**(不可删的显示灰色按钮) |
|
||||
| 内容 | 弹窗新增应用/海报/图文条目(选分区;开启子分组的分区可选分组或「直属分区」);列表逐条编辑(含换分区/分组)与删除 |
|
||||
| 分区 | 分区类型(应用/海报/图文)、标题、图标、色条、预览条数、**「子分组」开关**;开关打开时管理分组与首页卡片预览 |
|
||||
| 分区 | 分区类型(应用/海报/图文)、标题、图标、色条、预览条数、**「子分组」开关(拨动即保存)**;开关打开时管理分组与首页卡片预览 |
|
||||
| 侧栏导航 | 分类显示/隐藏、顺序、删除/新增;开启子分组的分区可显隐/排序/新建分组;空分组不出现在侧栏 |
|
||||
| 资料 | 头像 URL、名称、角色、简介、位置(应用到侧栏资料卡) |
|
||||
| 系统 | 修改密码(清空所有会话)、下载备份(VACUUM INTO 快照) |
|
||||
|
||||
+1
-1
@@ -152,7 +152,7 @@ DELETE /api/items/:id
|
||||
- **与类型解耦**:分区新增 `allow_groups` 开关(迁移 v18);三种类型共用同一模板,开启后可建分组,关闭后平铺;
|
||||
- **默认值**:老库升级时应用类开启、海报/图文关闭;已有分组的非应用分区自动开启;新建分区按类型给默认值,之后可随时改;
|
||||
- **平铺兼容**:关闭开关时读取会把(历史)分组内条目一起平铺,内容不丢;首页卡片若指向分组会自动挪回分区;
|
||||
- **后台**:「分区」每行新增「子分组」开关;「内容」弹窗的分组选择随开关出现;「侧栏导航」的建组入口同理;空分组不在侧栏显示;
|
||||
- **后台**:「分区」每行新增「子分组」开关,**拨动即保存并立刻出现/收起分组管理**;「内容」弹窗的分组选择随开关出现;「侧栏导航」的建组入口同理;空分组不在侧栏显示;
|
||||
- 验证:推荐电影(海报)开启开关 → 建组 → 加海报 → 详情按组显示;关闭 → 恢复平铺且条目仍在;`cargo test` 59 passed。
|
||||
|
||||
### P7(2026-10-30,统一内容模型)
|
||||
|
||||
@@ -130,6 +130,14 @@ export function AdminPage() {
|
||||
|
||||
const requestDelete = (card: CategorySummary) => setPendingDelete(card)
|
||||
|
||||
/** 只更新本地分类缓存(不改 draft),用于「子分组」开关即时生效 */
|
||||
const patchCategory = (id: string, patch: Partial<CategorySummary>) =>
|
||||
setCategories((current) =>
|
||||
current.map((category) =>
|
||||
category.id === id ? { ...category, ...patch } : category,
|
||||
),
|
||||
)
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (pendingDelete === null) return
|
||||
setDeleting(true)
|
||||
@@ -232,6 +240,7 @@ export function AdminPage() {
|
||||
categories={categories}
|
||||
onReload={load}
|
||||
onDelete={requestDelete}
|
||||
onToggleGroups={(id, allow) => patchCategory(id, { allowGroups: allow })}
|
||||
/>
|
||||
) : tab === 'sidebar' ? (
|
||||
<SidebarForm
|
||||
@@ -505,7 +514,7 @@ function SidebarForm({
|
||||
|
||||
{ordered.map((category, index) => {
|
||||
const visible = !value.hidden.includes(category.id)
|
||||
const groups = orderedGroups(category)
|
||||
const groups = category.allowGroups === true ? orderedGroups(category) : []
|
||||
|
||||
return (
|
||||
<div className="admin-nav-block" key={category.id}>
|
||||
@@ -745,10 +754,12 @@ function CategoryManager({
|
||||
categories,
|
||||
onReload,
|
||||
onDelete,
|
||||
onToggleGroups,
|
||||
}: {
|
||||
categories: CategorySummary[]
|
||||
onReload: () => void
|
||||
onDelete: (card: CategorySummary) => void
|
||||
onToggleGroups: (id: string, allow: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<Space direction="column" gap="lg" align="stretch" className="admin-form">
|
||||
@@ -764,6 +775,7 @@ function CategoryManager({
|
||||
category={category}
|
||||
onReload={onReload}
|
||||
onDelete={onDelete}
|
||||
onToggleGroups={onToggleGroups}
|
||||
/>
|
||||
{category.allowGroups === true && (
|
||||
<>
|
||||
@@ -785,10 +797,12 @@ function CategoryRow({
|
||||
category,
|
||||
onReload,
|
||||
onDelete,
|
||||
onToggleGroups,
|
||||
}: {
|
||||
category: CategorySummary
|
||||
onReload: () => void
|
||||
onDelete: (card: CategorySummary) => void
|
||||
onToggleGroups: (id: string, allow: boolean) => void
|
||||
}) {
|
||||
const [title, setTitle] = useState(category.title)
|
||||
const [kind, setKind] = useState(category.kind ?? 'apps')
|
||||
@@ -800,6 +814,23 @@ function CategoryRow({
|
||||
const [allowGroups, setAllowGroups] = useState(category.allowGroups === true)
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
/** 子分组开关即时保存:切完立刻出现 / 收起分组管理 */
|
||||
const toggleGroups = async (next: boolean) => {
|
||||
setAllowGroups(next)
|
||||
setBusy(true)
|
||||
const result = await updateCategory(category.id, { allowGroups: next })
|
||||
setBusy(false)
|
||||
|
||||
if (!result.ok) {
|
||||
setAllowGroups(!next)
|
||||
toast.error(result.error)
|
||||
return
|
||||
}
|
||||
|
||||
onToggleGroups(category.id, next)
|
||||
toast.success(next ? '已开启子分组' : '已关闭子分组')
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
setBusy(true)
|
||||
const result = await updateCategory(category.id, {
|
||||
@@ -856,7 +887,7 @@ function CategoryRow({
|
||||
<div className="admin-toggle">
|
||||
<Switch
|
||||
checked={allowGroups}
|
||||
onChange={setAllowGroups}
|
||||
onChange={(next) => void toggleGroups(next)}
|
||||
label={`${category.title} 允许子分组`}
|
||||
/>
|
||||
<Text size="sm" tone="muted">
|
||||
|
||||
Reference in New Issue
Block a user