UNPKG

@fesjs/fes-design

Version:
318 lines (312 loc) 9.33 kB
import { defineComponent, ref, computed, reactive, inject, watch, onUnmounted, nextTick, resolveComponent, openBlock, createElementBlock, normalizeClass, normalizeStyle, renderSlot, createElementVNode, createVNode, mergeProps, createBlock, createCommentVNode } from 'vue'; import { useThrottleFn, useEventListener } from '@vueuse/core'; import { isString } from 'lodash-es'; import getPrefixCls from '../_util/getPrefixCls'; import { PictureOutlined, PictureFailOutlined } from '../icon'; import { isHtmlElement, getScrollContainer, isInContainer } from '../_util/dom'; import { pxfy, noop, noopInNoop } from '../_util/utils'; import { ERROR_EVENT, LOAD_EVENT, CLOSE_EVENT } from '../_util/constants'; import download from '../_util/download'; import { useTheme } from '../_theme/useTheme'; import { PREVIEW_PROVIDE_KEY } from './props'; import Preview from './preview.js'; const prefixCls = getPrefixCls('img'); let curIndex = 0; let prevOverflow = ''; const imageProps = { src: { type: String, default: '' }, name: String, preview: { type: Boolean, default: false }, fit: { type: String }, lazy: { type: Boolean, default: false }, hideOnClickModal: { type: Boolean, default: false }, scrollContainer: [String, Object], download: { type: Boolean, default: false }, previewContainer: { type: Function }, height: [String, Number], width: [String, Number] }; var script = defineComponent({ name: 'FImage', componentName: 'FImage', components: { Preview, PictureOutlined, PictureFailOutlined }, props: imageProps, emits: [ERROR_EVENT, LOAD_EVENT, CLOSE_EVENT], setup(props, _ref) { let { attrs, emit } = _ref; useTheme(); const loading = ref(true); const isLoadError = ref(false); const container = ref(null); const isShowPreview = ref(false); const currentId = ref(curIndex++); const imgAttrs = computed(() => { const { crossorigin = undefined, decoding = 'auto', alt = undefined, sizes = undefined, srcset = undefined, usemap = undefined } = attrs; return { crossorigin, decoding, alt, sizes, srcset, usemap }; }); const style = computed(() => { const { width, height } = props; return { width: pxfy(width), height: pxfy(height) }; }); const imageSize = reactive({ height: 0, width: 0 }); const { isGroup, setShowPreview, setCurrent, registerImage } = inject(PREVIEW_PROVIDE_KEY, { setShowPreview: noop, isGroup: ref(false), setCurrent: noop, registerImage: noopInNoop }); const canPreview = computed(() => props.preview && !isLoadError.value); const canGroupPreview = computed(() => isGroup.value && !isLoadError.value); const _scrollContainer = computed(() => { let dom; const _container = props.scrollContainer; if (isString(_container) && _container !== '') { dom = document.querySelector(_container); } if (isHtmlElement(_container)) { dom = _container; } else if (container.value) { dom = getScrollContainer(container.value); } return dom; }); const imageStyle = computed(() => { const { fit } = props; const styleObj = { objectFit: 'fill', cursor: '' }; if (fit) { styleObj.objectFit = fit; } if (props.download || canPreview.value || canGroupPreview.value) { styleObj.cursor = 'pointer'; } return styleObj; }); const handleLoaded = (e, img) => { imageSize.width = img.width; imageSize.height = img.height; loading.value = false; isLoadError.value = false; emit(LOAD_EVENT, e); }; const handleError = e => { loading.value = false; isLoadError.value = true; emit(ERROR_EVENT, e); }; let currentImageId = 0; const loadImage = () => { // loading 为true 才会加载图片 if (!loading.value) { return; } const img = new Image(); const imageId = ++currentImageId; img.addEventListener('load', e => { // 检查 imageId 是否与 currentImageId 相同 if (imageId !== currentImageId) { return; } handleLoaded(e, img); }); img.addEventListener('error', e => { // 检查 imageId 是否与 currentImageId 相同 if (imageId !== currentImageId) { return; } handleError(e); }); // 赋值开始加载图片src img.src = props.src; }; const lazyLoadHandler = useThrottleFn(() => { // load image until image enter the container if (isInContainer(container.value, _scrollContainer.value)) { loadImage(); } }, 200); let clearScrollListener = noop; async function addLazyLoadListener() { await nextTick(); if (clearScrollListener) { clearScrollListener(); } if (_scrollContainer.value) { clearScrollListener = useEventListener(_scrollContainer, 'scroll', lazyLoadHandler); } lazyLoadHandler(); } function clickHandler() { if (canGroupPreview.value) { setCurrent(currentId.value); setShowPreview(true); } else if (canPreview.value) { // prevent body scroll prevOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; isShowPreview.value = true; } else if (props.download) { // 下载 download({ href: props.src, name: props.name }); } } function closeViewer() { document.body.style.overflow = prevOverflow; isShowPreview.value = false; emit(CLOSE_EVENT); } watch(() => props.src, _src => { if (_src) { // 将 loading 状态设置为 true loading.value = true; // 重置 isLoadError 状态 isLoadError.value = false; if (props.lazy) { addLazyLoadListener(); } else { loadImage(); } } }, { immediate: true }); let unRegister = noop; watch([() => props.src, () => props.name, () => props.download, canGroupPreview], () => { unRegister(); if (canGroupPreview.value) { unRegister = registerImage({ id: currentId.value, url: props.src, name: props.name, size: imageSize, download: props.download }); } }, { immediate: true }); onUnmounted(() => { if (unRegister) { unRegister(); } if (clearScrollListener) { clearScrollListener(); } }); return { imgAttrs, imageStyle, isShowPreview, clickHandler, closeViewer, container, prefixCls, isLoadError, loading, imageSize, style }; } }); const _hoisted_1 = ["src"]; function render(_ctx, _cache, $props, $setup, $data, $options) { const _component_PictureOutlined = resolveComponent("PictureOutlined"); const _component_PictureFailOutlined = resolveComponent("PictureFailOutlined"); const _component_Preview = resolveComponent("Preview"); return openBlock(), createElementBlock("div", { ref: "container", class: normalizeClass(_ctx.prefixCls), style: normalizeStyle(_ctx.style) }, [_ctx.loading ? renderSlot(_ctx.$slots, "placeholder", { key: 0 }, () => [createElementVNode("div", { class: normalizeClass(`${_ctx.prefixCls}__placeholder`) }, [createVNode(_component_PictureOutlined), _cache[1] || (_cache[1] = createElementVNode("span", null, "加载中", -1 /* HOISTED */))], 2 /* CLASS */)]) : _ctx.isLoadError ? renderSlot(_ctx.$slots, "error", { key: 1 }, () => [createElementVNode("div", { class: normalizeClass(`${_ctx.prefixCls}__error`) }, [createVNode(_component_PictureFailOutlined), _cache[2] || (_cache[2] = createElementVNode("span", null, "加载失败", -1 /* HOISTED */))], 2 /* CLASS */)]) : (openBlock(), createElementBlock("div", { key: 2, class: normalizeClass(`${_ctx.prefixCls}__inner`), onClick: _cache[0] || (_cache[0] = function () { return _ctx.clickHandler && _ctx.clickHandler(...arguments); }) }, [renderSlot(_ctx.$slots, "default", {}, () => [createElementVNode("img", mergeProps({ src: _ctx.src, class: `${_ctx.prefixCls}__inner-image`, style: _ctx.imageStyle }, _ctx.imgAttrs), null, 16 /* FULL_PROPS */, _hoisted_1)])], 2 /* CLASS */)), _ctx.isShowPreview ? (openBlock(), createBlock(_component_Preview, { key: 3, src: _ctx.src, name: _ctx.name, size: _ctx.imageSize, download: _ctx.download, "hide-on-click-modal": _ctx.hideOnClickModal, getContainer: _ctx.previewContainer, onClose: _ctx.closeViewer }, null, 8 /* PROPS */, ["src", "name", "size", "download", "hide-on-click-modal", "getContainer", "onClose"])) : createCommentVNode("v-if", true)], 6 /* CLASS, STYLE */); } script.render = render; script.__file = "components/image/image.vue"; export { script as default, imageProps };