فهرست منبع

feat(网页上传): 添加网页链接上传功能及多语言支持

- 在折叠工具栏中新增网页链接上传功能,包含URL验证和上传状态处理
- 添加多语言文案支持,包括中文、英文和繁体中文
- 实现URL验证逻辑,包含iframe和XHR双重验证机制
- 调整页面样式,优化上传界面用户体验
- 取消注释创建课程对话框代码
lsc 3 هفته پیش
والد
کامیت
54ebd8f76b
6فایلهای تغییر یافته به همراه339 افزوده شده و 13 حذف شده
  1. 306 8
      src/components/CollapsibleToolbar/index2.vue
  2. 1 0
      src/components/Message.vue
  3. 2 2
      src/views/Editor/index3.vue
  4. 10 1
      src/views/lang/cn.json
  5. 10 1
      src/views/lang/en.json
  6. 10 1
      src/views/lang/hk.json

+ 306 - 8
src/components/CollapsibleToolbar/index2.vue

@@ -306,7 +306,18 @@
           </div>
           <span class="submenu-label">{{ lang.ssWebpageCenter }}</span>
         </div> -->
-        <div class="submenu-item">
+        <div class="submenu-item" @click="handleToolClick('uploadWebpage')">
+          <div class="submenu-icon">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <circle cx="12" cy="12" r="10"></circle>
+              <path d="M2 12h20"></path>
+              <path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"></path>
+              <path d="M16 8l-4 4-4-4" stroke-width="1.5"></path>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssUploadWebpageLink }}</span>
+        </div>
+        <!-- <div class="submenu-item">
           <div class="submenu-icon">
             <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
               <g id="Component 1">
@@ -319,7 +330,7 @@
             </svg>
           </div>
           <span class="submenu-label">{{ lang.ssUploadWebpage }}</span>
-        </div>
+        </div> -->
         <div class="submenu-item" @click="handleToolClick('createWebpage')">
           <div class="submenu-icon">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -349,6 +360,33 @@
         </div> -->
       </div>
     </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'uploadWebpage' }">
+      <div class="submenu-title">
+        <div class="title">{{ lang.ssUploadWebpageLink }}</div>
+        <div class="close-icon" @click="activeSubmenu = 'h5page'">
+          <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 3">
+              <g id="Component 1">
+                <path id="Vector" d="M16 18L12 14L16 10" stroke="#9CA3AF" stroke-width="1.33333" />
+              </g>
+            </g>
+          </svg>
+        </div>
+      </div>
+      <div class="line_box">
+        <div class="webpage-link-container">
+          <h3 class="webpage-link-title">{{ lang.ssWebpageLink }}</h3>
+          <input type="text" class="webpage-link-input" :placeholder="lang.ssEnterCompleteUrl" v-model="webpageUrl"
+            @input="handleUrlInput" />
+          <button class="webpage-link-button"
+            :class="{ 'loading': isLoading, 'error': isValidUrl === false, 'disabled': isValidUrl === null || isLoading }"
+            :disabled="isValidUrl === null || isLoading || isValidUrl === false" @click="uploadWebpageLink">
+            {{ isLoading ? lang.ssUploading : isValidUrl === null ? lang.ssWaitingForInput : isValidUrl === false ?
+              lang.ssInvalidUrl : lang.ssStartUpload }}
+          </button>
+        </div>
+      </div>
+    </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'multimedia' }">
       <div class="submenu-title">
         <div class="title">{{ lang.ssAddMultimedia }}</div>
@@ -482,6 +520,9 @@ const isCollapsed = ref(props.defaultCollapsed)
 const activeSubmenu = ref<string | null>(null)
 const contentList = ref<ContentItem[]>([])
 const hoveredTool = ref<string | null>(null)
+const webpageUrl = ref('')
+const isLoading = ref(false)
+const isValidUrl = ref<boolean | null>(null) // null: 未输入, true: 有效, false: 无效
 
 const slidesStore = useSlidesStore()
 const { currentSlide } = storeToRefs(slidesStore)
@@ -489,6 +530,191 @@ const { currentSlide } = storeToRefs(slidesStore)
 const { createFrameElement } = useCreateElement()
 const { createSlide, createSlideByTemplate } = useSlideHandler()
 
