Repository Wiki
LyraVoid/Mizuki

功能组件与页面级组件

Mizuki 主题的 src/components/features/ 目录承载了所有面向具体业务场景的功能组件与页面级组件(如番剧卡片、相册卡片、归档面板、密码保护、页头、统计网格等)。它们位于原子组件(atoms/)之上、页面(src/pages/)之下,是主题中承担"内容表达"职责的中间层。

Purpose and Scope

本页面覆盖以下内容:

  • src/components/features/ 的目录组织方式、命名约定与模块导出(barrel index.ts)模式
  • 功能组件与页面级组件的分层职责:如何组合 atoms/ 原子组件形成业务级 UI
  • 以 AnimeCard.astro 为核心样例的完整实现走读(Props 契约、封面渲染、状态徽章、观看进度条)
  • 两种渲染范式在同一层的共存:Astro 静态组件(.astro)与 Svelte 交互组件(.svelte)
  • 边界条件处理(如除零保护)与设计意图

明确留给兄弟页面的主题:

  • 基础原子组件(Badge、Button、Chip、Icon、Image、FilterTabs、CustomScrollbar 等)的内部实现,参见原子组件相关页面
  • 页面路由与站点数据装配(src/pages/ 层)不属于本页范围

Overview

定位与用途

在 Mizuki 的三层组件体系中:

层级目录职责渲染时机
原子层src/components/atoms/无业务语义的通用 UI 原语(按钮、图标、图片、滚动条)构建期(Astro)或客户端(Svelte)
功能层src/components/features/面向具体内容的业务组件(番剧卡、相册卡、统计卡、密码保护等)Astro 静态 / Svelte 客户端
页面层src/pages/路由与数据装配,将 feature 组件编排进页面构建期生成 HTML

功能组件的关键特征是:它们携带业务数据契约(如 anime、album 等强类型 Props),并把领域字段(评分、观看进度、年份、工作室)翻译成视觉表达。这与 atoms/ 层"只认通用视觉参数"形成明确对比——分层的目的在于让业务变化(例如新增一个内容板块)不会污染基础原语,同时让基础原语可以被多个业务模块复用。

何时使用

  • 新增一个内容展示板块(如新的媒体类型卡片)→ 在 features/ 下新建目录
  • 调整某个业务模块的视觉与交互 → 修改该模块下的 .astro / .svelte 文件
  • 需要通用视觉能力(响应式图片、图标、徽章)→ 引用 atoms/,而不是在本层重造

Architecture

下面的架构图展示功能组件层的真实模块构成及其与上下两层的依赖方向:

Loading diagram...

架构解读:

  1. 依赖方向自上而下单向:页面层组合功能层,功能层组合原子层。features/ 不会反向引用 pages/,这保证了功能组件可在任意页面复用。
  2. 每个业务模块是一个自包含目录,内含:组件文件(.astro 或 .svelte)、types.ts(Props 类型契约)、index.ts(barrel 导出)。这是全目录一致的"三件套"约定(个别纯展示模块如 pio/ 只有组件与 index.ts)。
  3. 同层存在三种渲染形态:
    • 纯 Astro(构建期输出 HTML,零 JS)——大多数内容卡片
    • 纯 Svelte(客户端岛屿,承载交互状态)——如 ArchivePanel.svelte
    • Astro 壳 + Svelte 岛混合——如 auth/PasswordProtection.astro(壳负责静态结构与数据加密)配合 PasswordModal.svelte(客户端弹窗交互)

模块清单与导出契约

下表汇总各功能模块及其 barrel 导出(依据各模块 index.ts 的真实导出语句):

模块目录主要组件类型导出
anime/AnimeCard.astro、AnimeGrid.astro、AnimeFilters.astro、AnimeSortBar.astrotypes.ts
albums/AlbumCard.astro、PhotoCard.astroexport * from "./types"
ai-tools/AIToolCard.astrotypes.ts
archive/ArchivePanel.svelteArchivePanelProps、Post
auth/Encryptor.astro、PasswordProtection.astroEncryptorProps、PasswordProtectionProps
page-header/PageHeader.astroexport * from "./types"
pio/Pio.astro无类型导出
projects-category/ProjectsCategory.astroexport * from "./types"
section-title/SectionTitle.astroexport * from "./types"
stats/StatCard.astroexport * from "./types"
stats-grid/StatsGrid.astroexport * from "./types"

index.ts 是模块对外唯一入口,其标准写法如下(以 auth 模块为例):

