| | |
| | | <template> |
| | | <div v-if="visible" class="custom-image-viewer"> |
| | | <div v-if="visible" class="custom-image-viewer" @wheel.prevent="handleWheel"> |
| | | <!-- 遮罩层 --> |
| | | <div class="el-image-viewer__mask" @click.self="handleMaskClick"></div> |
| | | |
| | | <!-- 图片预览主体 --> |
| | | <!-- 关闭 --> |
| | | <div class="el-image-viewer__btn el-image-viewer__close" @click="close"> |
| | | <i class="el-icon-close"></i> |
| | | </div> |
| | | |
| | | <!-- 上一张按钮 --> |
| | | <!-- 工具栏 --> |
| | | <div class="viewer-toolbar"> |
| | | <div class="toolbar-btn" title="放大" @click="zoomIn"> |
| | | <i class="el-icon-zoom-in"></i> |
| | | </div> |
| | | <div class="toolbar-btn" title="缩小" @click="zoomOut"> |
| | | <i class="el-icon-zoom-out"></i> |
| | | </div> |
| | | <div class="toolbar-btn" title="顺时针旋转" @click="rotateRight"> |
| | | <i class="el-icon-refresh-right"></i> |
| | | </div> |
| | | <div class="toolbar-btn" title="逆时针旋转" @click="rotateLeft"> |
| | | <i class="el-icon-refresh-left"></i> |
| | | </div> |
| | | <div class="toolbar-btn" title="还原" @click="resetTransform"> |
| | | <i class="el-icon-s-home"></i> |
| | | </div> |
| | | <span class="toolbar-info">{{ currentIndex + 1 }} / {{ urlList.length }}</span> |
| | | </div> |
| | | |
| | | <!-- 上一张 --> |
| | | <div |
| | | class="el-image-viewer__btn el-image-viewer__prev" |
| | | :class="{ 'is-disabled': isFirst }" |
| | |
| | | <i class="el-icon-arrow-left"></i> |
| | | </div> |
| | | |
| | | <!-- 下一张按钮 --> |
| | | <!-- 下一张 --> |
| | | <div |
| | | class="el-image-viewer__btn el-image-viewer__next" |
| | | :class="{ 'is-disabled': isLast }" |
| | |
| | | </div> |
| | | |
| | | <!-- 图片展示 --> |
| | | <div class="el-image-viewer__canvas"> |
| | | <div |
| | | class="el-image-viewer__canvas" |
| | | @mousedown="handleMouseDown" |
| | | @mousemove="handleMouseMove" |
| | | @mouseup="handleMouseUp" |
| | | @mouseleave="handleMouseUp" |
| | | > |
| | | <img |
| | | v-if="currentUrl" |
| | | :src="currentUrl" |
| | | :key="currentIndex" |
| | | @error="handleError" |
| | | class="el-image-viewer__img" |
| | | :style="imgStyle" |
| | | draggable="false" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- 自定义提示信息 --> |
| | | <!-- 提示信息 --> |
| | | <div v-if="showTip" class="custom-viewer-tip"> |
| | | {{ tipMessage }} |
| | | </div> |
| | |
| | | export default { |
| | | name: 'CustomImageViewer', |
| | | props: { |
| | | urlList: { |
| | | type: Array, |
| | | default: () => [] |
| | | }, |
| | | initialIndex: { |
| | | type: Number, |
| | | default: 0 |
| | | }, |
| | | visible: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | urlList: { type: Array, default: () => [] }, |
| | | initialIndex: { type: Number, default: 0 }, |
| | | visible: { type: Boolean, default: false } |
| | | }, |
| | | data() { |
| | | return { |
| | | currentIndex: this.initialIndex, |
| | | showTip: false, |
| | | tipMessage: '' |
| | | tipMessage: '', |
| | | scale: 1, |
| | | rotate: 0, |
| | | // 拖拽 |
| | | dragging: false, |
| | | dragStart: { x: 0, y: 0 }, |
| | | offsetX: 0, |
| | | offsetY: 0, |
| | | }; |
| | | }, |
| | | computed: { |
| | |
| | | }, |
| | | isLast() { |
| | | return this.currentIndex === this.urlList.length - 1; |
| | | } |
| | | }, |
| | | imgStyle() { |
| | | return { |
| | | transform: `translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.scale}) rotate(${this.rotate}deg)`, |
| | | transition: this.dragging ? 'none' : 'transform 0.3s', |
| | | cursor: this.scale > 1 ? (this.dragging ? 'grabbing' : 'grab') : 'default', |
| | | }; |
| | | }, |
| | | }, |
| | | watch: { |
| | | initialIndex(newVal) { |
| | | this.currentIndex = newVal; |
| | | this.resetTransform(); |
| | | }, |
| | | visible(newVal) { |
| | | if (!newVal) { |
| | | this.showTip = false; |
| | | this.tipMessage = ''; |
| | | this.removeKeyListener(); |
| | | } else { |
| | | this.$nextTick(() => { |
| | | this.addKeyListener(); |
| | | }); |
| | | this.$nextTick(() => { this.addKeyListener(); }); |
| | | } |
| | | } |
| | | }, |
| | | }, |
| | | methods: { |
| | | close() { |
| | | this.$emit('update:visible', false); |
| | | this.$emit('close'); |
| | | }, |
| | | handleMaskClick() { |
| | | this.close(); |
| | | }, |
| | | handleMaskClick() { this.close(); }, |
| | | |
| | | // 切换图片 |
| | | goPrev() { |
| | | if (this.isFirst) { |
| | | this.showTipMessage('这已经是第一张了'); |
| | | return; |
| | | } |
| | | if (this.isFirst) { this.showTipMessage('已是第一张'); return; } |
| | | this.currentIndex--; |
| | | this.hideTip(); |
| | | this.resetTransform(); |
| | | }, |
| | | goNext() { |
| | | if (this.isLast) { |
| | | this.showTipMessage('这已经是最后一张了'); |
| | | return; |
| | | } |
| | | if (this.isLast) { this.showTipMessage('已是最后一张'); return; } |
| | | this.currentIndex++; |
| | | this.hideTip(); |
| | | }, |
| | | showTipMessage(message) { |
| | | this.tipMessage = message; |
| | | this.showTip = true; |
| | | setTimeout(() => { |
| | | this.hideTip(); |
| | | }, 2000); |
| | | }, |
| | | hideTip() { |
| | | this.showTip = false; |
| | | this.tipMessage = ''; |
| | | }, |
| | | handleError() { |
| | | console.error(`图片加载失败: ${this.currentUrl}`); |
| | | this.resetTransform(); |
| | | }, |
| | | |
| | | // 键盘事件处理 |
| | | handleKeydown(event) { |
| | | switch(event.key) { |
| | | case 'ArrowLeft': |
| | | event.preventDefault(); // 阻止默认行为 |
| | | this.goPrev(); |
| | | break; |
| | | case 'ArrowRight': |
| | | event.preventDefault(); |
| | | this.goNext(); |
| | | break; |
| | | case 'Escape': |
| | | event.preventDefault(); |
| | | this.close(); |
| | | break; |
| | | // 缩放 |
| | | zoomIn() { |
| | | this.scale = Math.min(this.scale + 0.25, 5); |
| | | if (this.scale <= 1) { this.offsetX = 0; this.offsetY = 0; } |
| | | }, |
| | | zoomOut() { |
| | | this.scale = Math.max(this.scale - 0.25, 0.25); |
| | | if (this.scale <= 1) { this.offsetX = 0; this.offsetY = 0; } |
| | | }, |
| | | handleWheel(e) { |
| | | if (e.deltaY < 0) this.zoomIn(); |
| | | else this.zoomOut(); |
| | | }, |
| | | |
| | | // 旋转 |
| | | rotateRight() { this.rotate = (this.rotate + 90) % 360; }, |
| | | rotateLeft() { this.rotate = (this.rotate - 90) % 360; }, |
| | | |
| | | // 还原 |
| | | resetTransform() { |
| | | this.scale = 1; |
| | | this.rotate = 0; |
| | | this.offsetX = 0; |
| | | this.offsetY = 0; |
| | | }, |
| | | |
| | | // 拖拽(仅在放大时启用) |
| | | handleMouseDown(e) { |
| | | if (this.scale <= 1) return; |
| | | this.dragging = true; |
| | | this.dragStart = { x: e.clientX - this.offsetX, y: e.clientY - this.offsetY }; |
| | | }, |
| | | handleMouseMove(e) { |
| | | if (!this.dragging) return; |
| | | this.offsetX = e.clientX - this.dragStart.x; |
| | | this.offsetY = e.clientY - this.dragStart.y; |
| | | }, |
| | | handleMouseUp() { |
| | | this.dragging = false; |
| | | }, |
| | | |
| | | showTipMessage(msg) { |
| | | this.tipMessage = msg; |
| | | this.showTip = true; |
| | | setTimeout(() => { this.hideTip(); }, 2000); |
| | | }, |
| | | hideTip() { this.showTip = false; this.tipMessage = ''; }, |
| | | handleError() { console.error('图片加载失败:', this.currentUrl); }, |
| | | |
| | | // 键盘 |
| | | handleKeydown(e) { |
| | | switch(e.key) { |
| | | case 'ArrowLeft': e.preventDefault(); this.goPrev(); break; |
| | | case 'ArrowRight': e.preventDefault(); this.goNext(); break; |
| | | case 'Escape': e.preventDefault(); this.close(); break; |
| | | case '+': case '=': e.preventDefault(); this.zoomIn(); break; |
| | | case '-': e.preventDefault(); this.zoomOut(); break; |
| | | case '0': e.preventDefault(); this.resetTransform(); break; |
| | | } |
| | | }, |
| | | addKeyListener() { |
| | | document.addEventListener('keydown', this.handleKeydown); |
| | | addKeyListener() { document.addEventListener('keydown', this.handleKeydown); }, |
| | | removeKeyListener() { document.removeEventListener('keydown', this.handleKeydown); }, |
| | | }, |
| | | removeKeyListener() { |
| | | document.removeEventListener('keydown', this.handleKeydown); |
| | | } |
| | | }, |
| | | mounted() { |
| | | if (this.visible) { |
| | | this.addKeyListener(); |
| | | } |
| | | }, |
| | | beforeUnmount() { |
| | | this.removeKeyListener(); |
| | | } |
| | | mounted() { if (this.visible) this.addKeyListener(); }, |
| | | beforeUnmount() { this.removeKeyListener(); }, |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .custom-image-viewer { |
| | | position: fixed; |
| | | top: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | left: 0; |
| | | z-index: 2000; /* 确保 z-index 足够高 */ |
| | | position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 2000; |
| | | } |
| | | |
| | | .el-image-viewer__mask { |
| | | position: absolute; |
| | | width: 100%; |
| | | height: 100%; |
| | | position: absolute; width: 100%; height: 100%; |
| | | background-color: rgba(0, 0, 0, 0.5); |
| | | } |
| | | |
| | | .el-image-viewer__btn { |
| | | position: absolute; |
| | | z-index: 1; |
| | | color: #fff; |
| | | background-color: rgba(0, 0, 0, 0.3); |
| | | border-radius: 50%; |
| | | width: 40px; |
| | | height: 40px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | cursor: pointer; |
| | | user-select: none; |
| | | position: absolute; z-index: 1; color: #fff; |
| | | background-color: rgba(0, 0, 0, 0.3); border-radius: 50%; |
| | | width: 40px; height: 40px; display: flex; |
| | | align-items: center; justify-content: center; cursor: pointer; user-select: none; |
| | | } |
| | | .el-image-viewer__btn.is-disabled { cursor: not-allowed; opacity: 0.5; } |
| | | .el-image-viewer__close { top: 20px; right: 20px; } |
| | | .el-image-viewer__prev { left: 20px; top: 50%; transform: translateY(-50%); } |
| | | .el-image-viewer__next { right: 20px; top: 50%; transform: translateY(-50%); } |
| | | |
| | | .el-image-viewer__btn.is-disabled { |
| | | cursor: not-allowed; |
| | | opacity: 0.5; |
| | | .viewer-toolbar { |
| | | position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); |
| | | z-index: 1; display: flex; align-items: center; gap: 8px; |
| | | padding: 8px 16px; background: rgba(0, 0, 0, 0.6); border-radius: 24px; |
| | | } |
| | | |
| | | .el-image-viewer__close { |
| | | top: 20px; |
| | | right: 20px; |
| | | .toolbar-btn { |
| | | width: 36px; height: 36px; border-radius: 50%; display: flex; |
| | | align-items: center; justify-content: center; cursor: pointer; |
| | | color: #fff; font-size: 18px; transition: background 0.2s; |
| | | } |
| | | |
| | | .el-image-viewer__prev { |
| | | left: 20px; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | } |
| | | |
| | | .el-image-viewer__next { |
| | | right: 20px; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | } |
| | | .toolbar-btn:hover { background: rgba(255, 255, 255, 0.2); } |
| | | .toolbar-info { color: rgba(255, 255, 255, 0.7); font-size: 13px; margin-left: 8px; white-space: nowrap; } |
| | | |
| | | .el-image-viewer__canvas { |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | height: 100%; |
| | | display: flex; justify-content: center; align-items: center; |
| | | height: 100%; overflow: hidden; |
| | | } |
| | | |
| | | .el-image-viewer__img { |
| | | max-width: 90%; |
| | | max-height: 90%; |
| | | object-fit: contain; |
| | | max-width: 90%; max-height: 90%; |
| | | object-fit: contain; user-select: none; -webkit-user-drag: none; |
| | | } |
| | | |
| | | .custom-viewer-tip { |
| | | position: absolute; |
| | | bottom: 50px; |
| | | left: 50%; |
| | | transform: translateX(-50%); |
| | | background-color: rgba(0, 0, 0, 0.7); |
| | | color: #fff; |
| | | padding: 10px 20px; |
| | | border-radius: 4px; |
| | | z-index: 2001; |
| | | position: absolute; bottom: 100px; left: 50%; transform: translateX(-50%); |
| | | background-color: rgba(0, 0, 0, 0.7); color: #fff; |
| | | padding: 10px 20px; border-radius: 4px; z-index: 2001; |
| | | } |
| | | </style> |