+const handleUrlInput = () => {
+  const url = webpageUrl.value.trim()
+  if (!url) {
+    isValidUrl.value = null
+  }
+  else {
+    // 改进的URL格式验证,支持查询参数
+    const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*(\?[\w=&-]+)?\/?$/i
+    isValidUrl.value = urlRegex.test(url)
+  }
+  console.log('URL输入:', webpageUrl.value, '验证结果:', isValidUrl.value)
+}
+
+const uploadWebpageLink = async () => {
+  if (!webpageUrl.value || isValidUrl.value !== true) {
+    // 可以添加提示信息
+    return
+  }
+
+  isLoading.value = true
+
+  // 模拟上传过程
+  // isLoading.value = false
+  // 上传成功后创建iframe元素
+
+  const isValid = await new Promise((resolve) => {
+    // 创建隐藏iframe
+    const iframe = document.createElement('iframe')
+    iframe.style.display = 'none'
+    iframe.src = webpageUrl.value
+    let finished = false
+    const timeout = setTimeout(() => {
+      if (finished) return
+      finished = true
+      // 超时,移除iframe,进入XHR判断
+      document.body.removeChild(iframe)
+      // 用XHR判断
+      const xhr = new XMLHttpRequest()
+      xhr.open('GET', iframe.src, true)
+      xhr.onreadystatechange = function() {
+        if (xhr.readyState === 4) {
+          if (xhr.status === 200) {
+            resolve(true)
+          }
+          else {
+            // 再试一次 getFile
+            getFile(iframe.src as any).then(res => {
+              if (res && res.data && res.data !== 1) {
+                resolve(true)
+              }
+              else {
+                resolve(false)
+              }
+            }).catch(() => {
+              resolve(false)
+            })
+          }
+        }
+      }.bind(this)
+      xhr.onerror = function() {
+        // 再试一次 getFile
+        getFile(iframe.src as any).then(res => {
+          if (res && res.data && res.data !== 1) {
+            resolve(true)
+          }
+          else {
+            resolve(false)
+          }
+        }).catch(() => {
+          resolve(false)
+        })
+      }.bind(this)
+      xhr.send()
+    }, 5000)
+
+    iframe.onload = function() {
+      if (finished) return
+      finished = true
+      clearTimeout(timeout)
+      try {
+        // 尝试访问contentWindow.document
+        const doc = iframe?.contentWindow?.document
+        document.body.removeChild(iframe)
+        resolve(true)
+      }
+      catch (e) {
+        // 跨域或其他异常,移除iframe,进入XHR判断
+        document.body.removeChild(iframe)
+        const xhr = new XMLHttpRequest()
+        xhr.open('GET', iframe.src, true)
+        xhr.onreadystatechange = function() {
+          if (xhr.readyState === 4) {
+            if (xhr.status === 200) {
+              resolve(true)
+            }
+            else {
+              // 再试一次 getFile
+              getFile(iframe.src as any).then(res => {
+                if (res && res.data && res.data !== 1) {
+                  resolve(true)
+                }
+                else {
+                  resolve(false)
+                }
+              }).catch(() => {
+                resolve(false)
+              })
+            }
+          }
+        }.bind(this)
+        xhr.onerror = function() {
+          // 再试一次 getFile
+          getFile(iframe.src as any).then(res => {
+            if (res && res.data && res.data !== 1) {
+              resolve(true)
+            }
+            else {
+              resolve(false)
+            }
+          }).catch(() => {
+            resolve(false)
+          })
+        }.bind(this)
+        xhr.send()
+      }
+    }
+    iframe.onerror = function() {
+      if (finished) return
+      finished = true
+      clearTimeout(timeout)
+      document.body.removeChild(iframe)
+      // iframe加载失败,进入XHR判断
+      const xhr = new XMLHttpRequest()
+      xhr.open('GET', iframe.src, true)
+      xhr.onreadystatechange = function() {
+        if (xhr.readyState === 4) {
+          if (xhr.status === 200) {
+            resolve(true)
+          }
+          else {
+            // 再试一次 getFile
+            getFile(iframe.src as any).then(res => {
+              if (res && res.data && res.data !== 1) {
+                resolve(true)
+              }
+              else {
+                resolve(false)
+              }
+            }).catch(() => {
+              resolve(false)
+            })
+          }
+        }
+      }.bind(this)
+      xhr.onerror = function() {
+        // 再试一次 getFile
+        getFile(iframe.src as any).then(res => {
+          if (res && res.data && res.data !== 1) {
+            resolve(true)
+          }
+          else {
+            resolve(false)
+          }
+        }).catch(() => {
+          resolve(false)
+        })
+      }.bind(this)
+      xhr.send()
+    }
+    document.body.appendChild(iframe)
+  })
+
+  if (!isValid) {
+    message.error(lang.ssCocoLinkTip)
+    isLoading.value = false
+    return
+  }
+  isLoading.value = false
+
+  createFrameElement(webpageUrl.value, 73) // 假设15是网页工具的类型
+  // 清空输入框和验证状态
+  webpageUrl.value = ''
+  isValidUrl.value = null
+}
+
 const toggleCollapse = () => {
   isCollapsed.value = !isCollapsed.value
   emit('toggle', isCollapsed.value)
@@ -568,6 +794,9 @@ const handleToolClick = (tool: string) => {
       window.open('https://cloud.cocorobo.hk/admin.html?type=cocoflow3', '_blank')
     }
   }
