Use Cases (更新: 2026/6/2)

用 Claude Code 构建高性能图片画廊

用Claude Code实现响应式图片画廊:React代码、srcset、灯箱、常见坑和发布前检查。

用 Claude Code 构建高性能图片画廊

图片画廊不是装饰,而是产品路径

用 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,再用手机宽度手动操作灯箱。

#Claude Code #图片画廊 #React #响应式 #性能
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

让 Claude Code 真正进入可验证的工作流

先用免费 PDF 固定基础,再用 Gumroad 教材复用工作流;如果涉及团队导入、权限或收入路径,可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。