Просмотр исходного кода

feat: 新增PPT文件导入功能相关的交互和文案

1.  新增多语言文案支持PPT导入流程
2.  新增PPT文件解析前的信息获取逻辑,读取文件页数
3.  重构上传区域UI,增加文件确认弹窗和上传进度展示
4.  注释调了无用的socket检查定时器启动代码
lsc 7 часов назад
Родитель
Сommit
534c980fa5

+ 433 - 118
src/components/CollapsibleToolbar/index2.vue

@@ -131,21 +131,94 @@
         </div>
       </div>
       <div class="submenu-content">
-        <FileInput accept=".pptx"
-          @change="handleFileUpload">
-          <div class="upload-dropzone">
-              <div class="upload-dropzone-icon">
-                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
-                      <path d="M12 15V4"></path>
-                      <path d="M7 9l5-5 5 5"></path>
-                      <path d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2"></path>
-                  </svg>
+        <template v-if="readingFile">
+          <div class="reading-file">
+            <div class="loading-spinner"></div>
+            <div class="reading-text">{{ lang.ssReadingFile }}</div>
+          </div>
+        </template>
+        <template v-else-if="!showFileConfirmModal && !exportingDialog">
+          <FileInput accept=".pptx"
+            @change="handleFileUpload">
+            <div class="upload-dropzone">
+                <div class="upload-dropzone-icon">
+                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+                        <path d="M12 15V4"></path>
+                        <path d="M7 9l5-5 5 5"></path>
+                        <path d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2"></path>
+                    </svg>
+                </div>
+                <div class="upload-dropzone-title">{{ lang.ssDragAndDrop }}</div>
+                <div class="upload-dropzone-subtitle">{{ lang.ssSupportPptx }}</div>
+                <!-- <div class="upload-dropzone-footnote">同类型文件支持批量导入,跨类型文件请分开处理</div> -->
+            </div>
+          </FileInput>
+        </template>
+        
+        <template v-else-if="!exportingDialog">
+          <div class="file-confirm-inline">
+            <div class="file-info">
+              <div class="file-title">{{ lang.ssFileDetected }}{{ currentFileName }}</div>
+              <div class="file-subtitle">{{ currentFileName }}({{ lang.ssTotalPages.replace('{count}', pageCount.toString()) }})</div>
+            </div>
+            
+            <div class="import-options">
+              <div 
+                class="import-option" 
+                :class="{ active: selectedImportOption === 'page' }"
+                @click="selectedImportOption = 'page'"
+              >
+                <div class="option-icon">
+                </div>
+                <div class="option-text">
+                  <div class="option-title">{{ lang.ssImportAsSlide }}</div>
+                  <div class="option-desc">{{ lang.ssImportAsSlideDesc }}</div>
+                </div>
+              </div>
+              
+              <div
+                v-show="false" 
+                class="import-option" 
+                :class="{ active: selectedImportOption === 'library' }"
+                @click="selectedImportOption = 'library'"
+              >
+                <div class="option-icon">
+                </div>
+                <div class="option-text">
+                  <div class="option-title">{{ lang.ssImportAndSave }}</div>
+                  <div class="option-desc">{{ lang.ssImportAndSaveDesc }}</div>
+                </div>
               </div>
-              <div class="upload-dropzone-title">拖拽文件至此,或点击选择</div>
-              <div class="upload-dropzone-subtitle">支持.pptx</div>
-              <!-- <div class="upload-dropzone-footnote">同类型文件支持批量导入,跨类型文件请分开处理</div> -->
+            </div>
+
+            <div class="modal-buttons">
+              <button class="cancel-btn" @click="cancelFileUpload">{{ lang.ssCancel }}</button>
+              <button class="confirm-btn" @click="confirmFileUpload">{{ lang.ssConfirm }}</button>
+            </div>
           </div>
-        </FileInput>
+        </template>
+
+        <template v-if="exportingDialog">
+          <div class="progress-inline">
+            <div class="progress-header">
+              <!-- <span class="file-name">{{ currentFileName }}</span>
+              <span class="progress-percent">{{ importProgress }}%</span> -->
+                <div class="upload-task-main">
+                    <div class="upload-task-name">{{ currentFileName }}</div>
+                    <div class="upload-task-meta">课件文件 · {{ formatFileSize(currentFileSize) }}</div>
+                </div>
+                <div class="upload-task-side">
+                    <div class="upload-task-percent">{{importProgress}}%</div>
+                    <button type="button" class="upload-task-action" :class="{ 'upload-task-close': !exporting}" @click="handleParsingClose">{{ exporting ? '取消' :'×'}}</button>
+                </div>
+            </div>
+            <div class="progress-bar">
+              <div class="progress-fill" :style="{ width: importProgress + '%' }"></div>
+            </div>
+            <div class="progress-loading" v-if="exporting">上传中...</div>
+            <!-- <button class="close-btn" @click="handleParsingClose">{{ lang.ssClose }}</button> -->
+          </div>
+        </template>
       </div>
     </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'page' }">
