|
@@ -1,7 +1,24 @@
|
|
|
-<template>
|
|
|
+<template>
|
|
|
<Modal :visible="visible" :width="800" :closeButton="true" @update:visible="val => emit('update:visible', val)">
|
|
|
<div class="shot-wrap">
|
|
|
- <img v-if="imageUrl" :src="imageUrl" alt="截图作业" class="shot-img" @click="openPreview" />
|
|
|
+ <div v-if="imageUrl" class="image-container">
|
|
|
+ <!-- Loading 状态 -->
|
|
|
+ <div v-if="imageLoading" class="image-loading">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ <div class="loading-text">图片加载中...</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 图片 -->
|
|
|
+ <img
|
|
|
+ v-show="!imageLoading"
|
|
|
+ :src="imageUrl"
|
|
|
+ alt="截图作业"
|
|
|
+ class="shot-img"
|
|
|
+ @click="openPreview"
|
|
|
+ @load="onImageLoad"
|
|
|
+ @error="onImageError"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
<div v-else class="shot-empty">暂无图片</div>
|
|
|
</div>
|
|
|
</Modal>
|
|
@@ -24,15 +41,29 @@
|
|
|
@mouseup="onDragEnd"
|
|
|
@mouseleave="onDragEnd"
|
|
|
@dblclick.stop="toggleZoom">
|
|
|
- <img :src="imageUrl" alt="预览" class="image-preview__img"
|
|
|
- :style="imgStyle" draggable="false" />
|
|
|
+ <!-- 预览中的 Loading 状态 -->
|
|
|
+ <div v-if="previewImageLoading" class="preview-loading">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ <div class="loading-text">图片加载中...</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <img
|
|
|
+ v-show="!previewImageLoading"
|
|
|
+ :src="imageUrl"
|
|
|
+ alt="预览"
|
|
|
+ class="image-preview__img"
|
|
|
+ :style="imgStyle"
|
|
|
+ draggable="false"
|
|
|
+ @load="onPreviewImageLoad"
|
|
|
+ @error="onPreviewImageError"
|
|
|
+ />
|
|
|
</div>
|
|
|
</div>
|
|
|
</Teleport>
|
|
|
</template>
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
-import { computed, ref } from 'vue'
|
|
|
+import { computed, ref, watch } from 'vue'
|
|
|
import Modal from '@/components/Modal.vue'
|
|
|
|
|
|
const props = defineProps<{
|
|
@@ -61,6 +92,18 @@ const imageUrl = computed(() => {
|
|
|
}
|
|
|
})
|
|
|
|
|
|
+// Loading 状态
|
|
|
+const imageLoading = ref(false)
|
|
|
+const previewImageLoading = ref(false)
|
|
|
+
|
|
|
+// 监听图片URL变化,重置loading状态
|
|
|
+watch(imageUrl, (newUrl) => {
|
|
|
+ if (newUrl) {
|
|
|
+ imageLoading.value = true
|
|
|
+ previewImageLoading.value = true
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
const previewVisible = ref(false)
|
|
|
const scale = ref(1)
|
|
|
const rotate = ref(0)
|
|
@@ -78,10 +121,32 @@ const imgStyle = computed(() => {
|
|
|
}
|
|
|
})
|
|
|
|
|
|
+// 图片加载事件处理
|
|
|
+const onImageLoad = () => {
|
|
|
+ imageLoading.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const onImageError = () => {
|
|
|
+ imageLoading.value = false
|
|
|
+ console.error('图片加载失败')
|
|
|
+}
|
|
|
+
|
|
|
+const onPreviewImageLoad = () => {
|
|
|
+ previewImageLoading.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const onPreviewImageError = () => {
|
|
|
+ previewImageLoading.value = false
|
|
|
+ console.error('预览图片加载失败')
|
|
|
+}
|
|
|
+
|
|
|
const openPreview = () => {
|
|
|
+ if (imageLoading.value) return // 如果主图片还在加载,不允许打开预览
|
|
|
previewVisible.value = true
|
|
|
+ previewImageLoading.value = true // 预览时重新显示loading
|
|
|
nextTickFit()
|
|
|
}
|
|
|
+
|
|
|
const closePreview = () => {
|
|
|
previewVisible.value = false
|
|
|
}
|
|
@@ -154,6 +219,15 @@ const onDragEnd = () => {
|
|
|
max-height: 70vh;
|
|
|
overflow: auto;
|
|
|
}
|
|
|
+
|
|
|
+.image-container {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ min-height: 200px;
|
|
|
+}
|
|
|
+
|
|
|
.shot-img {
|
|
|
display: block;
|
|
|
max-width: 100%;
|
|
@@ -161,9 +235,42 @@ const onDragEnd = () => {
|
|
|
border-radius: 6px;
|
|
|
cursor: zoom-in;
|
|
|
}
|
|
|
+
|
|
|
.shot-empty {
|
|
|
color: #999;
|
|
|
font-size: 14px;
|
|
|
+ text-align: center;
|
|
|
+ padding: 40px 20px;
|
|
|
+}
|
|
|
+
|
|
|
+/* Loading 样式 */
|
|
|
+.image-loading, .preview-loading {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 12px;
|
|
|
+ color: #666;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-spinner {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ border: 3px solid #f3f3f3;
|
|
|
+ border-top: 3px solid #3a8bff;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-text {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
}
|
|
|
|
|
|
.image-preview {
|
|
@@ -174,6 +281,7 @@ const onDragEnd = () => {
|
|
|
flex-direction: column;
|
|
|
z-index: 6000;
|
|
|
}
|
|
|
+
|
|
|
.image-preview__toolbar {
|
|
|
display: flex;
|
|
|
gap: 8px;
|
|
@@ -181,6 +289,7 @@ const onDragEnd = () => {
|
|
|
justify-content: center;
|
|
|
z-index: 9999;
|
|
|
}
|
|
|
+
|
|
|
.image-preview__toolbar button {
|
|
|
padding: 8px 12px;
|
|
|
border: none;
|
|
@@ -191,27 +300,36 @@ const onDragEnd = () => {
|
|
|
transition: transform .15s ease, box-shadow .2s ease, background .2s ease;
|
|
|
box-shadow: 0 2px 8px rgba(47,128,237,.3);
|
|
|
}
|
|
|
+
|
|
|
.image-preview__toolbar button:hover {
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 6px 16px rgba(47,128,237,.35);
|
|
|
}
|
|
|
+
|
|
|
.image-preview__toolbar button:active {
|
|
|
transform: translateY(0);
|
|
|
box-shadow: 0 2px 8px rgba(47,128,237,.28);
|
|
|
background: linear-gradient(180deg, #2f80ed 0%, #1b6dde 100%);
|
|
|
}
|
|
|
+
|
|
|
.image-preview__toolbar button:focus-visible {
|
|
|
outline: 2px solid rgba(47,128,237,.6);
|
|
|
outline-offset: 2px;
|
|
|
}
|
|
|
+
|
|
|
.image-preview__stage {
|
|
|
flex: 1;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
cursor: grab;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.image-preview__stage:active {
|
|
|
+ cursor: grabbing;
|
|
|
}
|
|
|
-.image-preview__stage:active { cursor: grabbing; }
|
|
|
+
|
|
|
.image-preview__img {
|
|
|
max-width: 92vw;
|
|
|
max-height: 92vh;
|
|
@@ -220,4 +338,24 @@ const onDragEnd = () => {
|
|
|
user-select: none;
|
|
|
will-change: transform;
|
|
|
}
|
|
|
-</style>
|
|
|
+
|
|
|
+/* 预览中的 loading 样式 */
|
|
|
+.preview-loading {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-loading .loading-spinner {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border-width: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-loading .loading-text {
|
|
|
+ color: #fff;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+</style>
|