diff --git a/HANDOFF.md b/HANDOFF.md index 4f77589..c6e85c5 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -100,7 +100,7 @@ README.md 运行、路由、库结构、登录说明 | 页头滚动信息 | 启用开关、速度 8–120、条目增删/排序、标签与色调、占位符 `{count}` `{project}` `{status}` | | 首页卡片 | 显示/隐藏、标题覆盖、预览条数、顺序、**新增分类**、**删除分类**(不可删的显示灰色按钮) | | 内容 | 弹窗新增应用/海报/图文条目(选分区;开启子分组的分区可选分组或「直属分区」);列表逐条编辑(含换分区/分组)与删除 | -| 分区 | 分区类型(应用/海报/图文)、标题、图标、色条、预览条数、**「子分组」开关**;开关打开时管理分组与首页卡片预览 | +| 分区 | 分区类型(应用/海报/图文)、标题、图标、色条、预览条数、**「子分组」开关(拨动即保存)**;开关打开时管理分组与首页卡片预览 | | 侧栏导航 | 分类显示/隐藏、顺序、删除/新增;开启子分组的分区可显隐/排序/新建分组;空分组不出现在侧栏 | | 资料 | 头像 URL、名称、角色、简介、位置(应用到侧栏资料卡) | | 系统 | 修改密码(清空所有会话)、下载备份(VACUUM INTO 快照) | diff --git a/docs/ADMIN-PLAN.md b/docs/ADMIN-PLAN.md index 4cbf29a..a1ed26c 100644 --- a/docs/ADMIN-PLAN.md +++ b/docs/ADMIN-PLAN.md @@ -152,7 +152,7 @@ DELETE /api/items/:id - **与类型解耦**:分区新增 `allow_groups` 开关(迁移 v18);三种类型共用同一模板,开启后可建分组,关闭后平铺; - **默认值**:老库升级时应用类开启、海报/图文关闭;已有分组的非应用分区自动开启;新建分区按类型给默认值,之后可随时改; - **平铺兼容**:关闭开关时读取会把(历史)分组内条目一起平铺,内容不丢;首页卡片若指向分组会自动挪回分区; -- **后台**:「分区」每行新增「子分组」开关;「内容」弹窗的分组选择随开关出现;「侧栏导航」的建组入口同理;空分组不在侧栏显示; +- **后台**:「分区」每行新增「子分组」开关,**拨动即保存并立刻出现/收起分组管理**;「内容」弹窗的分组选择随开关出现;「侧栏导航」的建组入口同理;空分组不在侧栏显示; - 验证:推荐电影(海报)开启开关 → 建组 → 加海报 → 详情按组显示;关闭 → 恢复平铺且条目仍在;`cargo test` 59 passed。 ### P7(2026-10-30,统一内容模型) diff --git a/stash-web/src/components/admin/AdminPage.tsx b/stash-web/src/components/admin/AdminPage.tsx index 48890fd..01906b9 100644 --- a/stash-web/src/components/admin/AdminPage.tsx +++ b/stash-web/src/components/admin/AdminPage.tsx @@ -130,6 +130,14 @@ export function AdminPage() { const requestDelete = (card: CategorySummary) => setPendingDelete(card) + /** 只更新本地分类缓存(不改 draft),用于「子分组」开关即时生效 */ + const patchCategory = (id: string, patch: Partial) => + 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' ? ( { const visible = !value.hidden.includes(category.id) - const groups = orderedGroups(category) + const groups = category.allowGroups === true ? orderedGroups(category) : [] return (
@@ -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 ( @@ -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({
void toggleGroups(next)} label={`${category.title} 允许子分组`} />