Browse Source

feat: 添加AI生成功能并优化iframe元素缩放

refactor(iframe): 优化iframe元素缩放逻辑
feat(ai): 新增AI聊天和生成功能
feat(analysis): 添加学生答题分析功能
style: 更新语言文件添加新词条
chore: 更新markdown-it和添加uuid依赖
lsc 2 weeks ago
parent
commit
2fb98d922b

+ 25 - 7
package-lock.json

@@ -22,7 +22,7 @@
         "html2canvas": "^1.4.1",
         "katex": "^0.16.22",
         "lodash": "^4.17.21",
-        "markdown-it": "^14.1.0",
+        "markdown-it": "^14.1.1",
         "mitt": "^3.0.1",
         "nanoid": "^5.0.7",
         "number-precision": "^1.6.0",
@@ -45,6 +45,7 @@
         "svg-pathdata": "^7.1.0",
         "tinycolor2": "^1.6.0",
         "tippy.js": "^6.3.7",
+        "uuid": "^13.0.0",
         "vue": "^3.5.17",
         "vuedraggable": "^4.1.0",
         "wangeditor": "^4.7.15",
@@ -4162,9 +4163,9 @@
       }
     },
     "node_modules/markdown-it": {
-      "version": "14.1.0",
-      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
-      "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+      "version": "14.1.1",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
+      "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
       "dependencies": {
         "argparse": "^2.0.1",
         "entities": "^4.4.0",
@@ -5888,6 +5889,18 @@
         "base64-arraybuffer": "^1.0.2"
       }
     },
+    "node_modules/uuid": {
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
+      "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "bin": {
+        "uuid": "dist-node/bin/uuid"
+      }
+    },
     "node_modules/validate-npm-package-license": {
       "version": "3.0.4",
       "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -9250,9 +9263,9 @@
       "dev": true
     },
     "markdown-it": {
-      "version": "14.1.0",
-      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
-      "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+      "version": "14.1.1",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
+      "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
       "requires": {
         "argparse": "^2.0.1",
         "entities": "^4.4.0",
@@ -10584,6 +10597,11 @@
         "base64-arraybuffer": "^1.0.2"
       }
     },
+    "uuid": {
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
+      "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="
+    },
     "validate-npm-package-license": {
       "version": "3.0.4",
       "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",

+ 2 - 1
package.json

@@ -29,7 +29,7 @@
     "html2canvas": "^1.4.1",
     "katex": "^0.16.22",
     "lodash": "^4.17.21",
-    "markdown-it": "^14.1.0",
+    "markdown-it": "^14.1.1",
     "mitt": "^3.0.1",
     "nanoid": "^5.0.7",
     "number-precision": "^1.6.0",
@@ -52,6 +52,7 @@
     "svg-pathdata": "^7.1.0",
     "tinycolor2": "^1.6.0",
     "tippy.js": "^6.3.7",
+    "uuid": "^13.0.0",
     "vue": "^3.5.17",
     "vuedraggable": "^4.1.0",
     "wangeditor": "^4.7.15",

+ 1 - 0
src/App.vue

@@ -15,6 +15,7 @@
       <Editor3
         v-else-if="viewMode === 'editor3' && _isPC && !screening"
         :courseid="urlParams.courseid"
+        :userid="urlParams.userid"
         key="editor3"
       />
       <Student

+ 519 - 0
src/components/CollapsibleToolbar/componets/aiChat.vue

@@ -0,0 +1,519 @@
+<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>
+                <div class="message-content ai-message chat" v-if="message.aiContent">
+                    <div v-html="message.aiContent"></div>
+                    <button class="confirm-btn" v-if="message.jsonData?.gType !== 'chat'" @click="generate(message)">确定</button>
+                </div>
+            </div>
+        </div>
+        <!-- 输入区域 -->
+        <div class="input-section">
+            <div class="input-wrapper">
+                <textarea class="ai-input"
+                    :placeholder="messages.length === 0 ? '例如:创建45分钟的四年级教学内容为水的三态变化的课程' : '输入 / 获取快捷操作短语'"
+                    v-model="inputText" @keyup.enter.exact="sendMessage" rows="5" />
+                <div class="input-actions">
+                    <button class="attach-btn" v-show="false">
+                        <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>
+                    <button class="send-btn" @click="sendMessage">
+                        <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>
+                </div>
+            </div>
+        </div>
+        <!-- 初始状态 -->
+        <div class="initial-state" v-if="messages.length === 0">
+            <!-- 快捷操作 -->
+            <div class="quick-actions">
+                <button class="quick-action-btn" @click="sendQuickAction('为当前页面内容生成2道选择题')">为当前页面内容生成2道选择题</button>
+                <button class="quick-action-btn" @click="sendQuickAction('为当前页面生成2页内容页面')">为当前页面生成2页内容页面</button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, useTemplateRef, nextTick } from 'vue'
+import { chat_no_stream, chat_stream, getAgentModel, chat_no_stream2 } from '@/tools/aiChat'
+import { useSlidesStore } from '@/store'
+import { lang } from '@/main'
+import MarkdownIt from 'markdown-it'
+import { getWorkPageId } from '@/services/course'
+
+interface ChatMessage {
+    uid?: string
+    role: 'ai' | 'user'
+    content?: string
+    aiContent?: string
+    oldContent?: string
+    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
+        headUrl?: string
+        assistantName?: string
+        sourceArray?: Array<{
+            text?: string
+            id?: string
+            title?: string
+        }>
+    }
+}
+
+const props = withDefaults(defineProps<{
+    userid?: string | null
+}>(), {
+  userid: null,
+})
+
+
+
+const inputText = ref('')
+const messages = ref<ChatMessage[]>([])
+const chatSection = useTemplateRef<HTMLElement>('chatSection')
+const chatLoading = ref(false)
+
+const sendMessage = () => {
+  if (inputText.value.trim()) {
+    // 添加用户消息
+    messages.value.push({
+      role: 'user',
+      content: inputText.value
+    })
+
+    // 模拟AI回复
+    // setTimeout(() => {
+
+    //   setTimeout(() => {
+    //     messages.value.at(-1).aiContent = '课程生成完成!为您创建了5个内容页面和3个互动工具。您可以查看底部课程大纲,或在中央区域开始编辑。',
+    //     messages.value.at(-1).jsonData = {
+    //       isChoice: true
+    //     }
+    //   }, 1000)
+    // }, 500)
+    prevChatResult()
+    messages.value.at(-1).loading = true
+    chatLoading.value = true
+    sendAction(inputText.value)
+    inputText.value = ''
+  }
+}
+
+const prevChatResult = () => {
+  nextTick(() => {
+    if (chatSection.value) {
+      chatSection.value.scrollTop = chatSection.value.scrollHeight
+    }
+  })
+}
+
+const sendQuickAction = (action: string) => {
+  // 添加用户消息
+  messages.value.push({
+    role: 'user',
+    content: action
+  })
+
+  // 模拟AI回复
+  //   setTimeout(() => {
+  //     messages.value.at(-1).aiContent = '请问您想聚焦哪一重点?'
+  //   }, 500)
+  sendAction(action)
+}
+
+import { v4 as uuidv4 } from 'uuid'
+const session_name = ref('')
+const slidesStore = useSlidesStore()
+const gType = ref('chat')
+
+const sendAction = async (action: string) => {
+  const md = new MarkdownIt()
+  if (gType.value === 'chat') {
+    const content = await chat_no_stream(action, agentid1.value, props.userid || '', lang.lang)
+    console.log(content)
+    // 渲染 Markdown 格式
+    try {
+      gType.value = JSON.parse(content).intent
+    }
+    catch (error) {
+      gType.value = 'chat'
+    }
+
+    // generate_qa // generate_choice_question
+    if (gType.value !== 'chat') {
+      messages.value.at(-1).jsonData = {
+        gType: gType.value
+      }
+    }
+  }
+  else {
+    messages.value.at(-1).jsonData = {
+      gType: gType.value
+    }
+  }
+
+
+
+  const prompt = `
+  #当前页面内容 ${JSON.stringify(slidesStore.currentSlide || '')}
+  #query: ${action}
+  `
+  chat_stream(prompt, agentid2.value, props.userid || '', lang.lang, (event) => {
+    if (event.type === 'message') {
+      messages.value.at(-1).aiContent = md.render(event.data)
+
+      messages.value.at(-1).loading = false
+      prevChatResult()
+    }
+    else if (event.type === 'messageEnd') {
+      messages.value.at(-1).aiContent = md.render(event.data)
+      chatLoading.value = false
+      prevChatResult()
+    }
+  }, session_name.value).catch(err => {
+    chatLoading.value = false
+    console.log('err', err)
+  })
+}
+import useCreateElement from '@/hooks/useCreateElement'
+import useSlideHandler from '@/hooks/useSlideHandler'
+const { createSlide } = useSlideHandler()
+const { createFrameElement } = useCreateElement()
+
+const generate = (message: ChatMessage) => {
+  if (message.jsonData?.gType === 'generate_choice_question') {
+    console.log(message.jsonData?.gType)
+    const prompt = [
+        {
+          role: 'user',
+          content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成选择题的json。输出一个json格式的回复,格式如下:{"testCount":1,"testTitle":"","testJson":[{"id":"7de1fdb4-bec3-4324-8986-4623f838e3d7","type":"2","teststitle":"1+1?","checkList":["1","2","3"],"timuList":[],"answer":[1],"userAnswer":[],"explanation":"解析"}]}。输出语言为${lang.lang === "en"? "英文" : lang.lang === "hk"? "繁体中文" : "简体中文"}`,
+        },
+      ];
+      chat_no_stream2(prompt, { type: 'json_object' }).then(async (res: any) => {
+        console.log('选择题', JSON.parse(res));
+        gType.value = 'chat'
+        setPageId(45, res).then(res => {
+            let url = `https://beta.pbl.cocorobo.cn/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${45}`;
+            createSlide()
+            createFrameElement(url, 45)
+        });
+      });
+  }
+  else if (message.jsonData?.gType === 'generate_qa') {
+    console.log(message.jsonData?.gType)
+        const prompt = [
+        {
+          role: 'user',
+          content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成问答题的json。输出一个json格式的回复,格式如下:{"answerQ":"问题","answer":"","fileList":[],"imageList":[],"evaluationCriteria":"评价标准"}。输出语言为${lang.lang === "en"? "英文" : lang.lang === "hk"? "繁体中文" : "简体中文"}`,
+        },
+      ];
+      chat_no_stream2(prompt, { type: 'json_object' }).then((res: any) => {
+        console.log('问答题', JSON.parse(res));
+        gType.value = 'chat'
+            setPageId(15, res).then(res => {
+            let url = `https://beta.pbl.cocorobo.cn/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${15}`;
+            createSlide()
+            createFrameElement(url, 15)
+        });
+      });
+  }
+}
+
+const setPageId = async (tool: any, json: any) => {
+    const res = await getWorkPageId({
+        userid: props.userid || '',
+        type: tool,
+        json: json
+    })
+    return res[0][0].id
+}
+
+
+
+const agentid1 = ref('cbb29b41-2a4a-4453-bf8d-357929ced4bd')// 判断意图
+const agentid2 = ref('f86aa63c-b7b7-4d03-9b37-b59f116d36f3')// 生成内容
+
+
+onMounted(() => {
+  session_name.value = uuidv4()
+  getAgentModel(agentid1.value)
+  getAgentModel(agentid2.value)
+})
+</script>
+
+<style lang="scss" scoped>
+.ai-chat-container {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    padding: 16px;
+    gap: 16px;
+}
+
+.input-section {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+}
+
+.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;
+}
+
+.ai-input {
+    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;
+}
+
+.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;
+    }
+
+    &:hover {
+        background: #FFF4E5;
+        color: #F78B22;
+        border-radius: 4px;
+    }
+}
+
+.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;
+
+    svg {
+        width: 20px;
+        height: 20px;
+    }
+
+    &:hover {
+        background: #E68A00;
+    }
+}
+
+.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;
+        }
+
+        +.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;
+    }
+
+    &::-webkit-scrollbar-track {
+        background: #F3F4F6;
+        border-radius: 3px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+        background: #D1D5DB;
+        border-radius: 3px;
+
+        &:hover {
+            background: #9CA3AF;
+        }
+    }
+}
+
+.chat-message {
+    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 {
+            margin-top: 10px;
+        }
+    }
+}
+
+.message-content {
+    &.ai-message {
+        align-self: flex-start;
+        background: #fafbfc;
+        border: 1.5px solid #e5e7eb;
+        border-bottom-left-radius: 2px;
+    }
+}
+
+.message-content {
+    &.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;
+}
+
+
+.confirm-btn {
+    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;
+    }
+}
+
+ul {
+    margin: 8px 0;
+    padding-left: 20px;
+
+    li {
+        margin: 4px 0;
+    }
+}
+</style>
+
+<style>
+.chat table {
+    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;
+}
+</style>

