用 Claude Code 构建高性能图片画廊
用Claude Code实现响应式图片画廊:React代码、srcset、灯箱、常见坑和发布前检查。
图片画廊不是装饰,而是产品路径
用 Claude Code 做图片画廊时,最容易犯的错误是只描述外观:“做一个瀑布流”“像作品集一样好看”。这样的提示词通常能得到漂亮 demo,却不一定能上线。真正影响质量的是图片重量、alt 文本、布局抖动、手机宽度、键盘关闭灯箱、CMS 传入坏数据,以及用户看完图片后应该去哪里。
这篇文章把任务拆成可复查的工程流程:先给 Claude Code 明确约束,再实现可复制的 React 组件,接着检查 srcset、sizes、懒加载、灯箱和发布前风险。相关基础可以继续看 Claude Code 图片处理、性能优化 和 可访问性实现。外部一手资料建议打开 Claude Code 官方文档、MDN 的响应式图片、Lazy loading 以及 WCAG 2.2。
我的做法是先让 Claude Code 固定数据契约,再写 UI。每张图必须有 id、分类、宽高和有意义的替代文本。这样后续无论换成 Next.js Image、Astro 图片资源,还是 CDN 转码服务,核心结构都不会乱。
先给 Claude Code 的提示词
下面这段可以直接复制。重点不是措辞,而是把“不要破坏现有代码”“必须处理失败状态”“返回可运行代码”说清楚。
请用 React 实现一个图片画廊。
目标是让文章、案例、商品截图和培训照片都能快速浏览。
要求:
- 不破坏现有路由和设计系统约定
- 定义图片数据类型,id、src、alt、width、height、category 必填
- 使用 CSS Grid 做响应式布局
- 正确使用 srcset、sizes、loading、fetchPriority
- 点击图片打开灯箱,按 Escape 可以关闭
- 处理空数组、图片加载失败、过长 alt、手机宽度
- 实现后说明改动文件、测试方法和剩余风险
请返回可以复制运行的 React/TypeScript 和 CSS,不要只给伪代码。
这个提示词会把 Claude Code 的输出从“单张漂亮截图”拉回到“可以 review 的补丁”。宽高能降低布局偏移,必填 alt 能避免“image 1”这种无意义文本,最后的风险说明能逼它进行第二轮自查。
推荐架构
在写代码前,先把责任画出来。Claude Code 看见输入、过滤、展示、预览、验证之间的关系后,更不容易把所有逻辑塞进一个巨大组件。
flowchart LR
A["原始图片"] --> B["生成多尺寸版本"]
B --> C["GalleryImage 数组"]
C --> D["分类过滤"]
D --> E["CSS Grid 卡片"]
E --> F["灯箱预览"]
E --> G["Lighthouse 与人工检查"]
| 判断项 | 推荐初始值 | 什么时候再升级 |
|---|---|---|
| 布局 | CSS Grid | 图片高度差异非常大 |
| 懒加载 | 首屏之后再 lazy | 首屏主图加载太慢 |
| 图片尺寸 | 约 480/960/1440px | 大屏用户占比很高 |
| 灯箱 | 先做最小可访问版本 | 图片直接影响购买决策 |
一开始不引入 masonry 库是有意的。很多业务页面需要的是稳定卡片、快速缩略图和清晰预览,而不是真正的瀑布流。依赖越少,Claude Code 生成的差异也越容易审。
可复制的 React 实现
下面的实现不绑定具体框架。它可以放进 Vite,也可以改成 Next.js 的 client component。实际项目里再替换成已有图片组件即可。
import { useEffect, useMemo, useState } from "react";
import "./image-gallery.css";
export type GalleryImage = {
id: string;
src: string;
alt: string;
width: number;
height: number;
category: string;
sources?: Array<{ width: number; src: string }>;
};
function buildSrcSet(image: GalleryImage) {
if (!image.sources?.length) return undefined;
return [...image.sources]
.sort((a, b) => a.width - b.width)
.map((source) => `${source.src} ${source.width}w`)
.join(", ");
}
export function ImageGallery({ images }: { images: GalleryImage[] }) {
const [category, setCategory] = useState("all");
const [activeId, setActiveId] = useState<string | null>(null);
const [brokenIds, setBrokenIds] = useState<Set<string>>(() => new Set());
const categories = useMemo(() => {
return ["all", ...Array.from(new Set(images.map((image) => image.category)))];
}, [images]);
const visibleImages = useMemo(() => {
if (category === "all") return images;
return images.filter((image) => image.category === category);
}, [category, images]);
const activeImage = visibleImages.find((image) => image.id === activeId);
useEffect(() => {
if (!activeImage) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") setActiveId(null);
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [activeImage]);
function markBroken(id: string) {
setBrokenIds((current) => new Set(current).add(id));
}
if (images.length === 0) {
return <p className="gallery-empty">No images are available yet.</p>;
}
return (
<section className="gallery" aria-label="Image gallery">
<div className="gallery-toolbar" aria-label="Filter images by category">
{categories.map((item) => (
<button
className={item === category ? "is-active" : ""}
key={item}
onClick={() => setCategory(item)}
type="button"
>
{item === "all" ? "All" : item}
</button>
))}
</div>
<div className="gallery-grid">
{visibleImages.map((image, index) => {
const isBroken = brokenIds.has(image.id);
return (
<button
className="gallery-card"
key={image.id}
onClick={() => setActiveId(image.id)}
type="button"
>
{isBroken ? (
<span className="gallery-fallback">Image unavailable</span>
) : (
<img
alt={image.alt}
width={image.width}
height={image.height}
src={image.src}
srcSet={buildSrcSet(image)}
sizes="(min-width: 960px) 33vw, (min-width: 640px) 50vw, 100vw"
loading={index < 2 ? "eager" : "lazy"}
fetchPriority={index === 0 ? "high" : "auto"}
style={{ aspectRatio: `${image.width} / ${image.height}` }}
onError={() => markBroken(image.id)}
/>
)}
<span>{image.alt}</span>
</button>
);
})}
</div>
{activeImage && (
<div
className="gallery-lightbox"
role="dialog"
aria-modal="true"
aria-label={activeImage.alt}
tabIndex={-1}
onClick={() => setActiveId(null)}
>
<button className="gallery-close" onClick={() => setActiveId(null)} type="button">
Close
</button>
<img
alt={activeImage.alt}
width={activeImage.width}
height={activeImage.height}
src={activeImage.src}
onClick={(event) => event.stopPropagation()}
/>
</div>
)}
</section>
);
}
.gallery {
display: grid;
gap: 1rem;
}
.gallery-toolbar {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.gallery-toolbar button,
.gallery-card,
.gallery-close {
border: 1px solid #d4d4d8;
background: #ffffff;
color: #18181b;
cursor: pointer;
}
.gallery-toolbar button {
border-radius: 999px;
padding: 0.45rem 0.8rem;
}
.gallery-toolbar .is-active {
background: #18181b;
color: #ffffff;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1rem;
}
.gallery-card {
display: grid;
gap: 0.5rem;
padding: 0;
overflow: hidden;
border-radius: 8px;
text-align: left;
}
.gallery-card img {
width: 100%;
object-fit: cover;
background: #f4f4f5;
}
.gallery-fallback {
display: grid;
min-height: 180px;
place-items: center;
background: #f4f4f5;
color: #71717a;
}
.gallery-card span {
padding: 0 0.75rem 0.75rem;
font-size: 0.875rem;
}
.gallery-lightbox {
position: fixed;
inset: 0;
z-index: 50;
display: grid;
place-items: center;
padding: 2rem;
background: rgb(0 0 0 / 0.86);
}
.gallery-lightbox img {
max-width: min(100%, 1100px);
max-height: 82vh;
object-fit: contain;
}
.gallery-close {
position: absolute;
top: 1rem;
right: 1rem;
border-radius: 6px;
padding: 0.5rem 0.75rem;
}
.gallery-empty {
color: #71717a;
}
真实项目中可以把原生 img 换成 Next.js、Astro 或 CDN 的图片组件,但数据契约不要丢。让 Claude Code 检查 fetchPriority 是否符合目标浏览器,以及项目里是否已经有统一图片组件。
数据模型和 3 个以上用例
图片数据放在组件外。即使来自 CMS,也先整理成同一种结构,测试和排错会简单很多。
import type { GalleryImage } from "./ImageGallery";
export const galleryImages: GalleryImage[] = [
{
id: "case-study-dashboard",
src: "/images/gallery/dashboard-960.webp",
alt: "Analytics dashboard after Claude Code refactoring",
width: 960,
height: 640,
category: "Case study",
sources: [
{ width: 480, src: "/images/gallery/dashboard-480.webp" },
{ width: 960, src: "/images/gallery/dashboard-960.webp" },
{ width: 1440, src: "/images/gallery/dashboard-1440.webp" },
],
},
{
id: "workshop-room",
src: "/images/gallery/workshop-960.webp",
alt: "Team workshop board with Claude Code review checklist",
width: 960,
height: 720,
category: "Training",
},
{
id: "product-shot",
src: "/images/gallery/template-pack-960.webp",
alt: "Claude Code template pack product preview",
width: 960,
height: 540,
category: "Product",
},
];
第一个用例是作品集或案例页。图片应该帮助读者比较成果,然后自然进入案例文章、咨询页或购买页。alt 不要写“截图1”,而要说明画面里真正展示的状态。
第二个用例是电商或数字产品页。缩略图、使用场景、对比图和购买后界面可以减少犹豫。但如果首屏之前就加载所有高清图,用户可能在看到 CTA 前离开。
第三个用例是培训、活动和内部知识库。白板、步骤截图、Before/After、错误画面都可以变成可复用素材。内部场景要特别检查截图里是否有客户名、邮箱、token 或个人信息。
第四个用例是技术文章。代码示例多时,概念图、流程图和验证截图能帮助读者在解释和实现之间来回切换。
发布前最容易漏掉的坑
第一,首屏图片不应该盲目 lazy-load。刚打开页面就可见的图片经常影响 LCP,可以考虑 loading="eager" 和 fetchPriority="high"。相反,页面下方的图片全部 eager 会拖慢初始加载。
第二,省略 width 和 height 会造成布局跳动。图片一张张加载时卡片高度变化,读者的点击位置也会变。请让 Claude Code 专门检查 CLS 风险。
第三,把 alt 当作关键词堆砌字段。alt 是图片不可见时的替代说明,不是 SEO 标签。“Claude Code 图片画廊 React”不如“Claude Code 重构后的分析仪表盘”清楚。
第四,灯箱只支持鼠标。关闭按钮要有名称,Escape 要能关闭,焦点要可见,手机宽度不能溢出。如果需要严格焦点陷阱,优先让 Claude Code 评估 Radix UI 或 React Aria 这类成熟实现。
第五,没有规定图片运营规则。CMS 用户上传一张 6 MB PNG 就可能毁掉性能。把最大尺寸、允许格式、命名规则和审核项写进 CLAUDE.md,后续每次让 Claude Code 实现图片功能时都能复用。
验证与复查
上线前至少检查分类过滤、灯箱、键盘、空数据、坏图片和 375px 手机宽度。如果项目使用 Playwright,可以从这类轻量测试开始。
import { expect, test } from "@playwright/test";
test("image gallery filters and opens a lightbox", async ({ page }) => {
await page.goto("/gallery");
await expect(page.getByRole("region", { name: "Image gallery" })).toBeVisible();
await page.getByRole("button", { name: "Training" }).click();
await expect(page.getByRole("button", { name: /workshop/i })).toBeVisible();
await page.getByRole("button", { name: /workshop/i }).click();
await expect(page.getByRole("dialog")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByRole("dialog")).toBeHidden();
});
复查时不要问“看起来可以吗”。要固定问题:首屏请求是否过多,srcset 与真实布局宽度是否一致,alt 是否说明图片含义,卡片是否是按钮或链接,手机宽度是否横向溢出,CMS 坏数据是否会崩溃,截图是否泄露内部信息。
CTA 和实际验证结果
图片画廊要连接业务目标。案例图可以链接到案例文章,商品图可以链接到购买说明,培训照片可以链接到 Claude Code 培训与咨询。想继续深入实现,可以看图片懒加载和React 开发指南。
我按这个流程实际拆分过任务:先让 Claude Code 固定数据类型,再写组件和 CSS,最后单独做 review。结果比一次性要求“做漂亮画廊”更容易审查。最终检查时,我用 Chrome DevTools Network 看首屏请求数量,用 Lighthouse 看 LCP 和 CLS,再用手机宽度手动操作灯箱。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。