
ATRI
科幻<script setup lang="ts">
import { Card, Image, Stack, Tag, Text } from '@hina-ui/vue'
</script>
<template>
<Card class="w-full max-w-xs" :padded="false">
<Image
src="/sample.webp"
alt="夏日午后的坡道"
:ratio="4 / 3"
:lazy="false"
eager
class="rounded-t-lg"
/>
<Stack gap="xs" align="start" class="p-4">
<Text class="font-medium">ATRI</Text>
<Tag>科幻</Tag>
</Stack>
</Card>
</template>
用法
import { Image } from '@hina-ui/vue'
图片在接近视口时才开始加载,加载期间由骨架占位,图片就绪后骨架淡出。用类名给出尺寸,或者用 ratio 提前占好高度。
<script setup lang="ts">
import { Image } from '@hina-ui/vue'
</script>
<template>
<Image src="/sample.webp" alt="夏日午后的坡道" class="h-40 w-full max-w-md rounded-md" />
</template>
示例
宽高比
ratio 是宽除以高。图片还不存在时框就已经是这个形状,因此图片到位不会顶动页面。
1 / 1
4 / 3
16 / 9
<script setup lang="ts">
import { Image, Inline, Stack, Text } from '@hina-ui/vue'
const ratios = [
{ label: '1 / 1', value: 1 },
{ label: '4 / 3', value: 4 / 3 },
{ label: '16 / 9', value: 16 / 9 },
]
</script>
<template>
<Inline align="start" class="gap-4">
<Stack v-for="ratio in ratios" :key="ratio.label" gap="xs" class="w-40">
<Text tone="muted" size="sm">{{ ratio.label }}</Text>
<Image src="/sample.webp" alt="夏日午后的坡道" :ratio="ratio.value" class="rounded-md" />
</Stack>
</Inline>
</template>
外框与图片样式
class、style 设置外框,imageClass、imageStyle 设置内部的 img。style 支持字符串、对象和数组;其中的 aspectRatio 会覆盖 ratio。
<script setup lang="ts">
import { Image } from '@hina-ui/vue'
</script>
<template>
<Image
src="/sample.webp"
alt="夏日午后的坡道"
preview
class="rounded-md"
:style="{ width: '240px', height: '160px' }"
:image-style="{ objectPosition: 'left center' }"
/>
</template>
填充方式
fit 决定图片如何填满外框,默认为 cover。
cover
contain
fill
<script setup lang="ts">
import { Image, Inline, Stack, Text } from '@hina-ui/vue'
const fits = ['cover', 'contain', 'fill'] as const
</script>
<template>
<Inline align="start" class="gap-4">
<Stack v-for="fit in fits" :key="fit" gap="xs" class="w-32">
<Text tone="muted" size="sm">{{ fit }}</Text>
<Image
src="/sample.webp"
alt="夏日午后的坡道"
:fit="fit"
:ratio="1"
class="bg-inset rounded-md"
/>
</Stack>
</Inline>
</template>
首屏图片
lazy 默认开启,图片要等观察器放行才开始请求,服务端渲染阶段也不会带上地址。首屏图片应当关闭 lazy 并开启 eager:地址随首屏 HTML 一同送达,请求以 fetchpriority="high" 发出,解码方式也改为同步。

