UNPKG

fox-preview-image

Version:

一个支持 vue3 的预览图片组件

607 lines (596 loc) 19.5 kB
import { Fragment, Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, h, isRef, normalizeStyle, onBeforeMount, onMounted, openBlock, reactive, ref, renderList, toDisplayString, toValue, unref, watch, withCtx, withKeys, withModifiers } from "vue"; //#region node_modules/.pnpm/@vueuse+shared@13.9.0_vue@3.5.22_typescript@5.9.3_/node_modules/@vueuse/shared/index.mjs const isWorker = typeof WorkerGlobalScope !== "undefined" && globalThis instanceof WorkerGlobalScope; const noop = () => {}; function createFilterWrapper(filter, fn) { function wrapper(...args) { return new Promise((resolve, reject) => { Promise.resolve(filter(() => fn.apply(this, args), { fn, thisArg: this, args })).then(resolve).catch(reject); }); } return wrapper; } function throttleFilter(...args) { let lastExec = 0; let timer; let isLeading = true; let lastRejector = noop; let lastValue; let ms; let trailing; let leading; let rejectOnCancel; if (!isRef(args[0]) && typeof args[0] === "object") ({delay: ms, trailing = true, leading = true, rejectOnCancel = false} = args[0]); else [ms, trailing = true, leading = true, rejectOnCancel = false] = args; const clear = () => { if (timer) { clearTimeout(timer); timer = void 0; lastRejector(); lastRejector = noop; } }; const filter = (_invoke) => { const duration = toValue(ms); const elapsed = Date.now() - lastExec; const invoke = () => { return lastValue = _invoke(); }; clear(); if (duration <= 0) { lastExec = Date.now(); return invoke(); } if (elapsed > duration && (leading || !isLeading)) { lastExec = Date.now(); invoke(); } else if (trailing) lastValue = new Promise((resolve, reject) => { lastRejector = rejectOnCancel ? reject : resolve; timer = setTimeout(() => { lastExec = Date.now(); isLeading = true; resolve(invoke()); clear(); }, Math.max(0, duration - elapsed)); }); if (!leading && !timer) timer = setTimeout(() => isLeading = true, duration); isLeading = false; return lastValue; }; return filter; } function cacheStringFunction(fn) { const cache = /* @__PURE__ */ Object.create(null); return (str) => { return cache[str] || (cache[str] = fn(str)); }; } const hyphenateRE = /\B([A-Z])/g; const hyphenate = cacheStringFunction((str) => str.replace(hyphenateRE, "-$1").toLowerCase()); const camelizeRE = /-(\w)/g; const camelize = cacheStringFunction((str) => { return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : ""); }); /* @__NO_SIDE_EFFECTS__ */ function useThrottleFn(fn, ms = 200, trailing = false, leading = true, rejectOnCancel = false) { return createFilterWrapper(throttleFilter(ms, trailing, leading, rejectOnCancel), fn); } //#endregion //#region src/icons.ts const genIcon = (d) => { return h("svg", { viewBox: "0 0 1024 1024", xmlns: "http://www.w3.org/2000/svg", class: "fox-svg-icon", "aria-role": "button" }, [h("path", { fill: "currentColor", d })]); }; const ArrowLeft = () => { return genIcon("M609.408 149.376 277.76 489.6a32 32 0 0 0 0 44.672l331.648 340.352a29.12 29.12 0 0 0 41.728 0 30.592 30.592 0 0 0 0-42.752L339.264 511.936l311.872-319.872a30.592 30.592 0 0 0 0-42.688 29.12 29.12 0 0 0-41.728 0z"); }; const ArrowRight = () => { return genIcon("M340.864 149.312a30.592 30.592 0 0 0 0 42.752L652.736 512 340.864 831.872a30.592 30.592 0 0 0 0 42.752 29.12 29.12 0 0 0 41.728 0L714.24 534.336a32 32 0 0 0 0-44.672L382.592 149.376a29.12 29.12 0 0 0-41.728 0z"); }; const Close = () => { return genIcon("M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"); }; const Download = () => { return genIcon("M160 832h704a32 32 0 1 1 0 64H160a32 32 0 1 1 0-64zm384-253.696 236.288-236.352 45.248 45.248L508.8 704 192 387.2l45.248-45.248L480 584.704V128h64v450.304z"); }; const RotateLeft = () => { return genIcon("M289.088 296.704h92.992a32 32 0 0 1 0 64H232.96a32 32 0 0 1-32-32V179.712a32 32 0 0 1 64 0v50.56a384 384 0 0 1 643.84 282.88 384 384 0 0 1-383.936 384 384 384 0 0 1-384-384h64a320 320 0 1 0 640 0 320 320 0 0 0-555.712-216.448z"); }; const RotateRight = () => { return genIcon("M784.512 230.272v-50.56a32 32 0 1 1 64 0v149.056a32 32 0 0 1-32 32H667.52a32 32 0 1 1 0-64h92.992A320 320 0 1 0 524.8 833.152a320 320 0 0 0 320-320h64a384 384 0 0 1-384 384 384 384 0 0 1-384-384 384 384 0 0 1 643.712-282.88z"); }; const ZoomIn = () => { return genIcon("m795.904 750.72 124.992 124.928a32 32 0 0 1-45.248 45.248L750.656 795.904a416 416 0 1 1 45.248-45.248zM480 832a352 352 0 1 0 0-704 352 352 0 0 0 0 704zm-32-384v-96a32 32 0 0 1 64 0v96h96a32 32 0 0 1 0 64h-96v96a32 32 0 0 1-64 0v-96h-96a32 32 0 0 1 0-64h96z"); }; const ZoomOut = () => { return genIcon("m795.904 750.72 124.992 124.928a32 32 0 0 1-45.248 45.248L750.656 795.904a416 416 0 1 1 45.248-45.248zM480 832a352 352 0 1 0 0-704 352 352 0 0 0 0 704zM352 448h256a32 32 0 0 1 0 64H352a32 32 0 0 1 0-64z"); }; //#endregion //#region src/switch.vue?vue&type=script&setup=true&lang.ts const _hoisted_1$2 = { class: "fox-preview-switch" }; var switch_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "switch", emits: ["prev", "next"], setup(__props, { emit: __emit }) { const emit = __emit; const handlePrevClick = () => { emit("prev"); }; const handleNextClick = () => { emit("next"); }; return (_ctx, _cache) => { return openBlock(), createElementBlock("div", _hoisted_1$2, [createElementVNode("div", { class: "fox-preview-switch-item fox-preview-switch-item-left", onClick: handlePrevClick }, [createVNode(unref(ArrowLeft), { class: "fox-preview-switch-icon" })]), createElementVNode("div", { class: "fox-preview-switch-item", onClick: handleNextClick }, [createVNode(unref(ArrowRight), { class: "fox-preview-switch-icon" })])]); }; } }); //#endregion //#region src/switch.vue var switch_default = switch_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/toolbar.vue?vue&type=script&setup=true&lang.ts const _hoisted_1$1 = { class: "fox-preview-toolbar" }; var toolbar_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ __name: "toolbar", props: { scale: { type: Number, required: true, default: 1 }, index: { type: String, required: true, default: "1/1" }, layout: { type: String, required: true, default: "zoomOut, zoomIn, scale, position, rotateLeft, rotateRight, download" } }, emits: ["click"], setup(__props, { emit: __emit }) { const props = __props; const emit = __emit; const handleClick = (type$1) => { emit("click", type$1); }; const layouts = computed(() => props.layout.split(",").map((item) => item.trim())); return (_ctx, _cache) => { return openBlock(), createElementBlock("div", _hoisted_1$1, [ layouts.value.includes("zoomOut") ? (openBlock(), createBlock(unref(ZoomOut), { key: 0, role: "button", title: "缩小", class: "fox-preview-toolbar-item", style: normalizeStyle({ order: layouts.value.indexOf("zoomOut") }), onClick: _cache[0] || (_cache[0] = ($event) => handleClick("zoom-out")) }, null, 8, ["style"])) : createCommentVNode("v-if", true), layouts.value.includes("zoomIn") ? (openBlock(), createBlock(unref(ZoomIn), { key: 1, role: "button", title: "放大", class: "fox-preview-toolbar-item", style: normalizeStyle({ order: layouts.value.indexOf("zoomIn") }), onClick: _cache[1] || (_cache[1] = ($event) => handleClick("zoom-in")) }, null, 8, ["style"])) : createCommentVNode("v-if", true), layouts.value.includes("scale") ? (openBlock(), createElementBlock("div", { key: 2, role: "button", title: "缩放倍数", tabindex: "-1", class: "fox-preview-toolbar-item fox-preview-toolbar-scale", style: normalizeStyle({ order: layouts.value.indexOf("scale") }) }, toDisplayString(props.scale), 5)) : createCommentVNode("v-if", true), layouts.value.includes("position") ? (openBlock(), createElementBlock("div", { key: 3, role: "button", title: "图片位置", class: "fox-preview-toolbar-item fox-preview-toolbar-position", style: normalizeStyle({ order: layouts.value.indexOf("position") }) }, toDisplayString(props.index), 5)) : createCommentVNode("v-if", true), layouts.value.includes("rotateLeft") ? (openBlock(), createBlock(unref(RotateLeft), { key: 4, role: "button", title: "左旋转", class: "fox-preview-toolbar-item", style: normalizeStyle({ order: layouts.value.indexOf("rotateLeft") }), onClick: _cache[2] || (_cache[2] = ($event) => handleClick("contraRotate")) }, null, 8, ["style"])) : createCommentVNode("v-if", true), layouts.value.includes("rotateRight") ? (openBlock(), createBlock(unref(RotateRight), { key: 5, role: "button", title: "右旋转", class: "fox-preview-toolbar-item", style: normalizeStyle({ order: layouts.value.indexOf("rotateRight") }), onClick: _cache[3] || (_cache[3] = ($event) => handleClick("clockwiseRotation")) }, null, 8, ["style"])) : createCommentVNode("v-if", true), layouts.value.includes("download") ? (openBlock(), createBlock(unref(Download), { key: 6, role: "button", title: "下载/保存", class: "fox-preview-toolbar-item", style: normalizeStyle({ order: layouts.value.indexOf("download") }), onClick: _cache[4] || (_cache[4] = ($event) => handleClick("download")) }, null, 8, ["style"])) : createCommentVNode("v-if", true) ]); }; } }); //#endregion //#region src/toolbar.vue var toolbar_default = toolbar_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/utils.ts const downloadFile = (url, name) => { const a = document.createElement("a"); a.download = name; a.href = url; a.style.display = "none"; document.body.append(a); a.click(); const timer = setTimeout(() => { a.remove(); clearTimeout(timer); }, 10); }; /** * 获取滚动条的宽度 */ const getScrollWidth = () => { const scroll = document.createElement("div"); const scrollIn = document.createElement("div"); scroll.append(scrollIn); scroll.style.width = "100px"; scroll.style.height = "50px"; scroll.style.overflow = "scroll"; scroll.style.marginLeft = "-100000px"; document.body.append(scroll); const scrollInWidth = scrollIn.offsetWidth; const scrollWidth = scroll.offsetWidth; const tmp = setTimeout(() => { scroll.remove(); clearTimeout(tmp); }, 10); return scrollWidth - scrollInWidth; }; function type(value) { return Object.prototype.toString.call(value).replaceAll(/(^[[a-z]+ )([A-Za-z]+)(\])/g, "$2").toLowerCase(); } const prototype = Object.create(null); function isObject(val) { return type(val) === "object"; } function isFunction(val) { return type(val) === "function"; } function isArray(val) { return type(val) === "array"; } function isSymbol(val) { return type(val) === "symbol"; } function isFalse(val) { return [ 0, void 0, null ].includes(val); } prototype.isObject = isObject; prototype.isFunction = isFunction; prototype.isArray = isArray; prototype.isSymbol = isSymbol; prototype.isFalse = isFalse; type.prototype = prototype; var utils_default = type; //#endregion //#region src/index.vue?vue&type=script&setup=true&lang.ts const _hoisted_1 = ["onKeyup"]; const _hoisted_2 = ["src"]; var index_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({ name: "FoxPreviewImage", __name: "index", props: { modelValue: { type: Boolean, required: true, default: false }, src: { type: [String, Array], required: true, default: "" }, zIndex: { type: Number, required: false, default: 9e3 }, initialIndex: { type: Number, required: false, default: 0 }, appendTo: { type: null, required: false, default: "body" }, showToolbar: { type: Boolean, required: false, default: true }, enableTeleport: { type: Boolean, required: false, default: false }, layout: { type: String, required: false, default: "zoomOut, zoomIn, scale, position, rotateLeft, rotateRight, download" } }, emits: ["update:modelValue"], setup(__props, { emit: __emit }) { const props = __props; const emit = __emit; let bodyStyleCache = ""; onBeforeMount(() => { bodyStyleCache = document.body.style.cssText; }); const refEl = ref(null); const flag = ref(false); const status = ref(0); const active = props.src && props.src.length ? ref(props.initialIndex) : ref(0); const angle = ref(0); const scale = ref(1); const cacheX = ref(0); const cacheY = ref(0); const x = ref(0); const y = ref(0); const uri = ref([]); let startLocation = reactive({ x: 0, y: 0 }); const init = () => { flag.value = props.modelValue; }; const close = () => { flag.value = false; emit("update:modelValue", flag.value); }; const move = (e) => { if (status.value !== 1) return; const { x: mouseX, y: mouseY } = e; const mvX = mouseX - startLocation.x; const mvY = mouseY - startLocation.y; x.value = mvX + x.value - cacheX.value; y.value = mvY + y.value - cacheY.value; cacheX.value = mvX; cacheY.value = mvY; }; /** * 缩小 */ const zoomOut = () => { if (scale.value > .5) scale.value -= .1; }; /** * 放大 */ const enlarge = () => { if (scale.value < 2) scale.value += .1; }; const mousewheel = (ev) => { requestAnimationFrame(() => { if ((ev.wheelDelta || ev.detail * -40) > 0) enlarge(); else zoomOut(); }); }; const handleMouseMove = useThrottleFn(move, 10); const handleMousewheel = useThrottleFn(mousewheel, 10); const mouseup = () => { status.value = 0; cacheX.value = 0; cacheY.value = 0; }; const mousedown = (e) => { status.value = 1; startLocation = { x: e.x, y: e.y }; }; /** * 顺时针旋转 */ const clockwiseRotation = () => { angle.value += 90; }; /** * 逆时针旋转 */ const anticlockwiseRotation = () => { angle.value -= 90; }; /**下载图片 */ const downloadImage = () => { const cur = uri.value[active.value]; downloadFile(cur, cur.split("/").at(-1)); }; const initConf = () => { angle.value = 0; scale.value = 1; x.value = 0; y.value = 0; startLocation.x = 0; startLocation.y = 0; cacheX.value = 0; cacheY.value = 0; }; const prev = () => { const len = uri.value.length || 0; if (active.value > 0) active.value--; else active.value = len - 1; initConf(); }; const next = () => { const len = uri.value.length || 0; if (active.value < len - 1) active.value++; else active.value = 0; initConf(); }; const getCurrScale = computed(() => { return Number.parseFloat(scale.value.toFixed(1)); }); const getCurrIndex = computed(() => { return `${active.value + 1}/${uri.value.length}`; }); const handleToolsClick = (type$1) => { switch (type$1) { case "zoom-out": zoomOut(); break; case "zoom-in": enlarge(); break; case "contraRotate": anticlockwiseRotation(); break; case "clockwiseRotation": clockwiseRotation(); break; case "download": downloadImage(); break; } }; onMounted(() => { init(); }); const hasScrollbar = (el) => { if (el.scrollHeight > window.innerHeight) return true; return false; }; watch(() => props.modelValue, (val) => { flag.value = val; if (val) { if (refEl.value !== null) refEl.value.focus(); if (hasScrollbar(document.body)) { document.body.style.paddingRight = `${getScrollWidth()}px`; document.body.classList.add("fox-lock-window"); } } else { document.body.classList.remove("fox-lock-window"); if (bodyStyleCache) document.body.style.cssText = bodyStyleCache; else document.body.removeAttribute("style"); } }); watch(() => props.src, (val) => { const type$1 = utils_default(val); if (type$1 === "string") { active.value = 0; initConf(); uri.value = [val]; } else if (type$1 === "array") { if (props.initialIndex >= 0 && props.initialIndex < val.length) active.value = props.initialIndex; else active.value = 0; initConf(); uri.value = val; } }, { immediate: true }); return (_ctx, _cache) => { return openBlock(), createBlock(Teleport, { to: props.appendTo, disabled: props.enableTeleport === false }, [createVNode(Transition, null, { default: withCtx(() => [flag.value ? (openBlock(), createElementBlock("div", { key: 0, ref_key: "refEl", ref: refEl, role: "dialog", class: "fox-preview", style: normalizeStyle({ "z-index": props.zIndex }), tabindex: "1", onKeyup: withKeys(withModifiers(close, ["exact"]), ["esc"]) }, [ createElementVNode("div", { class: "fox-preview-canvas", onMousewheel: _cache[1] || (_cache[1] = (...args) => unref(handleMousewheel) && unref(handleMousewheel)(...args)), "on:DOMMouseScroll": _cache[2] || (_cache[2] = (...args) => unref(handleMousewheel) && unref(handleMousewheel)(...args)) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(uri.value, (item, i) => { return openBlock(), createElementBlock(Fragment, { key: i }, [unref(active) === i ? (openBlock(), createElementBlock("div", { key: 0, style: normalizeStyle([{ transform: `rotate(${angle.value}deg) scale(${scale.value}) translate(${x.value}px,${y.value}px)` }, { "display": "inline-block" }]), onMousemove: _cache[0] || (_cache[0] = (...args) => unref(handleMouseMove) && unref(handleMouseMove)(...args)), onMouseup: mouseup, onMousedown: mousedown }, [createElementVNode("img", { class: "fox-preview-image", src: item, alt: "被拖拽的图片", draggable: "false" }, null, 8, _hoisted_2)], 36)) : createCommentVNode("v-if", true)], 64); }), 128))], 32), createCommentVNode(" 关闭按钮 "), createElementVNode("div", { class: "fox-preview-close", onClick: close }, [createVNode(unref(Close))]), createCommentVNode(" 左右切换按钮 "), uri.value && uri.value.length > 1 ? (openBlock(), createBlock(switch_default, { key: 0, onPrev: prev, onNext: next })) : createCommentVNode("v-if", true), createCommentVNode(" 工具栏 "), __props.showToolbar ? (openBlock(), createBlock(toolbar_default, { key: 1, scale: getCurrScale.value, index: getCurrIndex.value, layout: props.layout, onClick: handleToolsClick }, null, 8, [ "scale", "index", "layout" ])) : createCommentVNode("v-if", true) ], 44, _hoisted_1)) : createCommentVNode("v-if", true)]), _: 1 })], 8, ["to", "disabled"]); }; } }); //#endregion //#region src/index.vue var src_default$1 = index_vue_vue_type_script_setup_true_lang_default; //#endregion //#region src/index.ts src_default$1.install = (app) => { app.component(src_default$1.name, src_default$1); }; var src_default = src_default$1; //#endregion export { src_default as default };