+ 40 - 2
src/components/CollapsibleToolbar/index2.vue

@@ -2,6 +2,14 @@
   <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">
@@ -72,6 +80,23 @@
         </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>
@@ -495,6 +520,7 @@ import useCreateElement from '@/hooks/useCreateElement'
 import useSlideHandler from '@/hooks/useSlideHandler'
 import { useSlidesStore } from '@/store'
 import FileInput from '@/components/FileInput.vue'
+import AiChat from './componets/aiChat.vue'
 import { lang } from '@/main'
 import toolChoice from '@/assets/img/tool_choice.jpeg'
 import toolAnswer from '@/assets/img/tool_answer.png'
@@ -506,10 +532,14 @@ interface ContentItem {
   id?: string
 }
 
+
+
 const props = withDefaults(defineProps<{
   defaultCollapsed?: boolean
+  userid?: string | null
 }>(), {
-  defaultCollapsed: false
+  defaultCollapsed: false,
+  userid: null,
 })
 
 const emit = defineEmits<{
@@ -1063,8 +1093,8 @@ const handleParsingClose = () => {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  margin-bottom: 20px;
   border-bottom: 1px solid #f0f0f0;
+  margin-bottom: 20px;
   width: 100%;
   box-sizing: border-box;
   padding: 12px 15px;
@@ -1102,6 +1132,14 @@ const handleParsingClose = () => {
   }
 }
 
+.submenu-content {
+  width: 100%;
+  height: calc(100% - 50px);
+  box-sizing: border-box;
+  overflow: hidden;
+}
+
+
 .submenu-img {
   width: calc(100% - 30px);
   margin: 0 auto;

+ 27 - 1
src/hooks/useSlideHandler.ts

@@ -16,7 +16,7 @@ export default () => {
   const mainStore = useMainStore()
   const slidesStore = useSlidesStore()
   const { selectedSlidesIndex: _selectedSlidesIndex, activeElementIdList } = storeToRefs(mainStore)
-  const { currentSlide, slides, theme, slideIndex } = storeToRefs(slidesStore)
+  const { currentSlide, slides, theme, slideIndex, viewportSize, viewportRatio } = storeToRefs(slidesStore)
 
   const selectedSlidesIndex = computed(() => [..._selectedSlidesIndex.value, slideIndex.value])
   const selectedSlides = computed(() => slides.value.filter((item, index) => selectedSlidesIndex.value.includes(index)))
@@ -93,9 +93,35 @@ export default () => {
   // 根据模板创建新页面
   const createSlideByTemplate = (slide: Slide) => {
     const { groupIdMap, elIdMap } = createElementIdMap(slide.elements)
+    const slideWidth = viewportSize.value
+    const slideHeight = viewportSize.value * viewportRatio.value
+    
+    // 模板原始宽高(16:9比例)
+    const templateWidth = 1280
+    const templateHeight = 720
+    
+    // 计算缩放因子
+    const scaleX = slideWidth / templateWidth
+    const scaleY = slideHeight / templateHeight
 
     for (const element of slide.elements) {
       element.id = elIdMap[element.id]
+      element.top = (element.top || 0) * scaleY
+      element.left = (element.left || 0) * scaleX
+      element.width = (element.width || 0) * scaleX
+      if (element.type === 'text' || element.type === 'shape') {
+        element.height = (element.height || 0) * scaleY
+      }
+      
+      // 处理 content 中的字体大小
+      if (element.type === 'text') {
+        // 匹配 style 中的 font-size 属性
+        element.content = element.content.replace(/font-size:\s*(\d+)px/g, (match, fontSize) => {
+          const newFontSize = Math.round(parseInt(fontSize) * scaleY)
+          return `font-size: ${newFontSize}px`
+        })
+      }
+      
       if (element.groupId) element.groupId = groupIdMap[element.groupId]
     }
     const newSlide = {

+ 5 - 2
src/services/course.ts

@@ -194,7 +194,9 @@ export const getWorkDetail = (params: any): Promise<any> => {
 }
 
 
-
+export const getWorkPageId = (params: any): Promise<any> => {
+  return axios.post(`${API_URL}insert_workPage`, [params])
+}
 
 
 
@@ -216,6 +218,7 @@ export default {
   getChatList,
   getAgentData,
   clearDialogue,
-  getWorkDetail
+  getWorkDetail,
+  getWorkPageId
 }
 

+ 220 - 0
src/tools/aiChat.ts

@@ -0,0 +1,220 @@
+import axios from '@/services/config'
+import { v4 as uuidv4 } from 'uuid'
+import { fetchEventSource } from '@microsoft/fetch-event-source'
+
+const model = {}
+
+interface ChatParams {
+  id: string;
+  message: string;
+  userId: string;
+  model: string;
+  file_ids: string[];
+  sound_url: string;
+  temperature: number;
+  top_p: number;
+  max_completion_tokens: number;
+  stream: boolean;
+  uid: string;
+  session_name: string;
+  tts_language: string;
+}
+
+const DEFAULT_PARAMS: Omit<ChatParams, 'message' | 'uid' | 'stream'> = {
+  id: 'a7741704-ba56-40b7-a6b8-62a423ef9376',
+  userId: '6c56ec0e-2c74-11ef-bee5-005056b86db5',
+  model: 'open-doubao',
+  file_ids: [],
+  sound_url: '',
+  temperature: 0.2,
+  top_p: 1,
+  max_completion_tokens: 4096,
+  session_name: 'pptSession_name',
+  tts_language: 'zh-CN'
+}
+
+export const chat_no_stream = async (msg: string, agentId: string, userId: string, language: string, session_name?: string): Promise<string> => {
+  const agentData = await getAgentModel(agentId)
+  const params: ChatParams = {
+    ...DEFAULT_PARAMS,
+    id: agentId,
+    message: msg,
+    uid: uuidv4(),
+    stream: false,
+    model: agentData?.modelType || 'open-doubao',
+    userId: userId,
+    tts_language: getTtsLanguage(language),
+    session_name: session_name || uuidv4()
+  }
+
+  const res = await axios.post('https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat', params)
+  let content = res?.message || ''
+  console.log(content)
+  
+  // 清理可能的 markdown 格式
+  if (content.includes('```json')) {
+    // 提取 ```json 和 ``` 之间的内容
+    const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/)
+    if (jsonMatch) {
+      content = jsonMatch[1].trim()
+    }
+  }
+  else if (content.includes('```')) {
+    // 提取 ``` 和 ``` 之间的内容
+    const codeMatch = content.match(/```\s*([\s\S]*?)\s*```/)
+    if (codeMatch) {
+      content = codeMatch[1].trim()
+    }
+  }
+
+  return content
+
+}
+
+export const chat_stream = async (
+  msg: string,
+  agentId: string,
+  userId: string,
+  language: string,
+  onMessage: (event: { type: 'message' | 'close' | 'error' | 'messageEnd'; data: string }) => void,
+  session_name?: string
+): Promise<void> => {
+  const agentData = await getAgentModel(agentId)
+  const params: ChatParams = {
+    ...DEFAULT_PARAMS,
+    id: agentId,
+    message: msg,
+    uid: uuidv4(),
+    stream: true,
+    model: agentData?.modelType || 'open-doubao',
+    userId: userId,
+    tts_language: getTtsLanguage(language),
+    session_name: session_name || uuidv4()
+  }
+
+  const ctrl = new AbortController()
+  let content = ''
+  try {
+    await fetchEventSource('https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat', {
+      method: 'POST',
+      body: JSON.stringify(params),
+      signal: ctrl.signal,
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      onmessage(event) {
+        const data = JSON.parse(event.data)
+        if (data.content) {
+          if (data.content != '[DONE]') {
+            content += data.content
+            onMessage({
+              type: 'message',
+              data: content
+            })
+          }
+          else {
+            onMessage({
+              type: 'messageEnd',
+              data: content
+            })
+          }
+
+        }
+      },
+      onclose() {
+        onMessage({
+          type: 'close',
+          data: 'SSE Connection closed'
+        })
+      },
+      onerror(err) {
+        onMessage({
+          type: 'error',
+          data: err.message || 'Unknown error'
+        })
+        // 返回 undefined 以阻止自动重连,如需重连则删除此行
+        throw err
+      },
+    })
+  }
+  finally {
+    ctrl.abort()
+  }
+}
+
+export const getAgentModel = async (agentId: string) => {
+  if (model[agentId]) {
+    return model[agentId]
+  }
+  const res = await axios.get(`https://appapi.cocorobo.cn/api/agents/agent/${agentId}`)
+  model[agentId] = res
+  return model[agentId]
+}
+
+export const getTtsLanguage = (langCode: string) => {
+  switch (langCode) {
+    case 'en':return 'en-US'
+    case 'hk':return 'yue-CN'
+    default :return 'zh-CN'
+  }
+}
+
+// AI 模型常量
+const AI_MODEL_CONSTANTS = {
+  DEFAULT_MODEL: 'gpt-4o-2024-11-20'
+}
+
+export const chat_no_stream2 = async (prompt: any[] = [], response_format = {
+  "type": "text"
+}, model = AI_MODEL_CONSTANTS.DEFAULT_MODEL) => {
+  return await new Promise((resolve) => {
+    let uid = uuidv4();
+    let data = JSON.stringify({
+      model: model,
+      temperature: 0,
+      max_tokens: 4096,
+      top_p: 1,
+      frequency_penalty: 0,
+      presence_penalty: 0,
+      messages: prompt,
+      uid: uid,
+      mind_map_question: '',
+      stream: false,
+      response_format: response_format
+    });
+    let config = {
+      method: 'post',
+      url: 'https://appapi.cocorobo.cn/api/common/chat',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      data: data,
+    };
+    axios(config)
+      .then((response) => {
+        let content = response?.FunctionResponse?.choices[0]?.message?.content || '';
+
+        // 清理可能的 markdown 格式
+        if (content.includes('```json')) {
+          // 提取 ```json 和 ``` 之间的内容
+          const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/);
+          if (jsonMatch) {
+            content = jsonMatch[1].trim();
+          }
+        } else if (content.includes('```')) {
+          // 提取 ``` 和 ``` 之间的内容
+          const codeMatch = content.match(/```\s*([\s\S]*?)\s*```/);
+          if (codeMatch) {
+            content = codeMatch[1].trim();
+          }
+        }
+
+        resolve(content);
+      })
+      .catch( (error)=> {
+        console.log(error);
+        // this.$message.error('服务器繁忙');
+        resolve(false);
+      });
+  });
+}

+ 3 - 1
src/views/Editor/index3.vue

@@ -142,7 +142,7 @@
     </div>
     <!-- <EditorHeader class="layout-header" /> -->
     <div class="layout-content">
-      <CollapsibleToolbar class="layout-sidebar" @toggle="handleToolbarToggle" />
+      <CollapsibleToolbar class="layout-sidebar" @toggle="handleToolbarToggle" :userid="props.userid" />
       <div class="layout-content-center">
         <CanvasTool class="center-top" />
         <Canvas class="center-body" :style="{ height: `calc(100% - ${remarkHeight + 60}px  - 120px)` }"
@@ -217,10 +217,12 @@ const parentWindow = window.parent as ParentWindowWithToolList
 
 interface Props {
   courseid?: string | null
+  userid?: string | null
 }
 
 const props = withDefaults(defineProps<Props>(), {
   courseid: null,
+  userid: null,
 })
 
 const mainStore = useMainStore()

+ 2 - 0
src/views/Screen/ScreenElement.vue

@@ -15,6 +15,7 @@
     <component
       :is="currentElementComponent"
       :elementInfo="elementInfo"
+      :scale="scale"
       :is-visible="isVisible"
     ></component>
   </div>
@@ -39,6 +40,7 @@ import BaseFrameElement from '@/views/components/element/FrameElement/BaseFrameE
 
 const props = defineProps<{
   elementInfo: PPTElement
+  scale: number
   elementIndex: number
   animationIndex: number
   turnSlideToId: (id: string) => void

+ 3 - 0
src/views/Screen/ScreenSlide.vue

@@ -17,6 +17,7 @@
       :animationIndex="animationIndex"
       :turnSlideToId="turnSlideToId"
       :manualExitFullscreen="manualExitFullscreen"
+      :scale="scale"
       :is-visible="isVisible"
     />
   </div>
@@ -60,6 +61,7 @@ const hasIframe = computed(() => {
 
 // 计算scale:如果是iframe界面且scale大于1就按1,否则按原scale
 const iframeScale = computed(() => {
+  /*
   if (hasIframe.value) {
     // return Math.min(props.scale, 1)
     // return props.scale;
@@ -70,6 +72,7 @@ const iframeScale = computed(() => {
     }
 
   }
+  */
   return {
     width: viewportSize.value + 'px',
     height: viewportSize.value * viewportRatio.value + 'px',

+ 9 - 3
src/views/Student/components/answerTheResult.vue

@@ -265,9 +265,15 @@ const choiceQuestionList = () => {
     });
     (props.workArray ?? []).forEach(
       (studentWork: any, studentIndex: number) => {
-        const _studentContent: any = JSON.parse(
-          decodeURIComponent(studentWork.content)
-        ).testJson
+        let _studentContent: any = []
+        try {
+          _studentContent = JSON.parse( 
+            decodeURIComponent(studentWork.content)
+          ).testJson
+        }
+        catch (error) {
+          console.log(error)
+        }
         // let _studentContentCorrection = []
         // console.log(_workData)
         // console.log(_studentContent)

+ 324 - 43
src/views/Student/components/choiceQuestionDetailDialog.vue

@@ -43,18 +43,22 @@
         }">
           <div id="echartsArea1" ref="echartsArea1"></div>
         </div>
-        <!-- <div class="c_t45_aiAnalysis">
-          <div class="c_t45_aa_header">
-            <div class="c_t45_aa_h_title">
-              <svg viewBox="0 0 1024 1024" width="200" height="200"><path d="M512 170.666667C323.477333 170.666667 170.666667 323.477333 170.666667 512s152.810667 341.333333 341.333333 341.333333 341.333333-152.810667 341.333333-341.333333S700.522667 170.666667 512 170.666667zM85.333333 512C85.333333 276.352 276.352 85.333333 512 85.333333s426.666667 191.018667 426.666667 426.666667-191.018667 426.666667-426.666667 426.666667S85.333333 747.648 85.333333 512z"></path><path d="M693.013333 330.986667a42.666667 42.666667 0 0 1 10.304 43.648l-75.413333 226.282666a42.666667 42.666667 0 0 1-26.986667 26.986667l-226.282666 75.413333a42.666667 42.666667 0 0 1-53.973334-53.973333l75.434667-226.261333a42.666667 42.666667 0 0 1 26.986667-26.986667l226.282666-75.413333a42.666667 42.666667 0 0 1 43.648 10.304z m-222.72 139.306666l-41.685333 125.098667 125.077333-41.706667 41.706667-125.077333-125.077333 41.706667z"></path></svg>分析
+        <div class="aiAnalysis" v-if="props.workArray.length>0">
+          <div class="ai_header">
+            <div class="ai_title">
+              <svg viewBox="0 0 1024 1024" width="200" height="200"><path d="M512 170.666667C323.477333 170.666667 170.666667 323.477333 170.666667 512s152.810667 341.333333 341.333333 341.333333 341.333333-152.810667 341.333333-341.333333S700.522667 170.666667 512 170.666667zM85.333333 512C85.333333 276.352 276.352 85.333333 512 85.333333s426.666667 191.018667 426.666667 426.666667-191.018667 426.666667-426.666667 426.666667S85.333333 747.648 85.333333 512z"></path><path d="M693.013333 330.986667a42.666667 42.666667 0 0 1 10.304 43.648l-75.413333 226.282666a42.666667 42.666667 0 0 1-26.986667 26.986667l-226.282666 75.413333a42.666667 42.666667 0 0 1-53.973334-53.973333l75.434667-226.261333a42.666667 42.666667 0 0 1 26.986667-26.986667l226.282666-75.413333a42.666667 42.666667 0 0 1 43.648 10.304z m-222.72 139.306666l-41.685333 125.098667 125.077333-41.706667 41.706667-125.077333-125.077333 41.706667z"></path></svg>{{ lang.ssAnalysis }}
             </div>
-            <div class="c_t45_aa_h_refresh">
-              AI生成
+            <div class="ai_refresh" :class="{'disabled': currentAnalysis && currentAnalysis.loading}" @click="aiAnalysisRefresh45()">
+              {{ lang.ssAIGenerate }}
              <svg viewBox="0 0 1024 1024" width="200" height="200"><path d="M875 483c-33.4 0-60.5 27.1-60.5 60.5v0.1C814.4 710.3 678.8 846 512 846S209.5 710.3 209.5 543.5 345.2 241 512 241c36.8 0 71.7 7.6 104.4 19.7-32 3-57.4 29.1-57.4 61.9 0 34.8 28.2 63 63 63h201.9c34.8 0 63-28.2 63-63V120c0-34.8-28.2-63-63-63s-63 28.2-63 63v81.4C691 150.5 605.2 120 512 120 278.1 120 88.5 309.6 88.5 543.5S278.1 967 512 967s423.5-189.6 423.5-423.5c0-33.4-27.1-60.5-60.5-60.5z"></path></svg>
 
             </div>
           </div>
-        </div> -->
+          <div class="ai_content" v-if="currentAnalysis">
+            {{ currentAnalysis.json }}
+          </div>
+          <div class="ai_updateTime" v-if="currentAnalysis">{{ lang.ssUpdateTime }}:{{ currentAnalysis.update_at }}</div>
+        </div>
         <div class="cq_changeBtn" v-if="props.showData.choiceQuestionListData.length > 1">
           <div :class="{ cq_cb_disabled: props.showData.workIndex <= 0 }" @click="changeWorkIndex(0)">
             <svg style="transform: rotate(-90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
@@ -91,6 +95,24 @@
           </div>
         </div>
 
+       <div class="aiAnalysis" style="margin-top:1rem ;" v-if="processedWorkArray.length>0">
+          <div class="ai_header">
+            <div class="ai_title">
+              <svg viewBox="0 0 1024 1024" width="200" height="200"><path d="M512 170.666667C323.477333 170.666667 170.666667 323.477333 170.666667 512s152.810667 341.333333 341.333333 341.333333 341.333333-152.810667 341.333333-341.333333S700.522667 170.666667 512 170.666667zM85.333333 512C85.333333 276.352 276.352 85.333333 512 85.333333s426.666667 191.018667 426.666667 426.666667-191.018667 426.666667-426.666667 426.666667S85.333333 747.648 85.333333 512z"></path><path d="M693.013333 330.986667a42.666667 42.666667 0 0 1 10.304 43.648l-75.413333 226.282666a42.666667 42.666667 0 0 1-26.986667 26.986667l-226.282666 75.413333a42.666667 42.666667 0 0 1-53.973334-53.973333l75.434667-226.261333a42.666667 42.666667 0 0 1 26.986667-26.986667l226.282666-75.413333a42.666667 42.666667 0 0 1 43.648 10.304z m-222.72 139.306666l-41.685333 125.098667 125.077333-41.706667 41.706667-125.077333-125.077333 41.706667z"></path></svg>{{ lang.ssAnalysis }}
+            </div>
+            <div class="ai_refresh" :class="{'disabled': currentAnalysis && currentAnalysis.loading}" @click="aiAnalysisRefresh15()">
+              {{ lang.ssAIGenerate }}
+             <svg viewBox="0 0 1024 1024" width="200" height="200"><path d="M875 483c-33.4 0-60.5 27.1-60.5 60.5v0.1C814.4 710.3 678.8 846 512 846S209.5 710.3 209.5 543.5 345.2 241 512 241c36.8 0 71.7 7.6 104.4 19.7-32 3-57.4 29.1-57.4 61.9 0 34.8 28.2 63 63 63h201.9c34.8 0 63-28.2 63-63V120c0-34.8-28.2-63-63-63s-63 28.2-63 63v81.4C691 150.5 605.2 120 512 120 278.1 120 88.5 309.6 88.5 543.5S278.1 967 512 967s423.5-189.6 423.5-423.5c0-33.4-27.1-60.5-60.5-60.5z"></path></svg>
+
+            </div>
+          </div>
+          <div class="ai_content" v-if="currentAnalysis">
+            {{ currentAnalysis.json }}
+          </div>
+          <div class="ai_updateTime" v-if="currentAnalysis">{{ lang.ssUpdateTime }}:{{ currentAnalysis.update_at }}</div>
+        </div>
+
+
         <div class="c_t15_workDetail" v-if="lookWorkData">
           <div class="c_t15_wd_top">
             <img src="../../../assets/img/arrow_left.png" @click="lookWork('')" />
@@ -201,13 +223,15 @@
 </template>
 
 <script setup lang="ts">
-import { computed, ref, watch, onUnmounted, nextTick } from 'vue'
+import { computed, ref, watch, onUnmounted, nextTick, onMounted } from 'vue'
 import * as echarts from 'echarts'
 import previewImageTool from '../../components/tool/previewImageTool.vue'
 import MarkdownIt from 'markdown-it'
 import useImport from '@/hooks/useImport'
 import { lang } from '@/main'
 import selectUserDialog from './selectUserDialog.vue'
+import { chat_stream } from '@/tools/aiChat'
+import axios from '@/services/config'
 const props = defineProps<{
   visible: number[];
   workIndex: number;
@@ -216,6 +240,8 @@ const props = defineProps<{
   slideIndex: number;
   showData: any;
   workArray: any[];
+  courseDetail: any;
+  userId: string;
 }>()
 
 const emit = defineEmits<{
@@ -240,6 +266,9 @@ const previewImageToolRef = ref<any>(null)
 // 选择用户组件
 const selectUserDialogRef = ref<any>(null)
 
+// ai分析数据
+const aiAnalysisData = ref<Array<any>>([])
+
 const md = new MarkdownIt()
 const { getFile } = useImport()
 
@@ -455,6 +484,7 @@ const setEchartsArea1 = () => {
   if (myChart.value) {
     const _work =
       props.showData.choiceQuestionListData[props.showData.workIndex]
+    console.log('_work', _work)
     // 修正版,处理xAxis.data内为图片对象的case,formatter始终只拿到src或自定义label,保证无[object Object]问题
     const option = {
       tooltip: {
@@ -580,7 +610,7 @@ const setEchartsArea1 = () => {
       (option.series[0].data as any[]).push(i.user.length)
     })
 
-    console.log(option)
+    // console.log(option)
     // {
     //   title: {
     //     text: 'ECharts 入门示例'
@@ -613,6 +643,27 @@ const setEchartsArea1 = () => {
   }
 }
 
+// 获取分析
+const getAnalysis = () => {
+  if (!props.showData.workDetail.id) {
+    return
+  }
+  const params = {
+    pid: props.showData.workDetail.id,
+  }
+  axios.get('https://pbl.cocorobo.cn/api/pbl/select_pptAnalysisByPid?pid=' + params.pid).then(res => {
+    const data = res[0]
+    if (data.length) {
+      aiAnalysisData.value = data
+    }
+    else {
+      aiAnalysisData.value = []
+    }
+  }).catch(err => {
+    console.log('get_pptAnalysis_err', err)
+  })
+}
+
 // 监听选择题数据变化
 watch(
   () => props.showData,
@@ -639,6 +690,7 @@ watch(
     else {
       myChart.value = null
     }
+    getAnalysis()
   },
   { immediate: true, deep: true }
 )
@@ -725,6 +777,201 @@ const viewUnsubmittedStudents = () => {
   // }
 }
 
+// ai生成选择题分析 选择题
+const aiAnalysisRefresh45 = () => {
+  const _work = props.showData.choiceQuestionListData[props.showData.workIndex]
+  const msg = `# CONTEXT #
+你是K-12阶段的AI教育课堂分析助手,基于上传的课件、逐字稿,以及当页的学生答题数据(选择题/问答题/智能体对话)进行智能分析。
+
+# OBJECTIVE #
+输出当前学生答题的分析报告:整体表现、核心发现(共性问题/误区)、教学优化(改进方向),为教师提供教学策略的建议和支持。
+
+#INPUT#
+课程数据:
+- 课程名称:${props.courseDetail.title}
+- 课程学科:${props.courseDetail.name}
+当前页面答题数据(选择题):【分析重点】
+- 选择题题目:${_work.teststitle}
+- 选项数据:${JSON.stringify(_work.choiceUser)}
+- 题目图片:${_work.timuList.lenght ? _work.timuList[0].src : ''}
+- 未提交学生:${JSON.stringify(props.showData.unsubmittedStudents.map((item: any) => item.name))}
+
+# ANALYSIS RULES #
+1. **问题定位**:明确指出哪个知识点/概念/步骤掌握不佳
+2. **建议具体**:给出可执行的教学动作(如“增加XX实例演练”“补充XX对比图”)
+
+# RESPONSE #
+采用段落叙述,数据量化呈现,突出可操作建议,严格控制输出为80词内。采用三段式论述:
+1. 整体表现-1句话
+2. 核心发现-1-2句,指出共性问题+典型案例
+3. 改进建议-1句话,提出具体教学动作
+
+# EXAMPLES #
+样例:
+选择题正确率62%,核心概念“机器学习三步骤”(输入数据→训练模型→预测结果)掌握尚可,但“训练与预测区分”混淆率达38%;建议强化训练vs预测对比教学。`
+
+  if (!currentAnalysis.value) {
+    aiAnalysisData.value.push({
+      pid: props.showData.workDetail.id,
+      index: props.showData.workIndex,
+      loading: true,
+      json: '',
+      noEnd: true,
+      update_at: '',
+      create_at: '',
+    })
+  }
+  else {
+    aiAnalysisData.value.find((item:any) => {
+      return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+    }).loading = true
+    aiAnalysisData.value.find((item:any) => {
+      return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+    }).json = ''
+  }
+
+  chat_stream(msg, 'a7741704-ba56-40b7-a6b8-62a423ef9376', props.userId, lang.lang, (event) => {
+    if (event.type === 'message') { 
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).json = event.data
+
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).loading = false
+    }
+    else if (event.type === 'messageEnd') {
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).json = event.data
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).noEnd = false
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).update_at = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-')
+      saveAnalysis()
+    }
+  }).catch(err => {
+    console.log('err', err)
+  })
+}
+
+// 问答题
+const aiAnalysisRefresh15 = () => {
+  const msg = `# CONTEXT #
+你是K-12阶段的AI教育课堂分析助手,基于上传的课件、逐字稿,以及当页的学生答题数据(选择题/问答题/智能体对话)进行智能分析。
+
+# OBJECTIVE #
+输出当前学生答题的分析报告:整体表现、核心发现(共性问题/误区)、教学优化(改进方向),为教师提供教学策略的建议和支持。
+
+#INPUT#
+课程数据:
+- 课程名称:${props.courseDetail.title}
+- 课程学科:${props.courseDetail.name}
+当前页面答题数据(问答题):【分析重点】
+- 问答题题目:${props.showData.workDetail.json.answerQ}
+- 回答数据:${JSON.stringify(processedWorkArray.value.map((i) => ({user: i.name, answer: i.content.answer})))}
+- 未提交学生:${JSON.stringify(props.showData.unsubmittedStudents.map((item: any) => item.name))}
+
+# ANALYSIS RULES #
+1. **问题定位**:明确指出哪个知识点/概念/步骤掌握不佳
+2. **建议具体**:给出可执行的教学动作(如“增加XX实例演练”“补充XX对比图”)
+
+# RESPONSE #
+采用段落叙述,数据量化呈现,突出可操作建议,严格控制输出为80词内。采用三段式论述:
+1. 整体表现-1句话
+2. 核心发现-1-2句,指出共性问题+典型案例
+3. 改进建议-1句话,提出具体教学动作
+
+# EXAMPLES #
+样例:
+选择题正确率62%,核心概念“机器学习三步骤”(输入数据→训练模型→预测结果)掌握尚可,但“训练与预测区分”混淆率达38%;建议强化训练vs预测对比教学。`
+  if (!currentAnalysis.value) {
+    aiAnalysisData.value.push({
+      pid: props.showData.workDetail.id,
+      index: props.showData.workIndex,
+      loading: true,
+      json: '',
+      noEnd: true,
+      update_at: '',
+      create_at: '',
+    })
+  }
+  else {
+    aiAnalysisData.value.find((item:any) => {
+      return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+    }).loading = true
+    aiAnalysisData.value.find((item:any) => {
+      return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+    }).json = ''
+  }
+
+  chat_stream(msg, 'a7741704-ba56-40b7-a6b8-62a423ef9376', props.userId, lang.lang, (event) => {
+    if (event.type === 'message') { 
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).json = event.data
+
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).loading = false
+    }
+    else if (event.type === 'messageEnd') {
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).json = event.data
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).noEnd = false
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).update_at = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-')
+      saveAnalysis()
+    }
+  }).catch(err => {
+    console.log('err', err)
+  })
+}
+
+
+// 当前分析
+const currentAnalysis = computed(() => {
+  return aiAnalysisData.value.find((item:any) => {
+    return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+  })
+})
+
+// 保存分析
+const saveAnalysis = () => {
+  if (!currentAnalysis.value) {
+    return
+  }
+  const params = [{
+    pid: props.showData.workDetail.id,
+    idx: props.showData.workIndex,
+    json: currentAnalysis.value.json,
+  }]
+
+  axios.post('https://pbl.cocorobo.cn/api/pbl/insert_pptAnalysis', params).then(res => {
+    if (res == 1) {
+      console.log('保存成功')
+    }
+  }).catch(err => {
+    console.log('insert_pptAnalysis_err', err)
+  })
+}
+
+
+
+// watch(()=>props.slideIndex,()=>{
+//   getAnalysis()
+// })
+
+// onMounted(()=>{
+//   getAnalysis()
+// })
+
 // 组件卸载时清理ECharts实例
 onUnmounted(() => {
   // 清除定时器
@@ -897,40 +1144,7 @@ onUnmounted(() => {
         }
       }
 
-      .c_t45_aiAnalysis{
-        display: flex;
-        flex-direction: column;
-        padding: 1rem;
-        border: solid 1px #F6C82B;
-        border-left-width: 4px;
-        border-radius: 1rem;
-        &>.c_t45_aa_header{
-          display: flex;
-          align-items: center;
-          justify-content: space-between;
-          gap: 1rem;
-          &>div{
-            display: flex;
-            align-items: center;
-            gap: .5rem;
-            &>svg{
-              width: 1rem;
-              height: 1rem;
-              
-            }
-          }
-          &>.c_t45_aa_h_title{
-            color: #F7CD49;
-            font-weight: 500;
-            &>svg{
-              fill: #F7CD49;
-            }
-          }
-          // &>.c_t45_aa_h_refresh{
-            
-          // }
-        }
-      }
+
     }
 
     .c_t15 {
@@ -1458,4 +1672,71 @@ onUnmounted(() => {
     }
   }
 }
+
+      .aiAnalysis{
+        width: 100%;
+        height: auto;
+        display: flex;
+        flex-direction: column;
+        padding: 1rem;
+        border: solid 1px #F6C82B;
+        border-left-width: 4px;
+        border-radius: 1rem;
+        gap: 1rem;
+        &>.ai_header{
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+          gap: 1rem;
+          &>div{
+            display: flex;
+            align-items: center;
+            gap: .5rem;
+            &>svg{
+              width: 1rem;
+              height: 1rem;
+              
+            }
+          }
+          &>.ai_title{
+            color: #F7CD49;
+            font-weight: 500;
+            &>svg{
+              fill: #F7CD49;
+              width: 1.2rem;
+              height: 1.2rem;
+            }
+          }
+          &>.ai_refresh{
+            padding: .5rem 1rem;
+            border-radius: .5rem;
+            background: #F6C82B;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            cursor: pointer;
+            color: #000;
+            font-weight: bold;
+            font-size: .8rem;
+            gap: .5rem;
+            &>svg{
+              fill: #000;
+              width: .8rem;
+              height: .8rem;
+            }
+            &.disabled{
+              cursor: not-allowed !important;
+              background: #FEF8E9 !important;
+            }
+          }
+        }
+        &>.ai_content{
+          font-size: 1rem;
+          font-weight: 500;
+        }
+        &>.ai_updateTime{
+          font-size: .8rem;
+           font-weight: 500;
+        }
+      }
 </style>

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

@@ -89,7 +89,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)" :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 v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex)" :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"/>
 
 
           <div class="slide-bottom" v-if="!isFullscreen">

+ 1 - 1
src/views/Student/index2.vue → src/views/Student/index3.vue

@@ -99,7 +99,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)" :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 v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex)"  :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"/>
 
 
           <div class="slide-bottom" v-if="!isFullscreen">

+ 3 - 0
src/views/components/ThumbnailSlide/ThumbnailElement.vue

@@ -10,6 +10,7 @@
       :is="currentElementComponent"
       :elementInfo="elementInfo"
       :is-thumbnail="true"
+      :scale="scale"
       target="thumbnail"
     ></component>
   </div>
@@ -33,6 +34,8 @@ import BaseFrameElement from '@/views/components/element/FrameElement/BaseFrameE
 const props = defineProps<{
   elementInfo: PPTElement
   elementIndex: number
+  scale: number
+  slide: number
 }>()
 
 const currentElementComponent = computed<unknown>(() => {

+ 2 - 0
src/views/components/ThumbnailSlide/index.vue

@@ -20,6 +20,8 @@
       <ThumbnailElement
         v-for="(element, index) in slide.elements"
         :key="element.id"
+        :scale="scale"
+        :slide="slide"
         :elementInfo="element"
         :elementIndex="index + 1"
       />

+ 311 - 276
src/views/components/element/FrameElement/BaseFrameElement.vue

@@ -5,307 +5,342 @@
         height: elementInfo.height + 'px',
   
   -->
-    <div class="base-element-frame"
-      :style="{
-        top: elementInfo.top + 'px',
-        left: elementInfo.left + 'px',
-        width: '100%',
-        height: '100%',
-      }"
+  <div
+    class="base-element-frame"
+    :style="{
+      transform: isThumbnail ?  'scale(1)': `scale(${1 / props.scale})`,
+      transformOrigin: 'top left', // 关键点
+      top:  props.elementInfo.top + 'px',
+      left: props.elementInfo.left  + 'px',
+      width: (isThumbnail ? props.elementInfo.width : width) + 'px',
+      height: (isThumbnail ? props.elementInfo.height : height) + 'px',
+      overflow: 'hidden',
+    }"
+  >
+    <div
+      class="rotate-wrapper"
+      :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
     >
-      <div
-        class="rotate-wrapper"
-        :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
-      >
-        <div class="element-content">
-          <!-- 视频类型(type 74):使用 video 标签 -->
-          <video 
-            v-if="elementInfo.toolType === 74 && !isThumbnail && isVisible"
-            :key="`video-${iframeKey}`"
-            :src="elementInfo.url"
-            :width="elementInfo.width"
-            :height="elementInfo.height"
-            controls
-            :style="{ width: '100%', height: '100%', objectFit: 'contain' }"
-          ></video>
-          <!-- B站视频类型(type 75):使用 iframe -->
-          <iframe 
-            v-else-if="elementInfo.toolType === 75 && !isThumbnail && isVisible"
-            :key="`bilibili-${iframeKey}`"
-            :src="elementInfo.url"
-            :style="{
-              width: '100%',
-              height: '100%',
-            }"
-            :frameborder="0" 
-            :allowfullscreen="true"
-            allow="camera *; microphone *; display-capture; midi; encrypted-media; fullscreen; geolocation; clipboard-read; clipboard-write; accelerometer; autoplay; gyroscope; payment; picture-in-picture; usb; xr-spatial-tracking;"
-            @load="handleIframeLoad"
-          ></iframe>
-          <!-- 延迟加载iframe:只有在可见且不是缩略图时才加载 -->
-          <iframe 
-            :key="`html-${iframeKey}`"
-            :srcdoc="elementInfo.url" 
-            v-else-if="elementInfo.isHTML && !isThumbnail && isVisible"
-            :style="{
-              width: '100%',
-              height: '100%',
-            }"
-            :frameborder="0" 
-            :allowfullscreen="true"
-            allow="camera *; microphone *; display-capture; midi; encrypted-media; fullscreen; geolocation; clipboard-read; clipboard-write; accelerometer; autoplay; gyroscope; payment; picture-in-picture; usb; xr-spatial-tracking;"
-            @load="handleIframeLoad"
-          ></iframe>
-          <iframe 
-            :key="`src-${iframeKey}`"
-            v-else-if="!isThumbnail && isVisible"
-            :src="elementInfo.url"
-            :style="{
-              width: '100%',
-              height: '100%',
-            }"
-            :frameborder="0" 
-            :allowfullscreen="true"
-            allow="camera *; microphone *; display-capture; midi; encrypted-media; fullscreen; geolocation; clipboard-read; clipboard-write; accelerometer; autoplay; gyroscope; payment; picture-in-picture; usb; xr-spatial-tracking;"
-            @load="handleIframeLoad"
-          ></iframe>
-          <!-- 占位符:当不可见时显示 -->
-          <div v-else-if="!isThumbnail && !isVisible" class="iframe-placeholder">
-            <div class="placeholder-content">
-              <div class="placeholder-icon">🌐</div>
-              <div class="placeholder-text">{{ lang.ssInteract }}</div>
-              <div class="placeholder-type">({{ getTypeLabel(Number(elementInfo.toolType)) }})</div>
+      <div class="element-content">
+        <!-- 视频类型(type 74):使用 video 标签 -->
+        <video
+          v-if="elementInfo.toolType === 74 && !isThumbnail && isVisible"
+          :key="`video-${iframeKey}`"
+          :src="elementInfo.url"
+          :width="width"
+          :height="height"
+          controls
+          :style="{ width: '100%', height: '100%', objectFit: 'contain' }"
+        ></video>
+        <!-- B站视频类型(type 75):使用 iframe -->
+        <iframe
+          v-else-if="elementInfo.toolType === 75 && !isThumbnail && isVisible"
+          :key="`bilibili-${iframeKey}`"
+          :src="elementInfo.url"
+          :width="width"
+          :height="height"
+          :frameborder="0"
+          :allowfullscreen="true"
+          allow="camera *; microphone *; display-capture; midi; encrypted-media; fullscreen; geolocation; clipboard-read; clipboard-write; accelerometer; autoplay; gyroscope; payment; picture-in-picture; usb; xr-spatial-tracking;"
+          @load="handleIframeLoad"
+        ></iframe>
+        <!-- 延迟加载iframe:只有在可见且不是缩略图时才加载 -->
+        <iframe
+          :key="`html-${iframeKey}`"
+          :srcdoc="elementInfo.url"
+          v-else-if="elementInfo.isHTML && !isThumbnail && isVisible"
+          :width="width"
+          :height="height"
+          :frameborder="0" 
+          :allowfullscreen="true"
+          allow="camera *; microphone *; display-capture; midi; encrypted-media; fullscreen; geolocation; clipboard-read; clipboard-write; accelerometer; autoplay; gyroscope; payment; picture-in-picture; usb; xr-spatial-tracking;"
+          @load="handleIframeLoad"
+        ></iframe>
+        <iframe
+          :key="`src-${iframeKey}`"
+          v-else-if="!isThumbnail && isVisible"
+          :src="elementInfo.url"
+          :width="width"
+          :height="height"
+          :frameborder="0" 
+          :allowfullscreen="true"
+          allow="camera *; microphone *; display-capture; midi; encrypted-media; fullscreen; geolocation; clipboard-read; clipboard-write; accelerometer; autoplay; gyroscope; payment; picture-in-picture; usb; xr-spatial-tracking;"
+          @load="handleIframeLoad"
+        ></iframe>
+        <!-- 占位符:当不可见时显示 -->
+        <div v-else-if="!isThumbnail && !isVisible" class="iframe-placeholder">
+          <div class="placeholder-content">
+            <div class="placeholder-icon">🌐</div>
+            <div class="placeholder-text">{{ lang.ssInteract }}</div>
+            <div class="placeholder-type">
+              ({{ getTypeLabel(Number(elementInfo.toolType)) }})
             </div>
           </div>
