Jelajahi Sumber

Merge branches 'beta' and 'beta' of https://git.cocorobo.cn/jack/PPT into beta

jack 4 minggu lalu
induk
melakukan
f34f2a3c63
43 mengubah file dengan 6474 tambahan dan 750 penghapusan
  1. 9 6
      src/App.vue
  2. 680 407
      src/components/CollapsibleToolbar/componets/aiChat.vue
  3. 180 0
      src/components/CollapsibleToolbar/componets/aiWeb.vue
  4. 2 0
      src/components/CollapsibleToolbar/index.vue
  5. 1009 130
      src/components/CollapsibleToolbar/index2.vue
  6. 1649 0
      src/components/CollapsibleToolbar/index22.vue
  7. 1 0
      src/components/ColorButton.vue
  8. 143 0
      src/components/Contextmenu2/MenuContent.vue
  9. 80 0
      src/components/Contextmenu2/index.vue
  10. 14 0
      src/components/Contextmenu2/types.ts
  11. 27 2
      src/components/FileInput.vue
  12. 6 4
      src/components/Switch.vue
  13. 38 0
      src/components/TextColorButton2.vue
  14. 2 1
      src/hooks/useCreateElement.ts
  15. 30 5
      src/hooks/useImport.ts
  16. 64 0
      src/plugins/directive/contextmenu2.ts
  17. 2 0
      src/plugins/directive/index.ts
  18. 4 0
      src/plugins/icon.ts
  19. 1 1
      src/services/config.ts
  20. 14 1
      src/services/course.ts
  21. 4 3
      src/services/speaking.ts
  22. 5 4
      src/tools/aiChat.ts
  23. 1 0
      src/types/slides.ts
  24. 162 0
      src/utils/components/messageInstruction.vue
  25. 4 0
      src/utils/confirmDialog.ts
  26. 2 0
      src/views/Editor/CanvasTool/WebpageInput.vue
  27. 184 5
      src/views/Editor/CanvasTool/index2.vue
  28. 189 44
      src/views/Editor/Thumbnails/index2.vue
  29. 1 0
      src/views/Editor/Toolbar/ElementStylePanel/index.vue
  30. 59 0
      src/views/Editor/Toolbar/common/ElementFlip2.vue
  31. 476 0
      src/views/Editor/Toolbar/common/RichTextBase2.vue
  32. 2 2
      src/views/Editor/index3.vue
  33. 1 2
      src/views/Student/components/aiChat.vue
  34. 1 1
      src/views/Student/components/answerTheResult.vue
  35. 913 68
      src/views/Student/components/choiceQuestionDetailDialog.vue
  36. 285 45
      src/views/Student/index.vue
  37. 2 0
      src/views/Student/index2.vue
  38. 2 0
      src/views/components/element/FrameElement/BaseFrameElement.vue
  39. 1 1
      src/views/components/element/ProsemirrorEditor.vue
  40. 11 11
      src/views/components/tool/previewImageTool.vue
  41. 72 3
      src/views/lang/cn.json
  42. 71 2
      src/views/lang/en.json
  43. 71 2
      src/views/lang/hk.json

+ 9 - 6
src/App.vue

@@ -188,24 +188,27 @@ window.addEventListener('beforeunload', () => {
 .image-preview__toolbar button {
   padding: 8px 12px;
   border: none;
-  background: linear-gradient(180deg, #3a8bff 0%, #2f80ed 100%);
+  // background: linear-gradient(180deg, #ff9300 0%, #2f80ed 100%);
+  background: #ff9300;
   color: #fff;
   border-radius: 10px;
   cursor: pointer;
   transition: transform 0.15s ease, box-shadow 0.2s ease, background 0.2s ease;
-  box-shadow: 0 2px 8px rgba(47, 128, 237, 0.3);
+  // box-shadow: 0 2px 8px rgba(47, 128, 237, 0.3);
 }
 .image-preview__toolbar button:hover {
   transform: translateY(-1px);
-  box-shadow: 0 6px 16px rgba(47, 128, 237, 0.35);
+  // box-shadow: 0 6px 16px rgba(47, 128, 237, 0.35);
 }
 .image-preview__toolbar button:active {
   transform: translateY(0);
-  box-shadow: 0 2px 8px rgba(47, 128, 237, 0.28);
-  background: linear-gradient(180deg, #2f80ed 0%, #1b6dde 100%);
+  // box-shadow: 0 2px 8px rgba(47, 128, 237, 0.28);
+  // background: linear-gradient(180deg, #2f80ed 0%, #1b6dde 100%);
+  background: #ff9300;
 }
 .image-preview__toolbar button:focus-visible {
-  outline: 2px solid rgba(47, 128, 237, 0.6);
+  // outline: 2px solid rgba(47, 128, 237, 0.6);
+  outline: none;
   outline-offset: 2px;
 }
 .image-preview__stage {

+ 680 - 407
src/components/CollapsibleToolbar/componets/aiChat.vue

@@ -1,100 +1,184 @@
 <template>
-    <div class="ai-chat-container">
-        <!-- 聊天区域 -->
-        <div class="chat-section" v-if="messages.length > 0" ref="chatSection">
-            <!-- 消息列表 -->
-            <div v-for="(message, index) in messages" :key="index" class="chat-message">
-                <div class="message-content user-message chat" v-if="message.content">
-                    <div v-html="message.content"></div>
-                    <!-- 显示上传的文件 -->
-                    <div class="message-files" v-if="message.sourceFiles && message.sourceFiles.length > 0">
-                        <div v-for="(file, index) in message.sourceFiles" :key="index" class="message-file-item">
-                            <span>{{ file.title }}</span>
-                        </div>
-                    </div>
-                </div>
-                <div class="message-content ai-message chat" v-if="message.aiContent || message.loading">
-                    <div v-if="message.aiContent" v-html="message.aiContent"></div>
-                    <svg v-else xmlns="http://www.w3.org/2000/svg" width="32" height="32"
-                        viewBox="0 0 24 24"><!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
-                        <circle cx="4" cy="12" r="3" fill="currentColor">
-                            <animate id="svgSpinners3DotsBounce0" attributeName="cy"
-                                begin="0;svgSpinners3DotsBounce1.end+0.25s" calcMode="spline" dur="0.6s"
-                                keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
-                        </circle>
-                        <circle cx="12" cy="12" r="3" fill="currentColor">
-                            <animate attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.1s" calcMode="spline"
-                                dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
-                        </circle>
-                        <circle cx="20" cy="12" r="3" fill="currentColor">
-                            <animate id="svgSpinners3DotsBounce1" attributeName="cy"
-                                begin="svgSpinners3DotsBounce0.begin+0.2s" calcMode="spline" dur="0.6s"
-                                keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
-                        </circle>
+  <div class="ai-chat-container">
+    <div class="message-quick-box">
+      <div class="message-quick-box-item" :class="{ 'active': isQuickActions.includes('quick') }">
+        <button type="button" class="coco-collapse-trigger" @click="toggleQuickActions('quick')">
+          <span class="coco-collapse-icon soft-orange">
+            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
+              stroke-linecap="round">
+              <path d="M4 7h16"></path>
+              <path d="M4 12h10"></path>
+              <path d="M4 17h7"></path>
+            </svg>
+          </span>
+          <span class="coco-collapse-copy">
+            <span class="coco-collapse-title">{{ lang.ssAiChatQuickTitle }}</span>
+            <span class="coco-collapse-subtitle" id="cocoQuickSectionHint">{{ lang.ssAiChatQuickSubtitle }}</span>
+          </span>
+          <span class="coco-collapse-chevron">
+            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <polyline points="6 9 12 15 18 9"></polyline>
+            </svg>
+          </span>
+        </button>
+        <div class="coco-collapse-body" v-if="isQuickActions.includes('quick')">
+          <div class="coco-quick-tabs">
+            <button type="button" class="coco-quick-tab" :class="{ 'active': cocoQuickTab === 'page' }"
+              @click="setCocoQuickTab('page')">{{ lang.ssAiChatQuickTabPage }}</button>
+            <button type="button" class="coco-quick-tab" :class="{ 'active': cocoQuickTab === 'course' }"
+              @click="setCocoQuickTab('course')">{{ lang.ssAiChatQuickTabCourse }}</button>
+          </div>
+          <div class="coco-quick-list">
+            <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'page'" @click="sendQuickAction(lang.ssAiChatQuickAction3)">
+              <span class="coco-quick-action-copy">
+                <span class="coco-quick-action-title">{{ lang.ssAiChatQuickGenChoicesTitle }}</span>
+                <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickGenChoicesDesc }}</span>
+              </span>
+              <span class="coco-quick-action-arrow">
+                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
+                  stroke-linecap="round" stroke-linejoin="round">
+                  <path d="M5 12h14"></path>
+                  <path d="M13 5l7 7-7 7"></path>
+                </svg>
+              </span>
+            </button>
+
+            <!-- <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'page'" @click="sendQuickAction(lang.ssAiChatQuickAction4)">
+              <span class="coco-quick-action-copy">
+                <span class="coco-quick-action-title">{{ lang.ssAiChatQuickGenWebTitle }}</span>
+                <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickGenWebDesc }}</span>
+              </span>
+              <span class="coco-quick-action-arrow">
+                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
+                  stroke-linecap="round" stroke-linejoin="round">
+                  <path d="M5 12h14"></path>
+                  <path d="M13 5l7 7-7 7"></path>
+                </svg>
+              </span>
+            </button> -->
+            <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'course'"  @click="sendQuickAction(lang.ssAiChatQuickAction5)">
+                <span class="coco-quick-action-copy">
+                    <span class="coco-quick-action-title">{{ lang.ssAiChatQuickRecommendToolsTitle }}</span>
+                    <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickRecommendToolsDesc }}</span>
+                </span>
+                <span class="coco-quick-action-arrow">
+                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                        <path d="M5 12h14"></path>
+                        <path d="M13 5l7 7-7 7"></path>
                     </svg>
-                    <button class="confirm-btn" :class="{ disabled: message.jsonData?.isGenerate }" v-if="message.jsonData?.gType !== 'chat' && !message.chatloading && message.aiContent"
-                        @click="generate(message)">{{ message.gLoading ? lang.ssLoading : lang.ssConfirm}}</button>
-                </div>
-            </div>
+                </span>
+            </button>
+            <!-- <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'course'">
+                <span class="coco-quick-action-copy">
+                    <span class="coco-quick-action-title">{{ lang.ssAiChatQuickBatchGenToolsTitle }}</span>
+                    <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickBatchGenToolsDesc }}</span>
+                </span>
+                <span class="coco-quick-action-arrow">
+                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                        <path d="M5 12h14"></path>
+                        <path d="M13 5l7 7-7 7"></path>
+                    </svg>
+                </span>
+            </button> -->
+          </div>
         </div>
-        <!-- 输入区域 -->
-        <div class="input-section">
-            <div class="input-wrapper">
-                <div class="file-box" v-show="files.length">
-                    <div v-for="(file, index) in files" :key="index" class="file-item">
-                        <span class="file-name">{{ file.title }}</span>
-                        <button class="remove-file-btn" @click="removeFile(index)">
-                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
-                                <line x1="18" y1="6" x2="6" y2="18"></line>
-                                <line x1="6" y1="6" x2="18" y2="18"></line>
-                            </svg>
-                        </button>
-                    </div>
-                </div>
-                <textarea class="ai-input"
-                    :placeholder="messages.length === 0 ? lang.ssAiChatExample : lang.ssAiChatShortcut"
-                    v-model="inputText" @keyup.enter.exact="sendMessage" rows="5" />
-                <div class="input-actions">
-                    <FileInput accept="*" @change="handleFileUpload" >
-                        <button class="attach-btn">
-                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-                                <path
-                                    d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48">
-                                </path>
-                            </svg>
-                        </button>
-                    </FileInput>
-                    <button class="send-btn" @click="sendMessage" v-if="!chatLoading">
-                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
-                            stroke-linecap="round" stroke-linejoin="round">
-                            <line x1="22" y1="2" x2="11" y2="13"></line>
-                            <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
-                        </svg>
-                    </button>
-                    <button class="send-btn stop" @click="stopMessage" v-if="chatLoading">
-                        <svg width="32" height="32" viewBox="0 0 32 32"
-                            fill="none" xmlns="http://www.w3.org/2000/svg">
-                            <rect width="32" height="32" rx="16" fill="black" fill-opacity="0.4"></rect>
-                            <path
-                                d="M11.3333 12.333C11.3333 11.7807 11.781 11.333 12.3333 11.333H19.6666C20.2189 11.333 20.6666 11.7807 20.6666 12.333V19.6663C20.6666 20.2186 20.2189 20.6663 19.6666 20.6663H12.3333C11.781 20.6663 11.3333 20.2186 11.3333 19.6663V12.333Z"
-                                fill="white" fill-opacity="0.9"></path>
-                        </svg>
-                    </button>
-                </div>
+      </div>
+    </div>
+    <!-- 聊天区域 -->
+    <div class="chat-section" ref="chatSection">
+      <!-- 消息列表 -->
+      <div v-for="(message, index) in messages" :key="index" class="chat-message">
+        <div class="message-content user-message chat" v-if="message.content">
+          <div v-html="message.content"></div>
+          <!-- 显示上传的文件 -->
+          <div class="message-files" v-if="message.sourceFiles && message.sourceFiles.length > 0">
+            <div v-for="(file, index) in message.sourceFiles" :key="index" class="message-file-item">
+              <span>{{ file.title }}</span>
             </div>
-                    <!-- 输入时的快捷操作弹出 -->
-        <div class="quick-actions-popup" v-if="showQuickActions">
-            <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn" @click="sendQuickAction(action)">{{ action }}</button>
+          </div>
         </div>
+        <div class="message-content ai-message chat" v-if="message.aiContent || message.loading">
+          <div v-if="message.aiContent" v-html="message.aiContent"></div>
+          <svg v-else xmlns="http://www.w3.org/2000/svg" width="32" height="32"
+            viewBox="0 0 24 24"><!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
+            <circle cx="4" cy="12" r="3" fill="currentColor">
+              <animate id="svgSpinners3DotsBounce0" attributeName="cy" begin="0;svgSpinners3DotsBounce1.end+0.25s"
+                calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
+            </circle>
+            <circle cx="12" cy="12" r="3" fill="currentColor">
+              <animate attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.1s" calcMode="spline" dur="0.6s"
+                keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
+            </circle>
+            <circle cx="20" cy="12" r="3" fill="currentColor">
+              <animate id="svgSpinners3DotsBounce1" attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.2s"
+                calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
+            </circle>
+          </svg>
+          <button class="confirm-btn" :class="{ disabled: message.jsonData?.isGenerate }"
+            v-if="message.jsonData?.gType !== 'chat' && !message.chatloading && message.aiContent"
+            @click="generate(message)">{{ message.gLoading ? lang.ssLoading : lang.ssConfirm }}</button>
         </div>
-        <!-- 初始状态 -->
-        <div class="initial-state" v-if="messages.length === 0">
-            <!-- 快捷操作 -->
-            <div class="quick-actions">
-                <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn" @click="sendQuickAction(action)">{{ action }}</button>
-            </div>
+      </div>
+    </div>
+    <!-- 输入区域 -->
+    <div class="input-section">
+      <div class="input-wrapper">
+        <div class="file-box" v-show="files.length">
+          <div v-for="(file, index) in files" :key="index" class="file-item">
+            <span class="file-name">{{ file.title }}</span>
+            <button class="remove-file-btn" @click="removeFile(index)">
+              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
+                stroke-linejoin="round">
+                <line x1="18" y1="6" x2="6" y2="18"></line>
+                <line x1="6" y1="6" x2="18" y2="18"></line>
+              </svg>
+            </button>
+          </div>
+        </div>
+        <textarea class="ai-input" :placeholder="lang.ssAiChatShortcut"
+          v-model="inputText" @keyup.enter.exact="sendMessage" rows="5" />
+        <div class="input-actions">
+          <FileInput accept="*" @change="handleFileUpload">
+            <button class="attach-btn">
+              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <path
+                  d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48">
+                </path>
+              </svg>
+            </button>
+          </FileInput>
+          <button class="send-btn" @click="sendMessage" v-if="!chatLoading">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
+              stroke-linejoin="round">
+              <line x1="22" y1="2" x2="11" y2="13"></line>
+              <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
+            </svg>
+          </button>
+          <button class="send-btn stop" @click="stopMessage" v-if="chatLoading">
+            <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <rect width="32" height="32" rx="16" fill="black" fill-opacity="0.4"></rect>
+              <path
+                d="M11.3333 12.333C11.3333 11.7807 11.781 11.333 12.3333 11.333H19.6666C20.2189 11.333 20.6666 11.7807 20.6666 12.333V19.6663C20.6666 20.2186 20.2189 20.6663 19.6666 20.6663H12.3333C11.781 20.6663 11.3333 20.2186 11.3333 19.6663V12.333Z"
+                fill="white" fill-opacity="0.9"></path>
+            </svg>
+          </button>
         </div>
+      </div>
+      <!-- 输入时的快捷操作弹出 -->
+      <div class="quick-actions-popup" v-if="showQuickActions">
+        <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn"
+          @click="sendQuickAction(action)">{{ action }}</button>
+      </div>
     </div>
+    <!-- 初始状态 -->
+    <div class="initial-state" v-if="false">
+      <!-- 快捷操作 -->
+      <div class="quick-actions">
+        <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn"
+          @click="sendQuickAction(action)">{{ action }}</button>
+      </div>
+    </div>
+  </div>
 </template>
 
 <script lang="ts" setup>
