image —— 画像
画像コンポーネントの中心は表示ではありません(<img> で十分です)。それはプレビューです:クリックで拡大、ホイールで拡大縮小、回転、複数画像の切り替え。core/image が管理するのはプレビューアーの状態計算です——拡大のステップとクランプ、回転の正規化、画像一覧のループ切り替え。
- 各端末で
LhImageを実装するケース:商品画像、アバタープレビュー、証明写真、ギャラリー。 - クリックで拡大表示(拡大 / 回転 / 複数画像の切り替え)が必要なシーン。
- 使うべきでないケース:純粋な装飾画像(
<img>か背景画像でよく、コンポーネントのオーバーヘッドは不要);複雑な切り抜き / 編集(それは image-cropper の役割です)。
インポート方法
Section titled “インポート方法”import { getImageClassNames, normalizePreviewList, stepZoom, stepRotate, stepViewerIndex, getViewerImageStyle, getImageViewerAria,} from '@lh-ui/share/core/image'import { LH_IMAGE_ZOOM_MIN, LH_IMAGE_ZOOM_MAX, LH_IMAGE_ZOOM_STEP,} from '@lh-ui/share/core/image'import type { LhImageFit, LhImageState, LhImageViewerState } from '@lh-ui/share/core/image'拡大の浮動小数点クランプ:stepZoom は每次 LH_IMAGE_ZOOM_STEP(0.25)を加算したあと、Math.round(next * 100) / 100 してから [0.25, 4] にクランプします。丸めにより zoom は常にきれいな小数(0.25 / 0.5 / 0.75 …)となり、1.0000000000000002 のような値にはなりません——後者は scale() で目に見えるジャギーを生みます。
回転の負数剰余修正:(((rotate + 90) % 360) + 360) % 360——JS の % は負数に対して負の値を返します(-90 % 360 === -90)。外側の +360) % 360 がこれを [0, 360) に正規化します。この関数自体は +90 しかせず負になることはありませんが、この修正は防御的なものです(呼び出し側が負の角度を初期値として渡した場合)。
stepViewerIndex のループ切り替え:(index + delta + total) % total——最初の画像で「前へ」を押すと最後の画像に跳びます。total <= 0 のときは 0 を返します(空の一覧への防御)。
normalizePreviewList の退化:previewSrcList が渡されないときは [src] に退化します——単一画像のプレビューと複数画像のプレビューが同じロジックで動き、total が 1 になるだけです。切り替えボタンは自動的に非表示になります。
| 関数 | シグネチャ | 戻り値 | 役割 |
|---|---|---|---|
getImageClassNames |
(state: LhImageState) => string[] |
クラス名 | error / previewable |
normalizePreviewList |
(src, previewSrcList?) => string[] |
一覧 | 未指定時は [src] に退化 |
stepZoom |
(zoom, direction: 1 | -1) => number |
新しい拡大率 | ステップ + クランプ |
stepRotate |
(rotate: number) => number |
新しい角度 | +90°、[0,360) に正規化 |
stepViewerIndex |
(index, total, delta) => number |
新しい添字 | ループ |
getViewerImageStyle |
(state: LhImageViewerState) => Record<string, string> |
スタイル | transform |
getImageViewerAria |
(options?: { id?, label? }) => LhAriaAttrs |
aria 属性バッグ | dialog セマンティクス |
定数:LH_IMAGE_ZOOM_MIN = 0.25、LH_IMAGE_ZOOM_MAX = 4、LH_IMAGE_ZOOM_STEP = 0.25。
export type LhImageFit = 'contain' | 'cover' | 'fill' | 'none' | 'scale-down' // ネイティブ object-fit
export interface LhImageState { fit: LhImageFit error: boolean loaded: boolean preview: boolean // プレビュー可能か(クリックへの応答 + マスク表示を決める)}
export interface LhImageViewerState { zoom: number rotate: number index: number total: number}
export const LH_IMAGE_ZOOM_MIN = 0.25export const LH_IMAGE_ZOOM_MAX = 4export const LH_IMAGE_ZOOM_STEP = 0.25アクセシビリティ
Section titled “アクセシビリティ”プレビューアーは dialog として提示し、aria-modal を宣言します:
getImageViewerAria({ id: 'lh-viewer', label: '2 枚目、全 5 枚' })// { role: 'dialog', 'aria-modal': true, id: 'lh-viewer', 'aria-label': '2 枚目、全 5 枚' }
getImageViewerAria()// { role: 'dialog', 'aria-modal': true }aria-modal: trueはスクリーンリーダーに背景が到達不能であることを伝えます——core/shared/focus-trapと組み合わせてフォーカスを閉じ込め、Esc で閉じます。labelに連番を含める価値は大きい:複数画像のプレビュー時にスクリーンリーダーが「2 枚目、全 5 枚」と読み上げれば、ユーザーはあと何枚見られるか分かります。単一画像なら「画像プレビュー」で十分です。- サムネイル自体には
alt(またはrole="img" aria-label)を持たせます——画像が見えないとき、これが唯一のテキスト情報です。装飾的な画像にはalt=""を使います。 - ツールバーボタン(拡大 / 回転 / 前へ / 次へ)はアイコンボタンであり、必ず
aria-labelを付けます。 - 読み込み失敗のアクセシビリティ通知:
error状態のプレースホルダー画像にはrole="img"+aria-label="画像の読み込みに失敗しました"を付けます。そうでないと、スクリーンリーダーのユーザーはここに元々画像があったことを知りません。
import { stepZoom, stepRotate, stepViewerIndex, normalizePreviewList } from '@lh-ui/share/core/image'
stepZoom(1, 1) // 1.25stepZoom(4, 1) // 4(上限に達して増えない)stepZoom(0.25, -1) // 0.25(下限に達して減らない)stepRotate(0) // 90stepRotate(270) // 0([0, 360) に正規化)stepRotate(-90) // 0(負数剰余修正)stepViewerIndex(0, 5, -1) // 4(末尾にループ)stepViewerIndex(4, 5, 1) // 0(先頭にループ)stepViewerIndex(0, 0, 1) // 0(空の一覧)normalizePreviewList('a.jpg', ['b.jpg', 'c.jpg']) // ['b.jpg', 'c.jpg']normalizePreviewList('a.jpg', undefined) // ['a.jpg']normalizePreviewList('a.jpg', []) // ['a.jpg'](空の配列も退化)getViewerImageStyle({ zoom: 1.5, rotate: 90, index: 0, total: 1 })// { transform: 'scale(1.5) rotate(90deg)' }<!-- Vue3 --><div :class="getImageClassNames({ fit, error, loaded, preview })"> <img v-if="!error" class="lh-image__img" :src="src" :alt="alt" :style="{ objectFit: fit }" @load="loaded = true" @error="error = true" @click="preview && openViewer()" /> <div v-else class="lh-image__error" role="img" aria-label="画像の読み込みに失敗しました"> <LhIcon name="image" /> </div> <div v-if="preview && loaded" class="lh-image__mask"> <LhIcon name="zoom-in" /> <span>クリックしてプレビュー</span> </div></div>
<Teleport to="body"> <div v-if="visible" class="lh-image-viewer" v-bind="getImageViewerAria({ id: viewerId, label: viewerLabel })"> <div class="lh-image-viewer__mask" @click="close" /> <img class="lh-image-viewer__img" :src="list[index]" :style="getViewerImageStyle({ zoom, rotate, index, total: list.length })" /> <div class="lh-image-viewer__toolbar"> <button type="button" aria-label="縮小" @click="zoom = stepZoom(zoom, -1)">−</button> <span class="lh-image-viewer__zoom">{{ Math.round(zoom * 100) }}%</span> <button type="button" aria-label="拡大" @click="zoom = stepZoom(zoom, 1)">+</button> <button type="button" aria-label="回転" @click="rotate = stepRotate(rotate)">⟳</button> <template v-if="list.length > 1"> <button type="button" aria-label="前へ" @click="index = stepViewerIndex(index, list.length, -1)">‹</button> <button type="button" aria-label="次へ" @click="index = stepViewerIndex(index, list.length, 1)">›</button> </template> </div> </div></Teleport>const list = computed(() => normalizePreviewList(props.src, props.previewSrcList))const viewerLabel = computed(() => `${index.value + 1} 枚目、全 ${list.value.length} 枚`)// React:ホイールでの拡大(viewer に onWheel を設定)// const next = stepZoom(zoom, e.deltaY < 0 ? 1 : -1); setZoom(next)// uniapp:<image> は組み込みコンポーネント(mode 属性が object-fit に対応:widthFix / aspectFill など);// プレビューアーは <view> + <image> を使うか、uni.previewImage を呼ぶ(システムレベル、回転機能なし)スタイルとトークン
Section titled “スタイルとトークン”| クラス名 | 意味 |
|---|---|
.lh-image |
基本クラス |
.lh-image--error |
読み込み失敗 |
.lh-image--previewable |
プレビュー可能(hover でマスク表示) |
.lh-image__img |
画像本体 |
.lh-image__error |
失敗プレースホルダー |
.lh-image__mask |
hover マスク |
.lh-image-viewer |
全画面プレビューアー(fixed 配置) |
.lh-image-viewer__mask |
黒いマスク |
.lh-image-viewer__img |
大きな画像(transform アニメーション) |
.lh-image-viewer__toolbar / __zoom |
ツールバー / 拡大率パーセント |
マスク色 rgba(0, 0, 0, 0.5);ツールバーボタンは半透明の濃い背景;画像の transition は transform に(拡大・回転が滑らか)。
previewSrcListとsrcの関係:previewSrcListはプレビュー時に使う大きな画像の一覧で、ページ上のサムネイルsrcと異なっていてもかまいません(サムネイルは小さい画像で通信量を節約し、プレビューは原本を使う)。未指定時は[src]に退化します——この場合プレビューされるのはサムネイルなので、拡大するとぼやける可能性があります。zoomは境界で変化しない:stepZoomは [0.25, 4] にクランプします。ボタンはこれに基づいて disable するか視覚的なフィードバックを返し、押しても反応がない事態を避けてください。- 回転と拡大の順序:
transform: scale(zoom) rotate(rotate)——先に拡大、後に回転。順序が逆だと回転に拡大のずれが伴います(中心を基準にする場合は差は小さいですが、移動との組み合わせでは顕著です)。 - フォーカス管理:プレビューアーを開いたときフォーカスはツールバーに入れ、閉じたときサムネイルに戻します——
core/shared/focus-trapで処理しないと、Tab がページの背後に逃げてしまいます。 error状態の再試行:core は再試行関数を提供しません。UI 層はsrcにクエリパラメータ(?t=Date.now())を付けて強制的に再読み込みできますが、その前にerror状態をクリアしてください。loadedの用途:読み込み完了前はスケルトン(skeleton)や低解像度のぼかしたプレースホルダー(LQIP)を表示できます。- メモリへの配慮:大きな画像一覧のプレビューでは、
previewSrcListのうち本当に読み込まれるのはindexの近辺だけです(ブラウザがオンデマンドに読み込む)。ですが数十個の<img>を一度にレンダリングしないでください——core はレンダリング数を管理せず、UI 層が制限します。 - uniapp での
fitの対応付け:object-fitの 5 つの値はミニプログラム側では<image mode>の各値に対応します(scale-downは直接の対応がなく、通常はaspectFitで近似します)。UI 層が対応付けを行います。
@lh-ui/share/core/image → stepZoom( )(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue