feat(admin): 首页卡片配置 + 页头开关验证 + 编辑按钮仅登录可见

- settings.sections:显隐 / 顺序 / 标题覆盖 / 预览条数,HomeView 应用
- /admin 新增「首页卡片」Tab,卡片标题使用展示块标题(常用站点等)
- 详情页 [编辑] 仅在登录后渲染(canEdit)
- 测试:46 passed;页头禁用、卡片隐藏/改标题/限条数、编辑可见性均实测
- 文档:ADMIN-PLAN、README
This commit is contained in:
stash
2026-10-29 19:21:34 +08:00
parent 8f688885ed
commit 3c6c20e534
10 changed files with 251 additions and 21 deletions
+25 -10
View File
@@ -4,33 +4,46 @@ import { ArticleGrid } from './ArticleGrid'
import { Empty } from '@stash/ui'
import { PosterGrid } from './PosterGrid'
import { SectionCard } from './SectionCard'
import {
applySectionSettings,
sectionLimit,
sectionTitle,
type SectionSettings,
} from '../../settings'
/**
* 首页:把每个分类渲染成一张分区卡片。
*
* 每个分区只展示前若干条(由分类的 previewLimit 决定),
* 右上角 ••• 进入该分类明细,点击卡片 / 海报则进入该条目的详情。
* 每个分区只展示前若干条;标题、条数、顺序与显隐都可以在 `/admin`
* 的「首页卡片」里覆盖(见 settings.sections)。
*/
type HomeViewProps = {
categories: Category[]
sections: SectionSettings
onOpenDetail: (categoryId: string) => void
onOpenItem: (categoryId: string, itemId: string) => void
}
export function HomeView({ categories, onOpenDetail, onOpenItem }: HomeViewProps) {
export function HomeView({ categories, sections, onOpenDetail, onOpenItem }: HomeViewProps) {
const cards = applySectionSettings(categories, sections)
return (
<div className="home-view">
{categories.map((category) => (
{cards.map((category) => (
<SectionCard
key={category.id}
// 卡片标题跟着实际展示的内容走:配了展示块就用块标题(如「常用站点」),
// 没配(如「本地服务」)才用分类标题
title={category.block?.title ?? category.title}
// 没配(如「本地服务」)才用分类标题;设置里的覆盖值优先
title={sectionTitle(
category.id,
category.block?.title ?? category.title,
sections,
)}
count={cardCount(category)}
barColor={category.barColor}
onMore={() => onOpenDetail(category.id)}
>
{renderPreview(category, onOpenItem)}
{renderPreview(category, sections, onOpenItem)}
</SectionCard>
))}
</div>
@@ -55,6 +68,7 @@ function cardCount(category: Category): number {
/** 按分类类型输出对应的预览网格;应用类为空时给出提示文案 */
function renderPreview(
category: Category,
sections: SectionSettings,
onOpenItem: (categoryId: string, itemId: string) => void,
) {
if (category.kind === 'apps') {
@@ -65,7 +79,8 @@ function renderPreview(
: undefined
const source = category.block !== undefined ? (blockGroup?.items ?? []) : flattenAppItems(category)
const limit = category.block?.previewLimit ?? category.previewLimit ?? 4
const fallback = category.block?.previewLimit ?? category.previewLimit ?? 4
const limit = sectionLimit(category.id, fallback, sections)
const items = source.slice(0, limit)
if (items.length === 0) {
@@ -78,7 +93,7 @@ function renderPreview(
if (category.kind === 'posters') {
return (
<PosterGrid
items={category.items.slice(0, category.previewLimit ?? 6)}
items={category.items.slice(0, sectionLimit(category.id, category.previewLimit ?? 6, sections))}
onOpen={(itemId) => onOpenItem(category.id, itemId)}
/>
)
@@ -86,7 +101,7 @@ function renderPreview(
return (
<ArticleGrid
items={category.items.slice(0, category.previewLimit ?? 4)}
items={category.items.slice(0, sectionLimit(category.id, category.previewLimit ?? 4, sections))}
onOpen={(itemId) => onOpenItem(category.id, itemId)}
/>
)