|
@@ -0,0 +1,345 @@
|
|
|
+<template>
|
|
|
+ <div
|
|
|
+ v-if="visible"
|
|
|
+ ref="imageListPreview"
|
|
|
+ v-customDialogResizeImg="resizeKey"
|
|
|
+ class="preview-custom-dialog"
|
|
|
+ :class="[resizeKey]"
|
|
|
+ >
|
|
|
+ <div class="preview-head">
|
|
|
+ <span>查看全卷</span>
|
|
|
+ <div class="head-btn-box flex justify-center items-center" @click="closeDialog">
|
|
|
+ <el-icon><close /></el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="preview-body">
|
|
|
+ <div class="control-box">
|
|
|
+ <el-icon :size="30" color="#eee" class="zoom-icon" @click="add"><zoom-in /></el-icon>
|
|
|
+ <el-icon :size="30" color="#eee" class="zoom-icon" @click="minus"><zoom-out /></el-icon>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Tab导航 -->
|
|
|
+ <div class="tab-navigation">
|
|
|
+ <div class="tab-container">
|
|
|
+ <div
|
|
|
+ v-for="(image, index) in imageList"
|
|
|
+ :key="index"
|
|
|
+ class="tab-item"
|
|
|
+ :class="{ active: currentIndex === index }"
|
|
|
+ @click="switchImage(index)"
|
|
|
+ >
|
|
|
+ <span>第{{ index + 1 }}页</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 当前图片显示 -->
|
|
|
+ <div class="image-container">
|
|
|
+ <img
|
|
|
+ v-if="currentImageUrl"
|
|
|
+ v-show="!!currentImageUrl"
|
|
|
+ class="current-img small-img"
|
|
|
+ :src="currentImageUrl"
|
|
|
+ alt=""
|
|
|
+ />
|
|
|
+ <div v-else class="no-image">
|
|
|
+ <span>暂无图片</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts" name="ImageListPreview">
|
|
|
+import useVModel from '@/hooks/useVModel'
|
|
|
+import { ref, watch, computed } from 'vue'
|
|
|
+import { Close, ZoomIn, ZoomOut } from '@element-plus/icons-vue'
|
|
|
+import { ElIcon, ElButton } from 'element-plus'
|
|
|
+import { localKeyMap } from '@/directives/customDialogResizeImg'
|
|
|
+
|
|
|
+interface ImageItem {
|
|
|
+ url: string
|
|
|
+ title?: string
|
|
|
+}
|
|
|
+
|
|
|
+const imageListPreview = ref()
|
|
|
+const currentIndex = ref(0)
|
|
|
+
|
|
|
+const add = () => {
|
|
|
+ const w = imageListPreview.value?.clientWidth
|
|
|
+ if (w > window.innerWidth) return
|
|
|
+ if (w) {
|
|
|
+ imageListPreview.value.style.width =
|
|
|
+ (Math.floor(w * 1.1) > window.innerWidth ? window.innerWidth : Math.floor(w * 1.1)) + 'px'
|
|
|
+ let l = parseFloat(imageListPreview.value?.style?.left)
|
|
|
+ l && (imageListPreview.value.style.left = (l - Math.floor(w * 0.05) < 0 ? 0 : l - Math.floor(w * 0.05)) + 'px')
|
|
|
+ localStorage.setItem(
|
|
|
+ localKeyMap.positions[props.resizeKey],
|
|
|
+ JSON.stringify({
|
|
|
+ left: l + 'px',
|
|
|
+ top: imageListPreview.value?.style?.top,
|
|
|
+ })
|
|
|
+ )
|
|
|
+ localStorage.setItem(
|
|
|
+ localKeyMap.resize[props.resizeKey],
|
|
|
+ JSON.stringify({
|
|
|
+ width: imageListPreview.value.style.width || 'auto',
|
|
|
+ height: imageListPreview.value?.style?.height || 'auto',
|
|
|
+ })
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const minus = () => {
|
|
|
+ const w = imageListPreview.value?.clientWidth
|
|
|
+ if (w <= 290) return
|
|
|
+ if (w) {
|
|
|
+ imageListPreview.value.style.width = (Math.floor(w * 0.9) < 290 ? 290 : Math.floor(w * 0.9)) + 'px'
|
|
|
+ let l = parseFloat(imageListPreview.value?.style?.left)
|
|
|
+ l && (imageListPreview.value.style.left = l + Math.floor(w * 0.05) + 'px')
|
|
|
+ localStorage.setItem(
|
|
|
+ localKeyMap.positions[props.resizeKey],
|
|
|
+ JSON.stringify({
|
|
|
+ left: l + 'px',
|
|
|
+ top: imageListPreview.value?.style?.top,
|
|
|
+ })
|
|
|
+ )
|
|
|
+ localStorage.setItem(
|
|
|
+ localKeyMap.resize[props.resizeKey],
|
|
|
+ JSON.stringify({
|
|
|
+ width: imageListPreview.value.style.width || 'auto',
|
|
|
+ height: imageListPreview.value?.style?.height || 'auto',
|
|
|
+ })
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const props = withDefaults(
|
|
|
+ defineProps<{
|
|
|
+ modelValue: boolean
|
|
|
+ imageList?: (string | ImageItem)[]
|
|
|
+ resizeKey?: string
|
|
|
+ defaultIndex?: number
|
|
|
+ }>(),
|
|
|
+ {
|
|
|
+ modelValue: false,
|
|
|
+ imageList: () => [],
|
|
|
+ resizeKey: 'can-resize-normal-img',
|
|
|
+ defaultIndex: 0,
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+const emit = defineEmits(['update:modelValue', 'change'])
|
|
|
+
|
|
|
+const visible = useVModel(props)
|
|
|
+
|
|
|
+// 标准化图片列表
|
|
|
+const imageList = computed(() => {
|
|
|
+ return props.imageList.map((item, index) => {
|
|
|
+ if (typeof item === 'string') {
|
|
|
+ return { url: item, title: `第${index + 1}页` }
|
|
|
+ }
|
|
|
+ return { ...item, title: item.title || `第${index + 1}页` }
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+// 当前图片URL
|
|
|
+const currentImageUrl = computed(() => {
|
|
|
+ return imageList.value[currentIndex.value]?.url || ''
|
|
|
+})
|
|
|
+
|
|
|
+// 切换图片
|
|
|
+const switchImage = (index: number) => {
|
|
|
+ if (index >= 0 && index < imageList.value.length) {
|
|
|
+ currentIndex.value = index
|
|
|
+ emit('change', index, imageList.value[index])
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 上一页
|
|
|
+const prevImage = () => {
|
|
|
+ if (currentIndex.value > 0) {
|
|
|
+ switchImage(currentIndex.value - 1)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 下一页
|
|
|
+const nextImage = () => {
|
|
|
+ if (currentIndex.value < imageList.value.length - 1) {
|
|
|
+ switchImage(currentIndex.value + 1)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 关闭对话框
|
|
|
+const closeDialog = () => {
|
|
|
+ visible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+// 监听默认索引变化
|
|
|
+watch(
|
|
|
+ () => props.defaultIndex,
|
|
|
+ (newIndex) => {
|
|
|
+ if (newIndex >= 0 && newIndex < imageList.value.length) {
|
|
|
+ currentIndex.value = newIndex
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+)
|
|
|
+
|
|
|
+// 监听图片列表变化
|
|
|
+watch(
|
|
|
+ () => props.imageList,
|
|
|
+ () => {
|
|
|
+ if (currentIndex.value >= imageList.value.length) {
|
|
|
+ currentIndex.value = Math.max(0, imageList.value.length - 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+// 监听可见性变化
|
|
|
+watch(
|
|
|
+ () => visible.value,
|
|
|
+ (newVisible) => {
|
|
|
+ if (newVisible) {
|
|
|
+ currentIndex.value = props.defaultIndex
|
|
|
+ }
|
|
|
+ }
|
|
|
+)
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.preview-custom-dialog {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ position: fixed;
|
|
|
+ z-index: 500;
|
|
|
+ border-radius: 6px;
|
|
|
+ box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.04), 0px 8px 20px rgba(0, 0, 0, 0.08);
|
|
|
+ max-width: 100vw;
|
|
|
+ max-height: 100vh;
|
|
|
+ overflow: auto;
|
|
|
+
|
|
|
+ .preview-head {
|
|
|
+ background-color: #f8f8f8;
|
|
|
+ border-radius: 6px 6px 0 0;
|
|
|
+ color: #333;
|
|
|
+ font-size: 14px;
|
|
|
+ height: 44px;
|
|
|
+ line-height: 44px;
|
|
|
+ padding: 0 10px;
|
|
|
+ position: relative;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ .head-btn-box {
|
|
|
+ position: absolute;
|
|
|
+ right: 0;
|
|
|
+ top: 0;
|
|
|
+ width: 44px;
|
|
|
+ height: 44px;
|
|
|
+ z-index: 1;
|
|
|
+ cursor: pointer;
|
|
|
+ &:hover {
|
|
|
+ :deep(i) {
|
|
|
+ color: $color--primary;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .preview-body {
|
|
|
+ flex: 1;
|
|
|
+ padding: 2px;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ .control-box {
|
|
|
+ width: 42px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .control-box {
|
|
|
+ transition: all 0.2s;
|
|
|
+ overflow: hidden;
|
|
|
+ width: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: space-around;
|
|
|
+ align-items: center;
|
|
|
+ position: absolute;
|
|
|
+ right: 10px;
|
|
|
+ top: 65px;
|
|
|
+ z-index: 10;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
+ padding: 8px 0;
|
|
|
+ height: 100px;
|
|
|
+
|
|
|
+ .zoom-icon {
|
|
|
+ cursor: pointer;
|
|
|
+ &:hover {
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .tab-navigation {
|
|
|
+ flex-shrink: 0;
|
|
|
+ padding: 10px;
|
|
|
+ border-bottom: 1px solid #e4e7ed;
|
|
|
+
|
|
|
+ .tab-container {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ max-height: 120px;
|
|
|
+ overflow-y: auto;
|
|
|
+
|
|
|
+ .tab-item {
|
|
|
+ padding: 6px 12px;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #606266;
|
|
|
+ background-color: #fff;
|
|
|
+ transition: all 0.3s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: $color--primary;
|
|
|
+ color: $color--primary;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ background-color: $color--primary;
|
|
|
+ border-color: $color--primary;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .image-container {
|
|
|
+ flex: 1;
|
|
|
+ overflow: auto;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background-color: #f5f5f5;
|
|
|
+
|
|
|
+ .current-img {
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
+ }
|
|
|
+
|
|
|
+ .no-image {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|