feat(content): 分组对所有内容类型生效,统一分区/分组/条目模型

- 分区=容器,分组=任意类型的可选子容器,条目可挂分区或分组(直属分区)
- catalog 所有类型统一输出 groups + items,条目查询按节点取
- 详情/侧栏/首页卡片预览支持所有类型的分组;多组显示小标题 + 未分组
- 后台「分区」「侧栏导航」对所有类型可建组;「内容」弹窗分组可选直属分区,编辑可换组
- 修复首页卡片配置为「整个分区」时预览为空
- 测试 59 passed
This commit is contained in:
2026-10-30 18:56:17 +08:00
parent cc08188c1e
commit c84c41dc7d
11 changed files with 264 additions and 200 deletions
+40 -12
View File
@@ -1,5 +1,11 @@
import { Breadcrumb, Card, Empty, Space, Title } from '@stash/ui'
import { categoryCount, type Category } from '../../data/types'
import {
categoryCount,
type AppItem,
type ArticleItem,
type Category,
type PosterItem,
} from '../../data/types'
import { viewPath } from '../../data/view'
import { AppGrid } from './AppGrid'
import { CesCardGrid } from './CesCardGrid'
@@ -25,7 +31,13 @@ type DetailViewProps = {
}
export function DetailView({ category, groupId, onNavigate, onOpenItem }: DetailViewProps) {
const groups = category.kind === 'apps' ? category.groups : []
// 三种类型共用「分区 → 分组 → 条目」结构;这里抹平联合类型,渲染时再按 kind 分派
type RenderItem = AppItem | PosterItem | ArticleItem
const groups = category.groups as {
id: string
name: string
items: RenderItem[]
}[]
const activeGroup =
groupId !== undefined ? groups.find((group) => group.id === groupId) : undefined
@@ -43,32 +55,48 @@ export function DetailView({ category, groupId, onNavigate, onOpenItem }: Detail
{ label: category.title, href: viewPath({ type: 'detail', categoryId: category.id }) },
]
function renderBody() {
/** 按类型渲染一组条目 */
function renderItems(items: RenderItem[]) {
if (category.kind === 'posters') {
return <PosterGrid items={category.items} variant="detail" onOpen={onOpenItem} />
return <PosterGrid items={items as PosterItem[]} variant="detail" onOpen={onOpenItem} />
}
if (category.kind === 'articles') {
return <CesCardGrid items={category.items} onOpen={onOpenItem} />
return <CesCardGrid items={items as ArticleItem[]} onOpen={onOpenItem} />
}
return <AppGrid items={items as AppItem[]} variant="detail" onOpen={onOpenItem} />
}
function renderBody() {
if (activeGroup !== undefined) {
return <AppGrid items={activeGroup.items} variant="detail" onOpen={onOpenItem} />
return renderItems(activeGroup.items)
}
if (category.groups.length === 1) {
return <AppGrid items={category.groups[0].items} variant="detail" onOpen={onOpenItem} />
const direct = (category.items ?? []) as RenderItem[]
if (groups.length === 0) {
return direct.length > 0 ? (
renderItems(direct)
) : (
<Empty text={'emptyText' in category ? (category.emptyText ?? '暂无内容') : '暂无内容'} />
)
}
if (category.groups.length === 0) {
return <Empty text={category.emptyText ?? '暂无内容'} />
if (groups.length === 1 && direct.length === 0) {
return renderItems(groups[0].items)
}
return (
<Space direction="column" gap="lg" align="stretch">
{category.groups.map((group) => (
<GroupBlock key={group.id} group={group} onOpen={onOpenItem} />
{groups.map((group) => (
<GroupBlock key={group.id} name={group.name}>
{renderItems(group.items)}
</GroupBlock>
))}
{direct.length > 0 && (
<GroupBlock name="未分组">{renderItems(direct)}</GroupBlock>
)}
</Space>
)
}