Browse Source

feat: 实现AI课件生成功能,添加iframe弹窗和数据交互逻辑

lsc 1 day ago
parent
commit
1a1b4d9d42
2 changed files with 183 additions and 34 deletions
  1. 183 23
      src/components/CreateCourseDialog.vue
  2. 0 11
      src/views/Editor/index3.vue

+ 183 - 23
src/components/CreateCourseDialog.vue

@@ -40,7 +40,7 @@
           <h3>{{ lang.ssCreateBlank }}</h3>
           <p>{{ lang.ssStartFromScratch }}</p>
         </div>
-        <div class="option-card disabled">
+        <div class="option-card" @click="openAICreate">
           <div class="option-icon">
             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
               <path d="M12 2L2 7l10 5 10-5-10-5z" />
@@ -50,7 +50,6 @@
           </div>
           <h3>{{ lang.ssCreateFromAI }}</h3>
           <p>{{ lang.ssAIGenerateContent }}</p>
-          <div class="coming-soon">{{ lang.ssComingSoon }}</div>
         </div>
         <div class="option-card disabled">
           <div class="option-icon">
@@ -70,6 +69,28 @@
       </div>
     </div>
 
+    <!-- AI 课件生成 iframe 弹窗(全屏) -->
+    <div v-if="showAIIFrame" class="ai-frame-overlay" @mousedown.self="closeAIIFrame">
+      <div class="ai-frame-header">
+        <span class="ai-frame-title">{{ lang.ssCreateFromAI }}</span>
+        <button class="ai-frame-close" @click="closeAIIFrame">
+          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <line x1="18" y1="6" x2="6" y2="18" />
+            <line x1="6" y1="6" x2="18" y2="18" />
+          </svg>
+        </button>
+      </div>
+      <div class="ai-frame-wrap">
+        <iframe
+          ref="aiFrameRef"
+          :src="aiFrameSrc"
+          class="ai-frame"
+          frameborder="0"
+          allow="clipboard-read; clipboard-write"
+        ></iframe>
+      </div>
+    </div>
+
     <!-- 解析状态弹窗 -->
     <div v-if="exporting" class="parsing-modal">
       <div class="parsing-content">
@@ -95,7 +116,7 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, watch } from 'vue'
+import { ref, computed, onBeforeUnmount } from 'vue'
 import useImport from '@/hooks/useImport'
 import FileInput from '@/components/FileInput.vue'
 import message from '@/utils/message'