-          <!-- 缩略图模式 -->
-          <div v-else-if="isThumbnail" class="thumbnail-content">
-            <div class="thumbnail-content-inner">
-              <div>{{ lang.ssInteract }}</div>
-              <div>({{ getTypeLabel(Number(elementInfo.toolType)) }})</div>
-            </div>
+        </div>
+        <!-- 缩略图模式 -->
+        <div v-else-if="isThumbnail" class="thumbnail-content">
+          <div class="thumbnail-content-inner">
+            <div>{{ lang.ssInteract }}</div>
+            <div>({{ getTypeLabel(Number(elementInfo.toolType)) }})</div>
           </div>
-          <!-- 在放映模式下不显示遮罩层,允许用户与iframe交互 -->
-          <div class="mask" v-if="false"></div>
         </div>
+        <!-- 在放映模式下不显示遮罩层,允许用户与iframe交互 -->
+        <div class="mask" v-if="false"></div>
       </div>
     </div>
-  </template>
+  </div>
+</template>
   
   <script lang="ts" setup>
-  import type { PropType } from 'vue'
-  import type { PPTFrameElement } from '@/types/slides'
-  import { lang } from '@/main'
-  import { ref, watch, nextTick } from 'vue'
-  
-  const props = defineProps({
-    elementInfo: {
-      type: Object as PropType<PPTFrameElement>,
-      required: true,
-    },
-    isThumbnail: {
-      type: Boolean,
-      default: false,
-    },
-    isVisible: {
-      type: Boolean,
-      default: false,
-    },
-  })
-  
-  // 用于强制刷新iframe的key
-  const iframeKey = ref(0)
-  
-  // 监听elementInfo.url的变化
-  watch(() => props.elementInfo.url, (newUrl, oldUrl) => {
+import type { PropType } from "vue";
+import type { PPTFrameElement } from "@/types/slides";
+import { lang } from "@/main";
+import { ref, watch, nextTick } from "vue";
+import { computed } from 'vue'
+
+const props = defineProps({
+  elementInfo: {
+    type: Object as PropType<PPTFrameElement>,
+    required: true,
+  },
+  isThumbnail: {
+    type: Boolean,
+    default: false,
+  },
+  scale: {
+    type: Number,
+    default: 1
+  },
+  isVisible: {
+    type: Boolean,
+    default: false,
+  },
+});
+
+// 用于强制刷新iframe的key
+const iframeKey = ref(0);
+
+// 监听elementInfo.url的变化
+watch(
+  () => props.elementInfo.url,
+  (newUrl, oldUrl) => {
     if (newUrl !== oldUrl) {
       // 通过改变key来强制刷新iframe
-      iframeKey.value++
-    }
-  })
-  
-  // 获取类型标签
-  const getTypeLabel = (type: number): string => {
-    const typeMap: Record<number, keyof typeof lang> = {
-      45: 'ssChoiceQ',
-      15: 'ssEssayQ',
-      72: 'ssAIApp',
-      73: 'ssH5Page',
-      74: 'ssVideo',
-      75: lang.lang == 'cn' ? 'ssBiliVideo' : 'ssYouTube',
-      76: 'ssCreateSpace'
+      iframeKey.value++;
     }
-    const key = typeMap[type]
-    return (key ? lang[key] : lang.ssUnknown) as string
   }
-  
-  // 处理iframe加载完成事件
-  const handleIframeLoad = async (event: Event) => {
-    const iframe = event.target as HTMLIFrameElement
-    
-    try {
-      // 等待iframe完全加载
-      await nextTick()
-      setTimeout(async () => {
-        // 检查iframe是否可访问(同源检查)
-        if (iframe.contentWindow && iframe.contentDocument) {
-          const iframeDoc = iframe.contentDocument
-          const iframeHead = iframeDoc.head || iframeDoc.getElementsByTagName('head')[0]
-          
-          if (iframeHead) {
-            // 使用动态导入获取JS文件内容
-            const jsFiles = [
-              { id: 'aws-sdk', importPath: () => import('./aws-sdk-2.235.1.min.js?raw') },
-              { id: 'jquery', importPath: () => import('./jquery-3.6.0.min.js?raw') },
-              { id: 'jietu', importPath: () => import('./jietu.js?raw') }
-            ]
-            
-            for (const jsFile of jsFiles) {
-              try {
-                // 检查是否已经注入过
-                if (!iframeDoc.getElementById(jsFile.id)) {
-                  const jsModule = await jsFile.importPath()
-                  const jsContent = jsModule.default || jsModule
-                  
-                  const scriptElement = iframeDoc.createElement('script')
-                  scriptElement.id = jsFile.id
-                  scriptElement.textContent = jsContent
-                  iframeHead.appendChild(scriptElement)
-                  
-                  console.log(`已注入 ${jsFile.id} 到iframe中`)
-                }
-              }
-              catch (fetchError) {
-                console.error(`获取 ${jsFile.id} 失败:`, fetchError)
+);
+
+
+
+const width = computed(() => {
+  return props.elementInfo.width * props.scale;
+})
+
+const height = computed(() => {
+  return props.elementInfo.height * props.scale;
+})
+
+const left = computed(() => {
+  return props.elementInfo.left * props.scale;
+})
+
+const top = computed(() => {
+  return props.elementInfo.top * props.scale;
+})
+
+
+// 获取类型标签
+const getTypeLabel = (type: number): string => {
+  const typeMap: Record<number, keyof typeof lang> = {
+    45: "ssChoiceQ",
+    15: "ssEssayQ",
+    72: "ssAIApp",
+    73: "ssH5Page",
+    74: "ssVideo",
+    75: lang.lang == "cn" ? "ssBiliVideo" : "ssYouTube",
+    76: "ssCreateSpace",
+  };
+  const key = typeMap[type];
+  return (key ? lang[key] : lang.ssUnknown) as string;
+};
+
+// 处理iframe加载完成事件
+const handleIframeLoad = async (event: Event) => {
+  const iframe = event.target as HTMLIFrameElement;
+
+  try {
+    // 等待iframe完全加载
+    await nextTick();
+    setTimeout(async () => {
+      // 检查iframe是否可访问(同源检查)
+      if (iframe.contentWindow && iframe.contentDocument) {
+        const iframeDoc = iframe.contentDocument;
+        const iframeHead =
+          iframeDoc.head || iframeDoc.getElementsByTagName("head")[0];
+
+        if (iframeHead) {
+          // 使用动态导入获取JS文件内容
+          const jsFiles = [
+            {
+              id: "aws-sdk",
+              importPath: () => import("./aws-sdk-2.235.1.min.js?raw"),
+            },
+            {
+              id: "jquery",
+              importPath: () => import("./jquery-3.6.0.min.js?raw"),
+            },
+            { id: "jietu", importPath: () => import("./jietu.js?raw") },
+          ];
+
+          for (const jsFile of jsFiles) {
+            try {
+              // 检查是否已经注入过
+              if (!iframeDoc.getElementById(jsFile.id)) {
+                const jsModule = await jsFile.importPath();
+                const jsContent = jsModule.default || jsModule;
+
+                const scriptElement = iframeDoc.createElement("script");
+                scriptElement.id = jsFile.id;
+                scriptElement.textContent = jsContent;
+                iframeHead.appendChild(scriptElement);
+
+                console.log(`已注入 ${jsFile.id} 到iframe中`);
               }
+            } catch (fetchError) {
+              console.error(`获取 ${jsFile.id} 失败:`, fetchError);
             }
-            
-            // 可选:在iframe中执行一些初始化代码
-            try {
-              iframe.contentWindow.eval(`
+          }
+
+          // 可选:在iframe中执行一些初始化代码
+          try {
+            iframe.contentWindow.eval(`
                 console.log('iframe中的JS环境已准备就绪');
                 // 这里可以添加一些初始化代码
-              `)
-            }
-            catch (evalError) {
-              console.warn('无法在iframe中执行代码:', evalError)
-            }
+              `);
+          } catch (evalError) {
+            console.warn("无法在iframe中执行代码:", evalError);
           }
         }
-        else {
-          console.warn('无法访问iframe内容,可能是跨域限制')
-        }
-      }, 1000)
-    }
-    catch (error) {
-      console.error('注入JS到iframe失败:', error)
-    }
+      } else {
+        console.warn("无法访问iframe内容,可能是跨域限制");
+      }
+    }, 1000);
+  } catch (error) {
+    console.error("注入JS到iframe失败:", error);
   }
-  </script>
+};
+</script>
   
   <style lang="scss" scoped>
-  .base-element-frame {
-    position: absolute;
-  }
-  .element-content {
-    width: 100%;
-    height: 100%;
-    overflow: hidden;
-    
-    video {
-      width: 100%;
-      height: 100%;
-      object-fit: contain;
-      background-color: #000;
-    }
-  }
-  .mask {
-    position: absolute;
-    top: 0;
-    bottom: 0;
-    left: 0;
-    right: 0;
-  }
-  .rotate-wrapper {
-    width: 100%;
-    height: 100%;
-    overflow: hidden;
-  }
-  .thumbnail-content {
-    width: 100%;
-    height: 100%;
-    background-color: #fff;
-  }
-  
-  .thumbnail-content-inner {
+.base-element-frame {
+  position: absolute;
+}
+.element-content {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+
+  video {
     width: 100%;
     height: 100%;
-    color: #3681fc;
-    font-size: 110px;
-    font-weight: 600;
-    text-align: center;
-    line-height: 100%;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    flex-direction: column;
-    gap: 50px;
-  }
-  
-  /* iframe占位符样式 */
-  .iframe-placeholder {
-    width: 100%;
-    height: 100%;
-    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
-    border: 2px solid #dee2e6;
-    border-radius: 8px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    position: relative;
-    overflow: hidden;
-  }
-  
-  .placeholder-content {
-    text-align: center;
-    color: #6c757d;
-    font-family: Arial, sans-serif;
+    object-fit: contain;
+    background-color: #000;
   }
-  
-  .placeholder-icon {
-    font-size: 48px;
-    margin-bottom: 12px;
-    opacity: 0.7;
-  }
-  
-  .placeholder-text {
-    font-size: 16px;
-    font-weight: 600;
-    margin-bottom: 4px;
-  }
-  
-  .placeholder-type {
-    font-size: 12px;
-    opacity: 0.8;
-  }
-  
-  /* 添加加载动画效果 */
-  .iframe-placeholder::before {
-    content: '';
-    position: absolute;
-    top: 0;
+}
+.mask {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+}
+.rotate-wrapper {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+}
+.thumbnail-content {
+  width: 100%;
+  height: 100%;
+  background-color: #fff;
+}
+
+.thumbnail-content-inner {
+  width: 100%;
+  height: 100%;
+  color: #3681fc;
+  font-size: 110px;
+  font-weight: 600;
+  text-align: center;
+  line-height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  gap: 50px;
+}
+
+/* iframe占位符样式 */
+.iframe-placeholder {
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
+  border: 2px solid #dee2e6;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+  overflow: hidden;
+}
+
+.placeholder-content {
+  text-align: center;
+  color: #6c757d;
+  font-family: Arial, sans-serif;
+}
+
+.placeholder-icon {
+  font-size: 48px;
+  margin-bottom: 12px;
+  opacity: 0.7;
+}
+
+.placeholder-text {
+  font-size: 16px;
+  font-weight: 600;
+  margin-bottom: 4px;
+}
+
+.placeholder-type {
+  font-size: 12px;
+  opacity: 0.8;
+}
+
+/* 添加加载动画效果 */
+.iframe-placeholder::before {
+  content: "";
+  position: absolute;
+  top: 0;
+  left: -100%;
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(
+    90deg,
+    transparent,
+    rgba(255, 255, 255, 0.4),
+    transparent
+  );
+  animation: shimmer 2s infinite;
+}
+
+@keyframes shimmer {
+  0% {
     left: -100%;
-    width: 100%;
-    height: 100%;
-    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
-    animation: shimmer 2s infinite;
   }
-  
-  @keyframes shimmer {
-    0% {
-      left: -100%;
-    }
-    100% {
-      left: 100%;
-    }
+  100% {
+    left: 100%;
   }
-  </style>
+}
+</style>

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

@@ -725,5 +725,8 @@
   "ssAnswer":"回答",
   "ssViewUnsubmittedStudents":"点击查看未提交学生",
   "ssSelectUser":"选择{a}的成员",
-  "ssUnsubmittedStudents":"未提交学生"
+  "ssUnsubmittedStudents":"未提交学生",
+  "ssAnalysis":"分析",
+  "ssAIGenerate":"AI生成",
+  "ssUpdateTime":"最后更新"
 }

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

@@ -725,5 +725,8 @@
   "ssAnswer":"Answer",
   "ssViewUnsubmittedStudents":"Click to view unsubmitted students",
   "ssSelectUser":"Select members of {a}",
-  "ssUnsubmittedStudents":"Unsubmitted students"
+  "ssUnsubmittedStudents":"Unsubmitted students",
+  "ssAnalysis":"Analysis",
+  "ssAIGenerate":"AI Generate",
+  "ssUpdateTime":"Last Updated"
 }

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

@@ -725,5 +725,8 @@
   "ssAnswer":"回答",
   "ssViewUnsubmittedStudents":"點擊查看未提交學生",
   "ssSelectUser":"選擇{a}的成員",
-  "ssUnsubmittedStudents":"未提交學生"
+  "ssUnsubmittedStudents":"未提交學生",
+  "ssAnalysis":"分析",
+  "ssAIGenerate":"AI生成",
+  "ssUpdateTime":"最後更新"
 }