feat: 格式从分组下沉到条目(v21)
- entry 新增 format(apps/posters/articles),老数据按根分组 view_mode 回填;seed 按分区写入 - 分组管理移除格式字段/显示,根分组只作容器 - 内容弹窗新增逐条格式选择,列表增加格式列;ItemEditor 保存时保留原格式 - 前台按 entry.format 渲染:卡片/分组页支持同组混合格式,详情按条目格式选模板 - 迁移后 live 20 应用 / 13 海报;cargo test 59 passed;Playwright 验证混合展示
This commit is contained in:
+32
-14
@@ -144,8 +144,12 @@ function collectEntries(group: GroupNode): Record<string, unknown>[] {
|
||||
}
|
||||
|
||||
function defaultLimit(group: GroupNode): number {
|
||||
const fallback = group.format === 'apps' ? 4 : group.format === 'posters' ? 6 : 4
|
||||
return group.previewLimit ?? fallback
|
||||
return group.previewLimit ?? 6
|
||||
}
|
||||
|
||||
function entryFormat(entry: Record<string, unknown>): 'apps' | 'posters' | 'articles' {
|
||||
const value = entry.format
|
||||
return value === 'posters' || value === 'articles' ? value : 'apps'
|
||||
}
|
||||
|
||||
function matches(entry: Record<string, unknown>, keyword: string): boolean {
|
||||
@@ -273,13 +277,25 @@ export default function AppNext() {
|
||||
const items = filtered.slice(0, limit)
|
||||
const open = (itemId: string) => navigate(`/g/${encodeURIComponent(group.key)}/i/${encodeURIComponent(itemId)}`)
|
||||
|
||||
if (group.format === 'posters') {
|
||||
return <PosterGrid items={items as unknown as PosterItem[]} onOpen={open} />
|
||||
}
|
||||
if (group.format === 'articles') {
|
||||
return <CesCardGrid items={items as unknown as ArticleItem[]} onOpen={open} />
|
||||
}
|
||||
return <AppGrid items={items as unknown as AppItem[]} onOpen={open} />
|
||||
return (
|
||||
<>
|
||||
{(['apps', 'posters', 'articles'] as const).map((format) => {
|
||||
const subset = items.filter((entry) => entryFormat(entry) === format)
|
||||
if (subset.length === 0) return null
|
||||
return (
|
||||
<div className="mixed-format-block" key={format}>
|
||||
{format === 'posters' ? (
|
||||
<PosterGrid items={subset as unknown as PosterItem[]} onOpen={open} />
|
||||
) : format === 'articles' ? (
|
||||
<CesCardGrid items={subset as unknown as ArticleItem[]} onOpen={open} />
|
||||
) : (
|
||||
<AppGrid items={subset as unknown as AppItem[]} onOpen={open} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function renderGroupBlock(group: GroupNode, limit: number, showTitle: boolean) {
|
||||
@@ -423,7 +439,9 @@ export default function AppNext() {
|
||||
{ label: String(entry.name ?? entry.title ?? entry.id) },
|
||||
]
|
||||
|
||||
if (group.format === 'articles') {
|
||||
const format = entryFormat(entry)
|
||||
|
||||
if (format === 'articles') {
|
||||
const article = entry as unknown as ArticleItem
|
||||
return (
|
||||
<ArticleDetailView
|
||||
@@ -438,18 +456,18 @@ export default function AppNext() {
|
||||
}
|
||||
|
||||
const model: ItemDetailModel =
|
||||
group.format === 'posters'
|
||||
format === 'posters'
|
||||
? posterDetailModel(entry as unknown as PosterItem)
|
||||
: appDetailModel(entry as unknown as AppItem)
|
||||
|
||||
const related: RelatedItem[] =
|
||||
group.format === 'posters'
|
||||
format === 'posters'
|
||||
? group.entries
|
||||
.filter((item) => item.id !== entry.id)
|
||||
.filter((item) => item.id !== entry.id && entryFormat(item as unknown as Record<string, unknown>) === format)
|
||||
.slice(0, 4)
|
||||
.map((item) => posterToRelatedItem(item as unknown as PosterItem))
|
||||
: group.entries
|
||||
.filter((item) => item.id !== entry.id)
|
||||
.filter((item) => item.id !== entry.id && entryFormat(item as unknown as Record<string, unknown>) === format)
|
||||
.slice(0, 4)
|
||||
.map((item) => toRelatedItem(item as unknown as AppItem))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user