feat(admin): 分区增加「允许子分组」开关(与类型解耦)
- node 新增 allow_groups(迁移 v18):应用默认开、海报/图文默认关,已有分组的非应用分区自动开启 - 后台每行一个「子分组」开关;分组管理、卡片预览、内容弹窗选组、侧栏建组都跟随开关 - 关闭开关时条目平铺且不丢内容;首页卡片指向分组会自动挪回分区 - 三种类型共用同一模板,类型只决定渲染方式 - 测试 59 passed(迁移 15→18、开关建组、类型切换)
This commit is contained in:
@@ -985,11 +985,18 @@
|
||||
/* 归类页:一行一个分区,标题 / 类型 / 图标 / 色条 / 条数 / 保存 / 删除 */
|
||||
.admin-category-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) 150px 80px 110px 90px auto auto;
|
||||
grid-template-columns: minmax(0, 1.4fr) 150px 80px 110px 90px auto auto auto;
|
||||
align-items: center;
|
||||
gap: var(--su-space-2);
|
||||
}
|
||||
|
||||
.admin-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--su-space-2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-category-row > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ type CategorySummary = {
|
||||
icon?: string
|
||||
barColor?: string
|
||||
previewLimit?: number
|
||||
/** 是否允许子分组(与类型无关的开关) */
|
||||
allowGroups?: boolean
|
||||
/** 首页卡片标题与预览节点 */
|
||||
block?: { title?: string; nodeKey?: string } | null
|
||||
/** 应用类分区下的分组(子分类) */
|
||||
@@ -539,7 +541,7 @@ function SidebarForm({
|
||||
</div>
|
||||
))}
|
||||
|
||||
{category.kind === 'apps' && (
|
||||
{category.allowGroups === true && (
|
||||
<div className="admin-nav-group-row">
|
||||
<NewGroupRow sectionId={category.id} onReload={onReload} />
|
||||
</div>
|
||||
@@ -751,8 +753,9 @@ function CategoryManager({
|
||||
return (
|
||||
<Space direction="column" gap="lg" align="stretch" className="admin-form">
|
||||
<Text size="sm" tone="muted">
|
||||
分区决定内容归到哪一类(应用 / 海报 / 图文)。应用类分区下面可以建分组(子分类),
|
||||
分组会出现在侧栏,也可以作为首页卡片的预览来源;改类型会自动迁移条目。
|
||||
三种类型使用同一套模板:分区决定内容归到哪一类(应用 / 海报 / 图文),
|
||||
「子分组」开关决定要不要分组。开启后可建分组、分组会出现在侧栏,也可以作为首页卡片预览;
|
||||
关闭后条目按平铺渲染,已有分组里的内容不会丢。
|
||||
</Text>
|
||||
|
||||
{categories.map((category) => (
|
||||
@@ -762,7 +765,7 @@ function CategoryManager({
|
||||
onReload={onReload}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
{category.kind === 'apps' && (
|
||||
{category.allowGroups === true && (
|
||||
<>
|
||||
<BlockSelect section={category} onReload={onReload} />
|
||||
<GroupManager section={category} onReload={onReload} />
|
||||
@@ -794,6 +797,7 @@ function CategoryRow({
|
||||
const [limit, setLimit] = useState(
|
||||
category.previewLimit === undefined ? '' : String(category.previewLimit),
|
||||
)
|
||||
const [allowGroups, setAllowGroups] = useState(category.allowGroups === true)
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
const save = async () => {
|
||||
@@ -804,6 +808,7 @@ function CategoryRow({
|
||||
icon,
|
||||
barColor: color,
|
||||
previewLimit: limit.trim() === '' ? undefined : Number(limit),
|
||||
allowGroups,
|
||||
})
|
||||
setBusy(false)
|
||||
|
||||
@@ -848,6 +853,16 @@ function CategoryRow({
|
||||
aria-label={`${category.title} 预览条数`}
|
||||
onChange={(event) => setLimit(event.target.value)}
|
||||
/>
|
||||
<div className="admin-toggle">
|
||||
<Switch
|
||||
checked={allowGroups}
|
||||
onChange={setAllowGroups}
|
||||
label={`${category.title} 允许子分组`}
|
||||
/>
|
||||
<Text size="sm" tone="muted">
|
||||
子分组
|
||||
</Text>
|
||||
</div>
|
||||
<Button variant="secondary" size="sm" loading={busy} onClick={save}>
|
||||
保存
|
||||
</Button>
|
||||
@@ -1275,7 +1290,7 @@ function EntryDialog({
|
||||
|
||||
const section =
|
||||
categories.find((category) => category.id === sectionId) ?? categories[0]
|
||||
const groups = section?.kind === 'apps' ? (section.groups ?? []) : []
|
||||
const groups = section?.allowGroups === true ? (section.groups ?? []) : []
|
||||
|
||||
// 打开时重置:新建用当前选择,编辑先拉完整条目
|
||||
useEffect(() => {
|
||||
@@ -1288,7 +1303,7 @@ function EntryDialog({
|
||||
const startCategory = categories.find((category) => category.id === startSection)
|
||||
setGroupId(
|
||||
defaultLocation?.groupId ??
|
||||
(startCategory?.kind === 'apps' ? (startCategory.groups?.[0]?.id ?? '') : ''),
|
||||
(startCategory?.allowGroups === true ? (startCategory.groups?.[0]?.id ?? '') : ''),
|
||||
)
|
||||
|
||||
if (entryKey === null) {
|
||||
@@ -1326,9 +1341,9 @@ function EntryDialog({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, entryKey])
|
||||
|
||||
// 只有应用类分区有分组;其余类型始终是直属分区
|
||||
// 只有开启「允许子分组」的分区有分组;其余始终是直属分区
|
||||
useEffect(() => {
|
||||
if (section?.kind !== 'apps') {
|
||||
if (section?.allowGroups !== true) {
|
||||
if (groupId !== '') setGroupId('')
|
||||
return
|
||||
}
|
||||
@@ -1340,7 +1355,7 @@ function EntryDialog({
|
||||
const nodeKey =
|
||||
section === undefined
|
||||
? ''
|
||||
: section.kind === 'apps' && groupId !== ''
|
||||
: section.allowGroups === true && groupId !== ''
|
||||
? groupId
|
||||
: section.id
|
||||
|
||||
@@ -1439,7 +1454,7 @@ function EntryDialog({
|
||||
onChange={setSectionId}
|
||||
/>
|
||||
</div>
|
||||
{section?.kind === 'apps' && (
|
||||
{section?.allowGroups === true && (
|
||||
<div style={{ width: 240 }}>
|
||||
<Select
|
||||
options={[
|
||||
|
||||
@@ -38,11 +38,8 @@ export function CategoryNav({
|
||||
const items: MenuItem[] = [
|
||||
{ key: HOME_KEY, label: '首页' },
|
||||
...categories.map((category) => {
|
||||
// 只有应用类有分组;空分组不出现在侧栏(后台仍保留)
|
||||
const groups =
|
||||
category.kind === 'apps'
|
||||
? category.groups.filter((group) => group.items.length > 0)
|
||||
: []
|
||||
// 「允许子分组」的分区才有 groups;空分组不出现在侧栏(后台仍保留)
|
||||
const groups = category.groups.filter((group) => group.items.length > 0)
|
||||
|
||||
return {
|
||||
key: categoryKey(category.id),
|
||||
|
||||
@@ -296,6 +296,8 @@ export type UpdateCategoryInput = {
|
||||
emptyText?: string
|
||||
/** 首页卡片预览哪个节点(分区自己或其分组) */
|
||||
blockNodeKey?: string
|
||||
/** 是否允许子分组(与类型无关的开关) */
|
||||
allowGroups?: boolean
|
||||
}
|
||||
|
||||
export async function updateCategory(
|
||||
|
||||
Reference in New Issue
Block a user