Browse Source

feat(创建课程弹窗): 添加PPTX文件解析状态弹窗和关闭处理

添加解析状态弹窗显示文件解析进度,实现关闭按钮的父窗口回退功能,并优化文件上传处理逻辑
lsc 1 day ago
parent
commit
e717f0cb6b
2 changed files with 601 additions and 382 deletions
  1. 261 90
      src/components/CreateCourseDialog.vue
  2. 340 292
      src/hooks/useImport.ts

+ 261 - 90
src/components/CreateCourseDialog.vue

@@ -1,10 +1,10 @@
 <template>
   <div class="create-course-dialog">
     <div class="dialog-header">
-      <button class="close-btn" @click="$emit('close')">
+      <button class="close-btn" @click="handleClose">
         <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-          <line x1="18" y1="6" x2="6" y2="18"/>
-          <line x1="6" y1="6" x2="18" y2="18"/>
+          <line x1="18" y1="6" x2="6" y2="18" />
+          <line x1="6" y1="6" x2="18" y2="18" />
         </svg>
       </button>
     </div>
@@ -24,10 +24,8 @@
           <p>AI自动生成完整教学内容</p>
           <div class="coming-soon">待上线</div>
         </div>
-        <FileInput accept="application/vnd.openxmlformats-officedocument.presentationml.presentation" @change="files => {
-          importPPTXFile(files)
-          $emit('close')
-        }">
+        <FileInput accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
+          @change="handleFileUpload">
           <div class="option-card">
             <div class="option-icon">
               <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -71,24 +69,96 @@
         </div>
       </div>
     </div>
+
+    <!-- 解析状态弹窗 -->
+    <div v-if="exporting" class="parsing-modal">
+      <div class="parsing-content">
+        <div class="loading-spinner" v-if="exporting"></div>
+        <div class="success-icon" v-if="!exporting">
+          <svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 1">
+              <path id="Vector" d="M5.41675 14.084L9.75008 18.4173L20.5834 7.58398" stroke="#FF9300"
+                stroke-width="2.16667" stroke-linecap="round" stroke-linejoin="round" />
+            </g>
+          </svg>
+
+        </div>
+        <h3>{{ exporting ? '解析中...' : '导出完成' }}</h3>
+        <p v-if="exporting">正在解析 {{ currentFileName }}</p>
+        <p v-if="!exporting">解析完成,已生成课件</p>
+        <button class="close-btn2" @click="handleParsingClose">
+          {{ exporting ? '关闭' : '完成' }}
+        </button>
+      </div>
+    </div>
   </div>
 </template>
 
 <script lang="ts" setup>
+import { ref } from 'vue'
 import useImport from '@/hooks/useImport'
 import FileInput from '@/components/FileInput.vue'
+import message from '@/utils/message'
 
 const emit = defineEmits<{
   (e: 'close'): void
   (e: 'select', option: string): void
 }>()
 
-const { importPPTXFile } = useImport()
+const { importPPTXFile, exporting } = useImport()
+const currentFileName = ref('')
+const parsingStatus = ref<'parsing' | 'success'>('parsing')
+const parsingAbortController = ref<AbortController | null>(null)
 
 const handleOptionClick = (option: string) => {
   emit('select', option)
   emit('close')
 }
+
+const handleClose = () => {
+  interface ParentWindowWithToolList extends Window {
+    goBack?: () => void;
+  }
+  const parentWindow = window.parent as ParentWindowWithToolList
+  parentWindow?.goBack?.()
+}
+
+const handleFileUpload = async (files: FileList) => {
+  if (!files || files.length === 0) return
+
+  const file = files[0]
+  currentFileName.value = file.name
+
+  try {
+    // 创建AbortController用于取消操作
+    parsingAbortController.value = new AbortController()
+    const signal = parsingAbortController.value.signal
+
+    // 调用importPPTXFile并传入signal
+    await importPPTXFile(files, { signal, onclose: () => emit('close') })
+  }
+  catch (error) {
+    if (error instanceof DOMException && error.name === 'AbortError') {
+      console.log('文件解析已取消')
+    }
+    else {
+      console.error('文件解析失败:', error)
+      message.error('文件解析失败,请重试')
+    }
+  }
+}
+
+const handleParsingClose = () => {
+  if (exporting.value && parsingAbortController.value) {
+    parsingAbortController.value.abort()
+    exporting.value = false
+    parsingAbortController.value = null
+    // message.info('解析已取消')
+  }
+  else if (!exporting.value) {
+    emit('close')
+  }
+}
 </script>
 
 <style lang="scss" scoped>
@@ -150,104 +220,205 @@ const handleOptionClick = (option: string) => {
       gap: 20px;
 
       .option-card {
-          background: #fafbfc;
-          border: 1px solid #E5E7EB;
-          border-radius: 12px;
-          padding: 24px;
-          text-align: center;
-          cursor: pointer;
-          transition: all 0.3s;
-          position: relative;
+        background: #fafbfc;
+        border: 1px solid #E5E7EB;
+        border-radius: 12px;
+        padding: 24px;
+        text-align: center;
+        cursor: pointer;
+        transition: all 0.3s;
+        position: relative;
 
-          &:hover {
-            border-color: #FF9300;
-            // box-shadow: 0 4px 12px rgba(255, 147, 0, 0.15);
-            background: #FFFAF0;
+        &:hover {
+          border-color: #FF9300;
+          background: #FFFAF0;
 
-            .option-icon {
-              color: #FF9300;
-            }
+          .option-icon {
+            color: #FF9300;
           }
+        }
+
+        &.active {
+          background: #FFFAF0;
+          border-color: #FF9300;
+        }
 
-          &.active {
-            background: #FFFAF0;
-            border-color: #FF9300;
+        &.disabled {
+          background: #f8f8f9;
+          border-color: #eff0f3;
+          cursor: not-allowed;
+
+          h3 {
+            color: #7c7f86;
           }
 
-          &.disabled {
-            background: #f8f8f9;
-            border-color: #eff0f3;
-            cursor: not-allowed;
-
-            h3 {
-              color: #7c7f86;
-            }
-
-            p {
-              color: #b5b9bf;
-            }
-
-            .option-icon {
-              color: #a9aeb5;
-              background: #fff;
-            }
-
-            // &:hover {
-            //   border-color: #E5E7EB;
-            //   box-shadow: none;
-            //   background: #F3F4F6;
-
-            //   .option-icon {
-            //     color: #D1D5DB;
-            //   }
-            // }
+          p {
+            color: #b5b9bf;
           }
 
           .option-icon {
-            width: 48px;
-            height: 48px;
-            background: #eef3ff;
-            border-radius: 12px;
-            display: flex;
-            align-items: center;
-            justify-content: center;
-            margin: 0 auto 16px;
-            color: #6b7280;
-            transition: all 0.3s;
-
-            svg {
-              width: 24px;
-              height: 24px;
-            }
+            color: #a9aeb5;
+            background: #fff;
           }
+        }
 
-          h3 {
-            font-size: 18px;
-            font-weight: 600;
-            color: #333;
-            margin: 0 0 8px;
-          }
+        .option-icon {
+          width: 48px;
+          height: 48px;
+          background: #eef3ff;
+          border-radius: 12px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          margin: 0 auto 16px;
+          color: #6b7280;
+          transition: all 0.3s;
 
-          p {
-            font-size: 14px;
-            color: #999;
-            margin: 0 0 16px;
+          svg {
+            width: 24px;
+            height: 24px;
           }
+        }
 
-          .coming-soon {
-            position: absolute;
-            top: 12px;
-            right: 12px;
-            background: #c5c9d0;
-            color: #fff;
-            font-size: 14px;
-            font-weight: 500;
-            padding: 4px 8px;
-            border-radius: 15px;
-            text-transform: uppercase;
-          }
+        h3 {
+          font-size: 18px;
+          font-weight: 600;
+          color: #333;
+          margin: 0 0 8px;
+        }
+
+        p {
+          font-size: 14px;
+          color: #999;
+          margin: 0 0 16px;
+        }
+
+        .coming-soon {
+          position: absolute;
+          top: 12px;
+          right: 12px;
+          background: #c5c9d0;
+          color: #fff;
+          font-size: 14px;
+          font-weight: 500;
+          padding: 4px 8px;
+          border-radius: 15px;
+          text-transform: uppercase;
+        }
+      }
+    }
+  }
+
+  .parsing-modal {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, 0.5);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 1000;
+
+    .parsing-content {
+      background: white;
+      border-radius: 12px;
+      padding: 40px;
+      text-align: center;
+      max-width: 400px;
+      width: 90%;
+
+      .loading-spinner {
+        width: 48px;
+        height: 48px;
+        border: 4px solid #f3f3f3;
+        border-top: 4px solid #FF9300;
+        border-radius: 50%;
+        animation: spin 1s linear infinite;
+        margin: 0 auto 20px;
+      }
+
+      .success-icon {
+        width: 48px;
+        height: 48px;
+        margin: 0 auto 20px;
+        background: #FFFAF0;
+        border-radius: 5px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: white;
+        font-size: 24px;
+        font-weight: bold;
+      }
+
+      h3 {
+        font-size: 20px;
+        font-weight: 600;
+        color: #333;
+        margin: 0 0 12px;
+      }
+
+      p {
+        font-size: 14px;
+        color: #666;
+        margin: 0 0 24px;
+      }
+
+      .close-btn2 {
+        background: #FF9300;
+        color: white;
+        border: none;
+        border-radius: 8px;
+        padding: 12px 24px;
+        font-size: 14px;
+        font-weight: 500;
+        cursor: pointer;
+        width: 100%;
+        transition: all 0.3s;
+
+        &:hover {
+          background: #e68a00;
         }
+      }
+    }
+
+    @keyframes spin {
+      0% {
+        transform: rotate(0deg);
+      }
+
+      100% {
+        transform: rotate(360deg);
+      }
+    }
+
+    .close-btn {
+      width: 32px;
+      height: 32px;
+      border: none;
+      background: none;
+      cursor: pointer;
+      color: #999;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 4px;
+      transition: all 0.2s;
+
+      &:hover {
+        background: #f0f0f0;
+        color: #666;
+      }
+
+      svg {
+        width: 16px;
+        height: 16px;
+      }
     }
   }
+
+
 }
 </style>

File diff suppressed because it is too large
+ 340 - 292
src/hooks/useImport.ts


Some files were not shown because too many files changed in this diff