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

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

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

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

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

用 Claude Code 做图片画廊时,最容易犯的错误是只描述外观:“做一个瀑布流”“像作品集一样好看”。这样的提示词通常能得到漂亮 demo,却不一定能上线。真正影响质量的是图片重量、alt 文本、布局抖动、手机宽度、键盘关闭灯箱、CMS 传入坏数据,以及用户看完图片后应该去哪里。

这篇文章把任务拆成可复查的工程流程:先给 Claude Code 明确约束,再实现可复制的 React 组件,接着检查 srcsetsizes、懒加载、灯箱和发布前风险。相关基础可以继续看 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 会拖慢初始加载。

第二,省略 widthheight 会造成布局跳动。图片一张张加载时卡片高度变化,读者的点击位置也会变。请让 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,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

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