---
title: 封面图 CoverImage
url: https://ui.zhaozimin.com/components/cover-image/
markdown: https://ui.zhaozimin.com/components/cover-image.md
group: 组件
origin: 设计系统源码（src/system），本仓库是唯一源头
source:
  - src/system/components/v4/CoverImage.jsx  # https://ui.zhaozimin.com/src/system/components/v4/CoverImage.jsx
version: v0.2.0  # 设计系统版本，发布于 2026-09-23
scope: .zzm-v4   # 根节点必须带这个类，否则样式不生效
css: https://ui.zhaozimin.com/v/0.2.0/zzm.css   # 锁版本地址，本仓库再改也不影响你
---

# 封面图 CoverImage

> 封面三态：加载中只露父容器的暗底，就绪后 350ms 淡入，失败回退到线稿。杜绝「先画线稿、封面到了再跳变盖上」的闪跳。

### 样张 · 三种结局

React：

```jsx
import { CoverImage } from './components/v4/CoverImage.jsx'

const ACCENT = 'var(--zzm-accent, #A6402F)'
const COVER = 'data:image/svg+xml;utf8,' + encodeURIComponent(
  '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180"><rect width="320" height="180" fill="#F6F6F4"/>' +
  '<text x="24" y="92" font-family="-apple-system,PingFang SC,sans-serif" font-size="22" font-weight="700" fill="#161616">上传的课程封面</text>' +
  '<rect x="24" y="108" width="28" height="3" fill="#A6402F"/></svg>',
)

const Fallback = () => (
  <svg viewBox="0 0 240 135" preserveAspectRatio="xMidYMid slice" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: 0.5 }}>
    <polyline points="18,110 62,48 120,92 178,40 224,104" fill="none" stroke="rgba(255,255,255,.2)" strokeWidth="1" />
    <circle cx="120" cy="92" r="2.6" fill={ACCENT} />
  </svg>
)

const Frame = ({ label, children }) => (
  <div>
    <div style={{ aspectRatio: '16/9', borderRadius: 12, background: '#131316', position: 'relative', overflow: 'hidden' }}>{children}</div>
    <div style={{ marginTop: 8, fontSize: 12, color: '#8B8B85' }}>{label}</div>
  </div>
)

export default function CoverImageStates() {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 16, width: '100%' }}>
      <Frame label="无封面 → 直接画线稿"><CoverImage src={null} fallback={<Fallback />} /></Frame>
      <Frame label="正常 → 暗底上淡入 350ms"><CoverImage src={COVER} fallback={<Fallback />} /></Frame>
      <Frame label="失效 → 回退线稿"><CoverImage src="/v4/__missing-cover__.png" fallback={<Fallback />} /></Frame>
    </div>
  )
}
```

HTML（构建时由上面的 React 渲染得到，可直接粘贴）：

```html
<div
  style="
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
    width: 100%;
  "
>
  <div>
    <div
      style="
        aspect-ratio: 16/9;
        border-radius: 12px;
        background: #131316;
        position: relative;
        overflow: hidden;
      "
    >
      <svg
        viewBox="0 0 240 135"
        preserveAspectRatio="xMidYMid slice"
        style="position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0.5"
      >
        <polyline
          points="18,110 62,48 120,92 178,40 224,104"
          fill="none"
          stroke="rgba(255,255,255,.2)"
          stroke-width="1"
        ></polyline>
        <circle cx="120" cy="92" r="2.6" fill="var(--zzm-accent, #A6402F)"></circle>
      </svg>
    </div>
    <div style="margin-top: 8px; font-size: 12px; color: #8b8b85">无封面 → 直接画线稿</div>
  </div>
  <div>
    <div
      style="
        aspect-ratio: 16/9;
        border-radius: 12px;
        background: #131316;
        position: relative;
        overflow: hidden;
      "
    >
      <img
        src="data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20320%20180%22%3E%3Crect%20width%3D%22320%22%20height%3D%22180%22%20fill%3D%22%23F6F6F4%22%2F%3E%3Ctext%20x%3D%2224%22%20y%3D%2292%22%20font-family%3D%22-apple-system%2CPingFang%20SC%2Csans-serif%22%20font-size%3D%2222%22%20font-weight%3D%22700%22%20fill%3D%22%23161616%22%3E%E4%B8%8A%E4%BC%A0%E7%9A%84%E8%AF%BE%E7%A8%8B%E5%B0%81%E9%9D%A2%3C%2Ftext%3E%3Crect%20x%3D%2224%22%20y%3D%22108%22%20width%3D%2228%22%20height%3D%223%22%20fill%3D%22%23A6402F%22%2F%3E%3C%2Fsvg%3E"
        alt=""
        loading="lazy"
        decoding="async"
        style="
          position: absolute;
          inset: 0;
          width: 100%;
          height: 100%;
          object-fit: cover;
          opacity: 0;
          transition: opacity 350ms ease;
        "
      />
    </div>
    <div style="margin-top: 8px; font-size: 12px; color: #8b8b85">正常 → 暗底上淡入 350ms</div>
  </div>
  <div>
    <div
      style="
        aspect-ratio: 16/9;
        border-radius: 12px;
        background: #131316;
        position: relative;
        overflow: hidden;
      "
    >
      <img
        src="/v4/__missing-cover__.png"
        alt=""
        loading="lazy"
        decoding="async"
        style="
          position: absolute;
          inset: 0;
          width: 100%;
          height: 100%;
          object-fit: cover;
          opacity: 0;
          transition: opacity 350ms ease;
        "
      />
    </div>
    <div style="margin-top: 8px; font-size: 12px; color: #8b8b85">失效 → 回退线稿</div>
  </div>
</div>
```

## 属性

| 属性 | 取值 | 默认 | 说明 |
|---|---|---|---|
| `src` | string · null | — | 封面地址；为空直接渲染 fallback |
| `fallback` | ReactNode | null | 无封面或加载失败时的兜底（通常是星座线稿） |
| `fit` | 'cover' · 'contain' | 'cover' | 导学封面用 contain 保证完整展示 |
| `fadeMs` | number | 350 | 淡入时长 |

父容器负责暗底、比例和圆角（`aspect-ratio: 16/9; background: #131316; position: relative; overflow: hidden`）；图片绝对定位铺满。

## 一个真实踩过的坑

图片的 `load` 事件不冒泡，React 的 `onLoad` 要等 commit 后才绑定。磁盘缓存命中时，字节比监听先到，`load` 已经触发过了，组件会永远卡在「加载中」——图片明明就绪却 `opacity: 0`，回访用户只看见一片黑。所以这个组件挂载后先检查 `el.complete`，没就绪才补挂**原生**的 load / error 监听。复刻时别换回 `onLoad`。

## 铁律

- ✓ 要：封面按上传原图呈现
- ✗ 不要：提供「蒙层」参数——叠字可读性由那颗字自己解决

---

上一页：[课节卡](https://ui.zhaozimin.com/components/lesson-card.md) · 下一页：[神秘卡](https://ui.zhaozimin.com/components/mystery-card.md) · 全站目录：https://ui.zhaozimin.com/llms.txt
