feat(web): 对齐 Alright 主题视觉规范(配色/字体/图标/背景/动效),并补充设计规范文档

This commit is contained in:
2026-10-22 20:27:10 +08:00
parent e522a98f6f
commit 386c0f77c7
50 changed files with 6344 additions and 1094 deletions
+267
View File
@@ -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]
}
+75
View File
@@ -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,
}
}
+49
View File
@@ -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))
}
+54
View File
@@ -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' } },
],
},
]
+190
View File
@@ -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)
}