typescript
1/** 2 * Auth feature exports 3 */ 4 5export { default as Encryptor } from "./Encryptor.astro"; 6export { default as PasswordProtection } from "./PasswordProtection.astro"; 7export type { EncryptorProps, PasswordProtectionProps } from "./types";

Source: index.ts

这种 barrel 模式的设计意图是:消费方始终通过 @components/features/xxx 引用,模块内部文件重命名或拆分不会造成页面层的大面积改动;同时把 Props 类型一并导出,使页面在装配数据时能获得编译期类型检查。

Core Flow: 以 AnimeCard 为例的组件实现走读

AnimeCard.astro 是功能层最具代表性的实现:它把一条番剧业务数据渲染为带封面、状态徽章、评分与观看进度条的卡片。整个控制流分三段:Props 契约声明 → 派生计算 → 模板渲染。

第一步:Props 数据契约

astro
1--- 2import Image from "@components/atoms/Image/Image.astro"; 3 4interface Props { 5 anime: { 6 title: string; 7 cover: string; 8 link: string; 9 status: string; 10 rating: number; 11 progress: number; 12 totalEpisodes: number; 13 description: string; 14 year: string; 15 studio: string; 16 genre: string[]; 17 }; 18 statusInfo: { 19 text: string; 20 class: string; 21 icon: string; 22 }; 23 yearLabel: string; 24 studioLabel: string; 25} 26 27const { anime, statusInfo, yearLabel, studioLabel } = Astro.props; 28---

Source: AnimeCard.astro

设计意图:anime 是纯数据对象,而 statusInfo(状态文案/样式类/图标)由调用方在页面层计算后注入。这种"数据与表现分离"让同一个卡片能以不同状态样式复用,避免卡片内部硬编码状态→样式的映射表。yearLabel/studioLabel 作为独立标签参数,同样服务于多语言/文案定制场景。

第二步:派生计算(含边界保护)

astro
const progressPercent = anime.totalEpisodes > 0 ? (anime.progress / anime.totalEpisodes) * 100 : 0;

Source: AnimeCard.astro

这里显式做了除零保护:当 totalEpisodes 为 0(未知集数)时进度退化为 0%,避免产生 NaN/Infinity 破坏样式与文本。这是功能组件层典型的边界处理范式。

第三步:模板渲染关键片段

封面区使用原子组件 Image 完成响应式加载(widths 断点 + sizes 提示),并叠加悬停播放遮罩:

astro
1<div class="relative anime-cover-container aspect-2/3 overflow-hidden"> 2 <a href={anime.link} target="_blank" rel="noopener noreferrer" class="block w-full h-full"> 3 <Image 4 src={anime.cover} 5 alt={anime.title} 6 class="w-full h-full object-cover transition-transform duration-200 group-hover:scale-110" 7 widths={[200, 400, 600]} 8 sizes="(max-width: 640px) 50vw, (max-width: 1024px) 25vw, 20vw" 9 /> 10 </a> 11</div>

Source: AnimeCard.astro

观看进度条仅在 status === "watching" 时条件渲染,宽度绑定派生的 progressPercent:

astro
1{ 2 anime.status === "watching" && ( 3 <div class="absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/80 to-transparent p-2"> 4 <div class="w-full bg-white/20 rounded-full h-1.5 mb-1"> 5 <div 6 class="bg-linear-to-r from-emerald-400 to-teal-400 h-1.5 rounded-full transition-all duration-300" 7 style={`width: ${progressPercent}%`} 8 /> 9 </div> 10 <div class="text-white text-xs font-medium"> 11 {anime.progress}/{anime.totalEpisodes} ({Math.round(progressPercent)}%) 12 </div> 13 </div> 14 ) 15}

Source: AnimeCard.astro

完整渲染时序

Loading diagram...

为什么选择 Astro 而非 Svelte:内容卡片是纯展示、无状态、无交互(悬停效果由 CSS group-hover 完成,无需 JS)。Astro 在构建期直接输出 HTML,页面加载后没有组件水合开销;只有真正需要客户端状态的场景(如 ArchivePanel.svelte 归档面板、PasswordModal.svelte 密码弹窗)才使用 Svelte 岛屿。这是该层最重要的技术选型判据。

Usage Examples

新建一个功能模块(标准三件套)

依据目录内既有模块(如 albums、archive)的统一约定:

typescript
1// 1) 新建 src/components/features/albums/types.ts 声明 Props 契约 2// 2) 新建 AlbumCard.astro / PhotoCard.astro 组件实现 3// 3) index.ts 作为模块唯一对外入口: 4export { default as AlbumCard } from "./AlbumCard.astro"; 5export { default as PhotoCard } from "./PhotoCard.astro"; 6export * from "./types";

Source: index.ts

typescript
// archive 模块同样遵循三件套,且用显式类型导出(而非 * 导出) export { default as ArchivePanel } from "./ArchivePanel.svelte"; export type { ArchivePanelProps, Post } from "./types";

Source: index.ts

在页面中使用功能组件

页面层通过路径别名 @components/ 引用功能组件(与 AnimeCard 内部引用原子组件的写法一致):

astro
--- import AnimeCard from "@components/features/anime/AnimeCard.astro"; ---

Source: AnimeCard.astro

页面级组件的聚合用法

stats-grid/StatsGrid、projects-category/ProjectsCategory 属于"页面级组件"——它们不对应单一业务内容,而是把多个子组件聚合成页面区块。其依赖关系为 StatsGrid → StatCard(stats/ 模块)、ProjectsCategory → SectionTitle(section-title/ 模块),体现功能层内部的"聚合组件组合叶组件"模式。

Configuration Options

功能组件层的可配置性主要通过 Props 契约而非全局配置文件实现。以 AnimeCard 为例:

Prop类型默认值说明
anime.titlestring必填番剧标题
anime.coverstring必填封面图地址,交给 atoms/Image 处理响应式
anime.linkstring必填外链(新窗口打开,rel="noopener noreferrer")
anime.statusstring必填观看状态,驱动 data-anime-status 属性与进度条条件渲染
anime.ratingnumber必填评分角标
anime.progressnumber必填已看集数
anime.totalEpisodesnumber必填总集数;为 0 时进度显示 0%(除零保护)
anime.descriptionstring必填简介(line-clamp-2 截断,title 提示完整内容)
anime.year / anime.studio / anime.genre[]string / string[]必填元信息
statusInfo.text / .class / .iconstring必填状态徽章文案 / Tailwind 样式类 / 图标
yearLabel / studioLabelstring必填元信息标签文案(供文案定制)

API Reference

AnimeCard.astro(无导出方法的 Astro 组件)

渲染签名:<AnimeCard anime={...} statusInfo={...} yearLabel={...} studioLabel={...} />

  • 参数:见上表 Configuration Options
  • 返回:构建期静态 HTML 片段(包含 data-anime-status 数据属性,可供 CSS/JS 按状态筛选)
  • 依赖:atoms/Image(响应式封面)
  • 条件行为:anime.status === "watching" 时额外渲染进度条块

模块入口 index.ts(各模块统一契约)

typescript
export { default as ComponentName } from "./ComponentName.astro"; export type { XxxProps } from "./types"; // 或 export * from "./types";

行为:对页面层暴露组件默认导出与 Props 类型;重命名/拆分内部文件仅需改动此文件。

Professional Notes

Failure Modes 与边界处理

  • 除零边界:AnimeCard 对 totalEpisodes === 0 显式回退为 0% 进度(见上文派生计算片段),避免 NaN 进入样式与文本。
  • 未知外链安全:封面链接统一 target="_blank" rel="noopener noreferrer",防止反向 tab-nabbing。
  • 长文本溢出:简介使用 line-clamp-2 + 原生 title 属性兜底完整内容。
  • 状态徽章样式由外部注入:卡片自身不解析状态字符串到样式,异常状态只会得到调用方未定义的样式,而不会在组件内崩溃。

渲染范式选择(Astro vs Svelte)

判据选 Astro(如 AnimeCard)选 Svelte(如 ArchivePanel/PasswordModal)
是否有客户端状态否是
是否需要事件监听否(CSS hover 即可)是
首屏代价零 JS,纯静态 HTML需岛屿水合
典型场景内容卡片、页头、区块标题筛选面板、弹窗、看板娘

扩展点

  • 新增业务板块:在 features/ 下按"目录 + 组件 + types.ts + index.ts"三件套新建模块即可,无需改动既有模块。
  • 状态/文案定制:通过 statusInfo、yearLabel 等注入参数完成,无需改卡片源码。
  • 数据属性钩子:data-anime-status 之类的属性为页面级脚本/CSS 筛选提供了稳定挂载点。

测试

源码探索预算内未发现针对 features/ 的独立测试目录或测试文件;本页所述行为均直接依据组件源码与各模块 index.ts 导出语句得出。

  • 原子组件实现(Badge、Button、Chip、Icon、Image、FilterTabs、CustomScrollbar):src/components/atoms/
  • AnimeCard.astro — 功能卡片完整实现样例
  • auth/index.ts — barrel 导出标准写法
  • albums/index.ts — export * 类型导出变体
  • archive/index.ts — Svelte 组件模块入口写法

Sources

(1 files)