<script setup lang="ts">
import { Image } from '@hina-ui/vue'
</script>
<template>
<Image
src="/sample.webp"
alt="夏日午后的坡道"
:lazy="false"
eager
:ratio="16 / 9"
class="w-full max-w-md rounded-md"
/>
</template>
模糊占位
thumbhash 接收图片的 ThumbHash,格式为 base64 字符串,标准与 URL 安全两种字母表均可。加载期间外框显示由它还原的模糊缩略图,代替骨架;图片解码后与缩略图交叉淡化,图片淡入的同时缩略图淡出,两者同时开始、同时结束。
ThumbHash 由 thumbhash 包的 rgbaToThumbHash 生成,输入是缩小到 100 像素以内的图片像素,一般在上传时生成并与图片地址一起保存。
<script setup lang="ts">
import { shallowRef } from 'vue'
import { Button, Image, Stack } from '@hina-ui/vue'
const round = shallowRef(0)
</script>
<template>
<Stack align="start" class="w-full max-w-md">
<Image
:key="round"
:src="`/sample.webp?round=${round}`"
thumbhash="YMcJJIIhukWZenmwmHWb9Pc5nw=="
alt="夏日午后的坡道"
:ratio="16 / 9"
class="rounded-md"
/>
<Button size="sm" variant="soft" tone="neutral" @click="round++">重新加载</Button>
</Stack>
</template>
空态与失败
没有 src 时渲染 empty 插槽。图片加载失败且没有可用的回退地址时,渲染 error 插槽。
加载完成
没有图片
加载失败
<script setup lang="ts">
import { ImageOff } from '@lucide/vue'
import { Center, Image, Inline, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<Inline align="start" class="gap-4">
<Stack gap="xs" class="w-40">
<Text tone="muted" size="sm">加载完成</Text>
<Image src="/sample.webp" alt="夏日午后的坡道" :ratio="1" class="rounded-md" />
</Stack>
<Stack gap="xs" class="w-40">
<Text tone="muted" size="sm">没有图片</Text>
<Image :ratio="1" class="bg-inset rounded-md">
<template #empty>
<Center class="text-muted size-full">
<ImageOff class="size-6" />
</Center>
</template>
</Image>
</Stack>
<Stack gap="xs" class="w-40">
<Text tone="muted" size="sm">加载失败</Text>
<Image src="/missing.webp" alt="" :ratio="1" class="bg-inset rounded-md">
<template #error>
<Center class="text-muted size-full">
<Text size="sm">图片不可用</Text>
</Center>
</template>
</Image>
</Stack>
</Inline>
</template>
回退
src 加载失败后改用 fallback。回退地址同样失败时交给 error 插槽,并触发 error 事件。
<script setup lang="ts">
import { Image } from '@hina-ui/vue'
</script>
<template>
<Image
src="/missing.webp"
fallback="/sample.webp"
alt="夏日午后的坡道"
:ratio="16 / 9"
class="w-full max-w-md rounded-md"
/>
</template>
解析地址
组件只接收 src,不关心它是完整地址还是对象存储中的键。provideImageResolver 注入一个函数,由它把 src 变成最终地址;处理参数之类的细节由这个函数自行掌握,不必经过组件。没有注入解析器时,src 按原样使用。函数还会收到第二个参数说明用途:页面上的图片为 'image',预览时放大查看的大图为 'preview'。
这里的解析器为地址补充查询串,真实应用会将地址指向 CDN 与图片处理服务。
<script setup lang="ts">
import { Image, Stack, Text, provideImageResolver } from '@hina-ui/vue'
provideImageResolver(src => `${src}?preset=banner&quality=82`)
</script>
<template>
<Stack class="w-full max-w-md">
<Image src="/sample.webp" alt="夏日午后的坡道" :ratio="16 / 9" class="rounded-md" />
<Text tone="muted" size="sm">
这里的解析器为地址补充查询串,真实应用会将地址指向 CDN 与图片处理服务。
</Text>
</Stack>
</template>
预览
设置 preview 后,图片可以点击放大查看:图片从页面上的原位放大至屏幕中央,关闭时缩回原位。预览支持缩放、拖动、旋转与下载,向下拖动图片也可以关闭。
小图和大图可以是两种规格:解析器按用途给出各自的地址,或者直接把大图地址传给 preview。打开时先显示页面上的小图,大图就绪后替换。
<script setup lang="ts">
import { Image, provideImageResolver } from '@hina-ui/vue'
provideImageResolver(
(src, variant) =>
`https://imagesp.yurari.moe/${src}?w=${variant === 'preview' ? 2000 : 600}&f=webp&fit=scale-down&q=85`,
)
</script>
<template>
<Image
src="images/1ec4bca1-0674-4da6-b1a2-7433f1d79df5.webp"
alt="《街角魔族》第一卷的封面"
preview
class="size-48 rounded-md"
/>
</template>
需要独立控制预览时,可以直接使用 Lightbox。
预览原始尺寸
通过 previewSize 提供最终预览图片的原始像素宽高。存在独立的 preview 地址时,尺寸对应解析后的大图;否则对应当前图片。组件据此提前计算初始适配尺寸和缩放上限,打开时一次展开到位,大图加载完成后只替换画质。
宽高必须同时是有限正数。有效的显式尺寸始终优先于加载结果;未传入或无效时,继续自动获取尺寸。previewSize 只影响预览,页面外框仍由 class、style 和 ratio 控制。
下例对比自动获取尺寸与预先提供尺寸,两张图片使用同一份 320 × 180 缩略图和 1200 × 675 大图。
自动获取尺寸
预先提供尺寸
<script setup lang="ts">
import { Image, Inline, Stack, Text } from '@hina-ui/vue'
const image = {
thumbnail: '/sample-thumbnail.webp',
original: '/sample.webp',
width: 1200,
height: 675,
}
</script>
<template>
<Inline align="start" class="gap-6">
<Stack gap="sm" class="w-64">
<Text size="sm" tone="muted">自动获取尺寸</Text>
<Image
:src="image.thumbnail"
:preview="image.original"
alt="夏日午后的坡道"
:ratio="16 / 9"
class="rounded-xl"
/>
</Stack>
<Stack gap="sm" class="w-64">
<Text size="sm" tone="muted">预先提供尺寸</Text>
<Image
:src="image.thumbnail"
:preview="image.original"
:preview-size="{ width: image.width, height: image.height }"
alt="夏日午后的坡道"
:ratio="16 / 9"
class="rounded-xl"
/>
</Stack>
</Inline>
</template>
分组
把多张图片放进 ImageGroup,点击任意一张后可以在整组之间切换,顺序与页面上的顺序一致。开启 loop 后翻页首尾相接。
<script setup lang="ts">
import { Image, ImageGroup, Inline, provideImageResolver } from '@hina-ui/vue'
provideImageResolver(
(src, variant) =>
`https://imagesp.yurari.moe/${src}?w=${variant === 'preview' ? 2000 : 600}&f=webp&fit=scale-down&q=85`,
)
const pictures = [
{ src: 'galgame/10509/y71oty0w_2.jpg', alt: '《宿星的女友 3》的截图', class: 'w-48' },
{
src: 'galgame/10003/orhsttk4_2.jpg',
alt: '《抬头看看吧,看那天上的繁星》的封面',
class: 'w-20',
},
{ src: 'galgame/10012/co4a4kyg_2.png', alt: '《ONE.》的主视觉', class: 'w-20' },
{
src: 'images/1ec4bca1-0674-4da6-b1a2-7433f1d79df5.webp',
alt: '《街角魔族》第一卷的封面',
class: 'w-20',
},
{
src: 'images/74b0eddb-819f-4ee8-b406-fa096943a5b9.webp',
alt: '《抬头看看吧,看那天上的繁星 FINE DAYS》的封面',
class: 'w-40',
},
]
</script>
<template>
<ImageGroup loop>
<Inline>
<Image
v-for="picture in pictures"
:key="picture.src"
:src="picture.src"
:alt="picture.alt"
preview
:class="`h-28 rounded-md ${picture.class}`"
/>
</Inline>
</ImageGroup>
</template>
行为
- 预览的开关动画保留图片及外层裁剪容器的圆角;多图布局按每张图片实际接触的裁剪边界分别计算四角。
- 外框默认撑满容器宽度,开启或关闭
preview时一致;显式宽度类名可以覆盖默认值。 - 默认懒加载:交叉观察器观察外框,图片距视口不足
rootMargin时才带上地址开始请求,该值默认为 200 像素。 - 服务端渲染阶段观察器尚未介入,懒加载的图片先不带地址。首屏图片应当关闭
lazy并开启eager。 - 骨架铺满外框,
fit为contain时留出的空白同样被覆盖。图片解码完成后,在骨架上层以 300ms 淡入,骨架同时以 200ms 淡出,两者均使用 ease-out 曲线。 - 懒加载的图片在淡入开始前保持透明;关闭骨架后,图片仍然淡入。
- 关闭
lazy的图片位于骨架上层,浏览器完成绘制即可见,无需等待脚本;其下方的骨架直接移除,不做淡出。 - 设置
thumbhash后,加载期间显示还原出的缩略图而不是骨架,skeleton与自定义骨架都不起作用。值无法解析时按未设置处理。 - 缩略图与图片摆放方式相同:读取图片计算后的
object-fit、object-position、内边距与边框,经imageClass或imageStyle修改这些属性时缩略图随之调整。cover与none铺满,contain与scale-down完整显示,fill拉伸到外框大小。服务端渲染的 HTML 按fit铺放,脚本运行后改为跟随图片。 - ThumbHash 只记录近似的宽高比。
contain与scale-down时,缩略图在交叉淡化过程中缩放到图片的实际范围,淡化结束时两者重合。 - 缩略图随服务端渲染的 HTML 一同输出,脚本加载之前即可显示。
- 懒加载的图片解码后与缩略图以 450ms 交叉淡化,两者使用同一条 ease-in-out 曲线,任一时刻不透明度之和为 1:过渡中不透出外框背景,透明图片周围露出的缩略图随图片淡入同步消失。
- 交叉淡化只在图片与缩略图之间进行,外框的背景色不参与混合。
- 关闭
lazy的图片直接盖在缩略图上,加载完成后缩略图以 450ms 淡出。 - 已显示的图片更换
src时,旧图片以 450ms 淡出,下方直接换成新图片的缩略图。 - 加载失败时缩略图随图片一同撤走。
- 浏览器不支持交叉观察器时,组件挂载后立即加载,不会使图片始终无法显示。
- 组件未声明的其余特性会落到
img上,因此sizes、srcset之类照常可用。 - 更换
src会重置回退状态,新图片从自己的地址开始加载,而不是沿用上一张的回退地址。 - 预览打开期间页面不能滚动,焦点留在预览层内;关闭后焦点回到图片。
- 向下拖动时图片随指针下移并缩小,背景逐渐透出页面;松开时拖动距离不足则弹回原处。
- 双指捏合以两指中点为中心缩放,滚轮以指针所指的点为中心缩放。初次打开不放大小图,双击目标由图片尺寸决定,手动放大上限为原图的 2 倍;具体规则见 Lightbox 的尺寸与缩放。捏合超出上下限时阻力逐渐增大,松开后弹回限值。
- 放大之后才能拖动查看,拖到边缘有阻力,松开后弹回;快速滑动后画面继续滑行并逐渐停下,到达边界时回弹。放大状态下向下拖动只平移画面,不会关闭。
- 切换图片时上一张的缩放、位置与旋转复位;组内图片增删后,预览中的图片随之更新。只有一张图片,或者未开启
loop且已经到达两端时,继续拖动有阻力并回弹。 - 只为当前图片请求大图,切走后放弃请求,不会一次请求整组的大图。
- 关闭后再次打开时,缩放与位置都恢复初始状态。
- 系统开启减弱动态效果后,预览的打开、关闭、缩放、翻页与滑行都不再有过渡,直接切换到结果。
无障碍
alt直接落到img上。装饰性图片留空,屏幕阅读器会跳过它。- 骨架带
aria-hidden,加载状态由拥有这张图片的区域负责播报。 - 设置
preview的图片渲染为按钮,alt是它的可访问名称,也是预览层的标题;alt为空时开发环境会输出警告。 - 预览打开期间可用
←→键切换图片。缩略图栏中的每个按钮以图片的alt命名,当前项带aria-current。
API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
src | string | — | 图片地址,会经过解析器 |
alt | string | '' | 替代文本 |
fallback | string | — | src 失败后改用的地址 |
thumbhash | string | — | 图片的 ThumbHash,加载期间显示为模糊缩略图 |
fit | 'cover' | 'contain' | 'fill' | 'none' | 'scale-down' | 'cover' | 图片如何填满外框 |
ratio | number | — | 宽除以高,提前占位 |
lazy | boolean | true | 是否等接近视口再加载 |
rootMargin | string | '200px' | 提前多少距离开始加载 |
skeleton | boolean | true | 加载期间是否显示骨架 |
eager | boolean | false | 高优先级请求并同步解码 |
preview | boolean | string | false | 是否可以点击放大查看,传入字符串时作为大图地址 |
previewSize | { width: number; height: number } | — | 最终预览图片的原始像素尺寸,宽高须为有限正数 |
draggable | boolean | — | 图片是否可拖拽 |
class | string | — | 追加至外框的类名 |
style | StyleValue | — | 外框的内联样式 |
imageClass | string | — | 追加至 img 的类名 |
imageStyle | StyleValue | — | img 的内联样式 |
| 事件 | 参数 | 说明 |
|---|---|---|
load | size: { width, height } | 图片加载完成 |
error | — | 所有地址都已尝试过 |
| 插槽 | 说明 |
|---|---|
skeleton | 替换内置的加载骨架 |
empty | 没有 src 时渲染 |
error | 加载失败时渲染 |