feat(web): 对齐 Alright 主题视觉规范(配色/字体/图标/背景/动效),并补充设计规范文档
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
import type {
|
||||
AppCategory,
|
||||
AppGroup,
|
||||
AppItem,
|
||||
ArticleBlock,
|
||||
ArticleCategory,
|
||||
Category,
|
||||
PosterCategory,
|
||||
} from './types'
|
||||
|
||||
/**
|
||||
* 占位内容:沿用 index4.html 模板里的示例数据。
|
||||
*
|
||||
* 除「本地服务」由后端项目列表动态生成外,其余分类都是静态占位,
|
||||
* 等后端提供分类 / 收藏接口后再替换。
|
||||
*/
|
||||
|
||||
/**
|
||||
* 详情页占位统计:按 id 生成稳定的数值,
|
||||
* 保证同一站点每次打开看到的是同一组数字,而不是随机跳变。
|
||||
*/
|
||||
function stat(seed: string, min: number, max: number): number {
|
||||
let hash = 0
|
||||
for (const char of seed) {
|
||||
hash = (hash * 31 + char.charCodeAt(0)) % 100_000
|
||||
}
|
||||
return min + (hash % (max - min + 1))
|
||||
}
|
||||
|
||||
/** 只有少数条目带 HOT 徽标 —— 主题里也不是每个作者都带 */
|
||||
const HOT_IDS = new Set(['github', 'bilibili', 'wallhaven'])
|
||||
|
||||
/** 生成一条占位站点,详情页需要的头像 / 横幅 / 评价也一并补齐 */
|
||||
const app = (id: string, name: string, desc: string, icon: string, href = '#'): AppItem => ({
|
||||
id,
|
||||
name,
|
||||
desc,
|
||||
icon,
|
||||
href,
|
||||
cover: `https://picsum.photos/seed/${id}/200/200`,
|
||||
banner: `https://picsum.photos/seed/${id}-banner/800/300`,
|
||||
comment: '收录在本地导航里的常用站点,点开就能直达。',
|
||||
badge: '推荐好站',
|
||||
hot: HOT_IDS.has(id),
|
||||
views: stat(id, 60, 980),
|
||||
likes: stat(`${id}-like`, 3, 88),
|
||||
})
|
||||
|
||||
/** 常用推荐:第一个分组是首页预览展示的四项 */
|
||||
const RECOMMEND_GROUPS: AppGroup[] = [
|
||||
{
|
||||
id: 'common',
|
||||
name: '常用站点',
|
||||
items: [
|
||||
app('github', 'GitHub', '代码托管与开源社区', 'G'),
|
||||
app('zhihu', '知乎', '深度内容问答社区', 'Z'),
|
||||
app('bilibili', '哔哩哔哩', '弹幕视频分享网站', 'B'),
|
||||
app('v2ex', 'V2EX', '创意工作者讨论社区', 'V'),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ui',
|
||||
name: 'UI 设计资源',
|
||||
items: [
|
||||
app('wallhaven', 'Wallhaven', 'WallBase 创办人失踪后推出的精选壁纸站', 'W'),
|
||||
app('500px', '500px', '有着世界上最好的照片分享社区', '5'),
|
||||
app('chuangyiren', '创意人', '去掉了形式主义描述的原创社区', 'C'),
|
||||
app('topys', '顶尖文案', '全球顶尖创意分享平台 OPEN YOUR EYES', 'T'),
|
||||
app('uichina', 'UI中国', '前身为 iconfans,国内专业的 UI 设计平台', 'U'),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'vector',
|
||||
name: '矢量图标',
|
||||
items: [
|
||||
app('iconfont', 'Iconfont', '阿里妈妈矢量图标库', 'I'),
|
||||
app('feather', 'Feather Icons', '简单漂亮的开源图标库', 'F'),
|
||||
app('fontawesome', 'FontAwesome', '图标字体与 CSS 框架', 'A'),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'wallpaper',
|
||||
name: '免费壁纸',
|
||||
items: [
|
||||
app('unsplash', 'Unsplash', '免费高清无版权摄影图片', 'U'),
|
||||
app('pexels', 'Pexels', '才华横溢的摄影师分享的素材', 'P'),
|
||||
app('pixabay', 'Pixabay', '超过 2.7 百万张优质图片和视频', 'X'),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'dev',
|
||||
name: '开发工具',
|
||||
items: [
|
||||
app('codepen', 'CodePen', '前端在线代码演示和练习场', 'C'),
|
||||
app('stackoverflow', 'Stack Overflow', '全球程序员技术问答社区', 'S'),
|
||||
app('github-dev', 'GitHub', '代码托管与开源协作社区', 'G'),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const RECOMMEND: AppCategory = {
|
||||
id: 'recommend',
|
||||
kind: 'apps',
|
||||
title: '常用推荐',
|
||||
barColor: '#3858f6',
|
||||
icon: '🔖',
|
||||
groups: RECOMMEND_GROUPS,
|
||||
}
|
||||
|
||||
/** 电影占位数据:标题 + 一句简介,简介同时用作详情页正文 */
|
||||
const MOVIE_SEEDS: [string, string][] = [
|
||||
[
|
||||
'千与千寻 千と千尋の神隠し (2001)',
|
||||
'少女千寻误入神灵世界,为救回变成猪的父母,在汤屋打工并逐渐学会担当。',
|
||||
],
|
||||
[
|
||||
'美丽人生 La vita è bella (1997)',
|
||||
'二战集中营里,父亲用一场「游戏」保护儿子,让他相信世界仍然美好。',
|
||||
],
|
||||
[
|
||||
'这个杀手不太冷 Léon (1994)',
|
||||
'孤独的职业杀手莱昂收留了邻居少女玛蒂尔达,两个边缘人相互依靠。',
|
||||
],
|
||||
[
|
||||
'阿甘正传 Forrest Gump (1994)',
|
||||
'智商不高的阿甘凭着一股单纯的执着,意外见证并参与了几十年美国历史。',
|
||||
],
|
||||
['霸王别姬 (1993)', '两位京剧伶人半个世纪的悲欢离合,与时代洪流交织在一起。'],
|
||||
[
|
||||
'肖申克的救赎 The Shawshank Redemption',
|
||||
'蒙冤入狱的银行家安迪,在漫长牢狱中守住希望并完成自救。',
|
||||
],
|
||||
[
|
||||
'泰坦尼克号 Titanic (1997)',
|
||||
'沉船之夜,来自不同阶层的杰克与露丝相爱,也被人性的选择所考验。',
|
||||
],
|
||||
['盗梦空间 Inception (2010)', '盗梦团队潜入层层梦境,试图在目标的潜意识里植入一个念头。'],
|
||||
[
|
||||
'星际穿越 Interstellar (2014)',
|
||||
'地球濒临枯竭,父亲穿越虫洞寻找新家园,与女儿跨越时间重逢。',
|
||||
],
|
||||
[
|
||||
'楚门的世界 The Truman Show (1998)',
|
||||
'楚门渐渐发现,自己的整个人生其实是一档被全世界围观的真人秀。',
|
||||
],
|
||||
['疯狂动物城 Zootopia (2016)', '兔子警官与狐狸搭档,在动物城里侦破一桩失踪案,也打破偏见。'],
|
||||
[
|
||||
'大话西游之大圣娶亲 (1995)',
|
||||
'至尊宝在爱情与宿命之间反复抉择,最终戴上金箍踏上取经路。',
|
||||
],
|
||||
]
|
||||
|
||||
const MOVIES: PosterCategory = {
|
||||
id: 'movie',
|
||||
kind: 'posters',
|
||||
title: '推荐电影',
|
||||
barColor: '#ff6034',
|
||||
icon: '🎬',
|
||||
items: MOVIE_SEEDS.map(([title, summary], index) => {
|
||||
const id = `movie-${index + 1}`
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
cover: `https://picsum.photos/seed/m${index + 1}/300/450`,
|
||||
tags: ['推荐电影', '豆瓣电影 Top250'],
|
||||
comment: '收藏在本地片单里,闲下来就重看一遍。',
|
||||
summary,
|
||||
href: '#',
|
||||
views: stat(id, 300, 1500),
|
||||
likes: stat(`${id}-like`, 3, 60),
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
const ARTICLE_SEEDS = [
|
||||
['可塑性极强的玻璃,在设计师手里有了更多可能', '前沿科技'],
|
||||
['废旧共享单车如何变成微型共享书屋?', '环保设计'],
|
||||
['荧光灯管与混凝土块的组合,可以玩出多少花样?', '材料探索'],
|
||||
['混凝土做的模块化收纳框,还能改成搁架用', '家居模块'],
|
||||
['这个用拉伸材料做的头盔,还能帮你呼叫救护车', '智能穿戴'],
|
||||
['你如何选择字体,简单易读还是好看有趣?', '视觉体验'],
|
||||
['家里空间小?你需要几把可以悬挂在墙上的椅子', '空间收纳'],
|
||||
['宜家厨房家具又被改造了,色彩是亮点', '色彩灵感'],
|
||||
['这款诞生 50 年的扶手椅,推出了特别版配色', '经典产品'],
|
||||
['让家和办公室界限变得模糊的一组家具', '办公家具'],
|
||||
['这个酒店设计奖,挖掘过去一年体验最好的酒店', '空间建筑'],
|
||||
['一款可移动办公桌,挂墙上当作白板就能写写画画', '移动办公'],
|
||||
]
|
||||
|
||||
/** 生成演示用正文;接入真实数据后由后端提供 blocks */
|
||||
function buildArticleBlocks(title: string, desc: string, cover: string): ArticleBlock[] {
|
||||
return [
|
||||
{
|
||||
type: 'paragraph',
|
||||
text: `开放与明亮的氛围,在室内空间中尤为注重。这条以「${desc}」为主题的报道,围绕材料、结构与使用场景展开,我们挑了几个值得一看的案例。`,
|
||||
},
|
||||
{ type: 'heading', text: '设计细节' },
|
||||
{
|
||||
type: 'paragraph',
|
||||
text: `《${title}》之所以特别,不在于用了多昂贵的材料,而是把材料本身的特性放在了第一位:既没有沿用传统做法,也不是单纯的形式实验。`,
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
text: '在有限的空间里,它既保持了功能的完整性,也留出了呼吸感,适合日常使用。',
|
||||
},
|
||||
{ type: 'image', src: cover },
|
||||
{
|
||||
type: 'paragraph',
|
||||
text: '文字与图片均为本地演示用的占位内容,实际报道接入后由后端提供。',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const CES: ArticleCategory = {
|
||||
id: 'ces',
|
||||
kind: 'articles',
|
||||
title: 'CES 现场报道',
|
||||
barColor: '#ffba3a',
|
||||
icon: '🏷️',
|
||||
items: ARTICLE_SEEDS.map(([title, desc], index) => {
|
||||
const cover = `https://picsum.photos/seed/a${index + 1}/400/250`
|
||||
|
||||
return {
|
||||
id: `ces-${index + 1}`,
|
||||
title: `${title} | 这个设计了不起`,
|
||||
desc,
|
||||
cover,
|
||||
meta: ['2 年前', 'CES 现场报道'],
|
||||
blocks: buildArticleBlocks(title, desc, cover),
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
/**
|
||||
* 把后端项目列表转成「本地服务」分类。
|
||||
* href 用真实路由,详情页的「跳转」按钮直接打开对应静态站点。
|
||||
*/
|
||||
function buildLocalCategory(items: AppItem[]): AppCategory {
|
||||
return {
|
||||
id: 'local',
|
||||
kind: 'apps',
|
||||
title: '本地服务',
|
||||
barColor: '#3858f6',
|
||||
icon: '🖥️',
|
||||
groups: items.length > 0 ? [{ id: 'local-all', name: '全部项目', items }] : [],
|
||||
previewLimit: 8,
|
||||
emptyText: '还没有项目:在托管目录下建一个子目录并放入 index.html,刷新后就会出现在这里。',
|
||||
}
|
||||
}
|
||||
|
||||
/** 组装首页 + 明细所需的全部分类,本地服务始终排第一 */
|
||||
export function buildCategories(projects: { name: string; route_path: string }[]): Category[] {
|
||||
const localItems: AppItem[] = projects.map((project) => ({
|
||||
id: project.route_path,
|
||||
name: project.name,
|
||||
desc: project.route_path,
|
||||
icon: project.name.slice(0, 1).toUpperCase(),
|
||||
href: project.route_path,
|
||||
// 本地项目没有封面图,详情页退化为字符头像
|
||||
comment: '本地静态站点,由 Stash 直接托管。',
|
||||
badge: '本地项目',
|
||||
views: 0,
|
||||
likes: 0,
|
||||
}))
|
||||
|
||||
return [buildLocalCategory(localItems), RECOMMEND, MOVIES, CES]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { AppItem, PosterItem, RelatedItem } from './types'
|
||||
|
||||
/**
|
||||
* 条目详情页的通用视图模型。
|
||||
*
|
||||
* 应用(方形头像 + 横幅)和电影(竖版海报 + 多标签)共用同一套版式,
|
||||
* 差异通过 shape / tags / banner 这几个字段表达,避免维护两份几乎相同的页面。
|
||||
*/
|
||||
export type ItemDetailModel = {
|
||||
title: string
|
||||
/** 主图;缺失时用 fallbackIcon 显示字符块 */
|
||||
cover?: string
|
||||
/** 主图形态:应用用方形,电影用竖版海报 */
|
||||
shape: 'square' | 'portrait'
|
||||
/** 主图缺省时的字符 */
|
||||
fallbackIcon?: string
|
||||
/** 标签行(应用 1 个,电影 2 个) */
|
||||
tags: string[]
|
||||
/** 一句话评价 */
|
||||
comment?: string
|
||||
/** 正文段落 */
|
||||
paragraphs: string[]
|
||||
views: number
|
||||
likes: number
|
||||
href: string
|
||||
/** 应用详情页的横幅图 */
|
||||
banner?: string
|
||||
}
|
||||
|
||||
/** 把应用条目转成详情模型 */
|
||||
export function appDetailModel(item: AppItem): ItemDetailModel {
|
||||
return {
|
||||
title: item.name,
|
||||
cover: item.cover,
|
||||
shape: 'square',
|
||||
fallbackIcon: item.icon,
|
||||
tags: item.badge !== undefined ? [item.badge] : [],
|
||||
comment: item.comment,
|
||||
paragraphs: [item.desc],
|
||||
views: item.views ?? 0,
|
||||
likes: item.likes ?? 0,
|
||||
href: item.href,
|
||||
banner: item.banner,
|
||||
}
|
||||
}
|
||||
|
||||
/** 把电影海报转成详情模型 */
|
||||
export function posterDetailModel(item: PosterItem): ItemDetailModel {
|
||||
return {
|
||||
title: item.title,
|
||||
cover: item.cover,
|
||||
shape: 'portrait',
|
||||
tags: item.tags ?? [],
|
||||
comment: item.comment,
|
||||
paragraphs: item.summary !== undefined ? [item.summary] : [],
|
||||
views: item.views ?? 0,
|
||||
likes: item.likes ?? 0,
|
||||
href: item.href ?? '#',
|
||||
}
|
||||
}
|
||||
|
||||
/** 应用条目 → 推荐阅读条目 */
|
||||
export function toRelatedItem(item: AppItem): RelatedItem {
|
||||
return { id: item.id, name: item.name, desc: item.desc, cover: item.cover, icon: item.icon }
|
||||
}
|
||||
|
||||
/** 电影海报 → 推荐阅读条目 */
|
||||
export function posterToRelatedItem(item: PosterItem): RelatedItem {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.title,
|
||||
desc: item.tags?.join(' · ') ?? '推荐电影',
|
||||
cover: item.cover,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { Category } from './types'
|
||||
|
||||
/**
|
||||
* 按关键字过滤分类内容。
|
||||
*
|
||||
* 应用类分类会逐组过滤并丢掉空组,海报 / 图文类按标题或描述匹配;
|
||||
* 空关键字直接原样返回,避免无谓的对象重建。
|
||||
*/
|
||||
export function filterCategories(categories: Category[], keyword: string): Category[] {
|
||||
const query = keyword.trim().toLowerCase()
|
||||
if (query === '') return categories
|
||||
|
||||
const result: Category[] = []
|
||||
|
||||
for (const category of categories) {
|
||||
if (category.kind === 'apps') {
|
||||
const groups = category.groups
|
||||
.map((group) => ({
|
||||
...group,
|
||||
items: group.items.filter((item) => matches(query, item.name, item.desc)),
|
||||
}))
|
||||
.filter((group) => group.items.length > 0)
|
||||
|
||||
if (groups.length > 0) {
|
||||
result.push({ ...category, groups })
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (category.kind === 'posters') {
|
||||
const items = category.items.filter((item) => matches(query, item.title))
|
||||
if (items.length > 0) {
|
||||
result.push({ ...category, items })
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const items = category.items.filter((item) => matches(query, item.title, item.desc))
|
||||
if (items.length > 0) {
|
||||
result.push({ ...category, items })
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function matches(query: string, ...fields: string[]): boolean {
|
||||
return fields.some((field) => field.toLowerCase().includes(query))
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 侧栏分类导航的数据定义。
|
||||
*
|
||||
* 叶子节点通过 target 指向「分类 + 可选分组」,点击后右侧切换到对应明细。
|
||||
* 标签保持与现有侧栏一致,避免改动已定稿的侧栏。
|
||||
*/
|
||||
|
||||
export type NavTarget = {
|
||||
categoryId: string
|
||||
/** 指定后明细页只展示该分组 */
|
||||
groupId?: string
|
||||
}
|
||||
|
||||
export type NavLeaf = {
|
||||
id: string
|
||||
label: string
|
||||
target: NavTarget
|
||||
}
|
||||
|
||||
export type NavGroupNode = {
|
||||
id: string
|
||||
label: string
|
||||
leaves: NavLeaf[]
|
||||
}
|
||||
|
||||
/** 顶层直达链接 */
|
||||
export const NAV_TOP_LEVEL: NavLeaf[] = [
|
||||
{ id: 'recommend', label: '常用推荐', target: { categoryId: 'recommend' } },
|
||||
]
|
||||
|
||||
/** 可折叠分组 */
|
||||
export const NAV_GROUPS: NavGroupNode[] = [
|
||||
{
|
||||
id: 'assets',
|
||||
label: '素材资源',
|
||||
leaves: [
|
||||
{ id: 'ui', label: 'UI 设计资源', target: { categoryId: 'recommend', groupId: 'ui' } },
|
||||
{ id: 'vector', label: '矢量图标', target: { categoryId: 'recommend', groupId: 'vector' } },
|
||||
{
|
||||
id: 'wallpaper',
|
||||
label: '免费壁纸',
|
||||
target: { categoryId: 'recommend', groupId: 'wallpaper' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tools',
|
||||
label: '开发工具',
|
||||
leaves: [
|
||||
{ id: 'dev', label: '前端调试工具', target: { categoryId: 'recommend', groupId: 'dev' } },
|
||||
{ id: 'code', label: '在线编辑器', target: { categoryId: 'recommend', groupId: 'dev' } },
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 内容数据模型。
|
||||
*
|
||||
* 首页把每个「分类」渲染成一张卡片,明细页按分类的 kind 渲染不同网格,
|
||||
* 因此这里用可辨识联合把三种展示形态分开。
|
||||
*/
|
||||
|
||||
/** 一个可点击的应用 / 站点条目 */
|
||||
export type AppItem = {
|
||||
id: string
|
||||
name: string
|
||||
desc: string
|
||||
/** 卡片左侧的字符图标 */
|
||||
icon: string
|
||||
/** 跳转地址;本地项目为 /<目录名>/ */
|
||||
href: string
|
||||
/** 详情页头像 / 封面,缺省时退化为字符图标 */
|
||||
cover?: string
|
||||
/** 是否为热门条目:卡片名字后显示主题同款 HOT 徽标 */
|
||||
hot?: boolean
|
||||
/** 详情页的一句话评价 */
|
||||
comment?: string
|
||||
/** 详情页横幅图 */
|
||||
banner?: string
|
||||
/** 详情页头像下方的标签 */
|
||||
badge?: string
|
||||
/** 详情页浏览量(占位数据) */
|
||||
views?: number
|
||||
/** 详情页点赞数(占位数据) */
|
||||
likes?: number
|
||||
}
|
||||
|
||||
/** 应用分组:明细页按组展示,首页取所有分组的前几项做预览 */
|
||||
export type AppGroup = {
|
||||
id: string
|
||||
name: string
|
||||
items: AppItem[]
|
||||
}
|
||||
|
||||
/** 竖版海报条目(电影) */
|
||||
export type PosterItem = {
|
||||
id: string
|
||||
title: string
|
||||
cover: string
|
||||
/** 详情页标签行,如「推荐电影」「豆瓣电影 Top250」 */
|
||||
tags?: string[]
|
||||
/** 详情页一句话评价 */
|
||||
comment?: string
|
||||
/** 详情页正文段落 */
|
||||
summary?: string
|
||||
/** 跳转地址(占位数据暂无真实链接) */
|
||||
href?: string
|
||||
views?: number
|
||||
likes?: number
|
||||
}
|
||||
|
||||
/** 「推荐阅读」用的轻量条目,应用与电影都能映射过来 */
|
||||
export type RelatedItem = {
|
||||
id: string
|
||||
name: string
|
||||
desc: string
|
||||
cover?: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
/** 文章正文块:段落 / 小标题 / 配图 */
|
||||
export type ArticleBlock =
|
||||
| { type: 'paragraph'; text: string }
|
||||
| { type: 'heading'; text: string }
|
||||
| { type: 'image'; src: string }
|
||||
|
||||
/** 图文条目(报道) */
|
||||
export type ArticleItem = {
|
||||
id: string
|
||||
title: string
|
||||
desc: string
|
||||
cover: string
|
||||
/** 详情页标题下的元信息,如 ['2 年前', 'CES 现场报道'] */
|
||||
meta?: string[]
|
||||
/** 详情页正文 */
|
||||
blocks?: ArticleBlock[]
|
||||
}
|
||||
|
||||
/** 应用类分类:内部按分组组织 */
|
||||
export type AppCategory = {
|
||||
id: string
|
||||
kind: 'apps'
|
||||
title: string
|
||||
/** 标题左侧色条颜色,各分类用不同颜色区分 */
|
||||
barColor: string
|
||||
/** 明细页 hero 区的字符图标 */
|
||||
icon: string
|
||||
groups: AppGroup[]
|
||||
/** 首页预览条数,默认 4 */
|
||||
previewLimit?: number
|
||||
/** 首页预览为空时的提示文案 */
|
||||
emptyText?: string
|
||||
}
|
||||
|
||||
/** 海报类分类:竖版海报网格 */
|
||||
export type PosterCategory = {
|
||||
id: string
|
||||
kind: 'posters'
|
||||
title: string
|
||||
barColor: string
|
||||
icon: string
|
||||
items: PosterItem[]
|
||||
/** 首页预览条数,默认 6 */
|
||||
previewLimit?: number
|
||||
}
|
||||
|
||||
/** 图文类分类:上图下文卡片网格 */
|
||||
export type ArticleCategory = {
|
||||
id: string
|
||||
kind: 'articles'
|
||||
title: string
|
||||
barColor: string
|
||||
icon: string
|
||||
items: ArticleItem[]
|
||||
/** 首页预览条数,默认 4 */
|
||||
previewLimit?: number
|
||||
}
|
||||
|
||||
export type Category = AppCategory | PosterCategory | ArticleCategory
|
||||
|
||||
/** 分类内的条目总数,用于角标与明细页计数 */
|
||||
export function categoryCount(category: Category): number {
|
||||
if (category.kind === 'apps') {
|
||||
return category.groups.reduce((total, group) => total + group.items.length, 0)
|
||||
}
|
||||
return category.items.length
|
||||
}
|
||||
|
||||
/** 把应用类分类的分组拍平,首页预览用 */
|
||||
export function flattenAppItems(category: AppCategory): AppItem[] {
|
||||
return category.groups.flatMap((group) => group.items)
|
||||
}
|
||||
|
||||
/** 按 id 在分类里查找条目;详情页用 */
|
||||
export function findAppItem(category: Category, itemId: string): AppItem | undefined {
|
||||
if (category.kind !== 'apps') return undefined
|
||||
|
||||
for (const group of category.groups) {
|
||||
const found = group.items.find((item) => item.id === itemId)
|
||||
if (found !== undefined) return found
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 取同分类下的其他条目,用于详情页的「推荐阅读」 */
|
||||
export function relatedAppItems(
|
||||
category: Category,
|
||||
excludeId: string | undefined,
|
||||
limit = 4,
|
||||
): AppItem[] {
|
||||
if (category.kind !== 'apps') return []
|
||||
|
||||
return flattenAppItems(category)
|
||||
.filter((item) => item.id !== excludeId)
|
||||
.slice(0, limit)
|
||||
}
|
||||
|
||||
/** 按 id 查找海报条目 */
|
||||
export function findPosterItem(
|
||||
category: Category,
|
||||
itemId: string,
|
||||
): PosterItem | undefined {
|
||||
if (category.kind !== 'posters') return undefined
|
||||
return category.items.find((item) => item.id === itemId)
|
||||
}
|
||||
|
||||
/** 取同分类下的其他海报,用于详情页的「推荐阅读」 */
|
||||
export function relatedPosterItems(
|
||||
category: Category,
|
||||
excludeId: string | undefined,
|
||||
limit = 4,
|
||||
): PosterItem[] {
|
||||
if (category.kind !== 'posters') return []
|
||||
|
||||
return category.items.filter((item) => item.id !== excludeId).slice(0, limit)
|
||||
}
|
||||
|
||||
/** 按 id 查找图文条目 */
|
||||
export function findArticleItem(
|
||||
category: Category,
|
||||
itemId: string,
|
||||
): ArticleItem | undefined {
|
||||
if (category.kind !== 'articles') return undefined
|
||||
return category.items.find((item) => item.id === itemId)
|
||||
}
|
||||
Reference in New Issue
Block a user