@@ -143,7 +164,7 @@ const handleFileUpload = async (files: FileList) => {
     // 调用importPPTXFile并传入signal
     await importPPTXFile(files, { signal, onclose: () => {
       emit('setTitle', file.name.replace(/\.[^/.]+$/, ''), 2) 
-       // showConfirmDialog({
+      // showConfirmDialog({
       //   title: lang.ssUploadSuccess,
       //   content: lang.ssBatchInteractiveWebConfirm,
       //   confirmText: lang.ssBatchInteractiveWebConfirmDesc,
@@ -202,34 +223,173 @@ const handleParsingClose = () => {
   }
 }
 
+interface CocostudioExportPayload {
+  subject: string; // 学科
+  grade: string; // 年级
+  title: string; // 课件标题
+  version: string; // 生成器版本
+  createdAt: string; // ISO 时间
+  pages: string[]; // 每项一页完整 HTML 文档,顺序即页面顺序
+}
+import useCreateElement from '@/hooks/useCreateElement'
+import useSlideHandler from '@/hooks/useSlideHandler'
 const { createFrameElement } = useCreateElement()
 const { createSlide } = useSlideHandler()
-const generateAICourse = (json: CocostudioExportPayload) => {
-  console.log(json)
-  setTitle2(json.title)
-  parentWindow.courseSettings?.(json.subject, json.grade)
-  for (const page of json.pages) {
-    createSlide()
-
-    if ( page.includes('45') && page.includes('workPage.html')) {
-      createFrameElement(page, 45)
-    }
-    else if ( page.includes('15') && page.includes('workPage.html')) {
-      createFrameElement(page, 15)
-    }
-    else if ( page.includes('78') && page.includes('workPage.html')) {
-      createFrameElement(page, 78)
-    }
-    else {
-      createFrameElement(page, 85)
+interface ParentWindowWithToolList extends Window {
+  courseSettings?: (subject: string, className: string) => void;
+}
+const parentWindow = window.parent as ParentWindowWithToolList
+
+// AI 课件生成 iframe 弹窗
+const showAIIFrame = ref(false)
+const aiFrameRef = ref<HTMLIFrameElement | null>(null)
+const aiFrameBase = 'https://newcocostudio.cocorobo'
+const aiFrameSrc = computed(() => {
+  // lang.lang: 'cn' | 'hk' | 'en',对应不同的顶级域名
+  const tld = lang.lang === 'en' ? 'com' : lang.lang === 'hk' ? 'hk' : 'cn'
+  return `${aiFrameBase}.${tld}/create/courseware`
+})
+
+
+const openAICreate = () => {
+  showAIIFrame.value = true
+}
+const closeAIIFrame = () => {
+  showAIIFrame.value = false
+}
+
+const generateAICourse = () => {
+  let json: CocostudioExportPayload | null = null
+  try {
+    const iw = aiFrameRef.value?.contentWindow as any
+    if (iw && iw.__COCOSTUDIO_EXPORT__) {
+      console.log('[CreateCourseDialog] 读取到 iframe 全局导出数据')
+      json = iw.__COCOSTUDIO_EXPORT__ as CocostudioExportPayload
+      // 读完清理,避免重复触发
+      try {
+        iw.__COCOSTUDIO_EXPORT__ = null
+      }
+      catch {
+        // ignore cross-origin write
+      }
+      // 先关 iframe(如果还开着),避免组件卸载后 emit 失败
+      emit('setTitle', json.title, 1) 
+      parentWindow.courseSettings?.(json.subject, json.grade)
+      for (const page of json.pages) {
+        createSlide()
+
+        if ( page.includes('45') && page.includes('workPage.html')) {
+          createFrameElement(page, 45)
+        }
+        else if ( page.includes('15') && page.includes('workPage.html')) {
+          createFrameElement(page, 15)
+        }
+        else if ( page.includes('78') && page.includes('workPage.html')) {
+          createFrameElement(page, 78)
+        }
+        else {
+          createFrameElement(page, 85)
+        }
+      }
+      showAIIFrame.value = false
+      emit('close')
     }
   }
+  catch (error) {
+    console.error('读取 iframe 导出数据失败:', error)
+    message.error(lang.ssFileParseFailedRetry)
+  }
 }
 
-Object.assign(window, { generateAICourse })
+// 暴露给 iframe 调用的全局函数(iframe 主动调用时用)
+Object.assign(window, {
+  generateAICourse,
+  // iframe 完成后主动调 parent.cocoStudioExport(payload) 回传
+  cocoStudioExport: (data: CocostudioExportPayload) => {
+    if (data && Array.isArray(data.pages) && data.pages.length > 0) {
+      generateAICourse(data)
+    }
+    else {
+      console.warn('[CreateCourseDialog] cocoStudioExport 收到无效数据', data)
+    }
+  },
+  // iframe 主动关闭
+  closeCocoStudioFrame: () => closeAIIFrame(),
+})
+
+// 组件卸载时清理全局函数(避免残留)
+onBeforeUnmount(() => {
+  const w = window as any
+  delete w.generateAICourse
+  delete w.__COCOSTUDIO_EXPORT__
+  delete w.cocoStudioExport
+  delete w.closeCocoStudioFrame
+})
 </script>
 
 <style lang="scss" scoped>
+.ai-frame-overlay {
+  position: fixed;
+  inset: 0;
+  z-index: 2000;
+  background: #fff;
+  display: flex;
+  flex-direction: column;
+
+  .ai-frame-header {
+    flex-shrink: 0;
+    height: 48px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 16px;
+    background: #fafafa;
+    border-bottom: 1px solid #eee;
+
+    .ai-frame-title {
+      font-size: 16px;
+      font-weight: 500;
+      color: #333;
+    }
+
+    .ai-frame-close {
+      width: 32px;
+      height: 32px;
+      border: none;
+      background: none;
+      cursor: pointer;
+      color: #666;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 4px;
+      transition: all 0.2s;
+
+      &:hover {
+        background: #f0f0f0;
+        color: #333;
+      }
+
+      svg {
+        width: 18px;
+        height: 18px;
+      }
+    }
+  }
+
+  .ai-frame-wrap {
+    flex: 1;
+    min-height: 0;
+
+    .ai-frame {
+      width: 100%;
+      height: 100%;
+      border: none;
+      display: block;
+    }
+  }
+}
+
 .create-course-dialog {
   width: 100%;
   max-width: 800px;

+ 0 - 11
src/views/Editor/index3.vue

@@ -442,17 +442,6 @@ const getPPTSlide = () => {
   return JSON.stringify(slidesStore.slides)
 }
 
-interface CocostudioExportPayload {
-  subject: string; // 学科
-  grade: string; // 年级
-  title: string; // 课件标题
-  version: string; // 生成器版本
-  createdAt: string; // ISO 时间
-  pages: string[]; // 每项一页完整 HTML 文档,顺序即页面顺序
-}
-import useCreateElement from '@/hooks/useCreateElement'
-import useSlideHandler from '@/hooks/useSlideHandler'
-
 Object.assign(window, { getCourseDetail, setTitle, setPPTSlide, getPPTSlide })
 
 onMounted(() => {