| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- <!doctype html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>AI PPT Visual Test</title>
- <style>
- body { margin: 0; padding: 24px; background: #dfe4ea; font-family: "Microsoft Yahei", sans-serif; }
- h1 { margin: 0 0 20px; font-size: 24px; color: #172231; }
- .grid { display: grid; grid-template-columns: repeat(2, 720px); gap: 24px; }
- .slide { background: white; box-shadow: 0 8px 24px rgba(17, 34, 51, .16); }
- </style>
- </head>
- <body>
- <div id="app"></div>
- <script type="module">
- import { createApp, h } from "vue"
- import { createPinia, setActivePinia } from "pinia"
- import ThumbnailSlide from "/src/views/components/ThumbnailSlide/index.vue"
- import { aiCoursewareToSlides } from "/src/utils/aiCoursewareToSlides.ts"
- import { getAiCoursewareTemplate } from "/src/config/aiCoursewareTemplates.ts"
- import { hydrateSlideBackgroundImages } from "/src/libs/jsonTool.ts"
- const id = new URLSearchParams(location.search).get("template") || "ethnic-flower"
- const page = (pageNo, type, title, layout, points, extra = {}) => ({ pageNo, type, title, bullets: points, studentContent: { points }, imageKeywords: [], visualPlan: { layout }, ...extra })
- const outline = {
- title: "凤鸣山谷的故事", templateId: id, gradeName: "一年级", textbookName: "音乐一年级上册", unitName: "第一单元 奇妙的声音世界", chapterName: "凤鸣山谷的故事", courseType: "music", style: "cartoon",
- pages: [
- page(1, "cover", "凤鸣山谷的故事", "cover_scene", ["聆听自然的声音", "发现音乐的强弱变化"]),
- page(2, "scene_intro", "山谷里传来了什么声音?", "story_stage", ["观察画面", "模仿你听到的声音"], { interaction: { type: "question", question: "你觉得是谁在歌唱?", options: ["小鸟", "溪水", "风声"] } }),
- page(3, "objectives", "今天我们要学会", "goal_cards", ["分辨声音的高低", "感受音乐的强弱", "合作完成声音表演"]),
- page(4, "first_listen", "第一次完整聆听", "listen_wave", ["安静听完音乐", "用手势表示声音的远近", "记住最喜欢的声音"], { studentContent: { headline: "闭上眼睛,声音从哪里来?", points: ["安静听完音乐", "用手势表示声音的远近", "记住最喜欢的声音"] } }),
- page(5, "work_context", "走进凤鸣山谷", "split_image", ["清晨的山谷十分安静", "鸟鸣由远处慢慢靠近", "溪水一直轻轻流淌"]),
- page(6, "lyric_read", "读一读山谷歌谣", "lyric_focus", ["小鸟唱得清脆", "溪水流得轻柔", "山风吹得悠长"]),
- page(7, "melody_score", "旋律是怎样行走的?", "score_focus", ["向上表示声音变高", "向下表示声音变低", "平直表示保持稳定"]),
- page(8, "compare_listen", "两段声音有什么不同?", "compare_listen", ["比较速度", "比较力度"], { interaction: { type: "compare", question: "哪一段更像远处的鸟鸣?", options: ["第一段更轻", "第二段更强", "第一段更慢", "第二段更快"] } }),
- page(9, "performance_activity", "合作表演山谷音乐会", "performance_stage", ["保持自己的声部", "关注指挥手势", "结束时一起收声"], { activity: { task: "用声音演出山谷从清晨到热闹的变化", steps: ["分成鸟鸣、溪水、山风三组", "商量进入顺序", "完整表演并互相评价"] } }),
- page(10, "summary", "我的音乐收获", "summary_checklist", ["我能分辨声音的高低", "我能表现声音的强弱", "我能与同伴合作表演"], { learningEvidence: "用一种声音表现你心中的凤鸣山谷" })
- ]
- }
- const result = aiCoursewareToSlides(outline, { templateId: id })
- const app = createApp({
- render: () => h("main", [
- h("h1", `${getAiCoursewareTemplate(id).name} · 通用版式视觉回归`),
- h("section", { class: "grid" }, result.slides.map(slide => h("div", { class: "slide" }, [h(ThumbnailSlide, { slide, size: 720 })])))
- ])
- })
- const pinia = createPinia()
- setActivePinia(pinia)
- app.use(pinia)
- app.mount("#app")
- if (new URLSearchParams(location.search).get("export") === "1") {
- const status = document.createElement("p")
- status.id = "export-status"
- status.textContent = "正在导出"
- document.body.prepend(status)
- try {
- const workerSources = await Promise.all(["/pptworker/cos-js-sdk-v5.min.js", "/pptworker/upload.js", "/pptworker/pptJson.js"].map(async url => await (await fetch(url)).text()))
- const worker = new Worker(URL.createObjectURL(new Blob(workerSources)))
- let rpcId = 0
- const callbacks = new Map()
- let resolveWorkerReady
- const workerReady = new Promise(resolve => { resolveWorkerReady = resolve })
- let resolveExportDone
- let rejectExportDone
- const exportDone = new Promise((resolve, reject) => { resolveExportDone = resolve; rejectExportDone = reject })
- worker.addEventListener("message", event => {
- const data = event.data
- if (data?.type === "progress") {
- const progress = data.result || {}
- if (progress.status === "done" && Number(progress.progress) === 100) resolveExportDone(progress.extra)
- if (progress.status === "failed") rejectExportDone(new Error(progress.extra?.errorMsg || "PPT导出失败"))
- return
- }
- if (data?.type !== "RPC") return
- if (!data.id) {
- if (data.method === "ready") resolveWorkerReady()
- return
- }
- const callback = callbacks.get(data.id)
- if (!callback) return
- callbacks.delete(data.id)
- data.error ? callback.reject(new Error(data.error.message)) : callback.resolve(data.result)
- })
- const rpc = (method, ...params) => new Promise((resolve, reject) => {
- const id = ++rpcId
- callbacks.set(id, { resolve, reject })
- worker.postMessage({ type: "RPC", id, method, params })
- })
- await workerReady
- await rpc("createTaskController")
- const exportSlides = await hydrateSlideBackgroundImages(result.slides)
- const json = { width: 1920, height: 1080, slides: exportSlides, theme: { ...result.theme, viewportRatio: 0.5625 } }
- const jsonUrl = URL.createObjectURL(new Blob([JSON.stringify(json)], { type: "application/json" }))
- await rpc("addCourseWareTask", { type: "downloadCourseware", extra: { id: "ai-ppt-e2e", type: "pptx", outputName: "ai-ppt-e2e.pptx", name: "ai-ppt-e2e", jsonUrl } })
- const exported = await exportDone
- const sinkUrl = new URLSearchParams(location.search).get("sink")
- if (sinkUrl) {
- const response = await fetch(sinkUrl, { method: "POST", body: exported.buffer })
- if (!response.ok) throw new Error(`测试文件保存失败:HTTP ${response.status}`)
- }
- URL.revokeObjectURL(jsonUrl)
- const pptxUrl = URL.createObjectURL(new Blob([exported.buffer], { type: exported.contentType }))
- const link = document.createElement("a")
- link.href = pptxUrl
- link.download = exported.name
- if (!sinkUrl) link.click()
- status.textContent = `导出成功:${exported.buffer.byteLength} bytes`
- document.title = "EXPORT_DONE"
- await rpc("destoryTaskController")
- worker.terminate()
- } catch (error) {
- status.textContent = `导出失败:${error?.message || error}`
- document.title = "EXPORT_FAILED"
- }
- }
- </script>
- </body>
- </html>
|