fox-preview-image
Version:
一个支持 vue3 的预览图片组件
607 lines (596 loc) • 19.5 kB
JavaScript
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 };