@@ -109,48 +193,48 @@ import axios from '@/services/config'
 import message from '@/utils/message'
 
 interface ChatMessage {
-    uid?: string
-    role: 'ai' | 'user'
-    content?: string
-    aiContent?: string
-    oldContent?: string
-    loading?: boolean
-    chatloading?: boolean
-    gLoading?: boolean
-    rawContent?: string
-    timestamp?: Date
-    like?: boolean
-    unlike?: boolean
-    isTyping?: boolean
-    AI?: string
-    isShowSynchronization?: boolean
-    filename?: string
-    is_mind_map?: boolean
-    sourceFiles?: Array<{
-        title: string
-        id?: string
-        url?: string
+  uid?: string
+  role: 'ai' | 'user'
+  content?: string
+  aiContent?: string
+  oldContent?: string
+  loading?: boolean
+  chatloading?: boolean
+  gLoading?: boolean
+  rawContent?: string
+  timestamp?: Date
+  like?: boolean
+  unlike?: boolean
+  isTyping?: boolean
+  AI?: string
+  isShowSynchronization?: boolean
+  filename?: string
+  is_mind_map?: boolean
+  sourceFiles?: Array<{
+    title: string
+    id?: string
+    url?: string
+  }>
+  jsonData?: {
+    gType?: string
+    isGenerate?: boolean
+    headUrl?: string
+    assistantName?: string
+    files?: Array<{
+      title: string
+      id?: string
+      url?: string
     }>
-    jsonData?: {
-        gType?: string
-        isGenerate?: boolean
-        headUrl?: string
-        assistantName?: string
-        files?: Array<{
-            title: string
-            id?: string
-            url?: string
-        }>
-        sourceArray?: Array<{
-            text?: string
-            id?: string
-            title?: string
-        }>
-    }
+    sourceArray?: Array<{
+      text?: string
+      id?: string
+      title?: string
+    }>
+  }
 }
 
 const props = withDefaults(defineProps<{
-    userid?: string | null
+  userid?: string | null
 }>(), {
   userid: null,
 })
@@ -170,11 +254,14 @@ const files = ref<Array<{ title: string; id?: string | null; url?: string; isPro
 const quickActions = [
   lang.ssAiChatQuickAction1,
   lang.ssAiChatQuickAction2,
+  lang.ssAiChatQuickAction6,
+  lang.ssAiChatQuickAction7,
 ]
 
 // 监听输入变化,当输入"/"时显示快捷操作
 watch(inputText, (newValue) => {
-  if (messages.value.length > 0 && newValue === '/') {
+  // messages.value.length > 0 && 
+  if (newValue === '/') {
     showQuickActions.value = true
   }
   else if (newValue !== '/') {
@@ -242,7 +329,7 @@ const stopMessage = () => {
 const handleFileUpload = async (files2: File[]) => {
   const maxSize = 10 * 1024 * 1024 // 10MB
   const uploadPromises = []
-  
+
   for (let i = 0; i < files2.length; i++) {
     const file = files2[i]
     if (file.size > maxSize) {
@@ -281,10 +368,10 @@ const handleFileUpload = async (files2: File[]) => {
         files.value.splice(fileIndex, 1)
       }
     })
-    
+
     uploadPromises.push(uploadPromise)
   }
-  
+
   // 等待所有文件上传完成
   await Promise.allSettled(uploadPromises)
 }
@@ -367,7 +454,7 @@ const sendAction = async (action: string) => {
       const tempElement = document.createElement('div')
       tempElement.innerHTML = textElement.content
       return tempElement.textContent || tempElement.innerText || ''
-      
+
 
     })
     .filter(content => content.trim() !== '') || []
@@ -482,7 +569,7 @@ const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
     const timestamp = Date.now()
     const finalExtension = file.name.split('.').pop()?.toLowerCase() || ''
     const baseName = file.name.slice(0, -(finalExtension.length + 1))
-    
+
     formData.append(
       'file',
       new File([file], `${baseName}${timestamp}.${finalExtension}`)
@@ -504,7 +591,7 @@ const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
         signal: signal
       }
     )
-    
+
     console.log(res)
     return res
   }
@@ -522,6 +609,22 @@ const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
 const agentid1 = ref('cbb29b41-2a4a-4453-bf8d-357929ced4bd')// 判断意图
 const agentid2 = ref('f86aa63c-b7b7-4d03-9b37-b59f116d36f3')// 生成内容
 
+const isQuickActions = ref<string[]>([])
+const toggleQuickActions = (section: string) => {
+  if (isQuickActions.value.includes(section)) {
+    isQuickActions.value = isQuickActions.value.filter((item: string) => item !== section)
+  }
+  else {
+    isQuickActions.value.push(section)
+  }
+}
+
+const cocoQuickTab = ref('page')
+const setCocoQuickTab = (tab: string) => {
+  cocoQuickTab.value = tab
+}
+
+
 
 onMounted(() => {
   session_name.value = uuidv4()
@@ -532,379 +635,549 @@ onMounted(() => {
 
 <style lang="scss" scoped>
 .ai-chat-container {
-    width: 100%;
-    height: 100%;
-    display: flex;
-    flex-direction: column;
-    padding: 16px;
-    gap: 16px;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  padding: 16px;
+  gap: 16px;
 }
 
 .input-section {
-    display: flex;
-    flex-direction: column;
-    gap: 12px;
-    position: relative;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  position: relative;
 }
 
 .input-wrapper {
-    position: relative;
-    display: flex;
-    flex-direction: column;
-    background: #fafbfc;
-    border: 1.5px solid #e5e7eb;
-    border-radius: 8px;
-    padding: 8px 12px;
-    min-height: 120px;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  background: #fafbfc;
+  border: 1.5px solid #e5e7eb;
+  border-radius: 8px;
+  padding: 8px 12px;
+  min-height: 120px;
 }
 
 .ai-input {
-    flex: 1;
-    border: none;
-    background: transparent;
-    font-size: 14px;
-    color: #374151;
-    outline: none;
-    resize: none;
-    min-height: 80px;
-
-    &::placeholder {
-        color: #9CA3AF;
-    }
+  flex: 1;
+  border: none;
+  background: transparent;
+  font-size: 14px;
+  color: #374151;
+  outline: none;
+  resize: none;
+  min-height: 80px;
+
+  &::placeholder {
+    color: #9CA3AF;
+  }
 }
 
 .input-actions {
-    display: flex;
-    // justify-content: space-between;
-    align-items: center;
-    margin-top: 8px;
+  display: flex;
+  // justify-content: space-between;
+  align-items: center;
+  margin-top: 8px;
 }
 
 .attach-btn {
-    width: 32px;
-    height: 32px;
-    background: none;
-    border: none;
-    cursor: pointer;
-    padding: 4px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    transition: all 0.3s ease;
-    color: #6b7280;
-
-    svg {
-        width: 20px;
-        height: 20px;
-    }
+  width: 32px;
+  height: 32px;
+  background: none;
+  border: none;
+  cursor: pointer;
+  padding: 4px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.3s ease;
+  color: #6b7280;
+
+  svg {
+    width: 20px;
+    height: 20px;
+  }
 
-    &:hover {
-        background: #FFF4E5;
-        color: #F78B22;
-        border-radius: 4px;
-    }
+  &:hover {
+    background: #FFF4E5;
+    color: #F78B22;
+    border-radius: 4px;
+  }
 
-    input[type="file"] {
-        display: none;
-    }
+  input[type="file"] {
+    display: none;
+  }
 }
 
 .file-box {
-    margin-bottom: 8px;
-    min-height: 24px;
-    max-height: 70px;
-    overflow-y: auto;
-
-    .file-item {
-        display: flex;
-        align-items: center;
-        background: #f5f5f5;
-        padding: 4px 8px;
-        border-radius: 4px;
-        margin-bottom: 4px;
-
-        .file-name {
-            flex: 1;
-            font-size: 12px;
-            color: #374151;
-            overflow: hidden;
-            text-overflow: ellipsis;
-            white-space: nowrap;
-        }
+  margin-bottom: 8px;
+  min-height: 24px;
+  max-height: 70px;
+  overflow-y: auto;
 
-        .remove-file-btn {
-            background: none;
-            border: none;
-            cursor: pointer;
-            color: #9CA3AF;
-            font-size: 12px;
-            padding: 2px;
-            margin-left: 8px;
-            display: flex;
-            align-items: center;
-            justify-content: center;
+  .file-item {
+    display: flex;
+    align-items: center;
+    background: #f5f5f5;
+    padding: 4px 8px;
+    border-radius: 4px;
+    margin-bottom: 4px;
+
+    .file-name {
+      flex: 1;
+      font-size: 12px;
+      color: #374151;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
 
-            svg {
-                width: 14px;
-                height: 14px;
-            }
+    .remove-file-btn {
+      background: none;
+      border: none;
+      cursor: pointer;
+      color: #9CA3AF;
+      font-size: 12px;
+      padding: 2px;
+      margin-left: 8px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      svg {
+        width: 14px;
+        height: 14px;
+      }
 
-            &:hover {
-                color: #EF4444;
-            }
-        }
+      &:hover {
+        color: #EF4444;
+      }
     }
+  }
 }
 
 .send-btn {
-    margin-left: auto;
-    width: 32px;
-    height: 32px;
-    border: none;
-    background: #FF9300;
-    color: #fff;
-    border-radius: 50%;
-    cursor: pointer;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    transition: all 0.3s ease;
+  margin-left: auto;
+  width: 32px;
+  height: 32px;
+  border: none;
+  background: #FF9300;
+  color: #fff;
+  border-radius: 50%;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.3s ease;
+
+  svg {
+    width: 20px;
+    height: 20px;
+  }
 
-    svg {
-        width: 20px;
-        height: 20px;
-    }
+  &:hover {
+    background: #E68A00;
+  }
 
-    &:hover {
-        background: #E68A00;
-    }
+  &.stop {
+    background: unset;
+    width: auto;
 
-    &.stop {
-        background: unset;
-        width: auto;
-        svg {
-            width: 32px;
-            height: 32px;
-        }
+    svg {
+      width: 32px;
+      height: 32px;
     }
+  }
 }
 
 .quick-actions {
-    // display: flex;
-    // flex-direction: column;
-    // gap: 8px;
-
-    .quick-action-btn {
-        padding: 5px 10px;
-        border: 1px solid #f7c58f;
-        background: #FFF9F2;
-        color: #6b4a1f;
-        border-radius: 16px;
-        font-size: 12px;
-        cursor: pointer;
-        text-align: left;
-        display: block;
-
-        &:hover {
-            border-color: #F78B22;
-            background: #FFF4E5;
-            color: #111827;
-        }
+  // display: flex;
+  // flex-direction: column;
+  // gap: 8px;
+
+  .quick-action-btn {
+    padding: 5px 10px;
+    border: 1px solid #f7c58f;
+    background: #FFF9F2;
+    color: #6b4a1f;
+    border-radius: 16px;
+    font-size: 12px;
+    cursor: pointer;
+    text-align: left;
+    display: block;
 
-        +.quick-action-btn {
-            margin-top: 8px;
-        }
+    &:hover {
+      border-color: #F78B22;
+      background: #FFF4E5;
+      color: #111827;
+    }
+
+    +.quick-action-btn {
+      margin-top: 8px;
     }
+  }
 }
 
 
 
 .chat-section {
-    // flex: 1;
-    height: calc(100% - 155px);
-    overflow-y: auto;
-    display: flex;
-    flex-direction: column;
-    // gap: 16px;
-    padding-right: 8px;
-
-    &::-webkit-scrollbar {
-        width: 6px;
-    }
+  // flex: 1;
+  height: calc(100% - 155px);
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  // gap: 16px;
+  padding-right: 8px;
+
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
 
-    &::-webkit-scrollbar-track {
-        background: #F3F4F6;
-        border-radius: 3px;
-    }
+  &::-webkit-scrollbar-track {
+    background: #F3F4F6;
+    border-radius: 3px;
+  }
 
-    &::-webkit-scrollbar-thumb {
-        background: #D1D5DB;
-        border-radius: 3px;
+  &::-webkit-scrollbar-thumb {
+    background: #D1D5DB;
+    border-radius: 3px;
 
-        &:hover {
-            background: #9CA3AF;
-        }
+    &:hover {
+      background: #9CA3AF;
     }
+  }
 }
 
 .chat-message {
-    max-width: 100%;
-    margin-bottom: 10px;
+  max-width: 100%;
+  margin-bottom: 10px;
+  display: flex;
+  flex-direction: column;
+
+  .message-content {
     display: flex;
     flex-direction: column;
+    border-radius: 8px;
+    padding: 8px 10px;
+    font-size: 14px;
+    line-height: 1.5;
+    color: #374151;
+    width: fit-content;
 
-    .message-content {
-        display: flex;
-        flex-direction: column;
-        border-radius: 8px;
-        padding: 8px 10px;
-        font-size: 14px;
-        line-height: 1.5;
-        color: #374151;
-        width: fit-content;
-
-        +.message-content {
-            margin-top: 10px;
-        }
+    +.message-content {
+      margin-top: 10px;
     }
+  }
 }
 
 .message-content {
-    word-break: break-word;
+  word-break: break-word;
 
-    &.ai-message {
-        align-self: flex-start;
-        background: #fafbfc;
-        border: 1.5px solid #e5e7eb;
-        border-bottom-left-radius: 2px;
+  &.ai-message {
+    align-self: flex-start;
+    background: #fafbfc;
+    border: 1.5px solid #e5e7eb;
+    border-bottom-left-radius: 2px;
 
-        &>svg {
-            width: 17px;
-            height: 17px;
-        }
+    &>svg {
+      width: 17px;
+      height: 17px;
     }
+  }
 }
 
 .message-content {
-    &.user-message {
-        align-self: flex-end;
-        background: #FFF4E5;
-        border: 1.5px solid #F78B22;
-        border-bottom-right-radius: 2px;
-    }
+  &.user-message {
+    align-self: flex-end;
+    background: #FFF4E5;
+    border: 1.5px solid #F78B22;
+    border-bottom-right-radius: 2px;
+  }
 }
 
 .initial-state {
-    display: flex;
-    flex-direction: column;
-    justify-content: flex-start;
-    align-items: flex-start;
-    // padding: 24px;
-    gap: 16px;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  align-items: flex-start;
+  // padding: 24px;
+  gap: 16px;
 }
 
 
 .confirm-btn {
-    margin-top: 10px;
-    padding: 6px 15px;
-    background: #FF9300;
-    color: white;
+  margin-top: 10px;
+  padding: 6px 15px;
+  background: #FF9300;
+  color: white;
+  border: none;
+  border-radius: 8px;
+  font-size: 14px;
+  cursor: pointer;
+  margin-left: auto;
+  transition: all 0.3s ease;
+
+  &:hover {
+    background: #E68A00;
+  }
+
+  &.disabled {
+    background: #9CA3AF;
+    cursor: not-allowed;
+  }
+}
+
+ul {
+  margin: 8px 0;
+  padding-left: 20px;
+
+  li {
+    margin: 4px 0;
+  }
+}
+
+.quick-actions-popup {
+  position: absolute;
+  bottom: 100%;
+  left: 0;
+  right: 0;
+  background: white;
+  border: 1px solid #E5E7EB;
+  border-radius: 8px;
+  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  padding: 8px;
+  z-index: 100;
+  margin-bottom: 8px;
+
+  .quick-action-btn {
+    width: 100%;
+    text-align: left;
+    padding: 10px 12px;
+    background: white;
     border: none;
-    border-radius: 8px;
+    border-radius: 6px;
     font-size: 14px;
+    color: #374151;
     cursor: pointer;
-    margin-left: auto;
-    transition: all 0.3s ease;
+    transition: all 0.2s ease;
 
     &:hover {
-        background: #E68A00;
-    }
-    &.disabled {
-        background: #9CA3AF;
-        cursor: not-allowed;
+      background: #fff4e5;
     }
+  }
 }
 
-ul {
-    margin: 8px 0;
-    padding-left: 20px;
+.message-quick-box {
+  display: flex;
+  flex-direction: column;
+  flex-shrink: 0;
+  gap: 10px;
+
+  .message-quick-box-item {
+    box-shadow: rgba(17, 24, 39, 0.04) 0px 10px 30px;
+    border-width: 1px;
+    border-style: solid;
+    border-color: rgb(239, 229, 216);
+    border-image: initial;
+    border-radius: 18px;
+    background: rgba(255, 255, 255, 0.96);
+    overflow: hidden;
 
-    li {
-        margin: 4px 0;
+
+    .coco-collapse-trigger {
+      width: 100%;
+      display: flex;
+      align-items: center;
+      cursor: pointer;
+      text-align: left;
+      gap: 12px;
+      border-width: initial;
+      border-style: none;
+      border-color: initial;
+      border-image: initial;
+      background: transparent;
+      padding: 12px 14px;
+
+      &:hover {
+        background: rgb(255, 251, 245);
+      }
     }
-}
 
-.quick-actions-popup {
-    position: absolute;
-    bottom: 100%;
-    left: 0;
-    right: 0;
-    background: white;
-    border: 1px solid #E5E7EB;
-    border-radius: 8px;
-    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
-    padding: 8px;
-    z-index: 100;
-    margin-bottom: 8px;
-
-    .quick-action-btn {
-        width: 100%;
-        text-align: left;
-        padding: 10px 12px;
-        background: white;
-        border: none;
-        border-radius: 6px;
+    .coco-collapse-icon.soft-orange {
+      color: rgb(154, 91, 17);
+      background: rgb(255, 240, 219);
+    }
+
+    .coco-collapse-icon {
+      width: 34px;
+      height: 34px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-shrink: 0;
+      border-radius: 12px;
+    }
+
+    .coco-collapse-copy {
+      min-width: 0px;
+      display: flex;
+      flex-direction: column;
+      flex: 1 1 0%;
+      gap: 2px;
+
+      .coco-collapse-title {
         font-size: 14px;
-        color: #374151;
-        cursor: pointer;
-        transition: all 0.2s ease;
+        font-weight: 700;
+        color: rgb(17, 24, 39);
+      }
+
+      .coco-collapse-subtitle {
+        font-size: 12px;
+        color: rgb(139, 115, 86);
+        line-height: 1.5;
+      }
+    }
+
+    .coco-collapse-chevron {
+      color: rgb(156, 163, 175);
+      flex-shrink: 0;
+
+      svg {
+        transition: transform 0.2s;
+      }
+    }
+
+    &.active {
+      .coco-collapse-chevron svg {
+        transform: rotate(180deg);
+      }
+    }
 
-        &:hover {
-            background: #fff4e5;
+    .coco-collapse-body {
+      padding: 0px 14px 14px;
+      .coco-quick-tabs {
+        display: grid;
+        grid-template-columns: repeat(2, minmax(0px, 1fr));
+        margin-bottom: 12px;
+        gap: 8px;
+        .coco-quick-tab {
+          height: 34px;
+          color: rgb(124, 92, 56);
+          font-size: 12px;
+          font-weight: 600;
+          cursor: pointer;
+          border-radius: 12px;
+          border-width: 1px;
+          border-style: solid;
+          border-color: rgb(234, 223, 206);
+          border-image: initial;
+          background: rgb(250, 247, 242);
+          transition: 0.2s;
+          &.active {
+            color: rgb(154, 91, 17);
+            box-shadow: rgba(247, 139, 34, 0.12) 0px 8px 18px;
+            border-color: rgba(247, 139, 34, 0.48);
+            background: rgb(255, 244, 229);
+          }
         }
+      }
+
+      .coco-quick-list {
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+        .coco-quick-action {
+          width: 100%;
+          display: flex;
+          align-items: flex-start;
+          cursor: pointer;
+          text-align: left;
+          gap: 12px;
+          border-width: 1px;
+          border-style: solid;
+          border-color: rgb(239, 229, 216);
+          border-image: initial;
+          background: rgb(255, 253, 250);
+          border-radius: 14px;
+          padding: 12px;
+          transition: 0.2s;
+          .coco-quick-action-copy {
+            min-width: 0px;
+            display: flex;
+            flex-direction: column;
+            flex: 1 1 0%;
+            gap: 4px;
+            .coco-quick-action-title {
+                font-size: 13px;
+                font-weight: 700;
+                color: rgb(17, 24, 39);
+                line-height: 1.4;
+            }
+            
+
+            .coco-quick-action-desc {
+                font-size: 12px;
+                line-height: 1.6;
+                color: rgb(139, 115, 86);
+            }
+          }
+
+          .coco-quick-action-arrow {
+            color: rgb(212, 163, 115);
+            flex-shrink: 0;
+            padding-top: 2px;
+          }
+        }
+
+      }
+
     }
+  }
 }
 </style>
 
 <style>
 .chat table {
-    text-align: center;
-    border-spacing: 0;
-    border-left: 1px solid #000;
-    border-bottom: 1px solid #000;
+  text-align: center;
+  border-spacing: 0;
+  border-left: 1px solid #000;
+  border-bottom: 1px solid #000;
 }
 
 .chat table td,
 .chat table th {
-    border-top: 1px solid #000;
-    border-right: 1px solid #000;
-    padding: 10px;
+  border-top: 1px solid #000;
+  border-right: 1px solid #000;
+  padding: 10px;
 }
 
 .message-files {
-    margin-top: 8px;
-    display: flex;
-    flex-direction: column;
-    gap: 4px;
+  margin-top: 8px;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
 }
 
 .message-file-item {
-    display: flex;
-    align-items: center;
-    gap: 6px;
-    font-size: 12px;
-    color: #6b7280;
-    background: #f3f4f6;
-    padding: 4px 8px;
-    border-radius: 4px;
-    max-width: 200px;
-    overflow: hidden;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  color: #6b7280;
+  background: #f3f4f6;
+  padding: 4px 8px;
+  border-radius: 4px;
+  max-width: 200px;
+  overflow: hidden;
 }
 
 .message-file-item span {
-    flex: 1;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
+  flex: 1;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 </style>

+ 180 - 0
src/components/CollapsibleToolbar/componets/aiWeb.vue

@@ -0,0 +1,180 @@
+<template>
+  <div v-if="visible" class="aiweb-modal-overlay" @click.self="closeModal">
+    <div class="aiweb-modal">
+      <div class="aiweb-modal-header">
+        <span class="aiweb-modal-title">{{ lang.ssWebEditor }}</span>
+        <button class="aiweb-modal-close" @click="closeModal">
+          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M18 6L6 18"></path>
+            <path d="M6 6l12 12"></path>
+          </svg>
+        </button>
+      </div>
+      <div class="aiweb-modal-body">
+        <iframe :src="iframeUrl" frameborder="0" class="aiweb-iframe" ref="iframeRef"></iframe>
+      </div>
+      <div class="aiweb-modal-footer">
+        <button class="aiweb-btn aiweb-btn-close" @click="closeModal">{{ lang.ssClose }}</button>
+        <button class="aiweb-btn aiweb-btn-confirm" @click="handleConfirm" :disabled="isLoading">
+          {{ isLoading ? lang.ssCreating : lang.ssConfirmAdd }}
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref } from 'vue'
+import { lang } from '@/main'
+
+const props = defineProps<{
+  visible: boolean
+  webId: string
+  isLoading: boolean
+}>()
+
+const emit = defineEmits<{
+  (e: 'close'): void
+  (e: 'add', webCode: string): void
+}>()
+
+const iframeUrl = computed(() => {
+  let url = 'https://beta.app.cocorobo.cn'
+  if (window.location.href.includes('beta')) {
+    url = 'https://beta.app.cocorobo.cn'
+  }
+  else if (lang.lang === 'cn') {
+    url = 'https://app.cocorobo.cn'
+  }
+  else if (lang.lang === 'hk') {
+    url = 'https://app.cocorobo.hk'
+  }
+  else if (lang.lang === 'en') {
+    url = 'https://app.cocorobo.com'
+  }
+  if (!props.webId) return ''
+  return `${url}/#/web?id=${props.webId}&create=false&isPPT=true`
+})
+
+const closeModal = () => {
+  emit('close')
+}
+
+const iframeRef = ref<HTMLIFrameElement>()
+
+const handleConfirm = () => {
+  const webCode = iframeRef.value?.contentWindow?.ReturnWebCode() as string || ''
+  console.log(webCode)
+  if (!webCode) return
+  console.log('确定按钮被点击')
+  emit('add', webCode)
+}
+</script>
+
+<style lang="scss" scoped>
+.aiweb-modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.aiweb-modal {
+  width: 90%;
+  max-width: 1200px;
+  height: 90vh;
+  background-color: #fff;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+}
+
+.aiweb-modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 20px;
+  border-bottom: 1px solid #eee;
+  background-color: #f8f9fa;
+}
+
+.aiweb-modal-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #333;
+}
+
+.aiweb-modal-close {
+  background: none;
+  border: none;
+  cursor: pointer;
+  padding: 8px;
+  color: #666;
+  transition: color 0.2s;
+
+  &:hover {
+    color: #333;
+  }
+
+  svg {
+    width: 20px;
+    height: 20px;
+  }
+}
+
+.aiweb-modal-body {
+  height: calc(100% - 140px);
+}
+
+.aiweb-modal-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  padding: 16px 20px;
+  border-top: 1px solid #eee;
+  background-color: #f8f9fa;
+}
+
+.aiweb-btn {
+  padding: 8px 20px;
+  border-radius: 4px;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s;
+  border: 1px solid transparent;
+}
+
+.aiweb-btn-close {
+  background-color: #fff;
+  border-color: #d9d9d9;
+  color: #666;
+
+  &:hover {
+    background-color: #f5f5f5;
+    border-color: #bfbfbf;
+  }
+}
+
+.aiweb-btn-confirm {
+  background-color: #1890ff;
+  border-color: #1890ff;
+  color: #fff;
+
+  &:hover {
+    background-color: #40a9ff;
+    border-color: #40a9ff;
+  }
+}
+
+.aiweb-iframe {
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 2 - 0
src/components/CollapsibleToolbar/index.vue

@@ -283,11 +283,13 @@ const getTypeLabel = (type?: number) => {
     15: lang.ssQATest,
     72: lang.ssAiApp,
     73: lang.ssHPage,
+    81: lang.ssHPage,
     74: lang.ssVideo,
     75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
     76: lang.ssCreative,
     77: lang.ssEnglishSpeakingTool,
     78: lang.ssVote,
+    79: lang.ssPhoto,
   }
   return typeMap[type || 0] || lang.ssUnknown
 }

+ 1009 - 130
src/components/CollapsibleToolbar/index2.vue

@@ -2,7 +2,8 @@
   <div class="collapsible-toolbar" :class="{ collapsed: isCollapsed }">
     <div class="toolbar-content" v-show="!isCollapsed">
       <div class="sidebar-content">
-        <div class="sidebar-item" :class="{ active: activeSubmenu === 'cocoai' }" @click="toggleSubmenu('cocoai')">
+        <div class="sidebar-item feature-sidebar-item" :class="{ active: activeSubmenu === 'cocoai' }"
+          @click="toggleSubmenu('cocoai')">
           <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
             <path d="M12 2L2 7l10 5 10-5-10-5z"></path>
             <path d="M2 17l10 5 10-5"></path>
@@ -10,7 +11,18 @@
           </svg>
           <span class="item-label">Coco AI</span>
         </div>
-        <div class="sidebar-item" :class="{ active: activeSubmenu === 'page' }" @click="toggleSubmenu('page')">
+        <div class="sidebar-item feature-sidebar-item" :class="{ active: activeSubmenu === 'uploadFile' }"
+          @click="toggleSubmenu('uploadFile')">
+          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M12 16V4"></path>
+            <path d="M7 9l5-5 5 5"></path>
+            <path d="M4 16.5v1.5A2 2 0 006 20h12a2 2 0 002-2v-1.5"></path>
+            <path d="M5 14h14"></path>
+          </svg>
+          <span class="item-label">{{ lang.ssUploadFile }}</span>
+        </div>
+        <div class="sidebar-divider"></div>
+        <!-- <div class="sidebar-item" :class="{ active: activeSubmenu === 'page' }" @click="toggleSubmenu('page')">
           <svg class="item-icon" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
             <g id="Component 1">
               <path id="Vector"
@@ -20,7 +32,7 @@
             </g>
           </svg>
           <span class="item-label">{{ lang.ssPage }}</span>
-        </div>
+        </div> -->
         <div class="sidebar-item" :class="{ active: activeSubmenu === 'interactive' }"
           @click="toggleSubmenu('interactive')">
           <svg class="item-icon" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -78,10 +90,12 @@
           </svg>
           <span class="item-label">{{ lang.ssMultimedia }}</span>
         </div>
-        <div class="sidebar-item" :class="{ active: activeSubmenu === 'english' }"
-          @click="toggleSubmenu('english')">
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'english' }" @click="toggleSubmenu('english')">
           <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-            <path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"></path><path d="M19 10v2a7 7 0 01-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line>
+            <path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"></path>
+            <path d="M19 10v2a7 7 0 01-14 0v-2"></path>
+            <line x1="12" y1="19" x2="12" y2="23"></line>
+            <line x1="8" y1="23" x2="16" y2="23"></line>
           </svg>
           <span class="item-label">{{ lang.ssEnglish }}</span>
         </div>
@@ -104,6 +118,104 @@
         <AiChat :userid="props.userid" />
       </div>
     </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'uploadFile' }">
+      <div class="submenu-title" style="margin-bottom: 0;">
+        <div class="title">{{ lang.ssUploadFile }}</div>
+        <div class="close-icon" @click="toggleSubmenu('uploadFile')">
+          <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="submenu-content">
+        <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>
+
+            <div class="modal-buttons">
+              <button class="cancel-btn" @click="cancelFileUpload">{{ lang.ssCancel }}</button>
+              <button class="confirm-btn" @click="confirmFileUpload">{{ lang.ssConfirm }}</button>
+            </div>
+          </div>
+        </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' }">
       <div class="submenu-title">
         <div class="title">{{ lang.ssAddTemplatePage }}</div>
@@ -175,7 +287,7 @@
         </div>
       </div>
       <FileInput accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
-        @change="handleFileUpload">
+        @change="handleFileUpload" v-if="false">
         <div class="submenu-upload">
           <div class="submenu-icon">
             <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -232,6 +344,7 @@
         <img class="submenu-img" v-else-if="hoveredTool === 'qa'" key="qa" :src="toolAnswer" alt="">
         <img class="submenu-img" v-else-if="hoveredTool === 'choice'" key="choice" :src="toolChoice" alt="">
         <img class="submenu-img" v-else-if="hoveredTool === 'vote'" key="vote" :src="toolVote" alt="">
+        <img class="submenu-img" v-else-if="hoveredTool === 'photo'" key="photo" :src="toolPhoto" alt="">
       </transition>
       <div class="submenu-item-box">
         <div class="submenu-item" @click="handleToolClick('choice')" @mouseenter="hoveredTool = 'choice'"
@@ -250,19 +363,27 @@
           <span class="submenu-label">{{ lang.ssQandA }}</span>
         </div>
         <div class="submenu-item" @click="handleToolClick('vote')" @mouseenter="hoveredTool = 'vote'"
-          @mouseleave="hoveredTool = null" v-show="false">
+          @mouseleave="hoveredTool = null">
           <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <polyline points="9 11 12 14 22 4"></polyline>
-              <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
+            <polyline points="9 11 12 14 22 4"></polyline>
+            <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
           </svg>
           <span class="submenu-label">{{ lang.ssVote }}</span>
         </div>
+        <div class="submenu-item" @click="handleToolClick('photo')" @mouseenter="hoveredTool = 'photo'"
+          @mouseleave="hoveredTool = null">
+          <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
+            <circle cx="12" cy="13" r="4"></circle>
+          </svg>
+          <span class="submenu-label">{{ lang.ssPhoto }}</span>
+        </div>
         <div class="submenu-item" @click="handleToolClick('creative')" @mouseenter="hoveredTool = null"
           @mouseleave="hoveredTool = null">
           <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-            <circle cx="12" cy="12" r="10"/>
-            <line x1="12" y1="8" x2="12" y2="16"/>
-            <line x1="8" y1="12" x2="16" y2="12"/>
+            <circle cx="12" cy="12" r="10" />
+            <line x1="12" y1="8" x2="12" y2="16" />
+            <line x1="8" y1="12" x2="16" y2="12" />
           </svg>
           <span class="submenu-label">{{ lang.ssCreative }}</span>
         </div>
@@ -367,7 +488,23 @@
           </div>
           <span class="submenu-label">{{ lang.ssUploadWebpageLink }}</span>
         </div>
-        <!-- <div class="submenu-item">
+         <!-- @click="handleToolClick('createWebpage')" -->
+        <div class="submenu-item" :class="{ 'loading-state': create_app_loading }" @click="!create_app_loading && handle_add_aiWeb()">
+          <div class="submenu-icon" v-if="!create_app_loading">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <rect x="3" y="3" width="18" height="14" rx="2"></rect>
+              <path d="M12 8v6"></path>
+              <path d="M9 11h6"></path>
+            </svg>
+          </div>
+          <div class="submenu-icon loading-icon" v-else>
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin">
+              <circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ isExistingWebElement ? lang.ssEditWebpage : create_app_loading ? lang.ssCreating : lang.ssNewWebpage }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('uploadCode')">
           <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">
@@ -380,16 +517,6 @@
             </svg>
           </div>
           <span class="submenu-label">{{ lang.ssUploadWebpage }}</span>
-        </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">
-              <rect x="3" y="3" width="18" height="14" rx="2"></rect>
-              <path d="M12 8v6"></path>
-              <path d="M9 11h6"></path>
-            </svg>
-          </div>
-          <span class="submenu-label">{{ lang.ssNewWebpage }}</span>
         </div>
         <!-- <div class="submenu-item">
           <div class="submenu-icon">
@@ -437,6 +564,67 @@
         </div>
       </div>
     </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'uploadCode' }">
+      <div class="submenu-title">
+        <div class="title">{{ lang.ssUploadWebpage }}</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="upload-box">
+          <div class="upload-tabs">
+            <button class="upload-tab" :class="{ active: uploadTab === 'file' }"
+              @click="switchUploadTab('file')">{{ lang.ssUploadFile }}</button>
+            <button class="upload-tab" :class="{ active: uploadTab === 'code' }"
+              @click="switchUploadTab('code')">{{ lang.ssPasteCode }}</button>
+          </div>
+          <div class="uploadFilePanel" v-if="uploadTab === 'file'">
+            <div class="form-group">
+              <FileInput accept=".html,.htm" @change="handleFileCodeUpload" v-if="!uploadCodeFile">
+                <div class="file-upload-area">
+                  <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
+                    <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
+                    <polyline points="17 8 12 3 7 8"></polyline>
+                    <line x1="12" y1="3" x2="12" y2="15"></line>
+                  </svg>
+                  <p class="upload-text">{{ lang.ssDragAndDropHint }}</p>
+                  <p class="upload-hint">{{ lang.ssSupportHTML }}</p>
+                </div>
+              </FileInput>
+              <div class="file-name-display" v-else>
+                <span>{{ uploadCodeFile.name }}</span>
+                <button type="button" class="upload-task-action"
+                  @click="handleFileCodeUpload(null)">×</button>
+              </div>
+            </div>
+          </div>
+          <div class="pasteCodePanel" v-if="uploadTab === 'code'">
+            <div class="form-group">
+              <textarea class="code-textarea" :placeholder="lang.ssPasteHTML" v-model="codeInput"></textarea>
+            </div>
+          </div>
+          <button class="webpage-link-button"
+            :class="{ 'loading': isLoading, 'disabled': !uploadCodeFile || isLoading }"
+            :disabled="!uploadCodeFile || isLoading" @click="handleUploadCodeFile"
+            v-if="uploadTab === 'file'">
+            {{ isLoading ? lang.ssUploading : !uploadCodeFile ? lang.ssWaitingForInput2 : lang.ssStartUpload }}
+          </button>
+          <button class="webpage-link-button"
+            :class="{ 'loading': isLoading, 'disabled': !codeInput || isLoading }"
+            :disabled="!codeInput || isLoading" @click="handleUploadCode"
+            v-else-if="uploadTab === 'code'">
+            {{ isLoading ? lang.ssUploading : !codeInput ? lang.ssStartUpload : lang.ssStartUpload }}
+          </button>
+        </div>
+      </div>
+    </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'multimedia' }">
       <div class="submenu-title">
         <div class="title">{{ lang.ssAddMultimedia }}</div>
@@ -518,31 +706,14 @@
       <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>
+
+    <AiWeb :visible="showAiWebModal" :web-id="webId" @close="showAiWebModal = false" @add="addAiWeb" :is-loading="isLoading" />
   </div>
 </template>
 
 <script lang="ts" setup>
-import { ref, watch } from 'vue'
+import { ref, watch, computed } from 'vue'
 import { storeToRefs } from 'pinia'
 import useCreateElement from '@/hooks/useCreateElement'
 import useSlideHandler from '@/hooks/useSlideHandler'
@@ -550,11 +721,14 @@ import { useSlidesStore } from '@/store'
 import { useSpeakingStore } from '@/store/speaking'
 import FileInput from '@/components/FileInput.vue'
 import AiChat from './componets/aiChat.vue'
+import AiWeb from './componets/aiWeb.vue'
 import SpeakingPanel from '@/views/Editor/EnglishSpeaking/SpeakingPanel.vue'
 import { lang } from '@/main'
 import toolChoice from '@/assets/img/tool_choice.jpeg'
 import toolAnswer from '@/assets/img/tool_answer.png'
 import toolVote from '@/assets/img/tool_vote.png'
+import toolPhoto from '@/assets/img/tool_photo.png'
+import axios from '@/services/config'
 
 interface ContentItem {
   tool?: number
@@ -573,6 +747,16 @@ const props = withDefaults(defineProps<{
   userid: null,
 })
 
+
+const userJson = ref<any>({})
+watch(() => props.userid, async (newVal) => {
+  if (!newVal) return
+  const res = await axios.get('https://pbl.cocorobo.cn/api/pbl/selectUser', {
+    params: { userid: newVal }
+  })
+  userJson.value = res[0][0] || {}
+})
+
 const emit = defineEmits<{
   (e: 'toggle', collapsed: boolean): void
 }>()
@@ -767,8 +951,15 @@ const uploadWebpageLink = async () => {
   })
 
   if (!isValid) {
-    message.error(lang.ssCocoLinkTip)
+    // message.error(lang.ssCocoLinkTip)
+
+    message.warning(lang.ssCocoLinkTip2)
+    createSlide()
+    createFrameElement(webpageUrl.value, 81) // 假设15是网页工具的类型
     isLoading.value = false
+    // 清空输入框和验证状态
+    webpageUrl.value = ''
+    isValidUrl.value = null
     return
   }
   isLoading.value = false
@@ -848,6 +1039,9 @@ const handleToolClick = _.debounce((tool: string) => {
   else if (tool === 'vote') {
     parentWindow?.addTool?.(78)
   }
+  else if (tool === 'photo') {
+    parentWindow?.addTool?.(79)
+  }
   else if (tool === 'qa') {
     parentWindow?.addTool?.(15)
   }
@@ -877,6 +1071,9 @@ const handleToolClick = _.debounce((tool: string) => {
   else if (tool === 'uploadWebpage') {
     activeSubmenu.value = 'uploadWebpage'
   }
+  else if (tool === 'uploadCode') {
+    activeSubmenu.value = 'uploadCode'
+  }
 }, 300)
 
 const loadContentList = () => {
@@ -902,7 +1099,7 @@ const addContent = (data: ContentItem, type: number) => {
   // contentList.value.push(data)
   if (type === 2) {
     const elements = currentSlide.value?.elements || []
-    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78))
+    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78 || el.toolType === 79))
     if (frameElement) {
       slidesStore.updateElement({
         id: frameElement.id,
@@ -958,11 +1155,13 @@ const getTypeLabel = (type?: number) => {
     15: lang.ssQATest,
     72: lang.ssAiApp,
     73: lang.ssHPage,
+    81: lang.ssHPage,
     74: lang.ssVideo,
     75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
     76: lang.ssCreative,
     77: lang.ssEnglishSpeakingTool,
     78: lang.ssVote,
+    79: lang.ssPhoto,
   }
   return typeMap[type || 0] || lang.ssUnknown
 }
@@ -982,16 +1181,88 @@ const getTypeClass = (type?: number) => {
 
 import useImport from '@/hooks/useImport'
 import message from '@/utils/message'
-const { importPPTXFile, exporting, getFile } = useImport()
+const { importPPTXFile, exporting, getFile, getPPTInfo, uploadFileToS3 } = 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用于取消操作
@@ -999,7 +1270,7 @@ const handleFileUpload = async (files: FileList) => {
     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') {
@@ -1010,9 +1281,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
@@ -1022,6 +1310,128 @@ const handleParsingClose = () => {
   else if (!exporting.value) {
     emit('close')
   }
+
+  exportingDialog.value = false
+}
+
+const uploadTab = ref('file')
+const switchUploadTab = (tab: 'file' | 'code') => {
+  uploadTab.value = tab
+}
+
+const codeInput = ref('')
+const uploadCodeFile = ref<File | null>()
+const handleFileCodeUpload = (files: FileList | null) => {
+  
+  if (!files || files.length === 0) {
+    uploadCodeFile.value = null
+    return
+  }
+
+  const file = files[0]
+  uploadCodeFile.value = file
+}
+
+const handleUploadCodeFile = async () => {
+  if (!uploadCodeFile.value) return
+  const fileName = uploadCodeFile.value.name.toLowerCase()
+  if (fileName.endsWith('.html') || fileName.endsWith('.htm')) {
+    isLoading.value = true
+    const url = await uploadFileToS3(uploadCodeFile.value)
+    createSlide()
+    createFrameElement(url, 73)
+    uploadCodeFile.value = null
+    isLoading.value = false
+  }
+}
+
+const handleUploadCode = async () => {
+  if (!codeInput.value) return
+  const file = new File([codeInput.value], 'index.html', { type: 'text/html' })
+  isLoading.value = true
+  const url = await uploadFileToS3(file)
+  createSlide()
+  createFrameElement(url, 73)
+  codeInput.value = ''
+  isLoading.value = false
+}
+
+const isExistingWebElement = computed(() => {
+  const elements = currentSlide.value?.elements || []
+  const existingWebElement = elements.find((el: any) => el.toolType === 73 && el.webId && el.type === 'frame')
+  return existingWebElement && existingWebElement.webId
+})
+
+
+
+
+// AI Web 相关状态
+const create_app_loading = ref(false)
+const webId = ref('')
+const showAiWebModal = ref(false)
+const isEdit = ref(false)
+
+const handle_add_aiWeb = _.throttle(async () => {
+  create_app_loading.value = true
+  try {
+    const elements = currentSlide.value?.elements || []
+    console.log(elements)
+    const existingWebElement = elements.find((el: any) => el.toolType === 73 && el.webId && el.type === 'frame')
+    
+    if (existingWebElement && existingWebElement.webId) {
+      webId.value = existingWebElement.webId
+      create_app_loading.value = false
+      showAiWebModal.value = true
+      isEdit.value = true
+      return
+    }
+
+    const res = await axios.post('https://appapi.cocorobo.cn/api/agents/ai_edit', {
+      userid: props.userid || '',
+      username: userJson.value.name || '',
+      muti_name: lang.ssAddWebH5,
+      description: lang.ssAddWebH5,
+      headUrl: 'https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/default%2F%E9%BB%98%E8%AE%A4%E5%A4%B4%E5%83%8F51741770599274.svg',
+      organizeid: userJson.value.organizeid || '',
+      content: ''
+    })
+    console.log(res)
+    webId.value = res.id || ''
+
+    create_app_loading.value = false
+    showAiWebModal.value = true
+  }
+  catch (error) {
+    console.log(error)
+    create_app_loading.value = false
+  }
+}, 3000)
+
+
+const addAiWeb = async (code: string) => {
+  if (!code) return
+  const file = new File([code], 'index.html', { type: 'text/html' })
+  isLoading.value = true
+  const url = await uploadFileToS3(file)
+  if (isEdit.value) {
+    const elements = currentSlide.value?.elements || []
+    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 73))
+    if (frameElement) {
+      slidesStore.updateElement({
+        id: frameElement.id,
+        props: { url: url }
+      })
+    }
+  }
+  else {
+    createSlide()
+    createFrameElement(url, 73, webId.value)
+  }
+  
+  isLoading.value = false
+  showAiWebModal.value = false
+  webId.value = ''
+  isEdit.value = false
 }
 </script>
 
@@ -1049,6 +1459,44 @@ const handleParsingClose = () => {
   position: relative;
 }
 
+.sidebar-divider {
+  position: relative;
+  margin: 4px 2px 2px;
+  height: 12px;
+}
+
+.sidebar-divider::before {
+  content: "";
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 50%;
+  height: 1px;
+  background: #e8ddd0;
+}
+
+.feature-sidebar-item {
+  min-height: 82px;
+  padding-top: 12px;
+  padding-bottom: 12px;
+  gap: 7px;
+  border: 1px solid #f1e2cc;
+  background: #fffdfa;
+  box-shadow: 0 8px 18px rgba(15, 23, 42, 0.05);
+
+  &:hover {
+    box-shadow: #f78b221f 0px 12px 24px !important;
+    background: #fff8ef !important;
+    border-color: #f78b2252 !important;
+  }
+
+  &.active {
+    box-shadow: #f78b222e 0px 14px 28px !important;
+    background: #fff2df !important;
+    border-color: #f78b2266 !important;
+  }
+}
+
 .sidebar-item {
   width: 84px;
   padding: 12px 8px;
@@ -1451,112 +1899,129 @@ 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;
+    // }
+
+    // .progress-percent {
+    //   font-size: 14px;
+    //   font-weight: 600;
+    //   color: #FF9300;
+    // }
+
+    .upload-task-main {
+      min-width: 0;
     }
 
-    .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;
+    .upload-task-name {
+      font-size: 13px;
+      font-weight: 700;
+      color: #111827;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
     }
 
-    h3 {
-      font-size: 20px;
-      font-weight: 600;
-      color: #333;
-      margin: 0 0 12px;
+    .upload-task-meta {
+      margin-top: 2px;
+      font-size: 11px;
+      color: #8b7356;
     }
 
-    p {
-      font-size: 14px;
-      color: #666;
-      margin: 0 0 24px;
+    .upload-task-side {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      flex-shrink: 0;
     }
 
-    .close-btn2 {
-      background: #FF9300;
-      color: white;
-      border: none;
-      border-radius: 8px;
-      padding: 12px 24px;
-      font-size: 14px;
-      font-weight: 500;
+    .upload-task-percent {
+      font-size: 12px;
+      font-weight: 700;
+      color: #c76a0c;
+      min-width: 40px;
+      text-align: right;
+    }
+
+    .upload-task-action {
+      border: 0;
+      background: transparent;
+      color: #9a8a77;
+      font-size: 12px;
+      font-weight: 600;
       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;
     }
   }
 }
@@ -1629,4 +2094,418 @@ const handleParsingClose = () => {
     }
   }
 }
+
+.upload-dropzone {
+  position: relative;
+  border: 1.5px dashed rgba(247, 139, 34, 0.38);
+  border-radius: 20px;
+  background: #fffaf4;
+  min-height: 156px;
+  padding: 22px 20px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+  gap: 10px;
+  cursor: pointer;
+  transition: all 0.22s ease;
+  overflow: hidden;
+  width: calc(100% - 30px);
+  margin: 15px auto;
+  box-sizing: border-box;
+
+  &:hover {
+    border-style: solid;
+    border-color: #f78b22;
+    background: #fff3e2;
+    box-shadow: 0 18px 34px rgba(247, 139, 34, 0.16);
+    transform: translateY(-1px);
+  }
+
+  .upload-dropzone-icon {
+    width: 56px;
+    height: 56px;
+    border-radius: 18px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #f78b22;
+    background: rgba(247, 139, 34, 0.10);
+    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
+
+    svg {
+      width: 28px;
+      height: 28px;
+    }
+  }
+
+  .upload-dropzone-title {
+    font-size: 16px;
+    font-weight: 700;
+    color: #111827;
+  }
+
+  .upload-dropzone-subtitle {
+    font-size: 12px;
+    line-height: 1.6;
+    color: #7c6d5d;
+    max-width: 320px;
+  }
+
+  .upload-dropzone-footnote {
+    font-size: 11px;
+    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);
+    }
+  }
+}
+
+.line_box {
+  height: calc(100% - 75px);
+  overflow: hidden;
+}
+
+.upload-box {
+  padding: 20px;
+
+  .upload-tabs {
+    display: flex;
+    margin-bottom: 20px;
+    border-bottom: 1px solid rgb(229, 231, 235);
+
+    .upload-tab {
+      border-top-width: initial;
+      border-right-width: initial;
+      border-left-width: initial;
+      border-top-color: initial;
+      border-right-color: initial;
+      border-left-color: initial;
+      background-color: transparent;
+      cursor: pointer;
+      font-size: 14px;
+      color: rgb(107, 114, 128);
+      margin-bottom: -1px;
+      padding: 10px 16px;
+      border-style: none none solid;
+      border-image: initial;
+      border-bottom: 2px solid transparent;
+
+      &.active {
+        color: rgb(247, 139, 34);
+        border-bottom-color: rgb(247, 139, 34);
+        font-weight: 600;
+      }
+    }
+  }
+
+  .uploadFilePanel {
+
+    .form-group {
+      display: flex;
+      flex-direction: column;
+      gap: 12px;
+
+      .file-upload-area {
+        text-align: center;
+        cursor: pointer;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        min-height: 190px;
+        border-width: 2px;
+        border-style: dashed;
+        border-color: rgb(229, 231, 235);
+        border-image: initial;
+        border-radius: 12px;
+        background: rgb(250, 251, 252);
+        padding: 40px 20px;
+        transition: 0.2s;
+        gap: 10px;
+
+        svg {
+          color: rgb(156, 163, 175);
+          margin-bottom: 12px;
+        }
+
+        .upload-text {
+          font-size: 14px;
+          font-weight: 500;
+          color: rgb(55, 65, 81);
+          margin-bottom: 6px;
+        }
+
+        .upload-hint {
+          font-size: 12px;
+          color: rgb(107, 114, 128);
+        }
+
+        &:hover {
+          border-color: rgb(247, 139, 34);
+          background: rgb(255, 248, 240);
+        }
+      }
+
+      .file-name-display {
+        margin-top: 12px;
+        font-size: 13px;
+        color: rgb(75, 85, 99);
+        background-color: rgb(249, 250, 251);
+        border-radius: 6px;
+        padding: 8px 12px;
+        border-width: 1px;
+        border-style: solid;
+        border-color: rgb(229, 231, 235);
+        border-image: initial;
+        display: flex;
+        justify-content: space-between;
+
+        span {
+          flex: 1;
+        }
+
+        .upload-task-action {
+          border: 0;
+          background: transparent;
+          color: #9a8a77;
+          font-size: 12px;
+          font-weight: 600;
+          cursor: pointer;
+        }
+      }
+    }
+  }
+
+  .pasteCodePanel {
+    .form-group {
+      display: flex;
+      flex-direction: column;
+      gap: 10px;
+
+      .code-textarea {
+        width: 100%;
+        min-height: 190px;
+        font-family: monospace;
+        font-size: 13px;
+        resize: vertical;
+        border-width: 1px;
+        border-style: solid;
+        border-color: rgb(209, 213, 219);
+        border-image: initial;
+        border-radius: 8px;
+        padding: 12px;
+        transition: border-color 0.2s;
+        box-sizing: border-box;
+        resize: none;
+
+        &:focus {
+          outline: none;
+          border-color: rgb(247, 139, 34);
+        }
+      }
+    }
+  }
+
+  .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;
+    margin-top: 15px;
+
+    &:hover:not(:disabled) {
+      background-color: #e68a00;
+    }
+
+    &:disabled {
+      opacity: 0.5;
+      cursor: not-allowed;
+    }
+
+    &.loading {
+      cursor: not-allowed;
+      opacity: 0.8;
+    }
+  }
+
+  .loading-state {
+    opacity: 0.7;
+    cursor: not-allowed;
+    pointer-events: none;
+
+    .submenu-icon {
+      animation: pulse 1s infinite;
+    }
+
+    .spin {
+      animation: spin 1s linear infinite;
+    }
+  }
+}
 </style>

+ 1649 - 0
src/components/CollapsibleToolbar/index22.vue

@@ -0,0 +1,1649 @@
+<template>
+  <div class="collapsible-toolbar" :class="{ collapsed: isCollapsed }">
+    <div class="toolbar-content" v-show="!isCollapsed">
+      <div class="sidebar-content">
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'cocoai' }" @click="toggleSubmenu('cocoai')">
+          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M12 2L2 7l10 5 10-5-10-5z"></path>
+            <path d="M2 17l10 5 10-5"></path>
+            <path d="M2 12l10 5 10-5"></path>
+          </svg>
+          <span class="item-label">Coco AI</span>
+        </div>
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'page' }" @click="toggleSubmenu('page')">
+          <svg class="item-icon" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 1">
+              <path id="Vector"
+                d="M12.8332 1.83398H5.49984C5.01361 1.83398 4.54729 2.02714 4.20347 2.37096C3.85966 2.71477 3.6665 3.18109 3.6665 3.66732V18.334C3.6665 18.8202 3.85966 19.2865 4.20347 19.6303C4.54729 19.9742 5.01361 20.1673 5.49984 20.1673H16.4998C16.9861 20.1673 17.4524 19.9742 17.7962 19.6303C18.14 19.2865 18.3332 18.8202 18.3332 18.334V7.33398L12.8332 1.83398Z"
+                stroke="currentColor" stroke-width="1.83333" />
+              <path id="Vector_2" d="M12.8335 1.83398V7.33398H18.3335" stroke="currentColor" stroke-width="1.83333" />
+            </g>
+          </svg>
+          <span class="item-label">{{ lang.ssPage }}</span>
+        </div>
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'interactive' }"
+          @click="toggleSubmenu('interactive')">
+          <svg class="item-icon" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Frame">
+              <path id="Vector"
+                d="M4.44727 14.6738V14.5449L5.84805 13.041H6.02422L9.43164 16.2766V7.46797L9.4875 7.41211H11.9969L12.0098 7.425V13.5137H16.7234L16.8781 13.6684V18.9707H18.5969V12.7789L17.6086 11.7949H13.5996V6.62578L12.7531 5.7793H8.73125L7.8418 6.66875V12.6844L6.60859 11.4512H5.23789L2.85742 13.9863V15.2324L6.35508 18.9707H8.60234L4.44727 14.6738Z"
+                fill="currentColor" />
+              <path id="Vector_2"
+                d="M2.49219 2.79297V10.5273H6.40234V8.76562H4.25391V4.55469H17.7461V8.76562H15.0391V10.5273H19.4219V2.79297H2.49219Z"
+                fill="currentColor" />
+            </g>
+          </svg>
+          <span class="item-label">{{ lang.ssInteract }}</span>
+        </div>
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'aiapp' }" @click="toggleSubmenu('aiapp')">
+          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <rect x="3" y="3" width="7" height="7" />
+            <rect x="14" y="3" width="7" height="7" />
+            <rect x="14" y="14" width="7" height="7" />
+            <rect x="3" y="14" width="7" height="7" />
+          </svg>
+          <span class="item-label">{{ lang.ssAiApp }}</span>
+        </div>
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'h5page' }" @click="toggleSubmenu('h5page')">
+          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <circle cx="12" cy="12" r="10" />
+            <path d="M2 12h20" />
+            <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" />
+          </svg>
+          <span class="item-label">{{ lang.ssInteractiveWebpage }}</span>
+        </div>
+        <!-- <div class="sidebar-item" @click="handleToolClick('video')">
+          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <rect x="3" y="4" width="18" height="16" rx="2" ry="2" />
+            <polygon points="10 9 16 12 10 15 10 9" />
+          </svg>
+          <span class="item-label">{{ lang.ssVideo }}</span>
+        </div> -->
+        <!-- <div class="sidebar-item" @click="handleToolClick('creative')">
+          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <circle cx="12" cy="12" r="10" />
+            <line x1="12" y1="8" x2="12" y2="16" />
+            <line x1="8" y1="12" x2="16" y2="12" />
+          </svg>
+          <span class="item-label">{{ lang.ssCreative }}</span>
+        </div> -->
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'multimedia' }"
+          @click="toggleSubmenu('multimedia')">
+          <svg class="item-icon" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 1">
+              <path id="Vector"
+                d="M20.1668 17.4167C20.1668 17.9029 19.9737 18.3692 19.6299 18.713C19.286 19.0568 18.8197 19.25 18.3335 19.25H3.66683C3.1806 19.25 2.71428 19.0568 2.37047 18.713C2.02665 18.3692 1.8335 17.9029 1.8335 17.4167V4.58333C1.8335 4.0971 2.02665 3.63079 2.37047 3.28697C2.71428 2.94315 3.1806 2.75 3.66683 2.75H8.25016L10.0835 5.5H18.3335C18.8197 5.5 19.286 5.69315 19.6299 6.03697C19.9737 6.38079 20.1668 6.8471 20.1668 7.33333V17.4167Z"
+                stroke="currentColor" stroke-width="1.83333" />
+            </g>
+          </svg>
+          <span class="item-label">{{ lang.ssMultimedia }}</span>
+        </div>
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'english' }"
+          @click="toggleSubmenu('english')">
+          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"></path><path d="M19 10v2a7 7 0 01-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line>
+          </svg>
+          <span class="item-label">{{ lang.ssEnglish }}</span>
+        </div>
+      </div>
+    </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'cocoai' }">
+      <div class="submenu-title" style="margin-bottom: 0;">
+        <div class="title">Coco AI</div>
+        <div class="close-icon" @click="toggleSubmenu('cocoai')">
+          <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="submenu-content">
+        <AiChat :userid="props.userid" />
+      </div>
+    </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'page' }">
+      <div class="submenu-title">
+        <div class="title">{{ lang.ssAddTemplatePage }}</div>
+        <div class="close-icon" @click="toggleSubmenu('page')">
+          <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="submenu-item-box2">
+        <div class="submenu-item" @click="handleToolClick('titlepage')">
+          <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">
+                <path id="Vector" d="M8 14V8H40V14M18 40H30M24 8V40" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssTitlePage }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('ImagePage')">
+          <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">
+                <path id="Vector"
+                  d="M38 6H10C7.79086 6 6 7.79086 6 10V38C6 40.2091 7.79086 42 10 42H38C40.2091 42 42 40.2091 42 38V10C42 7.79086 40.2091 6 38 6Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2"
+                  d="M17 20C18.6569 20 20 18.6569 20 17C20 15.3431 18.6569 14 17 14C15.3431 14 14 15.3431 14 17C14 18.6569 15.3431 20 17 20Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3" d="M42 30L32 20L10 42" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssImagePage }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('ContentPage')">
+          <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">
+                <path id="Vector"
+                  d="M28 4H12C10.9391 4 9.92172 4.42143 9.17157 5.17157C8.42143 5.92172 8 6.93913 8 8V40C8 41.0609 8.42143 42.0783 9.17157 42.8284C9.92172 43.5786 10.9391 44 12 44H36C37.0609 44 38.0783 43.5786 38.8284 42.8284C39.5786 42.0783 40 41.0609 40 40V16L28 4Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3" d="M32 26H16" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_4" d="M32 34H16" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssContentPage }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('ImageTextPage')">
+          <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">
+                <path id="Vector"
+                  d="M18 6H8C6.89543 6 6 6.89543 6 8V40C6 41.1046 6.89543 42 8 42H18C19.1046 42 20 41.1046 20 40V8C20 6.89543 19.1046 6 18 6Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2"
+                  d="M40 6H30C28.8954 6 28 6.89543 28 8V40C28 41.1046 28.8954 42 30 42H40C41.1046 42 42 41.1046 42 40V8C42 6.89543 41.1046 6 40 6Z"
+                  stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssImageTextPage }}</span>
+        </div>
+      </div>
+      <FileInput accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
+        @change="handleFileUpload">
+        <div class="submenu-upload">
+          <div class="submenu-icon">
+            <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <g id="Component 1">
+                <path id="Vector"
+                  d="M24.5 17.5V22.1667C24.5 22.7855 24.2542 23.379 23.8166 23.8166C23.379 24.2542 22.7855 24.5 22.1667 24.5H5.83333C5.21449 24.5 4.621 24.2542 4.18342 23.8166C3.74583 23.379 3.5 22.7855 3.5 22.1667V17.5"
+                  stroke="currentColor" stroke-width="2.33333" />
+                <path id="Vector_2" d="M19.8334 9.33333L14.0001 3.5L8.16675 9.33333" stroke="currentColor"
+                  stroke-width="2.33333" />
+                <path id="Vector_3" d="M14 3.5V17.5" stroke="currentColor" stroke-width="2.33333" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssUploadPPT }}</span>
+        </div>
+      </FileInput>
+    </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'interactive' }">
+      <div class="submenu-title">
+        <div class="title">{{ lang.ssAddInteractiveTool }}</div>
+        <div class="close-icon" @click="toggleSubmenu('interactive')">
+          <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>
+      <transition name="fade" mode="out-in">
+        <div class="submenu-panel" v-if="!hoveredTool">
+          <svg key="svg" width="120" height="80" viewBox="0 0 120 80" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 1">
+              <path id="Vector"
+                d="M102 15H18C13.5817 15 10 18.5817 10 23V57C10 61.4183 13.5817 65 18 65H102C106.418 65 110 61.4183 110 57V23C110 18.5817 106.418 15 102 15Z"
+                fill="#FFFAF0" stroke="#FF9300" stroke-width="2" />
+              <path id="Vector_2"
+                d="M30 34C32.2091 34 34 32.2091 34 30C34 27.7909 32.2091 26 30 26C27.7909 26 26 27.7909 26 30C26 32.2091 27.7909 34 30 34Z"
+                fill="#FF9300" />
+              <path id="Vector_3"
+                d="M97 27H43C41.3431 27 40 28.3431 40 30C40 31.6569 41.3431 33 43 33H97C98.6569 33 100 31.6569 100 30C100 28.3431 98.6569 27 97 27Z"
+                fill="#FFD9A8" />
+              <path id="Vector_4"
+                d="M30 49C32.2091 49 34 47.2091 34 45C34 42.7909 32.2091 41 30 41C27.7909 41 26 42.7909 26 45C26 47.2091 27.7909 49 30 49Z"
+                fill="#D1D5DB" />
+              <path id="Vector_5"
+                d="M82 42H43C41.3431 42 40 43.3431 40 45C40 46.6569 41.3431 48 43 48H82C83.6569 48 85 46.6569 85 45C85 43.3431 83.6569 42 82 42Z"
+                fill="#E5E7EB" />
+            </g>
+          </svg>
+          <div class="detail">{{ lang.ssSelectToolCreateInteractive }}</div>
+        </div>
+        <img class="submenu-img" v-else-if="hoveredTool === 'qa'" key="qa" :src="toolAnswer" alt="">
+        <img class="submenu-img" v-else-if="hoveredTool === 'choice'" key="choice" :src="toolChoice" alt="">
+        <img class="submenu-img" v-else-if="hoveredTool === 'vote'" key="vote" :src="toolVote" alt="">
+        <img class="submenu-img" v-else-if="hoveredTool === 'photo'" key="photo" :src="toolPhoto" alt="">
+      </transition>
+      <div class="submenu-item-box">
+        <div class="submenu-item" @click="handleToolClick('choice')" @mouseenter="hoveredTool = 'choice'"
+          @mouseleave="hoveredTool = null">
+          <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <circle cx="12" cy="12" r="10" />
+            <path d="M12 16v-4m0-4h.01" />
+          </svg>
+          <span class="submenu-label">{{ lang.ssChoiceQ }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('qa')" @mouseenter="hoveredTool = 'qa'"
+          @mouseleave="hoveredTool = null">
+          <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
+          </svg>
+          <span class="submenu-label">{{ lang.ssQandA }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('vote')" @mouseenter="hoveredTool = 'vote'"
+          @mouseleave="hoveredTool = null">
+          <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <polyline points="9 11 12 14 22 4"></polyline>
+              <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
+          </svg>
+          <span class="submenu-label">{{ lang.ssVote }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('photo')" @mouseenter="hoveredTool = 'photo'"
+          @mouseleave="hoveredTool = null">
+          <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
+              <circle cx="12" cy="13" r="4"></circle>
+          </svg>
+          <span class="submenu-label">{{ lang.ssPhoto }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('creative')" @mouseenter="hoveredTool = null"
+          @mouseleave="hoveredTool = null">
+          <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <circle cx="12" cy="12" r="10"/>
+            <line x1="12" y1="8" x2="12" y2="16"/>
+            <line x1="8" y1="12" x2="16" y2="12"/>
+          </svg>
+          <span class="submenu-label">{{ lang.ssCreative }}</span>
+        </div>
+      </div>
+    </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'aiapp' }">
+      <div class="submenu-title">
+        <div class="title">{{ lang.ssAddAIApp }}</div>
+        <div class="close-icon" @click="toggleSubmenu('aiapp')">
+          <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="submenu-item-box2">
+        <div class="submenu-item" @click="handleToolClick('aiapp')">
+          <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">
+                <path id="Vector"
+                  d="M18 6H8C6.89543 6 6 6.89543 6 8V18C6 19.1046 6.89543 20 8 20H18C19.1046 20 20 19.1046 20 18V8C20 6.89543 19.1046 6 18 6Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2"
+                  d="M40 6H30C28.8954 6 28 6.89543 28 8V18C28 19.1046 28.8954 20 30 20H40C41.1046 20 42 19.1046 42 18V8C42 6.89543 41.1046 6 40 6Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3"
+                  d="M40 28H30C28.8954 28 28 28.8954 28 30V40C28 41.1046 28.8954 42 30 42H40C41.1046 42 42 41.1046 42 40V30C42 28.8954 41.1046 28 40 28Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_4"
+                  d="M18 28H8C6.89543 28 6 28.8954 6 30V40C6 41.1046 6.89543 42 8 42H18C19.1046 42 20 41.1046 20 40V30C20 28.8954 19.1046 28 18 28Z"
+                  stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssAppCenter }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('createApp')">
+          <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">
+                <path id="Vector"
+                  d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2" d="M24 16V32" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3" d="M16 24H32" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssCreateApp }}</span>
+        </div>
+      </div>
+    </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'h5page' }">
+      <div class="submenu-title">
+        <div class="title">{{ lang.ssAddInteractiveWebpage }}</div>
+        <div class="close-icon" @click="toggleSubmenu('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="submenu-item-box2">
+        <!-- <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">
+                <path id="Vector"
+                  d="M40 6H8C5.79086 6 4 7.79086 4 10V30C4 32.2091 5.79086 34 8 34H40C42.2091 34 44 32.2091 44 30V10C44 7.79086 42.2091 6 40 6Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2" d="M4 14H44" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3"
+                  d="M10 11C10.5523 11 11 10.5523 11 10C11 9.44772 10.5523 9 10 9C9.44772 9 9 9.44772 9 10C9 10.5523 9.44772 11 10 11Z"
+                  fill="currentColor" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_4"
+                  d="M14 11C14.5523 11 15 10.5523 15 10C15 9.44772 14.5523 9 14 9C13.4477 9 13 9.44772 13 10C13 10.5523 13.4477 11 14 11Z"
+                  fill="currentColor" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_5"
+                  d="M18 11C18.5523 11 19 10.5523 19 10C19 9.44772 18.5523 9 18 9C17.4477 9 17 9.44772 17 10C17 10.5523 17.4477 11 18 11Z"
+                  fill="currentColor" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_6" d="M16 22H32M16 28H26" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssWebpageCenter }}</span>
+        </div> -->
+        <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">
+                <path id="Vector"
+                  d="M42 30V38C42 39.0609 41.5786 40.0783 40.8284 40.8284C40.0783 41.5786 39.0609 42 38 42H10C8.93913 42 7.92172 41.5786 7.17157 40.8284C6.42143 40.0783 6 39.0609 6 38V30"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2" d="M34 16L24 6L14 16" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3" d="M24 6V30" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssUploadWebpage }}</span>
+        </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">
+              <rect x="3" y="3" width="18" height="14" rx="2"></rect>
+              <path d="M12 8v6"></path>
+              <path d="M9 11h6"></path>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssNewWebpage }}</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">
+                <path id="Vector"
+                  d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2" d="M4 24H44" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3"
+                  d="M24 4C29.0026 9.47671 31.8455 16.5841 32 24C31.8455 31.4159 29.0026 38.5233 24 44C18.9974 38.5233 16.1545 31.4159 16 24C16.1545 16.5841 18.9974 9.47671 24 4Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_4" d="M32 16L24 24L16 16" stroke="currentColor" stroke-width="3" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssCrawlWebpage }}</span>
+        </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>
+        <div class="close-icon" @click="toggleSubmenu('multimedia')">
+          <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="submenu-item-box2">
+        <div class="submenu-item" @click="handleToolClick('video')">
+          <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">
+                <path id="Vector"
+                  d="M38 8H10C7.79086 8 6 9.79086 6 12V36C6 38.2091 7.79086 40 10 40H38C40.2091 40 42 38.2091 42 36V12C42 9.79086 40.2091 8 38 8Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2" d="M20 18L32 24L20 30V18Z" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssVideo }}</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">
+                <path id="Vector" d="M18 36V10L42 6V32" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2"
+                  d="M12 42C15.3137 42 18 39.3137 18 36C18 32.6863 15.3137 30 12 30C8.68629 30 6 32.6863 6 36C6 39.3137 8.68629 42 12 42Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3"
+                  d="M36 38C39.3137 38 42 35.3137 42 32C42 28.6863 39.3137 26 36 26C32.6863 26 30 28.6863 30 32C30 35.3137 32.6863 38 36 38Z"
+                  stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssAudio }}</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">
+                <path id="Vector"
+                  d="M28 4H12C10.9391 4 9.92172 4.42143 9.17157 5.17157C8.42143 5.92172 8 6.93913 8 8V40C8 41.0609 8.42143 42.0783 9.17157 42.8284C9.92172 43.5786 10.9391 44 12 44H36C37.0609 44 38.0783 43.5786 38.8284 42.8284C39.5786 42.0783 40 41.0609 40 40V16L28 4Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2" d="M28 4V16H40" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3" d="M16 26H32" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_4" d="M16 34H28" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssDocument }}</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">
+                <path id="Vector"
+                  d="M18 8H10C7.79086 8 6 9.79086 6 12V32C6 34.2091 7.79086 36 10 36H18C20.2091 36 22 34.2091 22 32V12C22 9.79086 20.2091 8 18 8Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_2"
+                  d="M38 8H30C27.7909 8 26 9.79086 26 12V32C26 34.2091 27.7909 36 30 36H38C40.2091 36 42 34.2091 42 32V12C42 9.79086 40.2091 8 38 8Z"
+                  stroke="currentColor" stroke-width="4" />
+                <path id="Vector_3" d="M14 18V26" stroke="currentColor" stroke-width="4" />
+                <path id="Vector_4" d="M34 18V26" stroke="currentColor" stroke-width="4" />
+              </g>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssDocumentSet }}</span>
+        </div> -->
+      </div>
+    </div>
+    <div class="submenu submenu-english" :class="{ visible: activeSubmenu === 'english' }">
+      <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>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue'
+import { storeToRefs } from 'pinia'
+import useCreateElement from '@/hooks/useCreateElement'
+import useSlideHandler from '@/hooks/useSlideHandler'
+import { useSlidesStore } from '@/store'
+import { useSpeakingStore } from '@/store/speaking'
+import FileInput from '@/components/FileInput.vue'
+import AiChat from './componets/aiChat.vue'
+import SpeakingPanel from '@/views/Editor/EnglishSpeaking/SpeakingPanel.vue'
+import { lang } from '@/main'
+import toolChoice from '@/assets/img/tool_choice.jpeg'
+import toolAnswer from '@/assets/img/tool_answer.png'
+import toolVote from '@/assets/img/tool_vote.png'
+import toolPhoto from '@/assets/img/tool_photo.png'
+
+
+
+interface ContentItem {
+  tool?: number
+  title?: string
+  url?: string
+  id?: string
+}
+
+
+
+const props = withDefaults(defineProps<{
+  defaultCollapsed?: boolean
+  userid?: string | null
+}>(), {
+  defaultCollapsed: false,
+  userid: null,
+})
+
+const emit = defineEmits<{
+  (e: 'toggle', collapsed: boolean): void
+}>()
+
+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)
+
+const { createFrameElement } = useCreateElement()
+const { createSlide, createSlideByTemplate } = useSlideHandler()
+
+
+
+import _ from 'lodash'
+
+const handleUrlInput = _.debounce(() => {
+  const url = webpageUrl.value.trim()
+  if (!url) {
+    isValidUrl.value = null
+  }
+  else {
+    // 简化的URL格式验证,支持各种复杂URL
+    const urlRegex = /^https?:\/\/.+/
+    isValidUrl.value = urlRegex.test(url)
+  }
+  console.log('URL输入:', webpageUrl.value, '验证结果:', isValidUrl.value)
+}, 300)
+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
+
+  createSlide()
+  createFrameElement(webpageUrl.value, 73) // 假设15是网页工具的类型
+  // 清空输入框和验证状态
+  webpageUrl.value = ''
+  isValidUrl.value = null
+}
+
+const toggleCollapse = () => {
+  isCollapsed.value = !isCollapsed.value
+  emit('toggle', isCollapsed.value)
+}
+
+const toggleSubmenu = (menu: string) => {
+  if (activeSubmenu.value === menu) {
+    activeSubmenu.value = null
+  }
+  else {
+    activeSubmenu.value = menu
+  }
+}
+
+// 监听"打开口语配置面板"信号 → 自动展开 english 子菜单
+const speakingStore = useSpeakingStore()
+watch(() => speakingStore.openConfigSignal, (val, old) => {
+  if (val !== old) {
+    if (isCollapsed.value) {
+      isCollapsed.value = false
+      emit('toggle', false)
+    }
+    activeSubmenu.value = 'english'
+  }
+})
+
+import titlePage from './page/TitlePage.json'
+import ImagePage from './page/ImagePage.json'
+import ContentPage from './page/ContentPage.json'
+import ImageTextPage from './page/ImageTextPage.json'
+
+const handleToolClick = _.debounce((tool: string) => {
+  interface ParentWindowWithToolList extends Window {
+    addTool?: (id: number) => void;
+    openVideoUploadDialog?: () => void;
+    openApplicationCenter?: () => void;
+  }
+  const parentWindow = window.parent as ParentWindowWithToolList
+  console.log('点击工具:', tool)
+  if (tool === 'h5page') {
+    parentWindow?.addTool?.(73)
+  }
+  else if (tool === 'aiapp') {
+    parentWindow?.addTool?.(72)
+  }
+  else if (tool === 'createApp') {
+    if (lang.lang === 'cn') {
+      window.open('https://cloud.cocorobo.cn/admin.html?type=cocoflow1', '_blank')
+    }
+    else if (lang.lang === 'en') {
+      window.open('https://cloud.cocorobo.com/admin.html?type=cocoflow1', '_blank')
+    }
+    else if (lang.lang === 'hk') {
+      window.open('https://cloud.cocorobo.hk/admin.html?type=cocoflow1', '_blank')
+    }
+  }
+  else if (tool === 'video') {
+    parentWindow?.openVideoUploadDialog?.()
+  }
+  else if (tool === 'creative') {
+    parentWindow?.openApplicationCenter?.()
+  }
+  else if (tool === 'choice') {
+    parentWindow?.addTool?.(45)
+  }
+  else if (tool === 'vote') {
+    parentWindow?.addTool?.(78)
+  }
+  else if (tool === 'photo') {
+    parentWindow?.addTool?.(79)
+  } 
+  else if (tool === 'qa') {
+    parentWindow?.addTool?.(15)
+  }
+  else if (tool === 'titlepage') {
+    createSlideByTemplate(titlePage)
+  }
+  else if (tool === 'ImagePage') {
+    createSlideByTemplate(ImagePage)
+  }
+  else if (tool === 'ContentPage') {
+    createSlideByTemplate(ContentPage)
+  }
+  else if (tool === 'ImageTextPage') {
+    createSlideByTemplate(ImageTextPage)
+  }
+  else if (tool === 'createWebpage') {
+    if (lang.lang === 'cn') {
+      window.open('https://cloud.cocorobo.cn/admin.html?type=cocoflow3', '_blank')
+    }
+    else if (lang.lang === 'en') {
+      window.open('https://cloud.cocorobo.com/admin.html?type=cocoflow3', '_blank')
+    }
+    else if (lang.lang === 'hk') {
+      window.open('https://cloud.cocorobo.hk/admin.html?type=cocoflow3', '_blank')
+    }
+  }
+  else if (tool === 'uploadWebpage') {
+    activeSubmenu.value = 'uploadWebpage'
+  }
+}, 300)
+
+const loadContentList = () => {
+  try {
+    interface ParentWindowWithToolList extends Window {
+      pptToolList?: ContentItem[]
+    }
+    const parentWindow = window.parent as ParentWindowWithToolList
+    contentList.value = parentWindow?.pptToolList || []
+  }
+  catch (error) {
+    console.error('加载内容列表失败:', error)
+    contentList.value = []
+  }
+}
+
+const insertContent = (item: ContentItem) => {
+  if (!item.tool || !item.url) return
+  createFrameElement(item.url, item.tool)
+}
+
+const addContent = (data: ContentItem, type: number) => {
+  // contentList.value.push(data)
+  if (type === 2) {
+    const elements = currentSlide.value?.elements || []
+    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78 || el.toolType === 79))
+    if (frameElement) {
+      slidesStore.updateElement({
+        id: frameElement.id,
+        props: { url: data.url, toolType: data.tool }
+      })
+    }
+  }
+  else {
+    createSlide()
+    insertContent(data)
+  }
+}
+
+Object.assign(window, { addContent, loadContentList })
+// window.loadContentList = loadContentList
+// window.addContent = addContent
+
+const previewVideo = (item: ContentItem) => {
+  interface ParentWindowWithToolList extends Window {
+    previewVideo?: (item: ContentItem) => void;
+  }
+  const parentWindow = window.parent as ParentWindowWithToolList
+  parentWindow?.previewVideo?.(item)
+}
+
+const editContent = (item: ContentItem) => {
+  interface ParentWindowWithToolList extends Window {
+    toolBtn?: (action: number, id: string) => void;
+  }
+  const parentWindow = window.parent as ParentWindowWithToolList
+  parentWindow?.toolBtn?.(0, item.id || '')
+}
+
+const copyContent = (item: ContentItem) => {
+  interface ParentWindowWithToolList extends Window {
+    toolBtn?: (action: number, id: string) => void;
+  }
+  const parentWindow = window.parent as ParentWindowWithToolList
+  parentWindow?.toolBtn?.(1, item.id || '')
+}
+
+const deleteContent = (item: ContentItem) => {
+  interface ParentWindowWithToolList extends Window {
+    toolBtn?: (action: number, id: string) => void;
+  }
+  const parentWindow = window.parent as ParentWindowWithToolList
+  parentWindow?.toolBtn?.(2, item.id || '')
+}
+
+const getTypeLabel = (type?: number) => {
+  const typeMap: Record<number, string> = {
+    45: lang.ssChoiceQ,
+    15: lang.ssQATest,
+    72: lang.ssAiApp,
+    73: lang.ssHPage,
+    81: lang.ssHPage,
+    74: lang.ssVideo,
+    75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
+    76: lang.ssCreative,
+    77: lang.ssEnglishSpeakingTool,
+    78: lang.ssVote,
+    79: lang.ssPhoto,
+  }
+  return typeMap[type || 0] || lang.ssUnknown
+}
+
+const getTypeClass = (type?: number) => {
+  const classMap: Record<number, string> = {
+    45: 'type-choice',
+    15: 'type-question',
+    72: 'type-ai',
+    73: 'type-h5',
+    74: 'type-video',
+    75: 'type-bilibili',
+    76: 'type-app-center'
+  }
+  return classMap[type || 0] || 'type-default'
+}
+
+import useImport from '@/hooks/useImport'
+import message from '@/utils/message'
+const { importPPTXFile, exporting, getFile } = useImport()
+const currentFileName = ref('')
+const parsingStatus = ref<'parsing' | 'success'>('parsing')
+const parsingAbortController = ref<AbortController | null>(null)
+
+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 })
+  }
+  catch (error) {
+    if (error instanceof DOMException && error.name === 'AbortError') {
+      console.log(lang.ssFileParseCancelled)
+    }
+    else {
+      console.error(lang.ssFileParseFailed, error)
+      message.error(lang.ssFileParseFailedRetry)
+    }
+  }
+}
+
+const handleParsingClose = () => {
+  if (exporting.value && parsingAbortController.value) {
+    parsingAbortController.value.abort()
+    exporting.value = false
+    parsingAbortController.value = null
+    // message.info(lang.ssParseCancelled)
+  }
+  else if (!exporting.value) {
+    emit('close')
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.collapsible-toolbar {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  background: #fff;
+  border-right: 1px solid #e5e7eb;
+  transition: width 0.3s ease;
+}
+
+.toolbar-content {
+  flex: 1;
+  overflow: hidden;
+  padding: 16px 8px;
+}
+
+.sidebar-content {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  width: 84px;
+  position: relative;
+}
+
+.sidebar-item {
+  width: 84px;
+  padding: 12px 8px;
+  border-radius: 12px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  cursor: pointer;
+  transition: all 0.2s;
+  position: relative;
+
+  &:hover {
+    background: #f3f4f6;
+  }
+
+  &:active {
+    background: #e5e7eb;
+  }
+
+  &.active {
+    background: #fff5e5;
+    box-shadow: 0 2px 8px rgba(40, 92, 245, 0.15);
+  }
+
+  &.active::after {
+    content: '';
+    position: absolute;
+    left: -8px;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 4px;
+    height: 32px;
+    background: #FF9300;
+    border-radius: 0 2px 2px 0;
+  }
+}
+
+.item-icon {
+  width: 22px;
+  height: 22px;
+  flex-shrink: 0;
+  color: #6b7280;
+}
+
+.sidebar-item:hover .item-icon,
+.sidebar-item.active .item-icon {
+  color: #FF9300;
+}
+
+.item-label {
+  font-size: 11px;
+  font-weight: 500;
+  color: #6b7280;
+  text-align: center;
+}
+
+.sidebar-item:hover .item-label,
+.sidebar-item.active .item-label {
+  color: #FF9300;
+  font-weight: 600;
+}
+
+.submenu {
+  width: 0;
+  min-width: 0;
+  overflow: hidden;
+  // transition: all 0.3s ease;
+  background: #fff;
+  border-radius: 0 12px 12px 0;
+  z-index: 100;
+
+  &.visible {
+    width: 420px;
+    // padding: 16px;
+    border-left: 1px solid #E5E7EB;
+  }
+
+  @media screen and (max-width: 1920px) {
+    &.visible {
+      width: 360px;
+    }
+  }
+
+  @media screen and (max-width: 1440px) {
+    &.visible {
+      width: 320px;
+    }
+  }
+
+  &.submenu-english {
+    display: flex;
+    flex-direction: column;
+  }
+}
+
+.submenu-title {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-bottom: 1px solid #f0f0f0;
+  margin-bottom: 20px;
+  width: 100%;
+  box-sizing: border-box;
+  padding: 12px 15px;
+
+  .title {
+    font-size: 14px;
+    font-weight: 600;
+    color: #333;
+  }
+
+  .close-icon {
+    width: 28px;
+    height: 28px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 8px;
+    cursor: pointer;
+    transition: all 0.2s ease;
+
+    &:hover {
+      background-color: #f3f4f6;
+
+      svg {
+        stroke: #6b7280;
+      }
+    }
+
+    svg {
+      width: 25px;
+      height: 25px;
+      stroke: #9ca3af;
+      transition: all 0.2s ease;
+    }
+  }
+}
+
+.submenu-content {
+  width: 100%;
+  height: calc(100% - 50px);
+  box-sizing: border-box;
+  overflow: hidden;
+}
+
+
+.submenu-img {
+  width: calc(100% - 30px);
+  margin: 0 auto;
+  height: 130px;
+  display: flex;
+  object-fit: cover;
+  border-radius: 10px;
+  margin-bottom: 20px;
+}
+
+.submenu-panel {
+  width: calc(100% - 30px);
+  height: 130px;
+  margin: 0 auto;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 15px 0;
+  margin-bottom: 20px;
+  background: #FFFAF0;
+  border-radius: 10px;
+  box-sizing: border-box;
+
+  svg,
+  img {
+    width: 120px;
+    height: auto;
+    object-fit: cover;
+  }
+
+  .detail {
+    font-size: 12px;
+    font-weight: 500;
+    color: #6B7280;
+    text-align: center;
+    margin-top: 5px;
+  }
+}
+
+/* 淡入淡出过渡效果 */
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.3s ease, transform 0.3s ease;
+}
+
+.fade-enter-from,
+.fade-leave-to {
+  opacity: 0;
+  transform: scale(0.95);
+}
+
+.fade-enter-to,
+.fade-leave-from {
+  opacity: 1;
+  transform: scale(1);
+}
+
+.submenu-item-box {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 12px;
+  padding: 0 15px;
+
+  .submenu-item {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 12px;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    font-size: 14px;
+    color: #333;
+    border-radius: 12px;
+    background: #fff;
+    border: 1px solid #e5e7eb;
+
+    &:hover {
+      background-color: #fffdfa;
+      border-color: #fffdfa;
+      color: #FF9300;
+      transform: translateY(-1px);
+      box-shadow: 0 2px 8px rgba(40, 92, 245, 0.1);
+    }
+
+    &:active {
+      background-color: #fffdfa;
+      transform: translateY(0);
+    }
+  }
+
+  .submenu-icon {
+    width: 20px;
+    height: 20px;
+    flex-shrink: 0;
+    color: #6b7280;
+    background: #fffaf0;
+    border-radius: 10px;
+    padding: 8px;
+    border: 1px solid #e5e7eb;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .submenu-item:hover .submenu-icon {
+    color: #FF9300;
+    border-color: #FF9300;
+    box-shadow: 0 0 0 2px rgba(255, 147, 0, 0.1);
+  }
+
+  .submenu-label {
+    font-size: 14px;
+    font-weight: 500;
+    color: #333;
+    flex: 1;
+  }
+
+  .submenu-item:hover .submenu-label {
+    color: #FF9300;
+    font-weight: 600;
+  }
+}
+
+.submenu-item-box2 {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 12px;
+  padding: 0 15px;
+
+  .submenu-item {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 12px;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    font-size: 14px;
+    color: #333;
+    border-radius: 12px;
+    border: 1px solid #e5e7eb;
+    flex-direction: column;
+    background: #fafbfc;
+
+    &:hover {
+      background-color: #fffdfa;
+      border-color: #fffdfa;
+      color: #FF9300;
+      transform: translateY(-1px);
+      box-shadow: 0 2px 8px rgba(40, 92, 245, 0.1);
+    }
+
+    &:active {
+      background-color: #fffdfa;
+      transform: translateY(0);
+    }
+  }
+
+  .submenu-icon {
+    flex-shrink: 0;
+    color: #6b7280;
+    background: #fff;
+    border-radius: 10px;
+    padding: 25px;
+    border: 1px solid #e5e7eb;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 95%;
+
+    svg {
+      width: 40px;
+      height: 40px;
+      color: #d7dbe0;
+    }
+  }
+
+  .submenu-item:hover .submenu-icon {
+    svg {
+      color: #FF9300;
+    }
+
+    border-color: #FF9300;
+    box-shadow: 0 0 0 2px rgba(255, 147, 0, 0.1);
+  }
+
+  .submenu-label {
+    font-size: 14px;
+    font-weight: 600;
+    color: #333;
+    flex: 1;
+  }
+
+  .submenu-item:hover .submenu-label {
+    color: #FF9300;
+    font-weight: 600;
+  }
+}
+
+.submenu-upload {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 12px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  font-size: 14px;
+  color: #333;
+  border-radius: 12px;
+  border: 1px solid #e5e7eb;
+  background: #fafbfc;
+  width: calc(100% - 30px);
+  margin: 10px auto 0;
+  justify-content: center;
+
+  .submenu-icon {
+    width: 20px;
+    height: 20px;
+
+    svg {
+      color: #d7dbe0;
+      width: 20px;
+      height: 20px;
+    }
+  }
+
+  .submenu-label {
+    font-size: 14px;
+    font-weight: 600;
+    color: #333;
+
+  }
+
+  &:hover {
+    background-color: #fffdfa;
+    border-color: #fffdfa;
+    color: #FF9300;
+    transform: translateY(-1px);
+    box-shadow: 0 2px 8px rgba(40, 92, 245, 0.1);
+
+
+    .submenu-icon {
+
+      svg {
+        color: #FF9300;
+      }
+    }
+
+    .submenu-label {
+      color: #FF9300;
+    }
+  }
+}
+
+
+.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: 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;
+    }
+
+    .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;
+    }
+  }
+}
+
+.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/ColorButton.vue

