gohttpdUi/src/components/Image/index.vue

244 lines
6.1 KiB
Vue

<template>
<div ref="imageRef" class="image">
<slot v-if="loading" name="placeholder">
<div class="image__placeholder" />
</slot>
<slot v-else-if="error" name="error">
<div class="image__error">加载失败</div>
</slot>
<img
v-else
v-bind="$attrs"
:src="src"
:style="imageStyle"
:class="{ 'image__inner--center': alignCenter }"
class="image__inner"
>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, computed, watch, onMounted, onBeforeUnmount, getCurrentInstance, unref } from 'vue'
import { on, off, getScrollContainer, isInContainer } from '@/utils/dom-utils'
import { isString, isElement } from '@/utils/is'
import throttle from 'lodash-es/throttle'
const isSupportObjectFit = () => document.documentElement.style.objectFit !== undefined
const ObjectFit = {
NONE: 'none',
CONTAIN: 'contain',
COVER: 'cover',
FILL: 'fill',
SCALE_DOWN: 'scale-down'
}
export default defineComponent({
name: 'Image',
// inheritAttrs: false,
props: {
src: {
type: String as PropType<string>,
default: ''
},
fit: {
type: String as PropType<string>,
default: ''
},
lazy: {
type: Boolean as PropType<boolean>,
default: false
},
scrollContainer: {
type: Object as PropType<any>,
default: null
}
},
emits: ['error'],
setup(props, { emit }) {
const { ctx } = getCurrentInstance() as any
const imageRef = ref<HTMLElement | null>(null)
const loading = ref<boolean>(true)
const error = ref<boolean>(false)
const show = ref<boolean>(!props.lazy)
const imageWidth = ref<number>(0)
const imageHeight = ref<number>(0)
const imageStyle = computed((): any => {
const { fit } = props
// if (!isServer && fit) {
if (fit) {
return isSupportObjectFit()
? { 'object-fit': fit }
: getImageStyle(fit)
}
return {}
})
const alignCenter = computed((): boolean => {
const { fit } = props
// return !isServer && !isSupportObjectFit() && fit !== ObjectFit.FILL
return !isSupportObjectFit() && fit !== ObjectFit.FILL
})
let _scrollContainer: any = null
let _lazyLoadHandler: any = null
watch(
() => show.value,
(show: boolean) => {
show && loadImage()
}
)
watch(
() => props.src,
() => {
show.value && loadImage()
}
)
onMounted(() => {
if (props.lazy) {
addLazyLoadListener()
} else {
loadImage()
}
})
onBeforeUnmount(() => {
props.lazy && removeLazyLoadListener()
})
function loadImage(): void {
// reset status
loading.value = true
error.value = false
const img = new Image()
img.onload = (e: any) => handleLoad(e, img)
img.onerror = (e: any) => handleError(e)
// bind html attrs
// so it can behave consistently
Object.keys(ctx.$attrs)
.forEach((key) => {
const value = ctx.$attrs[key]
img.setAttribute(key, value)
})
img.src = props.src
}
function handleLoad(e: any, img: any): void {
imageWidth.value = img.width
imageHeight.value = img.height
loading.value = false
}
function handleError(e: any): void {
loading.value = false
error.value = true
emit('error', e)
}
function handleLazyLoad(): void {
const imageRefWrap = unref(imageRef) as any
if (isInContainer(imageRefWrap, _scrollContainer)) {
show.value = true
removeLazyLoadListener()
}
}
function addLazyLoadListener(): void {
// if (isServer) return
const { scrollContainer } = props
let __scrollContainer = null
if (isElement(scrollContainer)) {
__scrollContainer = scrollContainer
} else if (isString(scrollContainer)) {
__scrollContainer = document.querySelector(scrollContainer as any)
} else {
const imageRefWrap = unref(imageRef) as any
__scrollContainer = getScrollContainer(imageRefWrap)
}
if (__scrollContainer) {
_scrollContainer = __scrollContainer
_lazyLoadHandler = throttle(handleLazyLoad, 200)
on(__scrollContainer, 'scroll', _lazyLoadHandler)
handleLazyLoad()
}
}
function removeLazyLoadListener(): void {
// if (isServer || !_scrollContainer || !_lazyLoadHandler) return
if (!_scrollContainer || !_lazyLoadHandler) return
off(_scrollContainer, 'scroll', _lazyLoadHandler)
_scrollContainer = null
_lazyLoadHandler = null
}
/**
* simulate object-fit behavior to compatible with IE11 and other browsers which not support object-fit
*/
function getImageStyle(fit: string): object {
const imageRefWrap = unref(imageRef) as any
const {
clientWidth: containerWidth,
clientHeight: containerHeight
} = imageRefWrap
if (!imageWidth.value || !imageHeight.value || !containerWidth || !containerHeight) return {}
const vertical: boolean = imageWidth.value / imageHeight.value < 1
if (fit === ObjectFit.SCALE_DOWN) {
const isSmaller: boolean = imageWidth.value < containerWidth && imageHeight.value < containerHeight
fit = isSmaller ? ObjectFit.NONE : ObjectFit.CONTAIN
}
switch (fit) {
case ObjectFit.NONE:
return { width: 'auto', height: 'auto' }
case ObjectFit.CONTAIN:
return vertical ? { width: 'auto' } : { height: 'auto' }
case ObjectFit.COVER:
return vertical ? { height: 'auto' } : { width: 'auto' }
default:
return {}
}
}
return {
imageRef,
loading, error, show,
imageStyle, alignCenter
}
}
})
</script>
<style lang="less" scoped>
.image {
position: relative;
display: inline-block;
overflow: hidden;
.image__placeholder {
background: #f5f7fa;
}
.image__error {
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: #c0c4cc;
vertical-align: middle;
height: 100%;
height: 100%;
background: #f5f7fa;
}
}
</style>