|
|
@@ -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;
|