+  else if (tool === 'uploadWebpage') {
+    activeSubmenu.value = 'uploadWebpage'
+  }
 }
 
 const loadContentList = () => {
@@ -671,7 +900,7 @@ const getTypeClass = (type?: number) => {
 
 import useImport from '@/hooks/useImport'
 import message from '@/utils/message'
-const { importPPTXFile, exporting } = useImport()
+const { importPPTXFile, exporting, getFile } = useImport()
 const currentFileName = ref('')
 const parsingStatus = ref<'parsing' | 'success'>('parsing')
 const parsingAbortController = ref<AbortController | null>(null)
@@ -1142,19 +1371,19 @@ const handleParsingClose = () => {
   .parsing-content {
     background: white;
     border-radius: 12px;
-    padding: 40px;
+    padding: 24px;
+    width: 400px;
     text-align: center;
-    max-width: 400px;
-    width: 90%;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
 
     .loading-spinner {
       width: 48px;
       height: 48px;
-      border: 4px solid #f3f3f3;
+      border: 4px solid #f0f0f0;
       border-top: 4px solid #FF9300;
       border-radius: 50%;
-      animation: spin 1s linear infinite;
       margin: 0 auto 20px;
+      animation: spin 1s linear infinite;
     }
 
     .success-icon {
@@ -1236,4 +1465,73 @@ const handleParsingClose = () => {
     }
   }
 }
+
+.line_box {
+  .webpage-link-container {
+    padding: 20px;
+    // text-align: center;
+
+    .webpage-link-title {
+      margin: 0 0 16px;
+      font-size: 14px;
+      font-weight: 600;
+      color: #333;
+    }
+
+    .webpage-link-input {
+      width: 100%;
+      padding: 12px 12px;
+      border: 1px solid #d9d9d9;
+      border-radius: 8px;
+      font-size: 14px;
+      margin-bottom: 16px;
+      transition: all 0.3s;
+      box-sizing: border-box;
+
+      &:focus {
+        outline: none;
+        border-color: #FF9300;
+        background: #fff8f0;
+      }
+    }
+
+    .webpage-link-button {
+      text-align: center;
+      padding: 10px 24px;
+      border: none;
+      border-radius: 4px;
+      font-size: 14px;
+      font-weight: 500;
+      cursor: pointer;
+      transition: all 0.3s;
+      background-color: #FF9300;
+      color: white;
+      margin: 0 auto;
+      display: block;
+
+      &:hover:not(:disabled) {
+        background-color: #e68a00;
+      }
+
+      &:disabled {
+        opacity: 0.5;
+        cursor: not-allowed;
+      }
+
+      &.loading {
+        cursor: not-allowed;
+        opacity: 0.8;
+      }
+
+      &.error {
+        background-color: #ff4d4f;
+        color: white;
+
+        &:hover:not(:disabled) {
+          background-color: #ff7875;
+        }
+      }
+    }
+  }
+}
 </style>

+ 1 - 0
src/components/Message.vue

@@ -125,6 +125,7 @@ defineExpose({
   }
   .content {
     width: 100%;
+    padding: unset;
   }
   .description {
     line-height: 1.15;

+ 2 - 2
src/views/Editor/index3.vue

@@ -173,10 +173,10 @@
     <AIPPTDialog />
   </Modal>
 
-  <!-- <Modal class="createCourseDialog" :visible="showCreateCourseDialog" :closeOnClickMask="false" :closeOnEsc="false"
+  <Modal class="createCourseDialog" :visible="showCreateCourseDialog" :closeOnClickMask="false" :closeOnEsc="false"
     :closeButton="false" @closed="closeCreateCourseDialog()">
     <CreateCourseDialog @close="closeCreateCourseDialog" @select="handleCreateCourseSelect" />
-  </Modal> -->
+  </Modal>
 </template>
 
 <script lang="ts" setup>

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

@@ -704,10 +704,19 @@
   "ssDocument": "文档",
   "ssDocumentSet": "文档集",
   "ssNewWebpage": "新建网页",
+  "ssWebpageLink": "网页链接",
+  "ssEnterCompleteUrl": "请输入完整的网页URL地址",
+  "ssWaitingForInput": "等待输入...",
+  "ssInvalidUrl": "URL格式不正确,请检查",
+  "ssStartUpload": "开始上传",
+  "ssUploading": "上传中...",
+  "ssUpload": "上传",
   "ssFileParseCancelled": "文件解析已取消",
   "ssFileParseFailed": "文件解析失败",
   "ssFileParseFailedRetry": "文件解析失败,请重试",
   "ssParseCancelled": "解析已取消",
   "ssConfirmOperation": "确认操作",
-  "ssClearToolContent": "该操作将清除当前工具的编辑内容,是否继续?"
+  "ssClearToolContent": "该操作将清除当前工具的编辑内容,是否继续?",
+  "ssUploadWebpageLink": "上传链接",
+  "ssCocoLinkTip":"请添加 Cocorobo 同域、亚马逊或可访问的 HTML 链接。"
 }

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

@@ -704,10 +704,19 @@
   "ssDocument": "Document",
   "ssDocumentSet": "Document Set",
   "ssNewWebpage": "New Webpage",
+  "ssWebpageLink": "Webpage Link",
+  "ssEnterCompleteUrl": "Please enter complete webpage URL",
+  "ssWaitingForInput": "Waiting for input...",
+  "ssInvalidUrl": "URL format is incorrect, please check",
+  "ssStartUpload": "Start Upload",
+  "ssUploading": "Uploading...",
+  "ssUpload": "Upload",
   "ssFileParseCancelled": "File parsing cancelled",
   "ssFileParseFailed": "File parsing failed",
   "ssFileParseFailedRetry": "File parsing failed, please retry",
   "ssParseCancelled": "Parsing cancelled",
   "ssConfirmOperation": "Confirm Operation",
-  "ssClearToolContent": "This operation will clear the current tool's editing content. Continue?"
+  "ssClearToolContent": "This operation will clear the current tool's editing content. Continue?",
+  "ssUploadWebpageLink": "Upload Webpage Link",
+  "ssCocoLinkTip":"Please add Cocorobo, Amazon, or accessible HTML link."
 }

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

@@ -704,10 +704,19 @@
   "ssDocument": "文檔",
   "ssDocumentSet": "文檔集",
   "ssNewWebpage": "新建網頁",
+  "ssWebpageLink": "網頁鏈接",
+  "ssEnterCompleteUrl": "請輸入完整的網頁URL地址",
+  "ssWaitingForInput": "等待輸入...",
+  "ssInvalidUrl": "URL格式不正確,請檢查",
+  "ssStartUpload": "開始上傳",
+  "ssUploading": "上傳中...",
+  "ssUpload": "上傳",
   "ssFileParseCancelled": "文件解析已取消",
   "ssFileParseFailed": "文件解析失敗",
   "ssFileParseFailedRetry": "文件解析失敗,請重試",
   "ssParseCancelled": "解析已取消",
   "ssConfirmOperation": "確認操作",
-  "ssClearToolContent": "該操作將清除當前工具的編輯內容,是否繼續?"
+  "ssClearToolContent": "該操作將清除當前工具的編輯內容,是否繼續?",
+  "ssUploadWebpageLink": "上傳鏈接",
+  "ssCocoLinkTip":"請添加 Cocorobo 同域、亚马逊或可访问的 HTML 链接。"
 }