139 lines
4.3 KiB
TypeScript
139 lines
4.3 KiB
TypeScript
import { Breadcrumb, Card, Empty, Space, Title } from '@stash/ui'
|
||
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'
|
||
import { GroupBlock } from './GroupBlock'
|
||
import { PosterGrid } from './PosterGrid'
|
||
|
||
/**
|
||
* 分类明细视图:面包屑 + hero(图标 / 标题 / 数量)+ 内容区。
|
||
*
|
||
* 内容区按分类类型切换:
|
||
* - apps:指定分组时只渲染该组,否则单组扁平渲染、多组带小标题分组渲染;
|
||
* - posters:6 列海报;
|
||
* - articles:上图下文白卡网格。
|
||
*/
|
||
type DetailViewProps = {
|
||
category: Category
|
||
/** 从侧栏子菜单进入时指定的分组 */
|
||
groupId?: string
|
||
/** 点击面包屑中间项(SPA 路由) */
|
||
onNavigate: (href: string) => void
|
||
/** 点击卡片 / 海报进入条目详情 */
|
||
onOpenItem: (itemId: string) => void
|
||
}
|
||
|
||
export function DetailView({ category, groupId, onNavigate, onOpenItem }: DetailViewProps) {
|
||
// 三种类型共用「分区 → 分组 → 条目」结构;这里抹平联合类型,渲染时再按 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
|
||
|
||
const title = activeGroup?.name ?? category.title
|
||
const count = activeGroup !== undefined ? activeGroup.items.length : categoryCount(category)
|
||
const crumbs =
|
||
activeGroup !== undefined
|
||
? [
|
||
{ label: '首页', href: '/' },
|
||
{ label: category.title, href: viewPath({ type: 'detail', categoryId: category.id }) },
|
||
{ label: activeGroup.name },
|
||
]
|
||
: [
|
||
{ label: '首页', href: '/' },
|
||
{ label: category.title, href: viewPath({ type: 'detail', categoryId: category.id }) },
|
||
]
|
||
|
||
/** 按类型渲染一组条目 */
|
||
function renderItems(items: RenderItem[]) {
|
||
if (category.kind === 'posters') {
|
||
return <PosterGrid items={items as PosterItem[]} variant="detail" onOpen={onOpenItem} />
|
||
}
|
||
|
||
if (category.kind === 'articles') {
|
||
return <CesCardGrid items={items as ArticleItem[]} onOpen={onOpenItem} />
|
||
}
|
||
|
||
return <AppGrid items={items as AppItem[]} variant="detail" onOpen={onOpenItem} />
|
||
}
|
||
|
||
function renderBody() {
|
||
if (activeGroup !== undefined) {
|
||
return renderItems(activeGroup.items)
|
||
}
|
||
|
||
const direct = (category.items ?? []) as RenderItem[]
|
||
// 空分组不占篇幅(管理后台仍然保留,之后可以往里加条目)
|
||
const filledGroups = groups.filter((group) => group.items.length > 0)
|
||
|
||
if (filledGroups.length === 0) {
|
||
return direct.length > 0 ? (
|
||
renderItems(direct)
|
||
) : (
|
||
<Empty text={'emptyText' in category ? (category.emptyText ?? '暂无内容') : '暂无内容'} />
|
||
)
|
||
}
|
||
|
||
if (filledGroups.length === 1 && direct.length === 0) {
|
||
return renderItems(filledGroups[0].items)
|
||
}
|
||
|
||
return (
|
||
<Space direction="column" gap="lg" align="stretch">
|
||
{filledGroups.map((group) => (
|
||
<GroupBlock
|
||
key={group.id}
|
||
name={group.name}
|
||
barColor={category.barColor !== '' ? category.barColor : undefined}
|
||
>
|
||
{renderItems(group.items)}
|
||
</GroupBlock>
|
||
))}
|
||
{direct.length > 0 && (
|
||
<GroupBlock
|
||
name="未分组"
|
||
barColor={category.barColor !== '' ? category.barColor : undefined}
|
||
>
|
||
{renderItems(direct)}
|
||
</GroupBlock>
|
||
)}
|
||
</Space>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="detail-view">
|
||
<Card padding="none" className="detail-nav">
|
||
<Breadcrumb
|
||
className="detail-breadcrumb"
|
||
items={crumbs}
|
||
onNavigate={onNavigate}
|
||
/>
|
||
</Card>
|
||
|
||
<div className="detail-hero">
|
||
<span className="detail-hero-icon" aria-hidden="true">
|
||
{category.icon}
|
||
</span>
|
||
<Title level={1} as="h2" weight="strong" className="detail-hero-title">
|
||
{title}
|
||
</Title>
|
||
<span className="detail-hero-count">[{count}]</span>
|
||
</div>
|
||
|
||
{renderBody()}
|
||
</div>
|
||
)
|
||
}
|