lsc 4 gün önce
ebeveyn
işleme
c71d5b39dd

+ 1 - 0
src/components/Modal.vue

@@ -116,6 +116,7 @@ const onClickMask = () => {
   top: 16px;
   right: 16px;
   cursor: pointer;
+  z-index: 999;
 }
 
 .modal-fade-enter-active {

+ 145 - 7
src/views/Student/components/ShotWorkModal.vue

@@ -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>