mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
feat(components): [image-viewer] add error slot (#21961)
* feat(components): [image-viewer] add custom failed content * Update packages/components/image-viewer/src/image-viewer.vue Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com> * feat: rename error slot * test: add custom load failed slot tests for Image and ImageViewer * docs: perf dome * fix: update v * feat: add activeIndex and src properties * fix: add key binding to img element for better reactivity * fix: keep original structure * fix: restore error source in image load-failed example * feat: add image preview * refactor: remove unused var * fix: update demo * chore: better contrast for dark mode --------- Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com> Co-authored-by: Dsaquel <291874700n@gmail.com>
This commit is contained in:
@@ -25,8 +25,7 @@ image/placeholder
|
||||
|
||||
## Load Failed
|
||||
|
||||
:::demo Custom failed content when error occurs to image load by `slot = error`
|
||||
|
||||
:::demo Custom failed content when error occurs to image load by `slot = error` and `slot = viewer-error`.
|
||||
image/load-failed
|
||||
|
||||
:::
|
||||
@@ -118,13 +117,11 @@ image/custom-progress
|
||||
|
||||
### Image Slots
|
||||
|
||||
| Name | Description | Type |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| placeholder | custom placeholder content when image hasn't loaded yet. | - |
|
||||
| error | custom image load failed content. | - |
|
||||
| viewer | custom content when image preview. | - |
|
||||
| progress ^(2.9.4) | custom progress content when image preview. (Priority is higher than `show-progress` prop) | ^[object]`{ activeIndex: number, total: number }` |
|
||||
| toolbar ^(2.9.4) | custom toolbar content when image preview. | ^[object]`{actions: (action: ImageViewerAction, options?: ImageViewerActionOptions ) => void, prev: ()=> void, next: () => void,reset: () => void, activeIndex: number }, setActiveItem: (index: number) => void` |
|
||||
| Name | Description | Type |
|
||||
| ----------------------------------------- | --------------------------------------------------------------------- | ---- |
|
||||
| placeholder | custom placeholder content when image hasn't loaded yet. | - |
|
||||
| error | custom image load failed content. | - |
|
||||
| [image viewer slots](#image-viewer-slots) | when you allow big image preview, image viewer slots all can be used. | - |
|
||||
|
||||
### Image Exposes
|
||||
|
||||
@@ -156,9 +153,19 @@ image/custom-progress
|
||||
| Name | Description | Type |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
||||
| close | trigger when clicking on close button or when `hide-on-click-modal` enabled clicking on backdrop. | ^[Function]`() => void` |
|
||||
| error ^(2.11.3) | same as native error. | ^[Function]`(e: Event) => void` |
|
||||
| switch | trigger when switching images. | ^[Function]`(index: number) => void` |
|
||||
| rotate ^(2.3.13) | trigger when rotating images. | ^[Function]`(deg: number) => void` |
|
||||
|
||||
### Image Viewer Slots
|
||||
|
||||
| Name | Description | Type |
|
||||
| ---------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| viewer | custom content | - |
|
||||
| progress ^(2.9.4) | custom progress content (Priority is higher than `show-progress` prop) | ^[object]`{ activeIndex: number, total: number }` |
|
||||
| toolbar ^(2.9.4) | custom toolbar content | ^[object]`{actions: (action: ImageViewerAction, options?: ImageViewerActionOptions ) => void, prev: ()=> void, next: () => void,reset: () => void, activeIndex: number }, setActiveItem: (index: number) => void` |
|
||||
| viewer-error ^(2.11.3) | custom image load failed content. | ^[object]`{ activeIndex: number, src: string }` |
|
||||
|
||||
### Image Viewer Exposes
|
||||
|
||||
| Name | Description | Type |
|
||||
|
||||
@@ -1,61 +1,82 @@
|
||||
<template>
|
||||
<div class="demo-image__error">
|
||||
<div class="block">
|
||||
<span class="demonstration">Default</span>
|
||||
<el-image />
|
||||
</div>
|
||||
<div class="block">
|
||||
<span class="demonstration">Custom</span>
|
||||
<el-image>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon><icon-picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="demo-image__error" flex gap-2>
|
||||
<el-image />
|
||||
<el-image>
|
||||
<template #error>
|
||||
<div class="image-viewer-slot image-slot">
|
||||
<el-icon><icon-picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<el-image :src="url" :preview-src-list="srcList" show-progress>
|
||||
<template #viewer-error="{ activeIndex, src }">
|
||||
<div class="image-slot viewer-error">
|
||||
<el-icon><icon-picture /></el-icon>
|
||||
<span>
|
||||
this is viewer-error slot. current index: {{ activeIndex }}. src:
|
||||
{{ src }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<el-button @click="showPreview = true"> preview controlled </el-button>
|
||||
|
||||
<el-image-viewer
|
||||
v-if="showPreview"
|
||||
show-progress
|
||||
:url-list="srcList"
|
||||
@close="showPreview = false"
|
||||
>
|
||||
<template #viewer-error="{ activeIndex, src }">
|
||||
<div class="image-slot viewer-error">
|
||||
<el-icon><icon-picture /></el-icon>
|
||||
<span>
|
||||
this is viewer-error slot. current index: {{ activeIndex }}. src:
|
||||
{{ src }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image-viewer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Picture as IconPicture } from '@element-plus/icons-vue'
|
||||
|
||||
const showPreview = ref(false)
|
||||
|
||||
const srcList = [
|
||||
'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg',
|
||||
'https://errorSrc',
|
||||
]
|
||||
const url =
|
||||
'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-image__error .block {
|
||||
padding: 30px 0;
|
||||
text-align: center;
|
||||
border-right: solid 1px var(--el-border-color);
|
||||
display: inline-block;
|
||||
width: 49%;
|
||||
box-sizing: border-box;
|
||||
vertical-align: top;
|
||||
}
|
||||
.demo-image__error .demonstration {
|
||||
display: block;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.demo-image__error .el-image {
|
||||
padding: 0 5px;
|
||||
max-width: 300px;
|
||||
max-height: 200px;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.demo-image__error .image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-secondary);
|
||||
flex-direction: column;
|
||||
font-size: 30px;
|
||||
height: 200px;
|
||||
background: #fff;
|
||||
}
|
||||
.demo-image__error .image-slot .el-icon {
|
||||
font-size: 30px;
|
||||
}
|
||||
.image-viewer-slot {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
.viewer-error {
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -104,4 +104,24 @@ describe('<image-viewer />', () => {
|
||||
expect(innerText).toBe('1 / 2')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
test('custom ImageViewer load failed slot', async () => {
|
||||
const wrapper = mount(ImageViewer, {
|
||||
props: {
|
||||
urlList: [IMAGE_SUCCESS],
|
||||
initialIndex: 1,
|
||||
},
|
||||
slots: {
|
||||
'viewer-error': () => (
|
||||
<div class="load-failed-slot">load failed slot</div>
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
await doubleWait()
|
||||
const img = wrapper.find('.el-image-viewer__wrapper img')
|
||||
await img.trigger('error')
|
||||
await doubleWait()
|
||||
expect(wrapper.find('.load-failed-slot').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -103,6 +103,7 @@ export type ImageViewerPropsPublic = __ExtractPublicPropTypes<
|
||||
|
||||
export const imageViewerEmits = {
|
||||
close: () => true,
|
||||
error: (evt: Event) => evt instanceof Event,
|
||||
switch: (index: number) => isNumber(index),
|
||||
rotate: (deg: number) => isNumber(deg),
|
||||
}
|
||||
|
||||
@@ -83,19 +83,24 @@
|
||||
</div>
|
||||
<!-- CANVAS -->
|
||||
<div :class="ns.e('canvas')">
|
||||
<template v-for="(url, i) in urlList" :key="i">
|
||||
<img
|
||||
v-if="i === activeIndex"
|
||||
:ref="(el) => (imgRefs[i] = el as HTMLImageElement)"
|
||||
:src="url"
|
||||
:style="imgStyle"
|
||||
:class="ns.e('img')"
|
||||
:crossorigin="crossorigin"
|
||||
@load="handleImgLoad"
|
||||
@error="handleImgError"
|
||||
@mousedown="handleMouseDown"
|
||||
/>
|
||||
</template>
|
||||
<slot
|
||||
v-if="loadError && $slots['viewer-error']"
|
||||
name="viewer-error"
|
||||
:active-index="activeIndex"
|
||||
:src="currentImg"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
ref="imgRef"
|
||||
:key="currentImg"
|
||||
:src="currentImg"
|
||||
:style="imgStyle"
|
||||
:class="ns.e('img')"
|
||||
:crossorigin="crossorigin"
|
||||
@load="handleImgLoad"
|
||||
@error="handleImgError"
|
||||
@mousedown="handleMouseDown"
|
||||
/>
|
||||
</div>
|
||||
<slot />
|
||||
</el-focus-trap>
|
||||
@@ -164,7 +169,7 @@ const { t } = useLocale()
|
||||
const ns = useNamespace('image-viewer')
|
||||
const { nextZIndex } = useZIndex()
|
||||
const wrapper = ref<HTMLDivElement>()
|
||||
const imgRefs = ref<HTMLImageElement[]>([])
|
||||
const imgRef = ref<HTMLImageElement>()
|
||||
|
||||
const scopeEventListener = effectScope()
|
||||
|
||||
@@ -174,6 +179,7 @@ const scaleClamped = computed(() => {
|
||||
})
|
||||
|
||||
const loading = ref(true)
|
||||
const loadError = ref(false)
|
||||
const activeIndex = ref(props.initialIndex)
|
||||
const mode = shallowRef<ImageViewerMode>(modes.CONTAIN)
|
||||
const transform = ref({
|
||||
@@ -292,7 +298,9 @@ function handleImgLoad() {
|
||||
}
|
||||
|
||||
function handleImgError(e: Event) {
|
||||
loadError.value = true
|
||||
loading.value = false
|
||||
emit('error', e)
|
||||
;(e.target as HTMLImageElement).alt = t('el.image.error')
|
||||
}
|
||||
|
||||
@@ -330,7 +338,7 @@ function reset() {
|
||||
}
|
||||
|
||||
function toggleMode() {
|
||||
if (loading.value) return
|
||||
if (loading.value || loadError.value) return
|
||||
|
||||
const modeNames = keysOf(modes)
|
||||
const modeValues = Object.values(modes)
|
||||
@@ -342,6 +350,7 @@ function toggleMode() {
|
||||
}
|
||||
|
||||
function setActiveItem(index: number) {
|
||||
loadError.value = false
|
||||
const len = props.urlList.length
|
||||
activeIndex.value = (index + len) % len
|
||||
}
|
||||
@@ -357,7 +366,7 @@ function next() {
|
||||
}
|
||||
|
||||
function handleActions(action: ImageViewerAction, options = {}) {
|
||||
if (loading.value) return
|
||||
if (loading.value || loadError.value) return
|
||||
const { minScale, maxScale } = props
|
||||
const { zoomRate, rotateDeg, enableTransition } = {
|
||||
zoomRate: props.zoomRate,
|
||||
@@ -425,7 +434,7 @@ watch(
|
||||
|
||||
watch(currentImg, () => {
|
||||
nextTick(() => {
|
||||
const $img = imgRefs.value[0]
|
||||
const $img = imgRef.value
|
||||
if (!$img?.complete) {
|
||||
loading.value = true
|
||||
}
|
||||
|
||||
@@ -229,6 +229,38 @@ describe('Image.vue', () => {
|
||||
expect(wrapper.find('.el-image-viewer__progress').exists()).toBe(true)
|
||||
})
|
||||
|
||||
test('custom viewer load failed slot', async () => {
|
||||
const url = IMAGE_SUCCESS
|
||||
const srcList = ['error']
|
||||
const wrapper = _mount(
|
||||
`
|
||||
<el-image
|
||||
ref="imageRef"
|
||||
:src="url"
|
||||
:preview-src-list="srcList"
|
||||
>
|
||||
<template #viewer-error>
|
||||
<div class="load-failed-slot">
|
||||
load failed slot
|
||||
</div>
|
||||
</template>
|
||||
</el-image>`,
|
||||
() => ({
|
||||
url,
|
||||
srcList,
|
||||
})
|
||||
)
|
||||
|
||||
await doubleWait()
|
||||
wrapper.vm.$refs.imageRef.showPreview()
|
||||
await doubleWait()
|
||||
|
||||
const img = wrapper.find('.el-image-viewer__canvas img')
|
||||
await img.trigger('error')
|
||||
await doubleWait()
|
||||
expect(wrapper.find('.load-failed-slot').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('load', () => {
|
||||
mockImageEvent()
|
||||
|
||||
|
||||
@@ -50,6 +50,9 @@
|
||||
<template #toolbar="toolbar">
|
||||
<slot name="toolbar" v-bind="toolbar" />
|
||||
</template>
|
||||
<template v-if="$slots['viewer-error']" #viewer-error="viewerError">
|
||||
<slot name="viewer-error" v-bind="viewerError" />
|
||||
</template>
|
||||
</image-viewer>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user