@@ -571,26 +644,9 @@
       <SpeakingPanel />
     </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 ? lang.ssParsing : lang.ssExportCompleted }}</h3>
-        <p v-if="exporting">{{ lang.ssParsingFile }}{{ currentFileName }}</p>
-        <p v-if="!exporting">{{ lang.ssParsingCompleted }}</p>
-        <button class="close-btn2" @click="handleParsingClose">
-          {{ exporting ? lang.ssClose : lang.ssComplete }}
-        </button>
-      </div>
-    </div>
+
+
   </div>
 </template>
 
@@ -1042,24 +1098,96 @@ const getTypeClass = (type?: number) => {
 
 import useImport from '@/hooks/useImport'
 import message from '@/utils/message'
-const { importPPTXFile, exporting, getFile } = useImport()
+const { importPPTXFile, exporting, getFile, getPPTInfo } = useImport()
 const currentFileName = ref('')
+const currentFileSize = ref(0)
 const parsingStatus = ref<'parsing' | 'success'>('parsing')
 const parsingAbortController = ref<AbortController | null>(null)
+const showFileConfirmModal = ref(false)
+const pendingFile = ref<FileList | null>(null)
+const selectedImportOption = ref<'page' | 'library'>('page')
+const pageCount = ref(1)
+const readingFile = ref(false)
+const importProgress = ref(0)
+const progressInterval = ref<NodeJS.Timeout | null>(null)
+const exportingDialog = ref(false)
+
+// 格式化文件大小
+const formatFileSize = (bytes: number): string => {
+  if (bytes === 0) return '0 B'
+  const k = 1024
+  const sizes = ['B', 'KB', 'MB', 'GB']
+  const i = Math.floor(Math.log(bytes) / Math.log(k))
+  return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
+}
 
 const handleFileUpload = async (files: FileList) => {
   if (!files || files.length === 0) return
 
   const file = files[0]
   currentFileName.value = file.name
+  currentFileSize.value = file.size
+  pendingFile.value = files
+  selectedImportOption.value = 'page'
+  readingFile.value = true
+  
+  try {
+    // 读取 PPT 信息获取页码
+    const info = await getPPTInfo(file)
+    pageCount.value = info.pageCount
+  }
+  catch (error) {
+    console.error('获取 PPT 信息失败:', error)
+    pageCount.value = 1 // 失败时默认显示 1 页
+  }
+  finally {
+    readingFile.value = false
+  }
+  
+  showFileConfirmModal.value = true
+}
 
+const confirmFileUpload = async () => {
+  if (!pendingFile.value) return
+
+  showFileConfirmModal.value = false
+  importProgress.value = 0
+  exportingDialog.value = true
+  const startTimer = () => {
+    // 启动虚拟进度条(30秒从0-99%)
+    if (progressInterval.value) clearInterval(progressInterval.value)
+    const startTime = Date.now()
+    const duration = 30000 // 30秒
+    progressInterval.value = setInterval(() => {
+      console.log('progressInterval.value', progressInterval.value)
+      const elapsed = Date.now() - startTime
+      const progress = Math.min(99, Math.floor((elapsed / duration) * 99))
+      importProgress.value = progress
+      if (progress >= 99) {
+        importProgress.value = 99
+      }
+
+      if (!exporting.value) {
+        if (progressInterval.value) {
+          clearInterval(progressInterval.value)
+          importProgress.value = 100
+          progressInterval.value = null
+        }
+      }
+    }, 100)
+  }
+
+  const confirmOnclose = () => {
+    handleParsingClose()
+  }
+  
   try {
     // 创建AbortController用于取消操作
     parsingAbortController.value = new AbortController()
     const signal = parsingAbortController.value.signal
 
     // 调用importPPTXFile并传入signal
-    await importPPTXFile(files, { signal })
+    await importPPTXFile(pendingFile.value, { signal, startTimer, confirmOnclose })
   }
   catch (error) {
     if (error instanceof DOMException && error.name === 'AbortError') {
@@ -1070,9 +1198,26 @@ const handleFileUpload = async (files: FileList) => {
       message.error(lang.ssFileParseFailedRetry)
     }
   }
+  finally {
+    // if (progressInterval.value) {
+    //   clearInterval(progressInterval.value)
+    //   progressInterval.value = null
+    //   importProgress.value = 100
+    // }
+    pendingFile.value = null
+  }
+}
+
+const cancelFileUpload = () => {
+  showFileConfirmModal.value = false
+  pendingFile.value = null
 }
 
 const handleParsingClose = () => {
+  if (progressInterval.value) {
+    clearInterval(progressInterval.value)
+    progressInterval.value = null
+  }
   if (exporting.value && parsingAbortController.value) {
     parsingAbortController.value.abort()
     exporting.value = false
@@ -1082,6 +1227,8 @@ const handleParsingClose = () => {
   else if (!exporting.value) {
     emit('close')
   }
+
+  exportingDialog.value = false
 }
 </script>
 
@@ -1549,112 +1696,125 @@ const handleParsingClose = () => {
 }
 
 
-.parsing-modal {
-  position: fixed;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  background: rgba(0, 0, 0, 0.5);
+
+
+.progress-inline {
   display: flex;
-  align-items: center;
-  justify-content: center;
-  z-index: 1000;
+  flex-direction: column;
+  gap: 12px;
+  padding: 14px 14px 12px;
+  border-radius: 16px;
+  background: #fff;
+  border: 1px solid #f0ebe3;
+  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
+  width: calc(100% - 30px);
+  margin: 15px auto;
 
-  .parsing-content {
-    background: white;
-    border-radius: 12px;
-    padding: 24px;
-    width: 400px;
-    text-align: center;
-    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-
-    .loading-spinner {
-      width: 48px;
-      height: 48px;
-      border: 4px solid #f0f0f0;
-      border-top: 4px solid #FF9300;
-      border-radius: 50%;
-      margin: 0 auto 20px;
-      animation: spin 1s linear infinite;
-    }
+  .progress-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 12px;
+
+    // .file-name {
+    //   font-size: 14px;
+    //   font-weight: 600;
+    //   color: #333;
+    // }
 
-    .success-icon {
-      width: 48px;
-      height: 48px;
-      margin: 0 auto 20px;
-      background: #FFFAF0;
-      border-radius: 5px;
+    // .progress-percent {
+    //   font-size: 14px;
+    //   font-weight: 600;
+    //   color: #FF9300;
+    // }
+
+    .upload-task-main{
+      min-width: 0;
+    }
+    .upload-task-name{
+      font-size: 13px;
+      font-weight: 700;
+      color: #111827;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+    .upload-task-meta{
+      margin-top: 2px;
+      font-size: 11px;
+      color: #8b7356;
+    }
+    .upload-task-side{
       display: flex;
       align-items: center;
-      justify-content: center;
-      color: white;
-      font-size: 24px;
-      font-weight: bold;
+      gap: 8px;
+      flex-shrink: 0;
     }
-
-    h3 {
-      font-size: 20px;
-      font-weight: 600;
-      color: #333;
-      margin: 0 0 12px;
+    .upload-task-percent{
+      font-size: 12px;
+      font-weight: 700;
+      color: #c76a0c;
+      min-width: 40px;
+      text-align: right;
     }
 
-    p {
-      font-size: 14px;
-      color: #666;
-      margin: 0 0 24px;
+    .upload-task-action {
+        border: 0;
+        background: transparent;
+        color: #9a8a77;
+        font-size: 12px;
+        font-weight: 600;
+        cursor: pointer;
     }
 
-    .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;
-      }
+    .upload-task-close{
+      width: 20px;
+      height: 20px;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 999px;
+      color: #8b7356;
+      font-size: 16px;
+      line-height: 1;
     }
   }
 
-  @keyframes spin {
-    0% {
-      transform: rotate(0deg);
-    }
+  .progress-bar {
+    width: 100%;
+    height: 4px;
+    background: #f0f0f0;
+    border-radius: 4px;
+    overflow: hidden;
 
-    100% {
-      transform: rotate(360deg);
+    .progress-fill {
+      height: 100%;
+      background: linear-gradient(90deg, #f9b24e 0%, #f07815 100%);
+      transition: width 0.24s ease;
+      border-radius: 4px;
+      transition: width 0.1s ease;
     }
   }
 
+  .progress-loading{
+    font-size: 12px;
+    color: #111827;
+  }
+
   .close-btn {
-    width: 32px;
-    height: 32px;
+    background: #FF9300;
+    color: white;
     border: none;
-    background: none;
+    border-radius: 8px;
+    padding: 10px 20px;
+    font-size: 14px;
+    font-weight: 500;
     cursor: pointer;
-    color: #999;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    border-radius: 4px;
-    transition: all 0.2s;
+    width: 100%;
+    transition: all 0.3s;
 
     &:hover {
-      background: #f0f0f0;
-      color: #666;
-    }
-
-    svg {
-      width: 16px;
-      height: 16px;
+      background: #e68a00;
     }
   }
 }
@@ -1790,4 +1950,159 @@ const handleParsingClose = () => {
     color: #9a8a77;
   }
 }
+
+.reading-file {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 20px;
+  gap: 16px;
+
+  .loading-spinner {
+    width: 40px;
+    height: 40px;
+    border: 4px solid #f0f0f0;
+    border-top: 4px solid #FF9300;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+  }
+
+  .reading-text {
+    font-size: 14px;
+    color: #666;
+  }
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+.file-confirm-inline {
+  width: calc(100% - 30px);
+  margin: 15px auto;
+  box-sizing: border-box;
+  margin-top: 18px;
+  padding: 18px;
+  border-radius: 18px;
+  background: #fff;
+  border: 1px solid #f3e4cf;
+  box-shadow: 0 12px 28px rgba(15, 23, 42, 0.06);
+
+  .file-info {
+    margin-bottom: 16px;
+    .file-title {
+      font-size: 14px;
+      font-weight: 700;
+      color: #111827;
+      margin-bottom: 6px;
+    }
+    .file-subtitle {
+      font-size: 12px;
+      color: #7c6d5d;
+    }
+  }
+
+  .import-options {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    margin-bottom: 20px;
+
+    .import-option {
+      border: 1px solid #efe6d8;
+      border-radius: 16px;
+      padding: 14px;
+      display: flex;
+      gap: 12px;
+      cursor: pointer;
+      transition: all 0.22s ease;
+      background: #fffdfa;
+
+      &:hover {
+        border-color: rgba(247, 139, 34, 0.4);
+        background: #fffdfa;
+      }
+
+      &.active {
+        border-color: #ff9300;
+        background: #fff5e5;
+
+        .option-icon {
+          border-color: #f78b22;
+
+          &::after {
+              content: '';
+              position: absolute;
+              inset: 3px;
+              border-radius: 50%;
+              background: #f78b22;
+          }
+        }
+      }
+
+      .option-icon {
+        width: 18px;
+        height: 18px;
+        border-radius: 50%;
+        border: 1.5px solid #d6c0a1;
+        margin-top: 2px;
+        position: relative;
+        flex-shrink: 0;
+      }
+
+      .option-text {
+        flex: 1;
+
+        .option-title {
+          font-size: 14px;
+          font-weight: 600;
+          color: #111827;
+          margin-bottom: 3px;
+        }
+
+        .option-desc {
+          font-size: 12px;
+          color: #6b7280;
+          line-height: 1.4;
+        }
+      }
+    }
+  }
+
+  .modal-buttons {
+    display: flex;
+    gap: 10px;
+    justify-content: flex-end;
+
+    .cancel-btn, .confirm-btn {
+      min-width: 84px;
+      height: 34px;
+      padding: 0 14px;
+      border-radius: 10px;
+      font-size: 12px;
+      font-weight: 600;
+      cursor: pointer;
+      transition: all 0.2s ease;
+    }
+
+    .cancel-btn {
+      background: #fff;
+      border: 1px solid #e5e7eb;
+      color: #6b7280;
+    }
+
+    .confirm-btn {
+      background: linear-gradient(180deg, #f89a34 0%, #f07815 100%);
+      border: 0;
+      color: #fff;
+      box-shadow: 0 10px 20px rgba(240, 120, 21, 0.18); 
+    }
+  }
+}
 </style>

+ 30 - 5
src/hooks/useImport.ts

@@ -1,4 +1,4 @@
-import { ref, nextTick } from 'vue'
+import { ref, nextTick } from 'vue'
 import { storeToRefs } from 'pinia'
 import { parse, type Shape, type Element, type ChartItem, type BaseElement } from 'pptxtojson'
 import { nanoid } from 'nanoid'
@@ -15,6 +15,29 @@ import { showConfirmDialog } from '@/utils/confirmDialog'
 import { EMFJS, WMFJS } from 'rtf.js'
 import * as UTIF from 'utif2'
 
+/**
+ * 获取 PPT 文件的基本信息
+ */
+const getPPTInfo = async (file: File): Promise<{ pageCount: number; filename: string }> => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader()
+    reader.onload = async (e) => {
+      try {
+        const json = await parse(e.target!.result as ArrayBuffer)
+        resolve({
+          pageCount: json.slides.length,
+          filename: file.name
+        })
+      }
+      catch (error) {
+        reject(error)
+      }
+    }
+    reader.onerror = reject
+    reader.readAsArrayBuffer(file)
+  })
+}
+
 import type {
   Slide,
   TableCellStyle,
@@ -1620,13 +1643,13 @@ export default () => {
     }
   */
 
-  const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal, onclose?: () => void }) => {
+  const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal, onclose?: () => void, startTimer?: () => void, confirmOnclose?: () => void }) => {
     console.log('导入', files)
     const defaultOptions = {
       cover: false,
       fixedViewport: false,
     }
-    const { cover, fixedViewport, signal, onclose } = { ...defaultOptions, ...options }
+    const { cover, fixedViewport, signal, onclose, startTimer, confirmOnclose } = { ...defaultOptions, ...options }
 
     let isNone = false
     if (slides.value.length === 1 && slides.value[0].elements.length === 0) {
@@ -1638,7 +1661,7 @@ export default () => {
 
     exporting.value = true // 假设 exporting 是一个全局 ref
     imgExporting.value = true // 假设 imgExporting 是一个全局 ref
-
+    startTimer?.()
     // 预加载形状库(用于后续形状匹配)
     const shapeList: ShapePoolItem[] = []
     for (const item of SHAPE_LIST) {
@@ -1693,6 +1716,7 @@ export default () => {
             exporting.value = false
             imgExporting.value = false
             onclose?.()
+            confirmOnclose?.()
             return Promise.reject(new Error('用户取消导入'))
           }
         }
@@ -2502,6 +2526,7 @@ export default () => {
     getFile,
     getFile2,
     dataToFile,
-    uploadFileToS3
+    uploadFileToS3,
+    getPPTInfo
   }
 }

+ 1 - 1
src/views/Student/index.vue

@@ -4032,7 +4032,7 @@ const createWebSocketConnection = async (type = 1) => {
   }
 
   // 启动 socket 连接检查定时器
-  startSocketCheckTimer()
+  // startSocketCheckTimer()
 }
 
 // 处理连接断开

+ 8 - 1
src/views/lang/cn.json

@@ -876,5 +876,12 @@
   "flipHorizontally": "水平翻转",
   "ssUploadFile": "上传文件",
   "ssDragAndDrop": "拖拽文件至此,或点击选择",
-  "ssSupportPptx": "支持.pptx文件"
+  "ssSupportPptx": "支持.pptx文件",
+  "ssFileDetected": "检测到文件:",
+  "ssTotalPages": "共 {count} 页",
+  "ssImportAsSlide": "导入为课件页面",
+  "ssImportAsSlideDesc": "将文件导入为在线课件页面。",
+  "ssImportAndSave": "导入并保存到资源库",
+  "ssImportAndSaveDesc": "导入为课件页面,并同步保存到资源库。",
+  "ssReadingFile": "正在读取文件..."
 }

+ 8 - 1
src/views/lang/en.json

@@ -876,5 +876,12 @@
   "flipHorizontally": "Flip Horizontally",
   "ssUploadFile": "Upload File",
   "ssDragAndDrop": "Drag and Drop Files Here, or Click to Select",
-  "ssSupportPptx": "Supports .pptx Files"
+  "ssSupportPptx": "Supports .pptx Files",
+  "ssFileDetected": "File detected: ",
+  "ssTotalPages": "Total {count} pages",
+  "ssImportAsSlide": "Import as course slides",
+  "ssImportAsSlideDesc": "Import the file as online course slides.",
+  "ssImportAndSave": "Import and save to library",
+  "ssImportAndSaveDesc": "Import as course slides and save to library.",
+  "ssReadingFile": "Reading file..."
 }

+ 8 - 1
src/views/lang/hk.json

@@ -876,5 +876,12 @@
   "flipHorizontally": "水平翻轉",
   "ssUploadFile": "上傳文件",
   "ssDragAndDrop": "拖拽文件至此,或點擊選擇",
-  "ssSupportPptx": "支持.pptx文件"
+  "ssSupportPptx": "支持.pptx文件",
+  "ssFileDetected": "檢測到文件:",
+  "ssTotalPages": "共 {count} 頁",
+  "ssImportAsSlide": "導入為課件頁面",
+  "ssImportAsSlideDesc": "將文件導入為在線課件頁面。",
+  "ssImportAndSave": "導入並保存到資源庫",
+  "ssImportAndSaveDesc": "導入為課件頁面,並同步保存到資源庫。",
+  "ssReadingFile": "正在讀取文件..."
 }