@@ -33,6 +33,7 @@ defineProps<{
 .content {
   width: 100%;
   height: 100%;
+  padding: 0;
 }
 .color-btn-icon {
   width: 32px;

+ 143 - 0
src/components/Contextmenu2/MenuContent.vue

@@ -0,0 +1,143 @@
+<template>
+  <ul class="menu-content" 
+    :style="{width: lang.lang === 'en' ? '160px' : '120px'}"
+  >
+    <template v-for="(menu, index) in menus" :key="menu.text || index">
+      <li
+        v-if="!menu.hide"
+        class="menu-item"
+        @click.stop="handleClickMenuItem(menu)"
+        :class="{'divider': menu.divider, 'disable': menu.disable}"
+      >
+        <div 
+          class="menu-item-content" 
+          :class="{
+            'has-children': menu.children,
+            'has-handler': menu.handler,
+          }" 
+          v-if="!menu.divider"
+        >
+          <span class="text">{{menu.text}}</span>
+          <span class="sub-text" v-if="menu.subText && !menu.children">{{menu.subText}}</span>
+
+          <menu-content 
+            class="sub-menu"
+            :menus="menu.children" 
+            v-if="menu.children && menu.children.length"
+            :handleClickMenuItem="handleClickMenuItem" 
+          />
+        </div>
+      </li>
+    </template>
+  </ul>
+</template>
+
+<script lang="ts" setup>
+import type { ContextmenuItem } from './types'
+import { lang } from '@/main'
+defineProps<{
+  menus: ContextmenuItem[]
+  handleClickMenuItem: (item: ContextmenuItem) => void
+}>()
+</script>
+
+<style lang="scss" scoped>
+$menuWidth: 180px;
+$menuHeight: 30px;
+$subMenuWidth: 120px;
+
+.menu-content {
+  width: $menuWidth;
+  padding: 8px;
+  // background: #fff;
+  background: #fff;
+  box-shadow: $boxShadow;
+  border-radius: 10px;
+  list-style: none;
+  margin: 0;
+}
+.menu-item {
+  // padding: 0 20px;
+  color: #555;
+  font-size: 14px;
+  transition: all $transitionDelayFast;
+  white-space: nowrap;
+  height: 40px;
+  line-height: 40px;
+  // background-color: #fff;
+  background-color: #fff;
+  cursor: pointer;
+  border-radius: 10px;
+
+  &:not(.disable):hover > .menu-item-content > .sub-menu {
+    display: block;
+  }
+
+  &:not(.disable):hover > .has-children.has-handler::after {
+    transform: scale(1);
+  }
+
+  &:hover:not(.disable) {
+    // background-color: rgba($color: $themeColor, $alpha: .2);
+    background-color: rgba($color: #ff9300, $alpha: .2);
+  }
+
+  &.divider {
+    height: 1px;
+    overflow: hidden;
+    margin: 5px;
+    background-color: #e5e5e5;
+    line-height: 0;
+    padding: 0;
+  }
+
+  &.disable {
+    color: #b1b1b1;
+    cursor: no-drop;
+  }
+}
+.menu-item-content {
+  display: flex;
+  align-items: center;
+  // justify-content: space-between;
+  justify-content: center;
+  position: relative;
+
+  &.has-children::before {
+    content: '';
+    display: inline-block;
+    width: 8px;
+    height: 8px;
+    border-width: 1px;
+    border-style: solid;
+    border-color: #666 #666 transparent transparent;
+    position: absolute;
+    right: 0;
+    top: 50%;
+    transform: translateY(-50%) rotate(45deg);
+  }
+  &.has-children.has-handler::after {
+    content: '';
+    display: inline-block;
+    width: 1px;
+    height: 24px;
+    background-color: rgba($color: #fff, $alpha: .3);
+    position: absolute;
+    right: 18px;
+    top: 3px;
+    transform: scale(0);
+    transition: transform $transitionDelay;
+  }
+
+  .sub-text {
+    opacity: 0.6;
+  }
+  .sub-menu {
+    width: $subMenuWidth;
+    position: absolute;
+    display: none;
+    left: 112%;
+    top: -6px;
+  }
+}
+</style>

+ 80 - 0
src/components/Contextmenu2/index.vue

@@ -0,0 +1,80 @@
+<template>
+  <div 
+    class="mask"
+    @contextmenu.prevent="removeContextmenu()"
+    @mousedown.left="removeContextmenu()"
+  ></div>
+
+  <div 
+    class="contextmenu"
+    :style="{
+      left: style.left + 'px',
+      top: style.top + 'px',
+    }"
+    @contextmenu.prevent
+  >
+    <MenuContent 
+      :menus="menus"
+      :handleClickMenuItem="handleClickMenuItem" 
+    />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+import type { ContextmenuItem, Axis } from './types'
+
+import MenuContent from './MenuContent.vue'
+
+const props = defineProps<{
+  axis: Axis
+  el: HTMLElement
+  menus: ContextmenuItem[]
+  removeContextmenu: () => void
+}>()
+
+const style = computed(() => {
+  const MENU_WIDTH = 180
+  const MENU_HEIGHT = 30
+  const DIVIDER_HEIGHT = 11
+  const PADDING = 5
+
+  const { x, y } = props.axis
+  const menuCount = props.menus.filter(menu => !(menu.divider || menu.hide)).length
+  const dividerCount = props.menus.filter(menu => menu.divider).length
+
+  const menuWidth = MENU_WIDTH
+  const menuHeight = menuCount * MENU_HEIGHT + dividerCount * DIVIDER_HEIGHT + PADDING * 2
+
+  const screenWidth = document.body.clientWidth
+  const screenHeight = document.body.clientHeight
+
+  return {
+    left: screenWidth <= x + menuWidth ? x - menuWidth : x,
+    top: screenHeight <= y + menuHeight ? y - menuHeight : y,
+  }
+})
+
+const handleClickMenuItem = (item: ContextmenuItem) => {
+  if (item.disable) return
+  if (item.children && !item.handler) return
+  if (item.handler) item.handler(props.el)
+  props.removeContextmenu()
+}
+</script>
+
+<style lang="scss">
+.mask {
+  position: fixed;
+  left: 0;
+  top: 0;
+  width: 100vw;
+  height: 100vh;
+  z-index: 9998;
+}
+.contextmenu {
+  position: fixed;
+  z-index: 9999;
+  user-select: none;
+}
+</style>

+ 14 - 0
src/components/Contextmenu2/types.ts

@@ -0,0 +1,14 @@
+export interface ContextmenuItem {
+  text?: string
+  subText?: string
+  divider?: boolean
+  disable?: boolean
+  hide?: boolean
+  children?: ContextmenuItem[]
+  handler?: (el: HTMLElement) => void
+}
+
+export interface Axis {
+  x: number
+  y: number
+}

+ 27 - 2
src/components/FileInput.vue

@@ -1,5 +1,13 @@
 <template>
-  <div class="file-input" @click="handleClick()">
+  <div 
+    class="file-input" 
+    :class="{ 'drag-over': isDragOver }"
+    @click="handleClick()"
+    @dragenter.prevent="handleDragEnter"
+    @dragover.prevent="handleDragOver"
+    @dragleave.prevent="handleDragLeave"
+    @drop.prevent="handleDrop"
+  >
     <slot></slot>
     <input 
       class="input"
@@ -14,7 +22,7 @@
 </template>
 
 <script lang="ts" setup>
-import { useTemplateRef } from 'vue'
+import { ref, useTemplateRef } from 'vue'
 
 withDefaults(defineProps<{
   accept?: string
@@ -27,6 +35,7 @@ const emit = defineEmits<{
 }>()
 
 const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
+const isDragOver = ref(false)
 
 const handleClick = () => {
   if (!inputRef.value) return
@@ -37,6 +46,22 @@ const handleChange = (e: Event) => {
   const files = (e.target as HTMLInputElement).files
   if (files) emit('change', files)
 }
+const handleDragEnter = () => {
+  isDragOver.value = true
+}
+const handleDragOver = () => {
+  isDragOver.value = true
+}
+const handleDragLeave = () => {
+  isDragOver.value = false
+}
+const handleDrop = (e: DragEvent) => {
+  isDragOver.value = false
+  const files = e.dataTransfer?.files
+  if (files && files.length > 0) {
+    emit('change', files)
+  }
+}
 </script>
 
 <style lang="scss" scoped>

+ 6 - 4
src/components/Switch.vue

@@ -31,14 +31,16 @@ const handleChange = () => {
 
 <style lang="scss" scoped>
 .switch {
-  height: 20px;
-  display: inline-block;
+  // height: 20px;
+  // display: inline-block;
+  display: flex;
+  align-items: center;
   cursor: pointer;
 
   &:not(.disabled).active {
     .switch-core {
-      border-color: $themeColor;
-      background-color: $themeColor;
+      border-color: #f6c82b;
+      background-color: #f6c82b;
 
       &::after {
         left: 100%;

+ 38 - 0
src/components/TextColorButton2.vue

@@ -0,0 +1,38 @@
+<template>
+  <Button class="text-color-btn">
+    <slot></slot>
+    <div class="text-color-block">
+      <div class="text-color-block-content" :style="{ backgroundColor: color }"></div>
+    </div>
+  </Button>
+</template>
+
+<script lang="ts" setup>
+import Button from './Button.vue'
+
+defineProps<{
+  color: string
+}>()
+</script>
+
+<style lang="scss" scoped>
+.text-color-btn {
+  width: 100%;
+  display: flex !important;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  padding: 0 15px;
+}
+.text-color-block {
+  width: 17px;
+  height: 4px;
+  margin-top: 1px;
+  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAACdJREFUGFdjfPbs2X8GBgYGSUlJEMXAiCHw//9/sIrnz59DVKALAADNxxVfaiODNQAAAABJRU5ErkJggg==);
+
+  .text-color-block-content {
+    width: 100%;
+    height: 100%;
+  }
+}
+</style>

+ 2 - 1
src/hooks/useCreateElement.ts

@@ -318,7 +318,7 @@ export default () => {
    * 创建网页元素
    * @param url 网页链接地址
    */
-  const createFrameElement = (url: string, type: number) => {
+  const createFrameElement = (url: string, type: number, id?: string) => {
     // 检查当前幻灯片是否已经包含网页元素
     const { currentSlide } = storeToRefs(useSlidesStore())
     const hasWebpage = currentSlide.value?.elements?.some(element => element.type === 'frame')
@@ -334,6 +334,7 @@ export default () => {
     createElement({
       type: 'frame',
       id: nanoid(10),
+      webId: id || '',
       url,
       width: width,
       height: height,

+ 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
   }
 }

+ 64 - 0
src/plugins/directive/contextmenu2.ts

@@ -0,0 +1,64 @@
+import { type Directive, type DirectiveBinding, createVNode, render } from 'vue'
+import ContextmenuComponent from '@/components/Contextmenu2/index.vue'
+
+const CTX_CONTEXTMENU_HANDLER = 'CTX_CONTEXTMENU_HANDLER'
+
+interface CustomHTMLElement extends HTMLElement {
+  [CTX_CONTEXTMENU_HANDLER]?: (event: MouseEvent) => void
+} 
+
+const contextmenuListener = (el: HTMLElement, event: MouseEvent, binding: DirectiveBinding) => {
+  event.stopPropagation()
+  event.preventDefault()
+
+  const menus = binding.value(el)
+  if (!menus) return
+
+  let container: HTMLDivElement | null = null
+
+  // 移除右键菜单并取消相关的事件监听
+  const removeContextmenu = () => {
+    if (container) {
+      document.body.removeChild(container)
+      container = null
+    }
+    el.classList.remove('contextmenu-active')
+    document.body.removeEventListener('scroll', removeContextmenu)  
+    window.removeEventListener('resize', removeContextmenu)
+  }
+
+  // 创建自定义菜单
+  const options = {
+    axis: { x: event.x, y: event.y },
+    el,
+    menus,
+    removeContextmenu,
+  }
+  container = document.createElement('div')
+  const vm = createVNode(ContextmenuComponent, options, null)
+  render(vm, container)
+  document.body.appendChild(container)
+
+  // 为目标节点添加菜单激活状态的className
+  el.classList.add('contextmenu-active')
+
+  // 页面变化时移除菜单
+  document.body.addEventListener('scroll', removeContextmenu)
+  window.addEventListener('resize', removeContextmenu)
+}
+
+const ContextmenuDirective: Directive = {
+  mounted(el: CustomHTMLElement, binding) {
+    el[CTX_CONTEXTMENU_HANDLER] = (event: MouseEvent) => contextmenuListener(el, event, binding)
+    el.addEventListener('contextmenu', el[CTX_CONTEXTMENU_HANDLER])
+  },
+
+  unmounted(el: CustomHTMLElement) {
+    if (el && el[CTX_CONTEXTMENU_HANDLER]) {
+      el.removeEventListener('contextmenu', el[CTX_CONTEXTMENU_HANDLER])
+      delete el[CTX_CONTEXTMENU_HANDLER]
+    }
+  },
+}
+
+export default ContextmenuDirective

+ 2 - 0
src/plugins/directive/index.ts

@@ -1,12 +1,14 @@
 import type { App } from 'vue'
 
 import Contextmenu from './contextmenu'
+import Contextmenu2 from './contextmenu2'
 import ClickOutside from './clickOutside'
 import Tooltip from './tooltip'
 
 export default {
   install(app: App) {
     app.directive('contextmenu', Contextmenu)
+    app.directive('contextmenu2', Contextmenu2)
     app.directive('click-outside', ClickOutside)
     app.directive('tooltip', Tooltip)
   }

+ 4 - 0
src/plugins/icon.ts

@@ -131,6 +131,8 @@ import {
   LoadingFour, // 引入loadingIcon
   UpTwo,
   Refresh,
+  ThumbsUp,
+  CastScreen
 } from '@icon-park/vue-next'
 
 export interface Icons {
@@ -267,6 +269,8 @@ export const icons: Icons = {
   IconLoading: LoadingFour, // 添加loadingIcon
   UpTwo: UpTwo,
   IconRefresh: Refresh,
+  IconThumbsUp: ThumbsUp,
+  IconCastScreen: CastScreen,
 }
 
 export default {

+ 1 - 1
src/services/config.ts

@@ -17,7 +17,7 @@ instance.interceptors.request.use(
     // 确保每个请求都带上cookie
     config.withCredentials = true
 
-    if (url.includes('https://gpt4.cocorobo.cn') || url.includes('https://appapi.cocorobo.cn') || url.includes('https://ccrb.s3.cn-northwest-1.amazonaws.com.cn')) {
+    if (url.includes('https://yjsredis.cocorobo.cn/') || url.includes('https://gpt4.cocorobo.cn') || url.includes('https://appapi.cocorobo.cn') || url.includes('https://ccrb.s3.cn-northwest-1.amazonaws.com.cn')) {
       config.withCredentials = false 
     }
 

+ 14 - 1
src/services/course.ts

@@ -199,6 +199,17 @@ export const getWorkPageId = (params: any): Promise<any> => {
 }
 
 
+/**
+ * 
+ * 点赞功能
+ * @param any wid 作业id lid 用户id t 1点赞 2评论 c 评论内容
+ * @returns Promise<any>
+ */
+
+export const likeWork = (params: any): Promise<any> => {
+  return axios.post(`${API_URL}insertComment`, [params])
+}
+
 /**
  * 
  * 获取年级
@@ -229,6 +240,8 @@ export default {
   getAgentData,
   clearDialogue,
   getWorkDetail,
-  getWorkPageId
+  getWorkPageId,
+  likeWork,
+  getClassById,
 }
 

+ 4 - 3
src/services/speaking.ts

@@ -74,10 +74,11 @@ export async function listSpeakingSessionsByConfig(
   configId: string,
   userIds: string[],
 ): Promise<ListSessionsByConfigResponse> {
-  const params = new URLSearchParams({ configId, userIds: userIds.join(',') })
-  const res = await fetch(`${DIALOGUE_BASE}/sessions/by-config?${params}`, {
-    method: 'GET',
+  const res = await fetch(`${DIALOGUE_BASE}/sessions/by-config`, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
     credentials: 'include',
+    body: JSON.stringify({ configId, userIds }),
   })
   return parse<ListSessionsByConfigResponse>(res)
 }

+ 5 - 4
src/tools/aiChat.ts

@@ -50,7 +50,7 @@ export const chat_no_stream = (msg: string, agentId: string, userId: string, lan
         : language === 'hk'
           ? 'Traditional Chinese'
           : 'Chinese'
-      } ${msg} ${language === 'hk' ? '請用繁體中文回复' : language === 'en' ? 'Please reply in English' : '用中文回复'}`,
+      } ${msg} ${language === 'hk' ? '請用繁體中文回复' : language === 'en' ? 'Please reply in English' : '用中文回复'}`,
       uid: uuidv4(),
       stream: false,
       model: agentData?.modelType || 'open-doubao',
@@ -103,7 +103,8 @@ export const chat_stream = async (
   language: string,
   onMessage: (event: { type: 'message' | 'close' | 'error' | 'messageEnd'; data: string }) => void,
   session_name?: string,
-  file_ids?: Array<string>
+  file_ids?: Array<string>,
+  model?: string
 ): Promise<{ abort: () => void }> => {
   const agentData = await getAgentModel(agentId)
   const params: ChatParams = {
@@ -115,10 +116,10 @@ export const chat_stream = async (
       : language === 'hk'
         ? 'Traditional Chinese'
         : 'Chinese'
-    } ${msg}  ${language === 'hk' ? '請用繁體中文回复' : language === 'en' ? 'Please reply in English' : '用中文回复'}`,
+    } ${msg}  ${language === 'hk' ? '請用繁體中文回复' : language === 'en' ? 'Please reply in English' : '用中文回复'}`,
     uid: uuidv4(),
     stream: true,
-    model: agentData?.modelType || 'open-doubao',
+    model: model || agentData?.modelType || 'open-doubao',
     userId: userId,
     tts_language: getTtsLanguage(language),
     session_name: session_name || uuidv4()

+ 1 - 0
src/types/slides.ts

@@ -651,6 +651,7 @@ export interface PPTFrameElement extends PPTBaseElement {
   isHTML?: boolean,
   toolType?: number,
   isDone?: boolean,
+  webId?: string,
 }
 
 export type PPTElement = PPTTextElement | PPTImageElement | PPTShapeElement | PPTLineElement | PPTChartElement | PPTTableElement | PPTLatexElement | PPTVideoElement | PPTAudioElement | PPTFrameElement

+ 162 - 0
src/utils/components/messageInstruction.vue

@@ -0,0 +1,162 @@
+<template>
+  <div class="messageInstruction">
+    <div class="messageItem" :class="item.type" v-for="item in messageList" :key="item.id">
+      <div class="icon">
+        <svg v-if="['success'].includes(item.type)" viewBox="0 0 1024 1024"><path d="M512 85.333333c235.648 0 426.666667 191.018667 426.666667 426.666667s-191.018667 426.666667-426.666667 426.666667S85.333333 747.648 85.333333 512 276.352 85.333333 512 85.333333z m-74.965333 550.4L346.453333 545.152a42.666667 42.666667 0 1 0-60.330666 60.330667l120.704 120.704a42.666667 42.666667 0 0 0 60.330666 0l301.653334-301.696a42.666667 42.666667 0 1 0-60.288-60.330667l-271.530667 271.488z" fill="#67C239"></path></svg>
+        <svg v-if="['pptMessage'].includes(item.type)" viewBox="0 0 1024 1024" ><path d="M512 85.333c-234.667 0-426.667 192-426.667 426.667s192 426.667 426.667 426.667 426.667-192 426.667-426.667S746.667 85.333 512 85.333z m21.333 640h-42.666c-12.8 0-21.334-8.533-21.334-21.333v-42.667c0-12.8 8.534-21.333 21.334-21.333h42.666c12.8 0 21.334 8.533 21.334 21.333V704c0 12.8-8.534 21.333-21.334 21.333zM512 554.667c-25.6 0-42.667-17.067-42.667-42.667V341.333c0-25.6 17.067-42.666 42.667-42.666s42.667 17.066 42.667 42.666V512c0 25.6-17.067 42.667-42.667 42.667z" fill="#FCCF00"></path></svg>
+        <!-- <svg v-if="['pptMessage'].includes(item.type)" viewBox="0 0 1024 1024"><path d="M512 28.444444C244.906667 28.444444 28.444444 244.906667 28.444444 512s216.462222 483.555556 483.555556 483.555556 483.555556-216.462222 483.555556-483.555556S779.093333 28.444444 512 28.444444z m30.225067 755.5584A30.190933 30.190933 0 0 1 512 814.222222a30.190933 30.190933 0 0 1-30.225067-30.225066A30.190933 30.190933 0 0 1 512 753.777778a30.190933 30.190933 0 0 1 30.225067 30.225066z m0-120.888888A30.190933 30.190933 0 0 1 512 693.327644a30.190933 30.190933 0 0 1-30.225067-30.225066V240.008533A30.190933 30.190933 0 0 1 512 209.772089a30.190933 30.190933 0 0 1 30.225067 30.225067v423.105422z" fill="#FCCF00"></path></svg> -->
+      </div>
+      <div class="message" v-html="item.message"></div>
+      <div class="close" @click="close(item.id)" v-if="item.showClose">
+        <svg viewBox="0 0 1025 1024"><path d="M997.553471 154.252491 639.804427 512.001535 997.553471 869.751602l0 0c16.34988 16.374439 26.450623 38.948897 26.450623 63.898376 0 49.899981-40.450041 90.350022-90.351045 90.350022-24.949479 0-47.549519-10.100743-63.897353-26.475181l0 0L512.003581 639.775775 154.255561 997.525842l0 0c-16.34988 16.374439-38.948897 26.475181-63.899399 26.475181-49.901004 0-90.350022-40.450041-90.350022-90.350022 0-24.950502 10.099719-47.523938 26.449599-63.898376l0 0 357.750067-357.750067L26.454716 154.252491l0 0c-16.34988-16.350903-26.449599-38.949921-26.449599-63.900423 0-49.899981 40.449018-90.348999 90.350022-90.348999 24.950502 0 47.550543 10.099719 63.899399 26.474158l0 0 357.748021 357.749044L869.754672 26.477228l0 0c16.348857-16.374439 38.948897-26.474158 63.897353-26.474158 49.901004 0 90.351045 40.449018 90.351045 90.348999C1024.00307 115.301547 1013.902327 137.901588 997.553471 154.252491L997.553471 154.252491z" fill="#1A1A1A"></path></svg>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref } from 'vue'
+
+const messageList = ref([])
+
+const success = (data) => {
+  if (typeof data === 'string') {
+    let _id = Date.now()
+    messageList.value.push({
+      id: _id,
+      type: 'success',
+      message: data,
+      showClose: data.showClose || false
+    })
+    setTimeout(() => {
+      messageList.value = messageList.value.filter(item => item.id !== _id)
+    }, 3000)
+  } else if (typeof data === 'object' && data.message && data.duration) {
+    let _id = Date.now()
+    messageList.value.push({
+      id: _id,
+      type: 'success',
+      message: data.message,
+      showClose: data.showClose || false
+    })
+    setTimeout(() => {
+      messageList.value = messageList.value.filter(item => item.id !== _id)
+    }, data.duration)
+  }
+}
+
+const pptMessage = (data) => {
+  if (typeof data === 'string') {
+    let _id = Date.now()
+    messageList.value.push({
+      id: _id,
+      type: 'pptMessage',
+      message: data,
+      showClose: data.showClose || true
+    })
+    setTimeout(() => {
+      messageList.value = messageList.value.filter(item => item.id !== _id)
+    }, 3000)
+  } else if (typeof data === 'object' && data.message && data.duration) {
+    let _id = Date.now()
+    messageList.value.push({
+      id: _id,
+      type: 'pptMessage',
+      message: data.message,
+      showClose: data.showClose || true
+    })
+    setTimeout(() => {
+      messageList.value = messageList.value.filter(item => item.id !== _id)
+    }, data.duration)
+  }
+}
+
+const close = (id) => {
+  messageList.value = messageList.value.filter(item => item.id !== id)
+}
+defineExpose({
+  success,
+  pptMessage,
+  close
+})
+</script>
+
+<style lang="scss" scoped>
+.messageInstruction {
+  position: fixed;
+  top: 80px;
+  left: 50%;
+  transform: translate(-50%, 0%);
+  width: auto;
+  height: auto;
+  z-index: 9999;
+  display: flex;
+  flex-direction: column;
+  gap: .5rem;
+}
+
+.messageItem{
+  padding: 15px 20px;
+  border-radius: .2rem;
+  font-size: 1rem;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background: #FFFFFF;
+  box-shadow: 0 0 .2rem rgba(0, 0, 0, 0.1);
+  transition: .3s;
+}
+
+.messageItem>.icon{
+  width: 1.3rem;
+  height: 1.3rem;
+  margin-right: .8rem;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  //margin-top: -.1rem;
+  line-height: 1rem;
+}
+
+.messageItem>.icon>svg{
+  width: 100%;
+  height: 100%;
+}
+
+.messageItem>.close{
+  width: .6rem;
+  height: .6rem;
+  margin-left: 1.5rem;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-top: .3rem;
+}
+
+.messageItem>.close>svg{
+  width: 100%;
+  height: 100%;
+  cursor: pointer;
+}
+
+.messageItem>.message{
+  flex: 1;
+  max-width: 50vh;
+}
+
+.messageItem {
+  animation: slideDownFadeIn 0.3s ease-out;
+}
+
+@keyframes slideDownFadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(-20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+</style>

+ 4 - 0
src/utils/confirmDialog.ts

@@ -8,6 +8,8 @@ interface ConfirmDialogOptions {
   confirmText?: string
   cancelText?: string
   width?: number
+  onConfirm?: () => void
+  onCancel?: () => void
 }
 
 export function showConfirmDialog(options: ConfirmDialogOptions): Promise<boolean> {
@@ -27,6 +29,7 @@ export function showConfirmDialog(options: ConfirmDialogOptions): Promise<boolea
         visible.value = false
         setTimeout(() => {
           app.unmount()
+          options.onConfirm?.()
           document.body.removeChild(container)
           resolve(true)
         }, 300)
@@ -35,6 +38,7 @@ export function showConfirmDialog(options: ConfirmDialogOptions): Promise<boolea
         visible.value = false
         setTimeout(() => {
           app.unmount()
+          options.onCancel?.()
           document.body.removeChild(container)
           resolve(false)
         }, 300)

+ 2 - 0
src/views/Editor/CanvasTool/WebpageInput.vue

@@ -103,11 +103,13 @@ const getTypeLabel = (type: number) => {
     15: lang.ssQATest,
     72: lang.ssAiApp,
     73: lang.ssHPage,
+    81: lang.ssHPage,
     74: lang.ssVideo,
     75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
     76: lang.ssCreative,
     77: lang.ssEnglishSpeakingTool,
     78: lang.ssVote,
+    79: lang.ssPhoto,
   }
   return typeMap[type] || lang.ssUnknown
 }

+ 184 - 5
src/views/Editor/CanvasTool/index2.vue

@@ -17,13 +17,20 @@
             </svg>
             <span>{{ lang.ssQandA }}</span>
           </div>
-          <!-- <div class="popover-item" @click="editContent(78)" v-if="frametype != 78">
+          <div class="popover-item" @click="editContent(78)" v-if="frametype != 78">
             <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                 <polyline points="9 11 12 14 22 4"></polyline>
                 <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
             </svg>
             <span>{{ lang.ssVote }}</span>
-          </div> -->
+          </div>
+          <div class="popover-item" @click="editContent(79)" v-if="frametype != 79">
+            <svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
+                <circle cx="12" cy="13" r="4"></circle>
+            </svg>
+            <span>{{ lang.ssPhoto }}</span>
+          </div>
         </template>
         <div class="handler-item" :class="{ active: toolVisible }">
           <span class="svg-icon">
@@ -40,6 +47,10 @@
                 <polyline points="9 11 12 14 22 4"></polyline>
                 <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
             </svg>
+            <svg v-if="frametype == 79" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
+                <circle cx="12" cy="13" r="4"></circle>
+            </svg>
           </span>
           <span>{{ iframeLabel }}</span>
           <svg t="1776672009773" class="xia-icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
@@ -115,6 +126,73 @@
           <span>{{ lang.ssShape }}</span>
         </div>
       </Popover>
+      <RichTextBase  v-if="handleElement?.type == 'text'"/>
+      <div style="display: flex; align-items: center;" v-if="handleElement?.type == 'shape'">
+        <Select 
+          style="flex: 1;" 
+          :value="fillType" 
+          @update:value="value => updateFillType(value as 'fill' | 'gradient' | 'pattern')"
+          :options="[
+            { label: lang.ssSolidFill, value: 'fill' },
+            { label: lang.ssGradFill, value: 'gradient' },
+            // { label: lang.ssImgFill, value: 'pattern' },
+          ]"
+        />
+        <Popover trigger="click" v-if="fillType === 'fill'" style="flex: 1;">
+          <template #content>
+            <ColorPicker
+              :modelValue="fill"
+              @update:modelValue="value => updateFill(value)"
+            />
+          </template>
+          <ColorButton style="width: 100px;" :color="fill" />
+        </Popover>
+        <Select 
+          style="flex: 1;" 
+          :value="gradient.type" 
+          @update:value="value => updateGradient({ type: value as GradientType })"
+          v-else-if="fillType === 'gradient'"
+          :options="[
+            { label: lang.ssLinearGrad, value: 'linear' },
+            { label: lang.ssRadialGrad, value: 'radial' },
+          ]"
+        />
+      </div>
+
+      <template v-if="fillType === 'gradient' && handleElement?.type == 'shape'">
+        <div class="row">
+          <GradientBar
+            :value="gradient.colors"
+            :index="currentGradientIndex"
+            @update:value="value => updateGradient({ colors: value })"
+            @update:index="index => currentGradientIndex = index"
+          />
+        </div>
+        <div class="row">
+          <div style="width: 40%;">{{ lang.ssCurColorBlock }}</div>
+          <Popover trigger="click" style="width: 60%;">
+            <template #content>
+              <ColorPicker
+                :modelValue="gradient.colors[currentGradientIndex].color"
+                @update:modelValue="value => updateGradientColors(value)"
+              />
+            </template>
+            <ColorButton :color="gradient.colors[currentGradientIndex].color" />
+          </Popover>
+        </div>
+        <div class="row" v-if="gradient.type === 'linear'">
+          <div style="width: 40%;">{{ lang.ssGradAngle }}</div>
+          <Slider
+            style="width: 60%;"
+            :min="0"
+            :max="360"
+            :step="15"
+            :value="gradient.rotate"
+            @update:value="value => updateGradient({ rotate: value as number })" 
+          />
+        </div>
+      </template>
+      <ElementFlip  v-if="handleElement?.type == 'shape'"/>
 
       <!-- 英语口语工具:重置预览 -->
       <div
@@ -155,7 +233,7 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue'
+import { ref, computed, watch } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useMainStore, useSnapshotStore, useSlidesStore } from '@/store'
 import { useSpeakingStore } from '@/store/speaking'
@@ -182,6 +260,15 @@ import Popover from '@/components/Popover.vue'
 import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
 import Button from '@/components/Button.vue'
 
+import RichTextBase from '@/views/Editor/Toolbar/common/RichTextBase2.vue'
+import Select from '@/components/Select.vue'
+import ColorPicker from '@/components/ColorPicker/index.vue'
+import GradientBar from '@/components/GradientBar.vue'
+import Slider from '@/components/Slider.vue'
+import ColorButton from '@/components/ColorButton.vue'
+import ElementFlip from '@/views/Editor/Toolbar/common/ElementFlip2.vue'
+
+
 const mainStore = useMainStore()
 const slidesStore = useSlidesStore()
 const speakingStore = useSpeakingStore()
@@ -222,7 +309,7 @@ const viewMode = computed(() => getInitialViewMode())
 
 const hasInteractiveTool = computed(() => {
   const elements = currentSlide.value?.elements || []
-  return elements.some((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78))
+  return elements.some((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78 || el.toolType === 79))
 })
 
 const iframeLabel = computed(() => {
@@ -241,11 +328,13 @@ const getTypeLabel = (type: number) => {
     15: lang.ssQATest,
     72: lang.ssAiApp,
     73: lang.ssHPage,
+    81: lang.ssHPage,
     74: lang.ssVideo,
     75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
     76: lang.ssCreative,
     77: lang.ssEnglishSpeakingTool,
     78: lang.ssVote,
+    79: lang.ssPhoto,
   }
   return typeMap[type] || lang.ssUnknown
 }
@@ -441,12 +530,95 @@ const editContent = (toolType: number) => {
     }
     const parentWindow = window.parent as ParentWindowWithToolList
     const elements = currentSlide.value?.elements || []
-    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78))
+    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 45 || el.toolType === 15 || el.toolType === 78 || el.toolType === 79))
     parentWindow?.toolBtn2?.(0, frameElement?.url || '', toolType)
     toolVisible.value = false
   })
 }
 
+import type { GradientType, PPTShapeElement, Gradient, ShapeText } from '@/types/slides'
+const { handleElement, handleElementId } = storeToRefs(useMainStore())
+const fillType = ref('fill')
+const fill = ref<string>('#000')
+const textAlign = ref('middle')
+const pattern = ref('')
+import emitter, { EmitterEvents } from '@/utils/emitter'
+const { addHistorySnapshot } = useHistorySnapshot()
+const currentGradientIndex = ref(0)
+
+watch(handleElementId, () => {
+  currentGradientIndex.value = 0
+})
+
+const updateElement = (props: Partial<PPTShapeElement>) => {
+  slidesStore.updateElement({ id: handleElementId.value, props })
+  addHistorySnapshot()
+}
+
+const gradient = ref<Gradient>({
+  type: 'linear', 
+  rotate: 0,
+  colors: [
+    { pos: 0, color: '#fff' },
+    { pos: 100, color: '#fff' },
+  ],
+})
+
+watch(handleElement, () => {
+  if (!handleElement.value || handleElement.value.type !== 'shape') return
+
+  fill.value = handleElement.value.fill || '#fff'
+  const defaultGradientColor = [
+    { pos: 0, color: fill.value },
+    { pos: 100, color: '#fff' },
+  ]
+  gradient.value = handleElement.value.gradient || { type: 'linear', rotate: 0, colors: defaultGradientColor }
+  pattern.value = handleElement.value.pattern || ''
+  fillType.value = (handleElement.value.pattern !== undefined) ? 'pattern' : (handleElement.value.gradient ? 'gradient' : 'fill')
+  textAlign.value = handleElement.value?.text?.align || 'middle'
+
+  if (handleElement.value.text?.content) {
+    emitter.emit(EmitterEvents.SYNC_RICH_TEXT_ATTRS_TO_STORE)
+  }
+}, { deep: true, immediate: true })
+
+// 设置填充类型:渐变、纯色
+const updateFillType = (type: 'gradient' | 'fill' | 'pattern') => {
+  console.log('设置填充类型:', type)
+  if (type === 'fill') {
+    slidesStore.removeElementProps({ id: handleElementId.value, propName: ['gradient', 'pattern'] })
+    addHistorySnapshot()
+  }
+  else if (type === 'gradient') {
+    currentGradientIndex.value = 0
+    slidesStore.removeElementProps({ id: handleElementId.value, propName: 'pattern' })
+    updateElement({ gradient: gradient.value })
+  }
+  // else if (type === 'pattern') {
+  //   slidesStore.removeElementProps({ id: handleElementId.value, propName: 'gradient' })
+  //   updateElement({ pattern: '' })
+  // }
+}
+
+// 设置填充色
+const updateFill = (value: string) => {
+  updateElement({ fill: value })
+}
+
+// 设置渐变填充
+const updateGradient = (gradientProps: Partial<Gradient>) => {
+  if (!gradient.value) return
+  const _gradient = { ...gradient.value, ...gradientProps }
+  updateElement({ gradient: _gradient })
+}
+const updateGradientColors = (color: string) => {
+  const colors = gradient.value.colors.map((item, index) => {
+    if (index === currentGradientIndex.value) return { ...item, color }
+    return item
+  })
+  updateGradient({ colors })
+}
+
 </script>
 
 <style lang="scss" scoped>
@@ -683,4 +855,11 @@ const editContent = (toolType: number) => {
     }
   }
 }
+
+.row {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  width: 180px;
+}
 </style>

+ 189 - 44
src/views/Editor/Thumbnails/index2.vue

@@ -5,7 +5,7 @@
     v-click-outside="() => setThumbnailsFocus(false)"
     v-contextmenu="contextmenusThumbnails"
   >
-    <div class="add-slide">
+    <div class="add-slide" v-show="false">
       <div class="btn" @click="createSlide()"><IconPlus class="icon" /></div>
       <!-- 添加幻灯片 <Popover trigger="click" placement="bottom-start" v-model:value="presetLayoutPopoverVisible" center>
         <template #content>
@@ -56,14 +56,22 @@
               'selected': selectedSlidesIndex.includes(index),
             }"
             @mousedown="$event => handleClickSlideThumbnail($event, index)"
-            @dblclick="enterScreening()"
-            v-contextmenu="contextmenusThumbnailItem"
+            v-contextmenu2="contextmenusThumbnailItem"
           >
+            <!-- @dblclick="enterScreening()" -->
             <div class="label" :class="{ 'offset-left': index >= 99 }">{{ fillDigit(index + 1, 2) }}</div>
             <ThumbnailSlide class="thumbnail" :slide="element" :size="120" :visible="index < slidesLoadLimit" />
+            <div class="page_menu" @click.stop="$event => showPageMenu($event, index)">
+              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                  <circle cx="12" cy="12" r="1"></circle>
+                  <circle cx="12" cy="5" r="1"></circle>
+                  <circle cx="12" cy="19" r="1"></circle>
+              </svg>
+            </div>
 
             <div class="note-flag" v-if="element.notes && element.notes.length" @click="openNotesPanel()">{{ element.notes.length }}</div>
           </div>
+          <div class="add-page-between" @click="createSlide()">+</div>
         </div>
       </template>
     </Draggable>
@@ -73,7 +81,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, nextTick, ref, watch, useTemplateRef } from 'vue'
+import { computed, nextTick, ref, watch, useTemplateRef, createVNode, render } from 'vue'
 import { storeToRefs } from 'pinia'
 import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
 import { fillDigit } from '@/utils/common'
@@ -90,6 +98,7 @@ import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
 import Templates from './Templates.vue'
 import Popover from '@/components/Popover.vue'
 import Draggable from 'vuedraggable'
+import ContextmenuComponent from '@/components/Contextmenu2/index.vue'
 
 // 检测是否为移动设备(包括iPad和手机)
 const isMobileDevice = computed(() => {
@@ -315,54 +324,117 @@ const contextmenusThumbnails = (): ContextmenuItem[] => {
   ]
 }
 
-const contextmenusThumbnailItem = (): ContextmenuItem[] => {
-  return [
-    {
-      text: lang.ssCut,
-      subText: 'Ctrl + X',
-      handler: cutSlide,
-    },
-    {
-      text: lang.ssCopy,
-      subText: 'Ctrl + C',
-      handler: copySlide,
-    },
-    {
-      text: lang.ssPaste,
-      subText: 'Ctrl + V',
-      handler: pasteSlide,
-    },
-    {
-      text: lang.ssSelectAll,
-      subText: 'Ctrl + A',
-      handler: selectAllSlide,
+const showPageMenu = (event: MouseEvent, index: number) => {
+  event.stopPropagation()
+  
+  const menus = contextmenusThumbnailItem()
+  if (!menus) return
+
+  let container: HTMLDivElement | null = null
+
+  const removeContextmenu = () => {
+    if (container) {
+      document.body.removeChild(container)
+      container = null
+    }
+    document.body.removeEventListener('scroll', removeContextmenu)
+    window.removeEventListener('resize', removeContextmenu)
+  }
+
+  const options = {
+    axis: { x: event.clientX - 20, y: event.clientY - 20 },
+    el: event.currentTarget as HTMLElement,
+    menus,
+    removeContextmenu,
+  }
+  container = document.createElement('div')
+  const vm = createVNode(ContextmenuComponent, options, null)
+  render(vm, container)
+  document.body.appendChild(container)
+
+  document.body.addEventListener('scroll', removeContextmenu)
+  window.addEventListener('resize', removeContextmenu)
+}
+import { showConfirmDialog } from '@/utils/confirmDialog'
+
+const confirmDeleteSlide = () => {
+  showConfirmDialog({
+    title: lang.ssConfirmDel,
+    content: lang.ssConfirmDelContent,
+    confirmText: lang.ssConfirm,
+    cancelText: lang.ssCancel,
+    width: 400,
+    onConfirm: () => {
+      deleteSlide()
     },
-    { divider: true },
-    {
-      text: lang.ssNewPage,
-      subText: 'Enter',
-      handler: createSlide,
+    onCancel: () => {
+      console.log('取消删除')
     },
+  })
+
+}
+
+const contextmenusThumbnailItem = (): ContextmenuItem[] => {
+  return [
+    // {
+    //   text: lang.ssCut,
+    //   subText: 'Ctrl + X',
+    //   handler: cutSlide,
+    // },
+    // {
+    //   text: lang.ssCopy,
+    //   subText: 'Ctrl + C',
+    //   handler: copySlide,
+    // },
+    // {
+    //   text: lang.ssPaste,
+    //   subText: 'Ctrl + V',
+    //   handler: pasteSlide,
+    // },
+    // {
+    //   text: lang.ssSelectAll,
+    //   subText: 'Ctrl + A',
+    //   handler: selectAllSlide,
+    // },
+    // { divider: true },
+    // {
+    //   text: lang.ssNewPage,
+    //   subText: 'Enter',
+    //   handler: createSlide,
+    // },
+    // {
+    //   text: lang.ssDupPage,
+    //   subText: 'Ctrl + D',
+    //   handler: copyAndPasteSlide,
+    // },
+    // {
+    //   text: lang.ssDelPage,
+    //   subText: 'Delete',
+    //   handler: () => deleteSlide(),
+    // },
+    // {
+    //   text: lang.ssAddSect,
+    //   handler: createSection,
+    //   disable: !!currentSlide.value.sectionTag,
+    // },
+    // { divider: true },
+    // {
+    //   text: lang.ssPlayFromCur,
+    //   subText: 'Shift + F5',
+    //   handler: enterScreening,
+    // },
+
     {
-      text: lang.ssDupPage,
-      subText: 'Ctrl + D',
+      text: lang.ssDupPage2,
       handler: copyAndPasteSlide,
     },
     {
-      text: lang.ssDelPage,
-      subText: 'Delete',
-      handler: () => deleteSlide(),
-    },
-    {
-      text: lang.ssAddSect,
-      handler: createSection,
-      disable: !!currentSlide.value.sectionTag,
+      text: lang.ssNewPage2,
+      handler: createSlide,
     },
-    { divider: true },
     {
-      text: lang.ssPlayFromCur,
-      subText: 'Shift + F5',
-      handler: enterScreening,
+      text: lang.ssDelPage2,
+      handler: confirmDeleteSlide,
     },
   ]
 }
@@ -425,6 +497,18 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
   display: flex;
   align-items: center;
   gap: 10px;
+
+  .thumbnail-container {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+
+    &:hover {
+      .add-page-between {
+        display: flex;
+      }
+    }
+  }
 }
 .thumbnail-item {
   display: flex;
@@ -490,6 +574,10 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
   color: #999;
   width: 20px;
   cursor: grab;
+  position: absolute;
+  z-index: 999;
+  top: 10px;
+  left: 10px;
 
   &.offset-left {
     position: relative;
@@ -500,6 +588,39 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
     cursor: grabbing;
   }
 }
+
+.page_menu{
+  width: 24px;
+  height: 24px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+  position: absolute;
+  z-index: 999;
+  bottom: 10px;
+  right: 10px;
+  background: rgba(255, 255, 255, 0.95);
+  border: 1px solid #e5e7eb;
+  transition: .3s;
+  border-radius: 5px;
+
+  svg{
+    color: #6b7280;
+    height: 15px;
+  }
+
+  .active,
+  &:hover{
+    border-color: $themeColor2;
+    background-color: $themeColor2;
+
+    svg{
+      color: #fff;
+    }
+  }
+}
+
 .page-number {
   /* height: 100%; */
   font-size: 12px;
@@ -557,4 +678,28 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
     font-size: 12px;
   }
 }
+
+.add-page-between {
+    width: 30px;
+    height: 30px;
+    border-radius: 50%;
+    border: 2px dashed #d1d5db;
+    background: white;
+    cursor: pointer;
+    display: none;
+    // align-items: center;
+    justify-content: center;
+    color: #9ca3af;
+    font-size: 20px;
+    transition: all 0.2s;
+    flex-shrink: 0;
+    line-height: 23px;
+
+    &:hover {
+      border-color: #F78B22;
+      border-style: solid;
+      color: #F78B22;
+      background: #FFF8F0;
+    }
+}
 </style>

+ 1 - 0
src/views/Editor/Toolbar/ElementStylePanel/index.vue

@@ -37,6 +37,7 @@ const panelMap = {
 const { handleElement } = storeToRefs(useMainStore())
 
 const currentPanelComponent = computed<unknown>(() => {
+  console.log('handleElement.value', handleElement.value)
   return handleElement.value ? (panelMap[handleElement.value.type] || null) : null
 })
 </script>

+ 59 - 0
src/views/Editor/Toolbar/common/ElementFlip2.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="element-flip">
+    <ButtonGroup class="row">
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="flipV"
+        @click="updateFlip({ flipV: !flipV })"
+      ><IconFlipVertically /> {{lang.flipVertically}}</CheckboxButton>
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="flipH"
+        @click="updateFlip({ flipH: !flipH })"
+      ><IconFlipHorizontally /> {{lang.flipHorizontally}}</CheckboxButton>
+    </ButtonGroup>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue'
+import { storeToRefs } from 'pinia'
+import { useMainStore, useSlidesStore } from '@/store'
+import type { ImageOrShapeFlip } from '@/types/slides'
+import useHistorySnapshot from '@/hooks/useHistorySnapshot'
+
+import CheckboxButton from '@/components/CheckboxButton.vue'
+import ButtonGroup from '@/components/ButtonGroup.vue'
+
+import { lang } from '@/main'
+
+const slidesStore = useSlidesStore()
+const { handleElement } = storeToRefs(useMainStore())
+
+const flipH = ref(false)
+const flipV = ref(false)
+
+watch(handleElement, () => {
+  if (handleElement.value && (handleElement.value.type === 'image' || handleElement.value.type === 'shape')) {
+    flipH.value = !!handleElement.value.flipH
+    flipV.value = !!handleElement.value.flipV
+  }
+}, { deep: true, immediate: true })
+
+const { addHistorySnapshot } = useHistorySnapshot()
+
+const updateFlip = (flipProps: ImageOrShapeFlip) => {
+  if (!handleElement.value) return
+  slidesStore.updateElement({ id: handleElement.value.id, props: flipProps })
+  addHistorySnapshot()
+}
+</script>
+
+<style lang="scss" scoped>
+.row {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  // margin-bottom: 10px;
+}
+</style>

+ 476 - 0
src/views/Editor/Toolbar/common/RichTextBase2.vue

@@ -0,0 +1,476 @@
+<template>
+  <div class="rich-text-base">
+    <SelectGroup class="row">
+      <Select
+        style="width: 60%;"
+        :value="richTextAttrs.fontname"
+        search
+        :searchLabel="lang.searchFont"
+        @update:value="value => emitRichTextCommand('fontname', value as string)"
+        :options="FONTS"
+      >
+        <template #icon>
+          <IconFontSize />
+        </template>
+      </Select>
+      <Select
+        style="width: 40%;"
+        :value="richTextAttrs.fontsize"
+        search
+        :searchLabel="lang.searchFontSize"
+        @update:value="value => emitRichTextCommand('fontsize', value as string)"
+        :options="fontSizeOptions.map(item => ({
+          label: item, value: item
+        }))"
+      >
+        <template #icon>
+          <IconAddText />
+        </template>
+      </Select>
+    </SelectGroup>
+
+    <ButtonGroup class="row" passive>
+      <Popover trigger="click" style="width: 30%;">
+        <template #content>
+          <ColorPicker
+            :modelValue="richTextAttrs.color"
+            @update:modelValue="value => emitRichTextCommand('color', value)"
+          />
+        </template>
+        <TextColorButton first v-tooltip="lang.textColor" :color="richTextAttrs.color">
+          <IconText />
+        </TextColorButton>
+      </Popover>
+      <Popover trigger="click" style="width: 30%;">
+        <template #content>
+          <ColorPicker
+            :modelValue="richTextAttrs.backcolor"
+            @update:modelValue="value => emitRichTextCommand('backcolor', value)"
+          />
+        </template>
+        <TextColorButton v-tooltip="lang.highlight" :color="richTextAttrs.backcolor">
+          <IconHighLight />
+        </TextColorButton>
+      </Popover>
+      <Button 
+        style="display: flex;align-items: center;"
+        class="font-size-btn"
+        v-tooltip="lang.fontSizeAdd"
+        @click="emitRichTextCommand('fontsize-add')"
+      ><IconFontSize />+</Button>
+      <Button
+        style="display: flex;align-items: center;"
+        last
+        class="font-size-btn"
+        v-tooltip="lang.fontSizeReduce" 
+        @click="emitRichTextCommand('fontsize-reduce')"
+      ><IconFontSize />-</Button>
+    </ButtonGroup>
+
+    <ButtonGroup class="row">
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="richTextAttrs.bold"
+        v-tooltip="lang.bold"
+        @click="emitRichTextCommand('bold')"
+      ><IconTextBold /></CheckboxButton>
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="richTextAttrs.em"
+        v-tooltip="lang.italic"
+        @click="emitRichTextCommand('em')"
+      ><IconTextItalic /></CheckboxButton>
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="richTextAttrs.underline"
+        v-tooltip="lang.underline"
+        @click="emitRichTextCommand('underline')"
+      ><IconTextUnderline /></CheckboxButton>
+      <CheckboxButton 
+        style="flex: 1;"
+        :checked="richTextAttrs.strikethrough"
+        v-tooltip="lang.strikethrough"
+        @click="emitRichTextCommand('strikethrough')"
+      ><IconStrikethrough /></CheckboxButton>
+    </ButtonGroup>
+
+    <!-- <ButtonGroup class="row">
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="richTextAttrs.superscript"
+        v-tooltip="'上标'"
+        @click="emitRichTextCommand('superscript')"
+      >A²</CheckboxButton>
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="richTextAttrs.subscript"
+        v-tooltip="'下标'"
+        @click="emitRichTextCommand('subscript')"
+      >A₂</CheckboxButton>
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="richTextAttrs.code"
+        v-tooltip="'行内代码'"
+        @click="emitRichTextCommand('code')"
+      ><IconCode /></CheckboxButton>
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="richTextAttrs.blockquote"
+        v-tooltip="'引用'"
+        @click="emitRichTextCommand('blockquote')"
+      ><IconQuote /></CheckboxButton>
+    </ButtonGroup>
+
+    <ButtonGroup class="row" passive>
+      <Popover trigger="click" v-model:value="AIPopoverVisible" style="width: 25%;">
+        <template #content>
+          <PopoverMenuItem center @click="execAI('美化改写')">美化</PopoverMenuItem>
+          <PopoverMenuItem center @click="execAI('扩写丰富')">扩写</PopoverMenuItem>
+          <PopoverMenuItem center @click="execAI('精简提炼')">精简</PopoverMenuItem>
+        </template>
+        <CheckboxButton
+          first
+          style="width: 100%;"
+          v-tooltip="'AI辅助'"
+        ><span :class="{ 'ai-loading': isAIWriting }">{{ isAIWriting ? '' : 'AI' }}</span></CheckboxButton>
+      </Popover>
+      <CheckboxButton
+        style="flex: 1;"
+        v-tooltip="'清除格式'"
+        @click="emitRichTextCommand('clear')"
+      ><IconFormat /></CheckboxButton>
+      <CheckboxButton
+        style="flex: 1;"
+        :checked="!!textFormatPainter"
+        v-tooltip="'格式刷(双击连续使用)'"
+        @click="toggleTextFormatPainter()"
+        @dblclick="toggleTextFormatPainter(true)"
+      ><IconFormatBrush /></CheckboxButton>
+      <Popover placement="bottom-end" trigger="click" v-model:value="linkPopoverVisible" style="width: 25%;">
+        <template #content>
+          <div class="link-popover">
+            <Input v-model:value="link" placeholder="请输入超链接" />
+            <div class="btns">
+              <Button size="small" :disabled="!richTextAttrs.link" @click="removeLink()" style="margin-right: 5px;">移除</Button>
+              <Button size="small" type="primary" @click="updateLink(link)">确认</Button>
+            </div>
+          </div>
+        </template>
+        <CheckboxButton
+          last
+          style="width: 100%;"
+          :checked="!!richTextAttrs.link"
+          v-tooltip="'超链接'"
+          @click="openLinkPopover()"
+        ><IconLinkOne /></CheckboxButton>
+      </Popover>
+    </ButtonGroup>
+    <Divider />
+
+    <RadioGroup 
+      class="row" 
+      button-style="solid" 
+      :value="richTextAttrs.align"
+      @update:value="value => emitRichTextCommand('align', value)"
+    >
+      <RadioButton value="left" v-tooltip="'左对齐'" style="flex: 1;"><IconAlignTextLeft /></RadioButton>
+      <RadioButton value="center" v-tooltip="'居中'" style="flex: 1;"><IconAlignTextCenter /></RadioButton>
+      <RadioButton value="right" v-tooltip="'右对齐'" style="flex: 1;"><IconAlignTextRight /></RadioButton>
+      <RadioButton value="justify" v-tooltip="'两端对齐'" style="flex: 1;"><IconAlignTextBoth /></RadioButton>
+    </RadioGroup>
+
+    <div class="row" passive>
+      <ButtonGroup style="flex: 1;">
+        <Button
+          first
+          :type="richTextAttrs.bulletList ? 'primary' : 'default'"
+          style="flex: 1;"
+          v-tooltip="'项目符号'"
+          @click="emitRichTextCommand('bulletList')"
+        ><IconList /></Button>
+        <Popover trigger="click" v-model:value="bulletListPanelVisible">
+          <template #content>
+            <div class="list-wrap">
+              <ul class="list" 
+                v-for="item in bulletListStyleTypeOption" 
+                :key="item" 
+                :style="{ listStyleType: item }"
+                @click="emitRichTextCommand('bulletList', item)"
+              >
+                <li class="list-item" v-for="key in 3" :key="key"><span></span></li>
+              </ul>
+            </div>
+          </template>
+          <Button last class="popover-btn"><IconDown /></Button>
+        </Popover>
+      </ButtonGroup>
+      <div style="width: 10px;"></div>
+      <ButtonGroup style="flex: 1;" passive>
+        <Button
+          first
+          :type="richTextAttrs.orderedList ? 'primary' : 'default'"
+          style="flex: 1;"
+          v-tooltip="'编号'"
+          @click="emitRichTextCommand('orderedList')"
+        ><IconOrderedList /></Button>
+        <Popover trigger="click" v-model:value="orderedListPanelVisible">
+          <template #content>
+            <div class="list-wrap">
+              <ul class="list" 
+                v-for="item in orderedListStyleTypeOption" 
+                :key="item" 
+                :style="{ listStyleType: item }"
+                @click="emitRichTextCommand('orderedList', item)"
+              >
+                <li class="list-item" v-for="key in 3" :key="key"><span></span></li>
+              </ul>
+            </div>
+          </template>
+          <Button last class="popover-btn"><IconDown /></Button>
+        </Popover>
+      </ButtonGroup>
+    </div>
+
+    <div class="row">
+      <ButtonGroup style="flex: 1;" passive>
+        <Button first style="flex: 1;" v-tooltip="'减小段落缩进'" @click="emitRichTextCommand('indent', '-1')"><IconIndentLeft /></Button>
+        <Popover trigger="click" v-model:value="indentLeftPanelVisible">
+          <template #content>
+            <PopoverMenuItem center @click="emitRichTextCommand('textIndent', '-1')">减小首行缩进</PopoverMenuItem>
+          </template>
+          <Button last class="popover-btn"><IconDown /></Button>
+        </Popover>
+      </ButtonGroup>
+      <div style="width: 10px;"></div>
+      <ButtonGroup style="flex: 1;" passive>
+        <Button first style="flex: 1;" v-tooltip="'增大段落缩进'" @click="emitRichTextCommand('indent', '+1')"><IconIndentRight /></Button>
+        <Popover trigger="click" v-model:value="indentRightPanelVisible">
+          <template #content>
+            <PopoverMenuItem center @click="emitRichTextCommand('textIndent', '+1')">增大首行缩进</PopoverMenuItem>
+          </template>
+          <Button last class="popover-btn"><IconDown /></Button>
+        </Popover>
+      </ButtonGroup>
+    </div> -->
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue'
+import { storeToRefs } from 'pinia'
+import api from '@/services'
+import { useMainStore } from '@/store'
+import emitter, { EmitterEvents } from '@/utils/emitter'
+import { FONTS } from '@/configs/font'
+import useTextFormatPainter from '@/hooks/useTextFormatPainter'
+import message from '@/utils/message'
+import { htmlToText } from '@/utils/common'
+
+import TextColorButton from '@/components/TextColorButton2.vue'
+import CheckboxButton from '@/components/CheckboxButton.vue'
+import ColorPicker from '@/components/ColorPicker/index.vue'
+import Input from '@/components/Input.vue'
+import Button from '@/components/Button.vue'
+import ButtonGroup from '@/components/ButtonGroup.vue'
+import Select from '@/components/Select.vue'
+import SelectGroup from '@/components/SelectGroup.vue'
+import Divider from '@/components/Divider.vue'
+import Popover from '@/components/Popover.vue'
+import RadioButton from '@/components/RadioButton.vue'
+import RadioGroup from '@/components/RadioGroup.vue'
+import PopoverMenuItem from '@/components/PopoverMenuItem.vue'
+
+import { lang } from '@/main'
+
+
+
+const { handleElement, handleElementId, richTextAttrs, textFormatPainter } = storeToRefs(useMainStore())
+
+const { toggleTextFormatPainter } = useTextFormatPainter()
+
+const fontSizeOptions = [
+  '12px', '14px', '16px', '18px', '20px', '22px', '24px', '28px', '32px',
+  '36px', '40px', '44px', '48px', '54px', '60px', '66px', '72px', '76px',
+  '80px', '88px', '96px', '104px', '112px', '120px',
+]
+
+const emitRichTextCommand = (command: string, value?: string) => {
+  emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { action: { command, value } })
+}
+
+const bulletListPanelVisible = ref(false)
+const orderedListPanelVisible = ref(false)
+const indentLeftPanelVisible = ref(false)
+const indentRightPanelVisible = ref(false)
+
+const bulletListStyleTypeOption = ref(['disc', 'circle', 'square'])
+const orderedListStyleTypeOption = ref(['decimal', 'lower-roman', 'upper-roman', 'lower-alpha', 'upper-alpha', 'lower-greek'])
+
+const link = ref('')
+const linkPopoverVisible = ref(false)
+const AIPopoverVisible = ref(false)
+const isAIWriting = ref(false)
+
+watch(richTextAttrs, () => linkPopoverVisible.value = false)
+watch(handleElementId, () => {
+  if (isAIWriting.value) isAIWriting.value = false
+})
+
+const openLinkPopover = () => {
+  link.value = richTextAttrs.value.link
+}
+const updateLink = (link?: string) => {
+  const linkRegExp = /^(https?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/
+  if (!link || !linkRegExp.test(link)) return message.error('不是正确的网页链接地址')
+
+  emitRichTextCommand('link', link)
+  linkPopoverVisible.value = false
+}
+
+const removeLink = () => {
+  emitRichTextCommand('link')
+  linkPopoverVisible.value = false
+}
+
+const execAI = async (command: string) => {
+  AIPopoverVisible.value = false
+
+  if (!handleElement.value) return
+
+  let content = ''
+  if (handleElement.value.type === 'text' && handleElement.value.content) {
+    content = handleElement.value.content
+  }
+  if (handleElement.value.type === 'shape' && handleElement.value.text && handleElement.value.text.content) {
+    content = handleElement.value.text.content
+  }
+
+  if (!content) return message.error('没有可以执行的文本内容')
+
+  let resultText = ''
+
+  const stream = await api.AI_Writing({
+    content: htmlToText(content),
+    command,
+  })
+
+  isAIWriting.value = true
+
+  const reader: ReadableStreamDefaultReader = stream.body.getReader()
+  const decoder = new TextDecoder('utf-8')
+  
+  const readStream = () => {
+    reader.read().then(({ done, value }) => {
+      if (!isAIWriting.value) return
+      if (done) {
+        isAIWriting.value = false
+        return
+      }
+
+      const chunk = decoder.decode(value, { stream: true })
+      resultText += chunk
+      emitRichTextCommand('replace', resultText)
+
+      readStream()
+    })
+  }
+  readStream()
+}
+</script>
+
+<style lang="scss" scoped>
+.rich-text-base {
+  user-select: none;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+
+  ::v-deep(.ai-loading) {
+    width: 16px;
+    height: 16px;
+    display: inline-block;
+    margin-top: 8px;
+    border: 1px solid $themeColor;
+    border-top-color: transparent;
+    border-radius: 50%;
+    animation: spinner .8s linear infinite;
+  }
+}
+.row {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  // margin-bottom: 10px;
+}
+.font-size-btn {
+  padding: 0 15px;
+}
+.link-popover {
+  width: 240px;
+
+  .btns {
+    margin-top: 10px;
+    text-align: right;
+  }
+}
+.list-wrap {
+  width: 176px;
+  color: #666;
+  padding: 8px;
+  margin: -12px;
+  display: flex;
+  flex-wrap: wrap;
+  align-content: flex-start;
+}
+.list {
+  background-color: $lightGray;
+  padding: 4px 4px 4px 20px;
+  cursor: pointer;
+
+  &:not(:nth-child(3n)) {
+    margin-right: 8px;
+  }
+
+  &:nth-child(4),
+  &:nth-child(5),
+  &:nth-child(6) {
+    margin-top: 8px;
+  }
+
+  &:hover {
+    color: $themeColor;
+
+    span {
+      background-color: $themeColor;
+    }
+  }
+}
+.list-item {
+  width: 24px;
+  height: 12px;
+  position: relative;
+  font-size: 12px;
+  top: -3px;
+
+  span {
+    width: 100%;
+    height: 2px;
+    display: inline-block;
+    position: absolute;
+    top: 8px;
+    background-color: #666;
+  }
+}
+.popover-btn {
+  padding: 0 3px;
+}
+
+@keyframes spinner {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+</style>

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

@@ -164,7 +164,7 @@
     <div class="layout-content">
       <CollapsibleToolbar class="layout-sidebar" @toggle="handleToolbarToggle" :userid="props.userid" />
       <div class="layout-content-center">
-        <CanvasTool class="center-top" />
+        <CanvasTool class="center-top"  :userid="props.userid"/>
         <Canvas class="center-body" :style="{ height: `calc(100% - ${remarkHeight + 60}px  - 120px)` }"
           :courseid="props.courseid" @course-loaded="handleCourseLoaded"  ref="canvas"/>
         <!-- <Remark
@@ -222,7 +222,7 @@ import Modal from '@/components/Modal.vue'
 import CollapsibleToolbar from '@/components/CollapsibleToolbar/index2.vue'
 import CreateCourseDialog from '@/components/CreateCourseDialog.vue'
 import api from '@/services/course'
-import lang from '../lang/cn.json'
+import { lang } from '@/main'
 
 
 interface ParentWindowWithToolList extends Window {

+ 1 - 2
src/views/Student/components/aiChat.vue

@@ -117,11 +117,10 @@ import { chat_no_stream, chat_stream, getAgentModel, chat_no_stream2, getAgentCh
 import { useSlidesStore } from '@/store'
 import { lang } from '@/main'
 import MarkdownIt from 'markdown-it'
-import { getWorkPageId } from '@/services/course'
+import { getWorkPageId, getClassById } from '@/services/course'
 import FileInput from '@/components/FileInput2.vue'
 import axios from '@/services/config'
 import message from '@/utils/message'
-import { getClassById } from '@/services/course'
 
 interface ChatMessage {
   uid?: string

+ 1 - 1
src/views/Student/components/answerTheResult.vue

@@ -405,7 +405,7 @@ const changeShow = (op: any, idx: number) => {
 
 const lookDetail = () => {
   console.log(props.toolType)
-  if ([45, 15, 72, 73, 78].includes(props.toolType)) {
+  if ([45, 15, 72, 73, 78, 79].includes(props.toolType)) {
     emit('openChoiceQuestionDetail', props.slideIndex)
   }
 }

File diff ditekan karena terlalu besar
+ 913 - 68
src/views/Student/components/choiceQuestionDetailDialog.vue


+ 285 - 45
src/views/Student/index.vue

@@ -78,20 +78,20 @@
           left: isFullscreen ? '0' : `${(containerWidth - slideWidth * canvasScale) / 2}px`,
           top: isFullscreen ? '0' : `${(containerHeight - slideHeight * canvasScale) / 2}px`
         }" @mousemove="handleLaserMove">
-          <div class="homework-check-box" v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && props.type == '1'" v-show="currentSlideHasIframe" :style="{
+          <div class="homework-check-box" v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && (props.type == '1' || (props.type == '2' && currentIsResultArray.can))"  v-show="currentSlideHasIframe" :style="{
             top: isFullscreen ? '0' : `0`
           }">
             <div class="homework-check-box-item" @click="openChoiceQuestionDetail2(slideIndex)" :class="{'active': !choiceQuestionDetailDialogOpenList.includes(slideIndex)}">
               <div class="homework-check-box-item-title">{{ lang.ssQuestion }}</div>
             </div>
             <div class="homework-check-box-item" @click="openChoiceQuestionDetail3(slideIndex)" :class="{'active': choiceQuestionDetailDialogOpenList.includes(slideIndex)}">
-              <div class="homework-check-box-item-title">{{ lang.ssAnswer }}</div>
+              <div class="homework-check-box-item-title">{{ lang.ssResult }}</div>
             </div>
           </div>
-          <div class="aiBtn" ref="aiBtnRef" v-if="isQuestionFrame && hasWork && props.type == '2'" 
+          <div class="aiBtn" ref="aiBtnRef" v-if="isQuestionFrame && hasWork && props.type == '2' && aiAssistant" 
             :style="{ right: aiBtnPosition.x + 'px', bottom: aiBtnPosition.y + 'px' }" @click="openAiChat">
             <IconComment class="aiBtn-icon" />
-            <span>AI对话</span>
+            <span>{{ lang.ssAiChat }}</span>
           </div>
           <aiChat v-show="visibleAIChat" :position="aiBtnPosition" @close="visibleAIChat = false" :userid="props.userid" :workJson="myWork" :visible="visibleAIChat" :cid="props.cid"/>
          <!--  -->
@@ -106,7 +106,7 @@
           <ScreenSlideList :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"
             :animationIndex="0" :turnSlideToId="() => { }" :manualExitFullscreen="() => { }"  :slideIndex="slideIndex" v-show="!choiceQuestionDetailDialogOpenList.includes(slideIndex)"/>
 
-          <choiceQuestionDetailDialog v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex)" :cid="props.cid" :workId="workId"  :userId="props.userid" :courseDetail="courseDetail" :workArray="workArray" @changeWorkIndex="changeWorkIndex" v-model:visible="choiceQuestionDetailDialogOpenList" :showData="answerTheResultRef" :slideIndex="slideIndex" :workIndex="0" :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"/>
+          <choiceQuestionDetailDialog ref="choiceQuestionDetailDialogRef" v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex) && currentSlideToolType !== 77" :roleType="props.type" :cid="props.cid" :courseid="props.courseid" :workId="workId" :workUrl="workUrl" :userId="props.userid" :courseDetail="courseDetail" :workArray="workArray" @changeWorkIndex="changeWorkIndex" v-model:visible="choiceQuestionDetailDialogOpenList" :showData="answerTheResultRef" :slideIndex="slideIndex" :workIndex="0" :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale" :resultArray="currentIsResultArray" @setIsResultArray="setIsResultArray2" :isCreator="isCreator" @successLike="successLike" @sendMessage="sendMessage" :isFollowModeActive="isFollowModeActive"/>
           <SpeakingClassPanel
             v-else-if="choiceQuestionDetailDialogOpenList.includes(slideIndex) && currentSlideToolType === 77"
             ref="speakingPanelRef"
@@ -133,9 +133,9 @@
                 </div>
               </div>
               <div class="slide-bottom-right" v-if="!isFullscreen">
-                <Refresh class="tool-btn" v-tooltip="lang.ssRefresh" @click="handleRefreshPage" v-if="currentSlideHasIframe"/>
+                <Refresh class="tool-btn" v-tooltip="lang.ssRefresh" @click="handleRefreshPage" v-if="currentSlideHasIframe && !currentSlideHasEnglishSpeaking"/>
                 <!-- <UpTwo @click="handleHomeworkSubmit" v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && !isSubmitting" class="tool-btn upBtn" v-tooltip="lang.ssSubmitHW"/> -->
-                <svg @click="handleHomeworkSubmit" v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && !isSubmitting" class="tool-btn upBtn" v-tooltip="lang.ssSubmitHW" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
+                <svg @click="handleHomeworkSubmit" v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && !isSubmitting && !currentSlideHasEnglishSpeaking" class="tool-btn upBtn" v-tooltip="lang.ssSubmitHW" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
                   <!-- Document body -->
                   <path d="M15 10 L15 90 Q15 95 20 95 L80 95 Q85 95 85 90 L85 35 L60 10 Z"
                         fill="none" stroke="currentColor" stroke-width="6" stroke-linejoin="round" stroke-linecap="round"/>
@@ -154,7 +154,7 @@
                   <line x1="36" y1="88" x2="64" y2="88"
                         stroke="currentColor" stroke-width="6" stroke-linecap="round"/>
                 </svg>
-                <IconLoading v-else-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo" class="tool-btn loading" v-tooltip="lang.ssSubmitting"></IconLoading>
+                <IconLoading v-else-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && !currentSlideHasEnglishSpeaking" class="tool-btn loading" v-tooltip="lang.ssSubmitting"></IconLoading>
                 <IconStopwatchStart v-if="props.type == '1' && courseDetail.userid == props.userid && isFollowModeActive" class="tool-btn" v-tooltip="lang.ssTimer" @click="timerlVisible = !timerlVisible"  />
                 <IconWrite v-if="isFollowModeActive && props.type == '1' && courseDetail.userid == props.userid" class="tool-btn" v-tooltip="lang.ssPenTool" @click="writingBoardToolVisible = true"  />
                 <!-- <IconMagic v-if="isFollowModeActive && props.type == '1' && courseDetail.userid == props.userid" class="tool-btn" v-tooltip="lang.ssLaserPen" :class="{ 'active': laserPen }" @click="toggleLaserPen"  /> -->
@@ -170,7 +170,7 @@
         </div>
 
         <!-- 作业提交按钮 - 当当前幻灯片包含iframe时显示(排除B站视频) -->
-        <div v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && isFullscreen" class="homework-submit-btn" :class="{ 'submitting': isSubmitting }"
+        <div v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo && isFullscreen && !currentSlideHasEnglishSpeaking" class="homework-submit-btn" :class="{ 'submitting': isSubmitting }"
           :style="{ right: getHomeworkButtonRight() + 'px' }" @click="handleHomeworkSubmit"
           v-tooltip="isSubmitting ? lang.ssHwSubmitting : lang.ssHwSubmit">
           <!-- <IconEdit v-if="!isSubmitting" class="tool-btn" />
@@ -180,7 +180,7 @@
 
         <!-- 刷新iframe按钮 -->
         <div class="refresh-page-btn" 
-          v-if="currentSlideHasIframe && isFullscreen"
+          v-if="currentSlideHasIframe && isFullscreen && !currentSlideHasEnglishSpeaking"
           :style="{ right: getRefreshButtonRight() + 'px' }" 
           @click="handleRefreshPage"
           v-tooltip="lang.ssRefreshIframe">
@@ -309,7 +309,7 @@
         <!-- 回答结果内容 -->
         <div v-show="!workPanelCollapsed && rightPanelMode === 'homework'" class="panel-content">
           <div v-if="workLoading" class="homework-loading">{{ lang.ssHwLoading }}</div>
-          <answerTheResult :toolType="toolType" :workId="workId" :workArray="workArray" :unsubmittedStudents="unsubmittedStudents" :slideIndex="slideIndex" v-else ref="answerTheResultRef" @openChoiceQuestionDetail="openChoiceQuestionDetail" @openWorkModal="openWorkModal"/>
+          <answerTheResult :toolType="toolType" :workId="workId"  :workArray="workArray" :unsubmittedStudents="unsubmittedStudents" :slideIndex="slideIndex" v-else ref="answerTheResultRef" @openChoiceQuestionDetail="openChoiceQuestionDetail" @openWorkModal="openWorkModal"/>
           <!--<div class="homework-title">已提交</div>
           <div v-if="workLoading" class="homework-loading">正在加载作业...</div>
           <div v-else>
@@ -401,7 +401,10 @@
     <button @click="manualReconnect" class="reconnect-btn">
       {{ lang.ssReconnect }}
     </button>
+    
   </div>
+
+  <messageInstruction ref="messageInstructionRef" />
 </template>
 
 <script lang="ts" setup>
@@ -435,6 +438,9 @@ import answerTheResult from './components/answerTheResult.vue'
 import choiceQuestionDetailDialog from './components/choiceQuestionDetailDialog.vue'
 import SpeakingClassPanel from './components/SpeakingClassPanel/index.vue'
 import aiChat from './components/aiChat.vue'
+import messageInstruction from '@/utils/components/messageInstruction.vue'
+
+const messageInstructionRef = ref<typeof messageInstruction>()
 
 // 生成标准 UUID v4 格式(36位,符合 [0-9a-fA-F-] 格式)
 const generateUUID = (): string => {
@@ -579,6 +585,7 @@ const visibleChoice = ref(false)
 const visibleAI = ref(false)
 const choiceQuestionDetailDialogOpenList = ref<number[]>([])
 const speakingPanelRef = ref<InstanceType<typeof SpeakingClassPanel> | null>(null)
+const choiceQuestionDetailDialogRef = ref<InstanceType<typeof choiceQuestionDetailDialog> | null>(null)
 
 provide('notifySpeakingProgress', (status: 'active' | 'completed', payload: { configId: string; sessionId: string }) => {
   if (props.type !== '2') return // 只有学生客户端发广播
@@ -617,6 +624,8 @@ provide('choiceQuestionDetailDialogOpenList', choiceQuestionDetailDialogOpenList
 
 // 当前作业选择/问答题的ID
 const workId = ref<string>('')
+// 当前作业的url  
+const workUrl = ref<string>('')
 // 当前作业的type
 const toolType = ref<string>('')
 
@@ -630,7 +639,9 @@ const rightPanelMode = ref<'homework' | 'dialogue' | 'choice' | ''>('homework')
 // 移除定时器相关代码,改用socket监听
 
 const courseDetail = ref<any>({})
+const aiAssistant = ref<boolean>(false)
 const studentArray = ref<any>([])
+const isResultArray = ref<any>([])
 
 // 跟随模式相关状态
 const isCreator = ref(false) // 是否为创建人
@@ -660,6 +671,7 @@ const yWritingBoardState = ref<any | null>(null)
 const yTimerMessages = ref<any | null>(null)
 const yLaserMessages = ref<any | null>(null)
 const yWritingBoardMessages = ref<any | null>(null)
+const yIsResultArrayMessages = ref<any | null>(null)
 const providerSocket = ref<WebsocketProvider | null>(null)
 // 学生端画图同步数据
 const writingBoardSyncDataURL = ref<string | null>(null)
@@ -1056,10 +1068,23 @@ const currentSlideConfigId = computed(() => {
 // 检测当前幻灯片是否包含B站视频
 const currentSlideHasBilibiliVideo = computed(() => {
   return elementList.value.some(element => 
-    element.type === ElementTypes.FRAME && (element.toolType === 75 || element.toolType === 74 || element.toolType === 76)
+    element.type === ElementTypes.FRAME && (element.toolType === 75 || element.toolType === 74 || element.toolType === 76 || element.toolType === 81)
+  )
+})
+
+// 检测当前幻灯片是否是英语口语
+const currentSlideHasEnglishSpeaking = computed(() => {
+  return elementList.value.some(element => 
+    element.type === ElementTypes.FRAME && (element.toolType === 77)
   )
 })
 
+// 检查当前的结果状态
+const currentIsResultArray = computed(() => {
+  return isResultArray.value[slideIndex.value] || {}
+})
+
+
 // 跳转到指定幻灯片
 const goToSlide = (index: number) => {
   console.log('goToSlide 被调用,目标索引:', index)
@@ -1222,25 +1247,32 @@ const getWorkId = () => {
     typeof element === 'object' &&
     ('toolType' in element) &&
     (element as any).toolType !== undefined &&
-    ((element as any).toolType === 45 || (element as any).toolType === 15 || (element as any).toolType === 73 || (element as any).toolType === 72 || (element as any).toolType === 78)
+    ((element as any).toolType === 45 || (element as any).toolType === 15 || (element as any).toolType === 73 || (element as any).toolType === 72 || (element as any).toolType === 78 || (element as any).toolType === 79)
   ) {
     // 提取链接中的id参数
     const url = (element as any).url
     let id = ''
+    
     toolType.value = (element as any).toolType
     if (typeof url === 'string') {
       const match = url.match(/[?&]id=([^&]+)/)
       if (match) {
         id = match[1]
       }
+      workUrl.value = url
       workId.value = id
+      if ((element as any).toolType === 72) {
+        workId.value = (element as any).id
+      }
     }
     else {
       workId.value = ''
+      workUrl.value = ''
     }
   }
   else {
     workId.value = ''
+    workUrl.value = ''
   }
 }
 
@@ -2037,7 +2069,7 @@ const handleHomeworkSubmit = async () => {
             else {
               homeworkContent = lang.ssHwSubmitWp
             }
-            message.success(lang.ssHwSubmitSucc)
+            messageInstructionRef.value.success(lang.ssHwSubmitSucc)
             hasSubmitWork = true
             
             // 发送作业提交成功的socket消息
@@ -2085,7 +2117,7 @@ const handleHomeworkSubmit = async () => {
             
             // 使用上传后的链接提交作业
             await submitWork(iframeSlideIndex, '72', fileUrl, '20')
-            message.success(lang.ssHwSubmitSucc)
+            messageInstructionRef.value.success(lang.ssHwSubmitSucc)
             hasSubmitWork = true
             
             // 发送作业提交成功的socket消息
@@ -2169,24 +2201,49 @@ const handleHomeworkSubmit = async () => {
             for (let i = 0;i < b.length;i++) {
               b[i].crossOrigin = 'anonymous'
             }
-            
-            // 直接对iframe内部的body进行截图
-            const html2canvas = await import('html2canvas')
-            const canvas = await html2canvas.default(iframeBody, {
-              // useCORS: true,
-              // allowTaint: true,
-              // scale: 1,
-              // backgroundColor: '#ffffff',
-              // logging: false,
-              // foreignObjectRendering: true,
-              // removeContainer: true
-              scale: 2, // 提高清晰度
-              allowTaint: false, // 是否允许跨域污染画布
-              useCORS: true, // 尝试跨域加载图片
-              logging: true,
-            })
-            imageData = canvas.toDataURL('image/png', 0.95)
-            
+
+            // 1. 创建全局禁用动画的 style 标签
+            const style = document.createElement('style')
+            style.id = 'html2canvas-freeze'
+            style.textContent = `
+        *, *::before, *::after {
+            animation: none !important;
+            transition: none !important;
+        }
+    `
+            iframeElement.contentWindow.document.head.appendChild(style)
+
+            // 2. 强制重排,确保禁用生效
+            iframeElement.contentWindow.document.body.offsetHeight
+
+            // 3. 如果词云图使用 ECharts 且存在,等它渲染稳定
+            if (typeof iframeElement.contentWindow.myChart !== 'undefined' && iframeElement.contentWindow.myChart && !iframeElement.contentWindow.myChart.isDisposed()) {
+              // 延迟 500ms 等待 ECharts 内部的 Canvas 动画结束
+              await new Promise(resolve => setTimeout(resolve, 500))
+            }
+
+            try {
+              // 直接对iframe内部的body进行截图
+              const html2canvas = await import('html2canvas')
+              const canvas = await html2canvas.default(iframeBody, {
+                // useCORS: true,
+                // allowTaint: true,
+                // scale: 1,
+                // backgroundColor: '#ffffff',
+                // logging: false,
+                // foreignObjectRendering: true,
+                // removeContainer: true
+                scale: 2, // 提高清晰度
+                allowTaint: false, // 是否允许跨域污染画布
+                useCORS: true, // 尝试跨域加载图片
+                logging: true,
+              })
+              imageData = canvas.toDataURL('image/png', 0.95)
+            }
+            finally {
+              const freezeStyle = iframeElement.contentWindow.document.getElementById('html2canvas-freeze')
+              if (freezeStyle) freezeStyle.remove()
+            }
             console.log('成功截图iframe内部内容')
           }
           catch (html2canvasError) {
@@ -2304,7 +2361,8 @@ const handleHomeworkSubmit = async () => {
           homeworkContent = imageUrl // 保存截图URL作为作业内容
           // 提交截图
           await submitWork(slideIndex.value, '73', imageUrl, '1') // 73表示截图工具,21表示图片类型
-          message.success(lang.ssShotSucc)
+          console.log('messageInstructionRef', messageInstructionRef.value)
+          messageInstructionRef.value.success(lang.ssShotSucc)
           hasSubmitWork = true
           
           // 发送作业提交成功的socket消息
@@ -2429,7 +2487,7 @@ const handleHomeworkSubmit = async () => {
 }
 
 const successSubmit = () => {
-  message.success(lang.ssHwSubmitSucc)
+  messageInstructionRef.value.success(lang.ssHwSubmitSucc)
   sendMessage({
     type: 'homework_submitted',
     courseid: props.courseid,
@@ -2440,6 +2498,17 @@ const successSubmit = () => {
   getWork(true)
 }
 
+const successLike = () => {
+  messageInstructionRef.value.success(lang.ssLikeSucc)
+  sendMessage({
+    type: 'like_updated',
+    courseid: props.courseid,
+    slideIndex: slideIndex.value,
+    userid: props.userid
+  })
+  getWork(true)
+}
+
 // 刷新iframe功能
 const handleRefreshPage = () => {
   console.log('刷新iframe按钮被点击')
@@ -2690,6 +2759,7 @@ const getCourseDetail = async () => {
     selectWorksStudent()
     checkIsCreator()
     const pptJSONUrl = JSON.parse(courseData.chapters).pptData ? JSON.parse(courseData.chapters).pptData : ''
+    aiAssistant.value = JSON.parse(courseData.chapters).aiAssistant ? JSON.parse(courseData.chapters).aiAssistant : false
     console.log(pptJSONUrl)
     
     if (pptJSONUrl) { 
@@ -2727,6 +2797,37 @@ const getCourseDetail = async () => {
           const contentDescription = pptContent.join('\n')
           checkPPTFile(contentDescription)
           importJSON(jsonObj)
+          if (isCreator.value) { 
+            console.log('jsonObj', jsonObj)
+            for (let i = 0; i < jsonObj.slides.length; i++) {
+              const notTool = [74, 75, 76, 82]
+              const elements = jsonObj.slides[i].elements.some((element: any) => {
+                return element.type === 'frame' && !notTool.includes(element.toolType)
+              })
+              let toolType = 0
+              if (jsonObj.slides[i].elements.length && jsonObj.slides[i].elements[0].toolType) {
+                toolType = jsonObj.slides[i].elements[0].toolType
+              }
+              const json = {
+                isTool: elements,
+                can: false,
+                like: false,
+                anonymous: false,
+              }
+              if (toolType === 78) {
+                json.anonymous = true
+              }
+              isResultArray.value[i] = json
+            }
+            setIsResultArray()
+            // 广播消息
+            sendMessage({ 
+              type: 'isResultArray', 
+              isResultArray: isResultArray.value,
+              courseid: props.courseid 
+            })
+            console.log(isResultArray.value)
+          }
         }
         catch (e) {
           console.error('解析pptdata.data失败:', e)
@@ -2807,12 +2908,35 @@ const getWork = async (isUpdate = false) => {
     const frame = elementList.value.find(element => element.type === ElementTypes.FRAME)
     console.log('frame:', frame)
     const toolType = frame?.toolType ?? ''
+    const likeArray = res[1]
+    console.log('likeArray', likeArray)
     const newWorkArray = props.cid
       ? res[0].filter((work: any) => {
+        
         // console.log(work.ttype == '1' || (work.ttype == '2' && work.classid.includes(props.cid)) && (work.atool === toolType.value || !toolType.value))
         return work.ttype == '1' || (work.ttype == '2' && work.classid.includes(props.cid)) && (work.atool == toolType.value || !toolType.value)
+      }).map((work: any) => {
+        // 计算点赞数量:likeArray中wid等于当前work.id的记录数
+        const likesCount = likeArray.filter((like: any) => like.workId == work.id).length
+        // 判断当前用户是否点赞:likeArray中wid等于当前work.id且likesId等于当前用户id
+        const isLikes = likeArray.some((like: any) => like.workId == work.id && like.likesId == props.userid)
+        return {
+          ...work,
+          likesCount,
+          isLikes
+        }
+      })
+      : res[0].map((work: any) => {
+        // 计算点赞数量:likeArray中wid等于当前work.id的记录数
+        const likesCount = likeArray.filter((like: any) => like.workId == work.id).length
+        // 判断当前用户是否点赞:likeArray中wid等于当前work.id且likesId等于当前用户id
+        const isLikes = likeArray.some((like: any) => like.workId == work.id && like.likesId == props.userid)
+        return {
+          ...work,
+          likesCount,
+          isLikes
+        }
       })
-      : res[0]
     
     // 如果是更新模式,只有当数据真正变化时才更新
     if (isUpdate) {
@@ -2879,7 +3003,7 @@ const checkWorkArrayChanged = (oldArray: WorkItem[], newArray: WorkItem[]): bool
     const oldWork = oldArray[i]
     const newWork = newArray[i]
     
-    if (oldWork.id !== newWork.id || oldWork.name !== newWork.name || oldWork.content !== newWork.content) {
+    if (oldWork.id !== newWork.id || oldWork.name !== newWork.name || oldWork.content !== newWork.content || oldWork.isLikes !== newWork.isLikes || oldWork.likesCount !== newWork.likesCount) {
       return true
     }
   }
@@ -2962,6 +3086,38 @@ const checkParentMode = () => {
   }
 }
 
+const setIsResultArray = () => {
+  // @ts-ignore
+  if (window.parent && typeof window.parent.setIsResultArray === 'function') {
+    // @ts-ignore
+    window.parent.setIsResultArray(isResultArray.value)
+  }
+}
+
+const setIsResultArray2 = (value: boolean, key: string) => {
+  isResultArray.value[slideIndex.value][key] = value
+  console.log(isResultArray.value)
+  // @ts-ignore
+  if (window.parent && typeof window.parent.setIsResultArray === 'function') {
+    // @ts-ignore
+    window.parent.setIsResultArray(isResultArray.value)
+  }
+  sendMessage({ 
+    type: 'isResultArray', 
+    isResultArray: isResultArray.value,
+    courseid: props.courseid 
+  })
+}
+
+const setCan = (Array: any[]) => {
+  isResultArray.value = Array
+  sendMessage({ 
+    type: 'isResultArray', 
+    isResultArray: isResultArray.value,
+    courseid: props.courseid 
+  })
+}
+
 const forceLogout = () => {
   sendMessage({ type: 'logout' })
 }
@@ -3106,6 +3262,39 @@ const messageInit = () => {
     })
   }
 
+  if (!yIsResultArrayMessages.value) {
+    console.log('初始化isResultArray消息数组')
+    yIsResultArrayMessages.value = docSocket.value.getArray('isResultArrayMessages')
+
+    // 初始化时检查一次是否需要清理
+    if (yIsResultArrayMessages.value && yIsResultArrayMessages.value.length > 1 && isCreator.value && docSocket.value) {
+      smartCleanupMessages(yIsResultArrayMessages.value, 1, 'isResultArray', docSocket.value)
+    }
+
+    yIsResultArrayMessages.value.observe((e: any) => {
+      console.log('yIsResultArrayMessages', yIsResultArrayMessages.value.length)
+      console.log('yIsResultArrayMessages', yIsResultArrayMessages.value)
+      
+      // 每次有新消息添加后,检查是否需要清理
+      if (yIsResultArrayMessages.value && yIsResultArrayMessages.value.length > 1 && isCreator.value && docSocket.value) {  
+        // 使用 setTimeout 确保在 observe 回调执行完成后再清理
+        setTimeout(() => {
+          if (docSocket.value && yIsResultArrayMessages.value && yIsResultArrayMessages.value.length > 1) {
+            smartCleanupMessages(yIsResultArrayMessages.value, 1, 'isResultArray', docSocket.value)
+          }
+        }, 0)
+      }
+      
+      e.changes.added.forEach((i: any) => {
+        const message = i.content.getContent()[0]
+        if (message.mId !== mId.value) {
+          getMessages(message)
+        }
+      })
+    })
+    
+  }
+
   // 如果是首次进入且是创建者,清空所有同步状态
   if (isFirstEnter.value && isCreator.value && docSocket.value) {
     console.log('🧹 首次进入且为创建者,清空所有同步状态')
@@ -3218,13 +3407,15 @@ const isSpecialMessageType = (type: string): boolean => {
   const timerTypes = ['timer_start', 'timer_pause', 'timer_reset', 'timer_stop', 'timer_finish', 'timer_update']
   const laserTypes = ['laser_toggle'] // laser_move 直接通过 Map,不通过消息数组
   const writingBoardTypes = ['writing_board_update', 'writing_board_close', 'writing_board_blackboard']
-  return timerTypes.includes(type) || laserTypes.includes(type) || writingBoardTypes.includes(type)
+  const isResultArrayTypes = ['isResultArray']
+  
+  return timerTypes.includes(type) || laserTypes.includes(type) || writingBoardTypes.includes(type) || isResultArrayTypes.includes(type)
 }
 
 /**
  * 判断是否为开关类型的消息(需要保留的状态消息)
  */
-const isToggleMessageType = (type: string, category: 'timer' | 'laser' | 'writingBoard'): boolean => {
+const isToggleMessageType = (type: string, category: 'timer' | 'laser' | 'writingBoard' | 'isResultArray'): boolean => {
   if (category === 'laser') {
     return type === 'laser_toggle'
   }
@@ -3234,13 +3425,16 @@ const isToggleMessageType = (type: string, category: 'timer' | 'laser' | 'writin
   if (category === 'timer') {
     return ['timer_start', 'timer_stop', 'timer_reset'].includes(type)
   }
+  if (category === 'isResultArray') {
+    return ['isResultArray'].includes(type)
+  }
   return false
 }
 
 /**
  * 智能清理消息数组,保留最新的开关消息
  */
-const smartCleanupMessages = (messageArray: any, maxLength: number, category: 'timer' | 'laser' | 'writingBoard', docSocket: Y.Doc) => {
+const smartCleanupMessages = (messageArray: any, maxLength: number, category: 'timer' | 'laser' | 'writingBoard' | 'isResultArray', docSocket: Y.Doc) => {
   if (!messageArray || messageArray.length <= maxLength) return
   
   const allMessages = messageArray.toArray()
@@ -3296,6 +3490,7 @@ const getMessageArrayByType = (type: string): any | null => {
   const timerTypes = ['timer_start', 'timer_pause', 'timer_reset', 'timer_stop', 'timer_finish', 'timer_update']
   const laserTypes = ['laser_toggle'] // laser_move 直接通过 Map,不通过消息数组
   const writingBoardTypes = ['writing_board_update', 'writing_board_close', 'writing_board_blackboard']
+  const isResultArrayTypes = ['isResultArray']
   
   if (timerTypes.includes(type)) {
     return yTimerMessages.value
@@ -3306,6 +3501,9 @@ const getMessageArrayByType = (type: string): any | null => {
   if (writingBoardTypes.includes(type)) {
     return yWritingBoardMessages.value
   }
+  if (isResultArrayTypes.includes(type)) {
+    return yIsResultArrayMessages.value
+  }
   return null
 }
 
@@ -3383,6 +3581,17 @@ const getMessages = (msgObj: any) => {
       }
     }, 1000)
   }
+  
+  // 处理点赞消息 - 当有人点赞时,重新获取点赞数据
+  if (msgObj.type === 'like_updated' && msgObj.courseid === props.courseid) {
+    console.log('收到点赞消息,重新获取点赞数据')
+    // 延迟一点时间,确保后端数据已更新
+    setTimeout(() => {
+      if (currentSlideHasIframe.value) {
+        getWork(true) // 传入true表示是更新模式
+      }
+    }, 1000)
+  }
 
   // 处理英语口语状态更新 - 学生开始/完成对话时,刷新班级答题面板
   if (props.type == '1' && msgObj.type === 'speaking_session_updated' && msgObj.courseid === props.courseid) {
@@ -3481,6 +3690,26 @@ const getMessages = (msgObj: any) => {
     }
   }
 
+  // 获取是否展开结果数组
+  if (props.type == '2' && msgObj.type === 'isResultArray' && msgObj.courseid === props.courseid) {
+    isResultArray.value = msgObj.isResultArray || []
+  }
+
+  // 投屏
+  if (props.type == '2' && msgObj.type === 'cast_screen' && msgObj.courseid === props.courseid) {
+    openChoiceQuestionDetail3(slideIndex.value)
+    setTimeout(() => {
+      choiceQuestionDetailDialogRef.value?.castScreen?.(msgObj.workerData)
+    }, 500)
+  }
+
+  // 退出投屏
+  if (props.type == '2' && msgObj.type === 'exit_cast_screen' && msgObj.courseid === props.courseid) {
+    openChoiceQuestionDetail3(slideIndex.value)
+    setTimeout(() => {
+      choiceQuestionDetailDialogRef.value?.exitCastScreen?.()
+    }, 500)
+  }
 }
 
 
@@ -3689,6 +3918,7 @@ onMounted(() => {
     manualReconnect,
     connectionStatus: computed(() => connectionStatus.value),
     forceLogout,
+    setCan,
   }
 
   console.log('PPTist Student View 已加载,可通过 window.PPTistStudent 访问功能')
@@ -3918,7 +4148,11 @@ const createWebSocketConnection = async (type = 1) => {
     )
     
     // 启动定期更新 token
-    // updateAuthToken()
+    // 30分钟后再次更新
+    // if (authTokenUpdateTimer.value) {
+    //   clearTimeout(authTokenUpdateTimer.value)
+    // }
+    // authTokenUpdateTimer.value = setTimeout(updateAuthToken, 30 * 60 * 1000) as unknown as NodeJS.Timeout
 
     providerSocket.value.on('status', (event: any) => {
       console.log('👉 WebSocket状态:', event.status)
@@ -3991,7 +4225,9 @@ const createWebSocketConnection = async (type = 1) => {
         console.log('👉 WebSocket连接断开')
         connectionStatus.value = 'disconnected'
         isConnecting.value = false
-        createWebSocketConnection(2)
+        setTimeout(() => {
+          createWebSocketConnection(2)
+        }, 5000)
       }
     })
     
@@ -4000,7 +4236,9 @@ const createWebSocketConnection = async (type = 1) => {
       console.error('👉 WebSocket连接错误:', error)
       connectionStatus.value = 'disconnected'
       isConnecting.value = false
-      createWebSocketConnection(2)
+      setTimeout(() => {
+        createWebSocketConnection(2)
+      }, 5000)
     })
     
   }
@@ -4008,11 +4246,13 @@ const createWebSocketConnection = async (type = 1) => {
     console.error('👉 创建WebSocket连接失败:', error)
     connectionStatus.value = 'disconnected'
     isConnecting.value = false
-    createWebSocketConnection(2)
+    setTimeout(() => {
+      createWebSocketConnection(2)
+    }, 5000)
   }
 
   // 启动 socket 连接检查定时器
-  startSocketCheckTimer()
+  // startSocketCheckTimer()
 }
 
 // 处理连接断开

+ 2 - 0
src/views/Student/index2.vue

@@ -547,6 +547,7 @@ const rightPanelMode = ref<'homework' | 'dialogue' | 'choice' | ''>('homework')
 // 移除定时器相关代码,改用socket监听
 
 const courseDetail = ref<any>({})
+const aiAssistant = ref<any>(false) // 课程助手是否打开
 const studentArray = ref<any>([])
 
 // 跟随模式相关状态
@@ -2432,6 +2433,7 @@ const getCourseDetail = async () => {
     selectWorksStudent()
     checkIsCreator()
     const pptJSONUrl = JSON.parse(courseData.chapters).pptData ? JSON.parse(courseData.chapters).pptData : ''
+    aiAssistant.value = JSON.parse(courseData.chapters).aiAssistant ? JSON.parse(courseData.chapters).aiAssistant : false
     console.log(pptJSONUrl)
     
     if (pptJSONUrl) { 

+ 2 - 0
src/views/components/element/FrameElement/BaseFrameElement.vue

@@ -169,11 +169,13 @@ const getTypeLabel = (type: number): string => {
     15: 'ssEssayQ',
     72: 'ssAIApp',
     73: 'ssH5Page',
+    81: 'ssH5Page',
     74: 'ssVideo',
     75: lang.lang == 'cn' ? 'ssBiliVideo' : 'ssYouTube',
     76: 'ssCreateSpace',
     77: 'ssEnglishSpeakingTool',
     78: 'ssVote',
+    79: 'ssPhoto',
   }
   const key = typeMap[type]
   return (key ? lang[key] : lang.ssUnknown) as string

+ 1 - 1
src/views/components/element/ProsemirrorEditor.vue

@@ -131,7 +131,7 @@ const execCommand = ({ target, action }: RichTextCommand) => {
       addMark(editorView, mark)
 
       if (item.value && !document.fonts.check(`16px ${item.value}`)) {
-        message.warning(lang.ssFontLoadWait)
+        // message.warning(lang.ssFontLoadWait)
       }
     }
     else if (item.command === 'fontsize' && item.value) {

+ 11 - 11
src/views/components/tool/previewImageTool.vue

@@ -7,17 +7,6 @@
       @click.self="closePreview"
       @wheel.prevent="onWheel"
     >
-      <div class="image-preview__toolbar">
-        <button @click.stop="zoomOut">-</button>
-        <button @click.stop="zoomIn">+</button>
-        <button @click.stop="resetTransform">{{ lang.ssReset }}</button>
-        <button @click.stop="rotateLeft">⟲</button>
-        <button @click.stop="rotateRight">⟳</button>
-        <button @click.stop="toggleFit">
-          {{ fitMode ? lang.ssActualSize : lang.ssFitScreen }}
-        </button>
-        <button @click.stop="closePreview">{{ lang.ssClose }}</button>
-      </div>
       <div
         class="image-preview__stage"
         @mousedown="onDragStart"
@@ -43,6 +32,17 @@
           @error="onPreviewImageError"
         />
       </div>
+      <div class="image-preview__toolbar">
+        <button @click.stop="zoomOut">-</button>
+        <button @click.stop="zoomIn">+</button>
+        <button @click.stop="resetTransform">{{ lang.ssReset }}</button>
+        <button @click.stop="rotateLeft">⟲</button>
+        <button @click.stop="rotateRight">⟳</button>
+        <button @click.stop="toggleFit">
+          {{ fitMode ? lang.ssActualSize : lang.ssFitScreen }}
+        </button>
+        <button @click.stop="closePreview">{{ lang.ssClose }}</button>
+      </div>
     </div>
   </Teleport>
 </template>

+ 72 - 3
src/views/lang/cn.json

@@ -162,6 +162,8 @@
   "ssChoiceQ": "选择",
   "ssQandA": "问答",
   "ssVote": "投票",
+  "ssPhoto": "拍照",
+  "ssNoPhoto": "暂无拍照",
   "ssNoLearn": "暂无学习内容",
   "ssNeedUpload": "请先上传或创建学习内容",
   "ssPreview": "预览",
@@ -235,6 +237,8 @@
   "ssClose": "关闭",
   "ssPrevQ": "上一题",
   "ssNextQ": "下一题",
+  "ssPrevP": "上一张",
+  "ssNextP": "下一张",
   "ssSingleSel": "单选题",
   "ssMultiSel": "多选题",
   "ssNodeTitle": "节点*",
@@ -596,6 +600,9 @@
   "ssDupPage": "复制页面",
   "ssDelPage": "删除页面",
   "ssAddSect": "增加节",
+  "ssDupPage2": "复制",
+  "ssDelPage2": "删除",
+  "ssNewPage2": "新增",
   "ssPlayFromCur": "从当前放映",
   "ssDblClickEdit": "双击编辑",
   "ssElText": "文本",
@@ -700,7 +707,7 @@
   "ssCreateApp": "创建应用",
   "ssAddInteractiveWebpage": "添加交互网页",
   "ssWebpageCenter": "网页中心",
-  "ssUploadWebpage": "上传网页",
+  "ssUploadWebpage": "上传代码",
   "ssCrawlWebpage": "爬取网页",
   "ssAddMultimedia": "添加多媒体",
   "ssDocument": "文档",
@@ -709,6 +716,7 @@
   "ssWebpageLink": "网页链接",
   "ssEnterCompleteUrl": "请输入完整的网页URL地址",
   "ssWaitingForInput": "等待输入...",
+  "ssWaitingForInput2": "等待上传...",
   "ssInvalidUrl": "URL格式不正确,请检查",
   "ssStartUpload": "开始上传",
   "ssUploading": "上传中...",
@@ -721,6 +729,7 @@
   "ssClearToolContent": "该操作将清除当前工具的编辑内容,是否继续?",
   "ssUploadWebpageLink": "上传链接",
   "ssCocoLinkTip":"请添加 Cocorobo 同域、亚马逊或可访问的 HTML 链接。",
+  "ssCocoLinkTip2": "非平台域名支持链接,可能存在显示异常,且不支持学生端提交作业。",
   "ssChoiceQuestion":"选择题",
   "ssAnswerCount":"回答人数",
   "ssQuestion":"题目",
@@ -735,10 +744,24 @@
   "ssAiChatShortcut": "输入 / 获取快捷操作短语",
   "ssAiChatQuickAction1": "为当前页面内容生成2道选择题",
   "ssAiChatQuickAction2": "为当前页面内容生成1道问答题",
+  "ssAiChatQuickAction6": "为当前页面生成投票",
+  "ssAiChatQuickAction7": "为当前页面生成拍照题",
   "ssAiChatParsing": "解析中...",
   "ssAiChatWaitUpload": "请等待文件上传完成后再发送消息",
   "ssAiChatFileSizeLimit": "文件大小不能超过10MB",
   "ssAiChatUploadFailed": "文件上传失败:",
+  "ssAiChatQuickTitle": "快捷指令",
+  "ssAiChatQuickSubtitle": "快捷生成单页内容或单个元素",
+  "ssAiChatQuickTabPage": "针对单个页面",
+  "ssAiChatQuickTabCourse": "针对完整课件",
+  "ssAiChatQuickGenChoicesTitle": "生成 2 道选择题",
+  "ssAiChatQuickGenChoicesDesc": "为当前页生成课堂练习,支持除创作空间外的所有工具。",
+  "ssAiChatQuickGenWebTitle": "生成互动网页",
+  "ssAiChatQuickGenWebDesc": "为当前页生成一个交互网页(如模拟器、小游戏等)。",
+  "ssAiChatQuickRecommendToolsTitle": "互动工具推荐",
+  "ssAiChatQuickRecommendToolsDesc": "扫描课程内容,推荐适合页面的互动工具。",
+  "ssAiChatQuickBatchGenToolsTitle": "批量生成互动工具",
+  "ssAiChatQuickBatchGenToolsDesc": "按照课程结构批量生成互动工具,并在待选区确认。",
   "ssClassroomAiAssistant": "课堂AI助手",
   "ssRetryMessage": "网络有点慢,请稍后重试",
   "ssBackToList": "返回列表",
@@ -851,5 +874,51 @@
   "ssSpkAIGenerating": "AI 生成中…",
   "ssSpkJustNow": "刚刚生成",
   "ssSpkSecondsAgoTpl": "{n} 秒前生成",
-  "ssSpkMinutesAgoTpl": "{n} 分钟前生成"
-}
+  "ssSpkMinutesAgoTpl": "{n} 分钟前生成",
+  "ssResult": "结果",
+  "ssConfirmDel": "确认删除",
+  "ssConfirmDelContent": "此操作不可恢复,是否继续?",
+  "searchFont": "搜索字体",
+  "searchFontSize": "搜索字体大小",
+  "textColor": "文字颜色",
+  "highlight": "文字高亮",
+  "fontSizeAdd": "增加字号",
+  "fontSizeReduce": "减少字号",
+  "bold": "加粗",
+  "italic": "斜体",
+  "underline": "下划线",
+  "strikethrough": "删除线",
+  "flipVertically": "垂直翻转",
+  "flipHorizontally": "水平翻转",
+  "ssUploadFile": "上传文件",
+  "ssDragAndDrop": "拖拽文件至此,或点击选择",
+  "ssSupportPptx": "支持.pptx文件",
+  "ssFileDetected": "检测到文件:",
+  "ssTotalPages": "共 {count} 页",
+  "ssImportAsSlide": "导入为课件页面",
+  "ssImportAsSlideDesc": "将文件导入为在线课件页面。",
+  "ssImportAndSave": "导入并保存到资源库",
+  "ssImportAndSaveDesc": "导入为课件页面,并同步保存到资源库。",
+  "ssReadingFile": "正在读取文件...",
+  "ssAiChatQuickAction3": "为当前页面内容生成2道选择题",
+  "ssAiChatQuickAction4": "为当前页面生成互动网页",
+  "ssAiChatQuickAction5": "为课件推荐适合页面的互动工具",
+  "ssDragAndDropHint": "点击或拖拽文件到此处上传",
+  "ssSupportHTML": "支持 HTML、HTM 格式",
+  "ssPasteHTML": "请在此处粘贴完整的HTML代码",
+  "ssAddWebH5": "新建网页h5",
+  "ssCreating": "创建中...",
+  "ssWebEditor": "AI 网页编辑器",
+  "ssConfirmAdd": "确认添加",
+  "ssShowResult": "展示结果",
+  "ssLikeSucc": "点赞成功",
+  "ssLike": "点赞",
+  "ssAnonymous": "匿名",
+  "ssRealName": "匿名",
+  "ssAnonymousUser": "匿名用户",
+  "ssAiChat": "AI对话",
+  "ssCastScreen": "投屏",
+  "ssExitCastScreen": "退出投屏",
+  "ssPasteCode": "粘贴代码",
+  "ssEditWebpage": "修改网页"
+}

+ 71 - 2
src/views/lang/en.json

@@ -161,6 +161,8 @@
   "ssContentList": "Content list",
   "ssQandA": "Q&A",
   "ssVote": "Vote",
+  "ssPhoto": "Photo",
+  "ssNoPhoto": "No photo",
   "ssNoLearn": "No learning content",
   "ssNeedUpload": "Please upload or create learning content first",
   "ssPreview": "Preview",
@@ -235,6 +237,8 @@
   "ssClose": "Close",
   "ssPrevQ": "Previous question",
   "ssNextQ": "Next question",
+  "ssPrevP": "Previous photo",
+  "ssNextP": "Next photo",
   "ssSingleSel": "Single Choice",
   "ssMultiSel": "Multiple Choice",
   "ssNodeTitle": "Node *",
@@ -597,6 +601,9 @@
   "ssDupPage": "Duplicate Slide",
   "ssDelPage": "Delete Slide",
   "ssAddSect": "Add Section",
+  "ssDupPage2": "Duplicate",
+  "ssDelPage2": "Delete",
+  "ssNewPage2": "Add",
   "ssPlayFromCur": "Present from Current Slide",
   "ssDblClickEdit": "Double-click to edit",
   "ssElText": "Text",
@@ -701,7 +708,7 @@
   "ssCreateApp": "Create App",
   "ssAddInteractiveWebpage": "Add Interactive Webpage",
   "ssWebpageCenter": "Webpage Center",
-  "ssUploadWebpage": "Upload Webpage",
+  "ssUploadWebpage": "Upload Code",
   "ssCrawlWebpage": "Crawl Webpage",
   "ssAddMultimedia": "Add Multimedia",
   "ssDocument": "Document",
@@ -710,6 +717,7 @@
   "ssWebpageLink": "Webpage Link",
   "ssEnterCompleteUrl": "Please enter complete webpage URL",
   "ssWaitingForInput": "Waiting for input...",
+  "ssWaitingForInput2": "Waiting for upload...",
   "ssInvalidUrl": "URL format is incorrect, please check",
   "ssStartUpload": "Start Upload",
   "ssUploading": "Uploading...",
@@ -722,6 +730,7 @@
   "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.",
+  "ssCocoLinkTip2": "Non-platform domain links may cause display exceptions and do not support student submission of assignments.",
   "ssChoiceQuestion":"Choice Question",
   "ssAnswerCount":"Answer count",
   "ssQuestion":"Question",
@@ -736,10 +745,24 @@
   "ssAiChatShortcut": "Type / for quick actions",
   "ssAiChatQuickAction1": "Generate 2 multiple choice questions for the current page content",
   "ssAiChatQuickAction2": "Generate 1 essay question for the current page content",
+  "ssAiChatQuickAction6": "Generate a poll for the current page",
+  "ssAiChatQuickAction7": "Generate a photo question for the current page",
   "ssAiChatParsing": "Parsing...",
   "ssAiChatWaitUpload": "Please wait for the file upload to complete before sending a message",
   "ssAiChatFileSizeLimit": "File size cannot exceed 10MB",
   "ssAiChatUploadFailed": "File upload failed:",
+  "ssAiChatQuickTitle": "Quick Actions",
+  "ssAiChatQuickSubtitle": "Quickly generate single page content or single element",
+  "ssAiChatQuickTabPage": "For Single Page",
+  "ssAiChatQuickTabCourse": "For Entire Course",
+  "ssAiChatQuickGenChoicesTitle": "Generate 2 Multiple Choice Questions",
+  "ssAiChatQuickGenChoicesDesc": "Generate classroom practice for the current page, supports all tools except Creative Space.",
+  "ssAiChatQuickGenWebTitle": "Generate Interactive Webpage",
+  "ssAiChatQuickGenWebDesc": "Generate an interactive webpage for the current page (e.g., simulator, mini-games, etc.).",
+  "ssAiChatQuickRecommendToolsTitle": "Interactive Tool Recommendations",
+  "ssAiChatQuickRecommendToolsDesc": "Scan course content and recommend interactive tools suitable for pages.",
+  "ssAiChatQuickBatchGenToolsTitle": "Batch Generate Interactive Tools",
+  "ssAiChatQuickBatchGenToolsDesc": "Batch generate interactive tools according to course structure and confirm in the pending area.",
   "ssClassroomAiAssistant": "Classroom AI Assistant",
   "ssRetryMessage": "Network is slow, please try again later",
   "ssBackToList": "Back to List",
@@ -851,5 +874,51 @@
   "ssSpkAIGenerating": "Generating…",
   "ssSpkJustNow": "Just now",
   "ssSpkSecondsAgoTpl": "{n}s ago",
-  "ssSpkMinutesAgoTpl": "{n}m ago"
+  "ssSpkMinutesAgoTpl": "{n}m ago",
+  "ssResult": "Result",
+  "ssConfirmDel": "Confirm Delete",
+  "ssConfirmDelContent": "This operation cannot be recovered, continue?",
+  "searchFont": "Search Font",
+  "searchFontSize": "Search Font Size",
+  "textColor": "Text Color",
+  "highlight": "Text Highlight Color",
+  "fontSizeAdd": "Increase Font Size",
+  "fontSizeReduce": "Reduce Font Size",
+  "bold": "Bold",
+  "italic": "Italic",
+  "underline": "Underline",
+  "strikethrough": "Strikethrough",
+  "flipVertically": "Flip Vertically",
+  "flipHorizontally": "Flip Horizontally",
+  "ssUploadFile": "Upload File",
+  "ssDragAndDrop": "Drag and Drop Files Here, or Click to Select",
+  "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...",
+  "ssAiChatQuickAction3": "Generate 2 multiple-choice questions for the current page content",
+  "ssAiChatQuickAction4": "Generate an interactive web page for the current page",
+  "ssAiChatQuickAction5": "Recommend interactive tools for the current page",
+  "ssDragAndDropHint": "Click or drag files here to upload",
+  "ssSupportHTML": "Supports HTML, HTM format",
+  "ssPasteHTML": "Please paste the complete HTML code here",
+  "ssAddWebH5": "Add Web H5",
+  "ssCreating": "Creating...",
+  "ssWebEditor": "AI Web Editor",
+  "ssConfirmAdd": "Confirm Add",
+  "ssShowResult": "Show Result",
+  "ssLikeSucc": "Like Success",
+  "ssLike": "Like",
+  "ssAnonymous": "Anonymous",
+  "ssRealName": "Anonymous",
+  "ssAnonymousUser": "Anonymous User",
+  "ssAiChat": "AI Chat",
+  "ssCastScreen": "Cast Screen",
+  "ssExitCastScreen": "Exit Cast Screen",
+  "ssPasteCode": "Paste Code",
+  "ssEditWebpage": "Edit Webpage"
 }

+ 71 - 2
src/views/lang/hk.json

@@ -161,6 +161,8 @@
   "ssContentList": "內容列表",
   "ssQandA": "問答",
   "ssVote": "投票",
+  "ssPhoto": "拍照",
+  "ssNoPhoto": "暫無拍照",
   "ssNoLearn": "暫無學習內容",
   "ssNeedUpload": "請先上傳或創建學習內容",
   "ssPreview": "預覽",
@@ -235,6 +237,8 @@
   "ssClose": "關閉",
   "ssPrevQ": "上一題",
   "ssNextQ": "下一題",
+  "ssPrevP": "上一張",
+  "ssNextP": "下一張",
   "ssSingleSel": "單選題",
   "ssMultiSel": "多選題",
   "ssNodeTitle": "節點*",
@@ -597,6 +601,9 @@
   "ssDupPage": "複製頁面",
   "ssDelPage": "刪除頁面",
   "ssAddSect": "增加節",
+  "ssDupPage2": "複製",
+  "ssDelPage2": "刪除",
+  "ssNewPage2": "新增",
   "ssPlayFromCur": "從當前放映",
   "ssDblClickEdit": "雙擊編輯",
   "ssElText": "文本",
@@ -701,7 +708,7 @@
   "ssCreateApp": "創建應用",
   "ssAddInteractiveWebpage": "添加交互網頁",
   "ssWebpageCenter": "網頁中心",
-  "ssUploadWebpage": "上傳網頁",
+  "ssUploadWebpage": "上傳代碼",
   "ssCrawlWebpage": "爬取網頁",
   "ssAddMultimedia": "添加多媒體",
   "ssDocument": "文檔",
@@ -710,6 +717,7 @@
   "ssWebpageLink": "網頁鏈接",
   "ssEnterCompleteUrl": "請輸入完整的網頁URL地址",
   "ssWaitingForInput": "等待輸入...",
+  "ssWaitingForInput2": "等待上傳...",
   "ssInvalidUrl": "URL格式不正確,請檢查",
   "ssStartUpload": "開始上傳",
   "ssUploading": "上傳中...",
@@ -722,6 +730,7 @@
   "ssClearToolContent": "該操作將清除當前工具的編輯內容,是否繼續?",
   "ssUploadWebpageLink": "上傳鏈接",
   "ssCocoLinkTip":"請添加 Cocorobo 同域、亚马逊或可访问的 HTML 链接。",
+  "ssCocoLinkTip2": "非平台域名支持鏈接,可能存在顯示異常,且不支持學生端提交作業。",
   "ssChoiceQuestion":"選擇題",
   "ssAnswerCount":"回答人數",
   "ssQuestion":"題目",
@@ -736,10 +745,24 @@
   "ssAiChatShortcut": "輸入 / 獲取快捷操作短語",
   "ssAiChatQuickAction1": "為當前頁面內容生成2道選擇題",
   "ssAiChatQuickAction2": "為當前頁面內容生成1道問答題",
+  "ssAiChatQuickAction6": "為當前頁面生成投票",
+  "ssAiChatQuickAction7": "為當前頁面生成拍照題",
   "ssAiChatParsing": "解析中...",
   "ssAiChatWaitUpload": "請等待文件上傳完成後再發送消息",
   "ssAiChatFileSizeLimit": "文件大小不能超過10MB",
   "ssAiChatUploadFailed": "文件上傳失敗:",
+  "ssAiChatQuickTitle": "快捷指令",
+  "ssAiChatQuickSubtitle": "快捷生成單頁內容或單個元素",
+  "ssAiChatQuickTabPage": "針對單個頁面",
+  "ssAiChatQuickTabCourse": "針對完整課件",
+  "ssAiChatQuickGenChoicesTitle": "生成 2 道選擇題",
+  "ssAiChatQuickGenChoicesDesc": "為當前頁生成課堂練習,支持除創作空間外的所有工具。",
+  "ssAiChatQuickGenWebTitle": "生成互動網頁",
+  "ssAiChatQuickGenWebDesc": "為當前頁生成一個交互網頁(如模擬器、小遊戲等)。",
+  "ssAiChatQuickRecommendToolsTitle": "互動工具推薦",
+  "ssAiChatQuickRecommendToolsDesc": "掃描課程內容,推薦適合頁面的互動工具。",
+  "ssAiChatQuickBatchGenToolsTitle": "批量生成互動工具",
+  "ssAiChatQuickBatchGenToolsDesc": "按照課程結構批量生成互動工具,並在待選區確認。",
   "ssClassroomAiAssistant": "課堂AI助手",
   "ssRetryMessage": "網絡稍慢,請稍後重試",
   "ssBackToList": "返回列表",
@@ -851,5 +874,51 @@
   "ssSpkAIGenerating": "AI 生成中…",
   "ssSpkJustNow": "剛剛生成",
   "ssSpkSecondsAgoTpl": "{n} 秒前生成",
-  "ssSpkMinutesAgoTpl": "{n} 分鐘前生成"
+  "ssSpkMinutesAgoTpl": "{n} 分鐘前生成",
+  "ssResult": "結果",
+  "ssConfirmDel": "確認刪除",
+  "ssConfirmDelContent": "此操作不可恢復,是否繼續?",
+  "searchFont": "搜索字型",
+  "searchFontSize": "搜索字型大小",
+  "textColor": "文字顏色",
+  "highlight": "文字高亮",
+  "fontSizeAdd": "增加字号",
+  "fontSizeReduce": "減少字号",
+  "bold": "加粗",
+  "italic": "斜體",
+  "underline": "下劃線",
+  "strikethrough": "刪除線",
+  "flipVertically": "垂直翻轉",
+  "flipHorizontally": "水平翻轉",
+  "ssUploadFile": "上傳文件",
+  "ssDragAndDrop": "拖拽文件至此,或點擊選擇",
+  "ssSupportPptx": "支持.pptx文件",
+  "ssFileDetected": "檢測到文件:",
+  "ssTotalPages": "共 {count} 頁",
+  "ssImportAsSlide": "導入為課件頁面",
+  "ssImportAsSlideDesc": "將文件導入為在線課件頁面。",
+  "ssImportAndSave": "導入並保存到資源庫",
+  "ssImportAndSaveDesc": "導入為課件頁面,並同步保存到資源庫。",
+  "ssReadingFile": "正在讀取文件...",
+  "ssAiChatQuickAction3": "為當頁內容生成2道選擇題",
+  "ssAiChatQuickAction4": "為當頁生成互動網頁。",
+  "ssAiChatQuickAction5": "為課件推薦適合的互動工具。",
+  "ssDragAndDropHint": "點擊或拖拽文件至此,或點擊選擇",
+  "ssSupportHTML": "支持 HTML、HTM 格式",
+  "ssPasteHTML": "請在此处粘貼完整的HTML代碼",
+  "ssAddWebH5": "新增網頁H5",
+  "ssCreating": "創建中...",
+  "ssWebEditor": "AI 網頁編輯器",
+  "ssConfirmAdd": "確認添加",
+  "ssShowResult": "展示結果",
+  "ssLikeSucc": "點贊成功",
+  "ssLike": "點贊",
+  "ssAnonymous": "匿名",
+  "ssRealName": "匿名",
+  "ssAnonymousUser": "匿名用戶",
+  "ssAiChat": "AI對話",
+  "ssCastScreen": "投屏",
+  "ssExitCastScreen": "退出投屏",
+  "ssPasteCode": "粘貼代碼",
+  "ssEditWebpage": "修改網頁"
 }

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini