前言
最近在整理自己看过的电影和电视剧时,想在博客上添加一个”影视墙”栏目,用来记录和展示这些作品。这样不仅方便自己回顾,也能和朋友分享推荐。
这个功能的整个开发工作由 Claude Code(Anthropic 的 AI 编程助手)完成。我只需要提供需求描述和实施计划,Claude Code 就自动完成了从代码实现到问题排查的全部工作。
本文将记录整个开发过程,包括需求分析、技术选型、实现细节以及遇到的问题和解决方案,同时也展示了 AI 辅助编程的实际效果。
需求分析
在开始开发之前,我先明确了具体需求:
功能需求
- 展示内容:电影和电视剧的基础信息(名称、封面图、评分、描述)
- 页面类型:仅需列表页,不需要详情页
- 交互功能:简单展示即可,无需筛选和搜索
- 内容管理:使用 Markdown 文件管理,方便维护
设计需求
- 视觉风格:与博客现有风格保持一致
- 布局方式:卡片式网格布局
- 响应式:支持移动端、平板、桌面多种设备
- 简洁优先:最终决定只显示封面图和电影名称
技术架构
本博客基于 Fuwari 模板,使用以下技术栈:
- Astro 5.x:静态站点生成器
- Tailwind CSS:样式框架
- Zod:内容集合 schema 验证
- Svelte:部分交互组件
实现步骤
1. 定义内容集合 Schema
首先在 src/content/config.ts 中添加新的内容集合:
const moviesCollection = defineCollection({ schema: z.object({ title: z.string(), // 影视名称 published: z.date(), // 观看/添加日期 draft: z.boolean().optional().default(false), // 是否草稿 description: z.string().optional().default(""), // 简短描述 rating: z.number().min(0).max(10).optional(), // 评分 (0-10) image: z.string().optional().default(""), // 封面图路径 year: z.number().optional(), // 上映年份 director: z.string().optional(), // 导演 genre: z.array(z.string()).optional().default([]), // 类型标签 }),});
export const collections = { posts: postsCollection, spec: specCollection, movies: moviesCollection, // 新增};设计考虑:
- 大部分字段设为可选,降低使用门槛
draft字段支持草稿功能,生产环境自动隐藏- 评分限制在 0-10 范围,确保数据有效性
2. 创建工具函数
参考现有的 content-utils.ts,创建了 movie-utils.ts:
import { type CollectionEntry, getCollection } from "astro:content";
export type MovieForList = { slug: string; data: CollectionEntry<"movies">["data"];};
async function getRawSortedMovies() { const allMovies = await getCollection("movies", ({ data }) => { return import.meta.env.PROD ? data.draft !== true : true; });
return allMovies.sort((a, b) => { const dateA = new Date(a.data.published); const dateB = new Date(b.data.published); return dateA > dateB ? -1 : 1; });}
export async function getSortedMoviesList(): Promise<MovieForList[]> { const sortedFullMovies = await getRawSortedMovies(); return sortedFullMovies.map((movie) => ({ slug: movie.slug, data: movie.data, }));}关键点:
- 按发布日期降序排序(最新的在前)
- 生产环境自动过滤草稿
- 只返回必要的数据,减少传递开销
3. 设计卡片组件
最初设计的 MovieCard.astro 组件包含了丰富的信息:
- 封面图(2:3 竖版海报比例)
- 评分徽章(右上角星级图标)
- 年份标签(左上角)
- 导演、类型标签
- 描述文字
- 添加日期
但经过实际展示和调整,最终简化为只显示:
- 封面图
- 电影名称(单行,超出显示省略号)
简化后的代码:
<div class:list={["card-base flex flex-col w-full rounded-[var(--radius-large)] overflow-hidden relative", className]} style={style}> <!-- 封面图区域 --> {hasCover && ( <div class="relative w-full aspect-[2/3] overflow-hidden rounded-t-xl"> <div class="absolute pointer-events-none z-10 w-full h-full bg-black/0 hover:bg-black/20 transition"></div> <ImageWrapper src={image} basePath={path.join("content/movies/", slug)} alt={`Cover of ${title}`} class="w-full h-full" /> </div> )}
<!-- 内容区域 - 只保留标题 --> <div class="px-3 pb-3 pt-2"> <h3 class="text-base font-bold text-90 text-center truncate">{title}</h3> </div></div>设计优化:
truncate类名确保标题单行显示- 调整内边距减少下方空白
- 字体从
text-lg改为text-base更适合单行
4. 创建列表页面
src/pages/movies.astro 使用响应式网格布局:
<MainGridLayout title="影视墙"> <div class="flex flex-col gap-8"> <!-- 页面标题和统计 --> <div class="card-base px-8 py-6"> <h1 class="text-3xl font-bold text-90 mb-2">影视墙</h1> <p class="text-60"> 共收录 {sortedMoviesList.length} 部影视作品 </p> </div>
<!-- 影视卡片网格 --> {sortedMoviesList.length > 0 ? ( <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6 px-2"> {sortedMoviesList.map((movie) => ( <MovieCard slug={movie.slug} title={movie.data.title} published={movie.data.published} rating={movie.data.rating} year={movie.data.year} director={movie.data.director} genre={movie.data.genre} image={movie.data.image} description={movie.data.description} style="" /> ))} </div> ) : ( <div class="card-base px-8 py-12 text-center"> <p class="text-60 text-lg">暂无影视作品</p> <p class="text-50 text-sm mt-2">在 src/content/movies/ 目录下添加 Markdown 文件即可</p> </div> )} </div></MainGridLayout>响应式设计:
- 移动端(默认):2 列
- 平板(md,≥768px):3 列
- 桌面(lg,≥1024px):4 列
- 大屏(xl,≥1280px):5 列
5. 添加导航链接
在 src/config.ts 中添加导航配置:
export const navBarConfig: NavBarConfig = { links: [ LinkPreset.Home, LinkPreset.Archive, { name: "影视墙", url: "/movies/", // 注意末尾的斜杠 }, LinkPreset.About, // ... ],};重要细节:URL 必须以 / 结尾(/movies/),否则会出现 404 错误。
遇到的问题与解决方案
问题 1:README 文件导致构建失败
错误信息:
[InvalidContentEntryDataError] movies → readme data does not match collection schema.title**: **title: Required原因:Astro 会处理 src/content/movies/ 目录下的所有 .md 文件,包括 README.md
解决方案:将 README.md 重命名为 _.README.md(前缀下划线),Astro 会忽略以 _ 开头的文件
问题 2:导航链接 404
现象:直接访问 /movies/ 正常,但点击导航栏链接显示 404
原因:导航配置中 URL 是 /movies(无末尾斜杠),而 Astro 生成的路由是 /movies/
解决方案:将导航链接改为 /movies/(带末尾斜杠)
问题 3:TypeScript 类型错误
错误信息:
Type 'MovieForList' is missing the following properties from type 'CollectionEntry<"movies">': id, render, body, collection原因:最初 MovieCard 组件的 entry prop 要求完整的 CollectionEntry 类型,但 movie-utils.ts 返回的是简化类型
解决方案:将 MovieCard 组件改为接收 slug 字符串而不是完整的 entry 对象
内容管理
创建影视条目非常简单,只需在 src/content/movies/ 目录下创建 Markdown 文件:
---title: '肖申克的救赎'published: 2026-02-15rating: 9.7year: 1994director: 'Frank Darabont'genre: ['剧情', '犯罪']image: '/images/movies/shawshank.jpg'description: '一场关于希望与自由的深刻诠释'---
这里可以写观影笔记(可选)图片放在 public/images/movies/ 目录下,在 frontmatter 中引用即可。
总结
通过本次开发,我成功为博客添加了一个简洁优雅的影视墙功能。值得一提的是,整个功能的代码实现完全由 Claude Code 完成,包括:
- ✅ 6 个文件的创建和修改
- ✅ 完整的 TypeScript 类型定义
- ✅ 响应式组件和页面设计
- ✅ 问题排查和修复
- ✅ 构建测试验证
整个开发过程非常流畅:
- 我提供详细的需求和实施计划
- Claude Code 按照计划逐步实现
- 实时测试验证,遇到问题立即修复
- 从开始到完成约 30 分钟
这次体验展示了 AI 辅助编程的强大能力:
- 效率高:从需求到实现快速完成
- 质量好:代码规范,类型安全,无新增错误
- 可维护:代码结构清晰,文档完善
- 零门槛:只需描述需求,无需手写代码