UNPKG

vxe-pc-ui

Version:
643 lines (594 loc) 19.8 kB
import { defineComponent, h, provide, PropType, ref, reactive, computed, watch, onMounted, onUnmounted, createCommentVNode, onBeforeUnmount } from 'vue' import { VxeUI, getConfig, createEvent, getIcon, globalEvents, GLOBAL_EVENT_KEYS, getI18n } from '../../ui' import XEUtils from 'xe-utils' import { getDomNode, addClass, removeClass } from '../..//ui/src/dom' import type { VxeImagePreviewConstructor, ImagePreviewReactData, ImagePreviewPrivateRef, VxeGlobalIcon, VxeImagePreviewEmits, VxeImagePreviewPrivateMethods, ImagePreviewPrivateMethods, ImagePreviewPrivateComputed, ImagePreviewMethods, VxeImagePreviewPropTypes, ValueOf } from '../../../types' export default defineComponent({ name: 'VxeImagePreview', props: { modelValue: Number as PropType<VxeImagePreviewPropTypes.ModelValue>, urlList: Array as PropType<VxeImagePreviewPropTypes.UrlList>, urlField: { type: String as PropType<VxeImagePreviewPropTypes.UrlField>, default: () => getConfig().imagePreview.urlField }, maskClosable: { type: Boolean as PropType<VxeImagePreviewPropTypes.MaskClosable>, default: () => getConfig().imagePreview.maskClosable }, marginSize: { type: String as PropType<VxeImagePreviewPropTypes.MarginSize>, default: () => getConfig().imagePreview.marginSize }, showPrintButton: { type: Boolean as PropType<VxeImagePreviewPropTypes.ShowPrintButton>, default: () => getConfig().imagePreview.showPrintButton }, showDownloadButton: { type: Boolean as PropType<VxeImagePreviewPropTypes.ShowDownloadButton>, default: () => getConfig().imagePreview.showDownloadButton }, beforeDownloadMethod: Function as PropType<VxeImagePreviewPropTypes.BeforeDownloadMethod>, downloadMethod: Function as PropType<VxeImagePreviewPropTypes.DownloadMethod> }, emits: [ 'update:modelValue', 'change', 'download', 'download-fail', 'close' ] as VxeImagePreviewEmits, setup (props, context) { const { emit } = context const xID = XEUtils.uniqueId() const refElem = ref<HTMLDivElement>() const refMaps: ImagePreviewPrivateRef = { refElem } const reactData = reactive<ImagePreviewReactData>({ activeIndex: props.modelValue || 0, offsetPct11: false, offsetScale: 0, offsetRotate: 0, offsetLeft: 0, offsetTop: 0 }) const computeUrlProp = computed(() => { return props.urlField || 'url' }) const computeMarginSize = computed(() => { return XEUtils.toNumber(props.marginSize || 0) || 16 }) const computeRotateText = computed(() => { const { offsetRotate } = reactData if (offsetRotate) { return `${offsetRotate}°` } return '0°' }) const computeScaleText = computed(() => { const { offsetScale } = reactData if (offsetScale) { return `${XEUtils.ceil((1 + offsetScale) * 100)}%` } return '100%' }) const computeImgList = computed(() => { const { urlList } = props const urlProp = computeUrlProp.value if (urlList && urlList.length) { return urlList.map(item => { if (XEUtils.isString(item)) { return item } if (item[urlProp]) { return item[urlProp] } return '' }) } return [] }) const computeImgTransform = computed(() => { let { offsetScale, offsetRotate, offsetLeft, offsetTop } = reactData const stys: string[] = [] let targetScale = 1 if (offsetScale) { targetScale = 1 + offsetScale stys.push(`scale(${targetScale})`) } if (offsetRotate) { stys.push(`rotate(${offsetRotate}deg)`) } if (offsetLeft || offsetTop) { // 缩放与位移 offsetLeft /= targetScale offsetTop /= targetScale let targetOffsetLeft = offsetLeft let targetOffsetTop = offsetTop if (offsetRotate) { // 转向与位移 switch (offsetRotate % 360) { case 90: case -270: targetOffsetLeft = offsetTop targetOffsetTop = -offsetLeft break case 180: case -180: targetOffsetLeft = -offsetLeft targetOffsetTop = -offsetTop break case 270: case -90: targetOffsetLeft = -offsetTop targetOffsetTop = offsetLeft break } } stys.push(`translate(${targetOffsetLeft}px, ${targetOffsetTop}px)`) } return stys.length ? stys.join(' ') : '' }) const computeMaps: ImagePreviewPrivateComputed = { computeImgList } const $xeImagePreview = { xID, props, context, reactData, getRefMaps: () => refMaps, getComputeMaps: () => computeMaps } as unknown as VxeImagePreviewConstructor & VxeImagePreviewPrivateMethods const dispatchEvent = (type: ValueOf<VxeImagePreviewEmits>, params: Record<string, any>, evnt: Event | null) => { emit(type, createEvent(evnt, { $imagePreview: $xeImagePreview }, params)) } const imagePreviewMethods: ImagePreviewMethods = { dispatchEvent } const emitModel = (value: VxeImagePreviewPropTypes.ModelValue) => { reactData.activeIndex = value emit('update:modelValue', value) } const handleCloseEvent = (evnt: MouseEvent) => { dispatchEvent('close', {}, evnt) } const imagePreviewPrivateMethods: ImagePreviewPrivateMethods = { } const resetStyle = () => { const elem = refElem.value removeClass(elem, 'is--move') Object.assign(reactData, { offsetPct11: false, offsetScale: 0, offsetRotate: 0, offsetLeft: 0, offsetTop: 0 }) } const getOffsetZoomStep = () => { const { offsetScale } = reactData let stepNum = 0.02 if (offsetScale >= -0.6) { stepNum = 0.04 if (offsetScale >= -0.4) { stepNum = 0.07 if (offsetScale >= 0) { stepNum = 0.1 if (offsetScale >= 3) { stepNum = 0.25 if (offsetScale >= 8) { stepNum = 0.4 if (offsetScale >= 16) { stepNum = 0.6 if (offsetScale >= 24) { stepNum = 0.9 if (offsetScale >= 32) { stepNum = 1.3 if (offsetScale >= 39) { stepNum = 1.9 if (offsetScale >= 45) { stepNum = 2.5 } } } } } } } } } } return stepNum } const handleZoom = (isAdd: boolean) => { const { offsetScale } = reactData const stepNum = getOffsetZoomStep() if (isAdd) { reactData.offsetScale = Number(Math.min(49, offsetScale + stepNum).toFixed(2)) } else { reactData.offsetScale = Number(Math.max(-0.9, offsetScale - stepNum).toFixed(2)) } } const handleChange = (isNext: boolean) => { let activeIndex = reactData.activeIndex || 0 const imgList = computeImgList.value if (isNext) { if (activeIndex >= imgList.length - 1) { activeIndex = 0 } else { activeIndex++ } } else { if (activeIndex <= 0) { activeIndex = imgList.length - 1 } else { activeIndex-- } } resetStyle() reactData.activeIndex = activeIndex emitModel(activeIndex) } const handleRotateImg = (isRight: boolean) => { let offsetRotate = reactData.offsetRotate if (isRight) { offsetRotate += 90 } else { offsetRotate -= 90 } reactData.offsetRotate = offsetRotate } const handlePct11 = () => { resetStyle() reactData.offsetPct11 = true } const handlePrintImg = () => { const { activeIndex } = reactData const imgList = computeImgList.value const imgUrl = imgList[activeIndex || 0] if (VxeUI.print) { VxeUI.print({ align: 'center', pageBreaks: [ { bodyHtml: `<img src="${imgUrl}" style="max-width:100%;max-height:100%;">` } ] }) } } const handleDownloadEvent = (evnt: MouseEvent, imgUrl: string) => { dispatchEvent('download', { url: imgUrl }, evnt) } const handleDefaultDownload = (evnt: MouseEvent, imgUrl: string) => { if (VxeUI.saveFile) { fetch(imgUrl).then(res => { return res.blob().then(blob => { VxeUI.saveFile({ filename: imgUrl, content: blob }) handleDownloadEvent(evnt, imgUrl) }) }).catch(() => { if (VxeUI.modal) { VxeUI.modal.message({ content: getI18n('vxe.error.downErr'), status: 'error' }) } }) } } const handleDownloadImg = (evnt: MouseEvent) => { const { activeIndex } = reactData const imgList = computeImgList.value const imgUrl = imgList[activeIndex || 0] const beforeDownloadFn = props.beforeDownloadMethod || getConfig().imagePreview.beforeDownloadMethod const downloadFn = props.downloadMethod || getConfig().imagePreview.downloadMethod Promise.resolve( beforeDownloadFn ? beforeDownloadFn({ $imagePreview: $xeImagePreview, url: imgUrl, index: activeIndex || 0 }) : true ).then(status => { if (status) { if (downloadFn) { Promise.resolve( downloadFn({ $imagePreview: $xeImagePreview, url: imgUrl, index: activeIndex || 0 }) ).then(() => { handleDownloadEvent(evnt, imgUrl) }).catch(e => e) } else { handleDefaultDownload(evnt, imgUrl) } } }) } const handleOperationBtn = (evnt: MouseEvent, code: string) => { const { activeIndex } = reactData const imgList = computeImgList.value const imgUrl = imgList[activeIndex || 0] if (imgUrl) { switch (code) { case 'zoomOut': handleZoom(false) break case 'zoomIn': handleZoom(true) break case 'pctFull': resetStyle() break case 'pct11': handlePct11() break case 'rotateLeft': handleRotateImg(false) break case 'rotateRight': handleRotateImg(true) break case 'print': handlePrintImg() break case 'download': handleDownloadImg(evnt) break } } } const wheelEvent = (evnt: WheelEvent) => { const delta = evnt.deltaY if (delta > 0) { handleZoom(false) } else if (delta < 0) { handleZoom(true) } } const moveEvent = (evnt: MouseEvent) => { const { offsetTop, offsetLeft } = reactData const elem = refElem.value evnt.preventDefault() const domMousemove = document.onmousemove const domMouseup = document.onmouseup const startX = evnt.pageX const startY = evnt.pageY const marginSize = computeMarginSize.value document.onmousemove = et => { const { pageX, pageY } = et const { visibleHeight, visibleWidth } = getDomNode() et.preventDefault() addClass(elem, 'is--move') // 限制边界值 if (pageX > marginSize && pageY > marginSize && pageX < (visibleWidth - marginSize) && pageY < (visibleHeight - marginSize)) { reactData.offsetLeft = offsetLeft + pageX - startX reactData.offsetTop = offsetTop + pageY - startY } } document.onmouseup = () => { document.onmousemove = domMousemove document.onmouseup = domMouseup removeClass(elem, 'is--move') } } const handleGlobalKeydownEvent = (evnt: KeyboardEvent) => { const hasCtrlKey = evnt.ctrlKey const hasShiftKey = evnt.shiftKey const isUpArrow = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ARROW_UP) const isDownArrow = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ARROW_DOWN) const isLeftArrow = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ARROW_LEFT) const isRightArrow = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.ARROW_RIGHT) const isR = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.R) const isP = globalEvents.hasKey(evnt, GLOBAL_EVENT_KEYS.P) if (isUpArrow) { evnt.preventDefault() if (hasShiftKey) { reactData.offsetTop -= 1 } else { handleZoom(true) } } else if (isDownArrow) { evnt.preventDefault() if (hasShiftKey) { reactData.offsetTop += 1 } else { handleZoom(false) } } else if (isLeftArrow) { evnt.preventDefault() if (hasShiftKey) { reactData.offsetLeft -= 1 } else { handleChange(false) } } else if (isRightArrow) { evnt.preventDefault() if (hasShiftKey) { reactData.offsetLeft += 1 } else { handleChange(true) } } else if (isR && hasCtrlKey) { evnt.preventDefault() if (hasShiftKey) { handleRotateImg(false) } else { handleRotateImg(true) } } else if (isP && hasCtrlKey) { evnt.preventDefault() handlePrintImg() } } const handleClickMaskEvent = (evnt: MouseEvent) => { if (props.maskClosable) { if (evnt.target === evnt.currentTarget) { dispatchEvent('close', {}, evnt) } } } Object.assign($xeImagePreview, imagePreviewMethods, imagePreviewPrivateMethods) const renderImgWrapper = () => { const { activeIndex } = reactData const imgList = computeImgList.value const imgTransform = computeImgTransform.value return h('div', { class: 'vxe-image-preview--img-list', onClick: handleClickMaskEvent }, imgList.map((url, index) => { const isActive = activeIndex === index return h('img', { class: ['vxe-image-preview--img-item', { 'is--active': isActive }], src: url, style: isActive ? { transform: imgTransform } : null, onMousedown (evnt) { moveEvent(evnt) } }) })) } const renderOperationBtn = (code: string, icon: keyof VxeGlobalIcon) => { return h('div', { class: 'vxe-image-preview--operation-btn', title: getI18n(`vxe.imagePreview.operBtn.${code}`), onClick (evnt) { handleOperationBtn(evnt, code) } }, [ h('i', { class: getIcon()[icon] }) ]) } const renderBtnWrapper = () => { const { showPrintButton, showDownloadButton } = props const { activeIndex } = reactData const imgList = computeImgList.value const rotateText = computeRotateText.value const scaleText = computeScaleText.value return h('div', { class: 'vxe-image-preview--btn-wrapper' }, [ h('div', { class: 'vxe-image-preview--close-wrapper' }, [ h('div', { class: 'vxe-image-preview--close-btn', onClick: handleCloseEvent }, [ h('i', { class: getIcon().IMAGE_PREVIEW_CLOSE }) ]), h('div', { class: 'vxe-image-preview--close-bg' }) ]), imgList.length > 1 ? h('div', { class: 'vxe-image-preview--previous-btn', onClick () { handleChange(false) } }, [ h('i', { class: getIcon().IMAGE_PREVIEW_PREVIOUS }) ]) : createCommentVNode(), imgList.length > 1 ? h('div', { class: 'vxe-image-preview--next-btn', onClick () { handleChange(true) } }, [ h('i', { class: getIcon().IMAGE_PREVIEW_NEXT }) ]) : createCommentVNode(), h('div', { class: 'vxe-image-preview--operation-info' }, [ h('div', { class: 'vxe-image-preview--operation-deg' }, rotateText), h('div', { class: 'vxe-image-preview--operation-pct' }, scaleText) ]), h('div', { class: 'vxe-image-preview--operation-wrapper' }, [ h('div', { class: 'vxe-image-preview--operation-active-count' }, [ h('span', { class: 'vxe-image-preview--operation-active-current' }, `${(activeIndex || 0) + 1}`), h('span', { class: 'vxe-image-preview--operation-active-total' }, `/${imgList.length}`) ]), renderOperationBtn('zoomOut', 'IMAGE_PREVIEW_ZOOM_OUT'), renderOperationBtn('zoomIn', 'IMAGE_PREVIEW_ZOOM_IN'), renderOperationBtn('pctFull', 'IMAGE_PREVIEW_PCT_FULL'), renderOperationBtn('pct11', 'IMAGE_PREVIEW_PCT_1_1'), renderOperationBtn('rotateLeft', 'IMAGE_PREVIEW_ROTATE_LEFT'), renderOperationBtn('rotateRight', 'IMAGE_PREVIEW_ROTATE_RIGHT'), showPrintButton ? renderOperationBtn('print', 'IMAGE_PREVIEW_PRINT') : createCommentVNode(), showDownloadButton ? renderOperationBtn('download', 'IMAGE_PREVIEW_DOWNLOAD') : createCommentVNode() ]) ]) } const renderVN = () => { const { offsetPct11 } = reactData return h('div', { ref: refElem, class: ['vxe-image-preview', { 'is--pct11': offsetPct11 }], onWheel: wheelEvent }, [ renderImgWrapper(), renderBtnWrapper() ]) } watch(() => props.modelValue, val => { reactData.activeIndex = val resetStyle() }) onMounted(() => { globalEvents.on($xeImagePreview, 'keydown', handleGlobalKeydownEvent) }) onBeforeUnmount(() => { const elem = refElem.value if (elem) { removeClass(elem, 'is--move') } }) onUnmounted(() => { globalEvents.off($xeImagePreview, 'keydown') }) provide('$xeImagePreview', $xeImagePreview) $xeImagePreview.renderVN = renderVN return renderVN } })