yonge пре 1 недеља
родитељ
комит
c732207f5c

+ 1 - 1
dist/assets/index-CGErALTN.js → dist/assets/index-B63Vs1kC.js

@@ -1 +1 @@
-import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,en as t,ae as r,af as c,bM as _,bN as d,ak as s,bH as p}from"./index-DauVgfvu.js";const i=e=>(_("data-v-dda25179"),e=e(),d(),e),n={class:"errorPage"},m=i(()=>s("div",{class:"error"},[s("img",{src:a,class:"img",alt:""}),s("div",{class:"tit"},"页面找不到了~")],-1)),l=[m],f=o({__name:"errorPage",setup(e){return t(),(u,h)=>(r(),c("div",n,l))}}),I=p(f,[["__scopeId","data-v-dda25179"]]);export{I as default};
+import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,en as t,ae as r,af as c,bM as _,bN as d,ak as s,bH as p}from"./index-Roqv-ag3.js";const i=e=>(_("data-v-dda25179"),e=e(),d(),e),n={class:"errorPage"},m=i(()=>s("div",{class:"error"},[s("img",{src:a,class:"img",alt:""}),s("div",{class:"tit"},"页面找不到了~")],-1)),l=[m],f=o({__name:"errorPage",setup(e){return t(),(u,h)=>(r(),c("div",n,l))}}),I=p(f,[["__scopeId","data-v-dda25179"]]);export{I as default};

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
dist/assets/index-BXrTBHKe.css


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
dist/assets/index-BlldfNpE.css


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
dist/assets/index-Bq0KUkHP.js


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
dist/assets/index-BvtkywZ4.js


+ 1 - 1
dist/assets/index-_zT24kbe.js → dist/assets/index-CmiJx1j9.js

@@ -1 +1 @@
-import{u as s,af as r}from"./index-Bj86IcgK.js";import{a5 as c,cB as n,R as p,ae as t,af as _,al as u,ag as i,bH as f}from"./index-DauVgfvu.js";const l={class:"pptScreen"},m=c({__name:"pptScreen",setup(d){const a=n(),e=p(!0),o=s();return a.setScreenMode("pptScreen"),o.initPPTData().then(()=>{e.value=!1}),(S,k)=>(t(),_("div",l,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),P=f(m,[["__scopeId","data-v-678905e9"]]);export{P as default};
+import{u as s,af as r}from"./index-IymdfAbv.js";import{a5 as c,cB as n,R as p,ae as t,af as _,al as u,ag as i,bH as f}from"./index-Roqv-ag3.js";const l={class:"pptScreen"},m=c({__name:"pptScreen",setup(d){const a=n(),e=p(!0),o=s();return a.setScreenMode("pptScreen"),o.initPPTData().then(()=>{e.value=!1}),(S,k)=>(t(),_("div",l,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),P=f(m,[["__scopeId","data-v-678905e9"]]);export{P as default};

+ 1 - 1
dist/assets/index-m83BKUnd.js → dist/assets/index-Css4rIox.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-BDhv7eXx.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,aS as p,bM as m,bN as f,bH as u}from"./index-DauVgfvu.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),h={class:"login"},b={class:"error"},g=s(()=>o("img",{src:t,class:"img",alt:""},null,-1)),k=s(()=>o("div",{class:"tit"},"登录已过期或服务器错误!",-1)),v=n({__name:"login",setup(a){function e(){window.close()}return(x,B)=>(i(),r("div",h,[o("div",b,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),S=u(v,[["__scopeId","data-v-340bbdaa"]]);export{S as default};
+import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-Bq0KUkHP.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,aS as p,bM as m,bN as f,bH as u}from"./index-Roqv-ag3.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),h={class:"login"},b={class:"error"},g=s(()=>o("img",{src:t,class:"img",alt:""},null,-1)),k=s(()=>o("div",{class:"tit"},"登录已过期或服务器错误!",-1)),v=n({__name:"login",setup(a){function e(){window.close()}return(x,B)=>(i(),r("div",h,[o("div",b,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),S=u(v,[["__scopeId","data-v-340bbdaa"]]);export{S as default};

+ 1 - 1
dist/assets/index-CtdFWWSD.js → dist/assets/index-D883sfse.js

@@ -1 +1 @@
-import{u as c,af as l}from"./index-Bj86IcgK.js";import{w as p,bA as d,a5 as g,cB as m,R as t,ae as r,af as u,al as f,ag as w,bH as k}from"./index-DauVgfvu.js";import"./shapes-JTioW6s2.js";const S=()=>{function o(a){if(window.parent===window)return;const{api:e,playState:n}=a.data||{};e==="headerTogge"?window.parent.postMessage({api:"headerTogge",playState:n},"*"):e==="changeTogge"?window.parent.postMessage({api:"changeTogge"},"*"):["clickTempo","clickViewFigner"].includes(e)&&window.parent.postMessage({api:e},"*")}p(()=>{window.addEventListener("message",o)}),d(()=>{window.removeEventListener("message",o)})},_=g({__name:"mobileScreen",setup(o){S();const a=m(),e=t(!0),n=t(!1),i=c();return a.setScreenMode("mobileScreen"),i.initPPTData().then(()=>{e.value=!1}),(v,s)=>(r(),u("div",{class:"mobileScreen",onClick:s[0]||(s[0]=b=>n.value=!1)},[e.value?w("",!0):(r(),f(l,{key:0}))]))}}),B=k(_,[["__scopeId","data-v-90b2692a"]]);export{B as default};
+import{u as c,af as l}from"./index-IymdfAbv.js";import{w as p,bA as d,a5 as g,cB as m,R as t,ae as r,af as u,al as f,ag as w,bH as k}from"./index-Roqv-ag3.js";import"./shapes-DyqWWBUj.js";const S=()=>{function o(a){if(window.parent===window)return;const{api:e,playState:n}=a.data||{};e==="headerTogge"?window.parent.postMessage({api:"headerTogge",playState:n},"*"):e==="changeTogge"?window.parent.postMessage({api:"changeTogge"},"*"):["clickTempo","clickViewFigner"].includes(e)&&window.parent.postMessage({api:e},"*")}p(()=>{window.addEventListener("message",o)}),d(()=>{window.removeEventListener("message",o)})},_=g({__name:"mobileScreen",setup(o){S();const a=m(),e=t(!0),n=t(!1),i=c();return a.setScreenMode("mobileScreen"),i.initPPTData().then(()=>{e.value=!1}),(v,s)=>(r(),u("div",{class:"mobileScreen",onClick:s[0]||(s[0]=b=>n.value=!1)},[e.value?w("",!0):(r(),f(l,{key:0}))]))}}),B=k(_,[["__scopeId","data-v-90b2692a"]]);export{B as default};

Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
dist/assets/index-IymdfAbv.js


Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
dist/assets/index-Roqv-ag3.js


+ 1 - 1
dist/assets/shapes-JTioW6s2.js → dist/assets/shapes-DyqWWBUj.js

@@ -1,4 +1,4 @@
-import{cE as k,bB as $1}from"./index-DauVgfvu.js";import{ag as Z,ah as H1,ai as U,aj as n0,ak as c0,al as i0,am as j1,an as K1,ao as Y1,ap as R}from"./index-Bj86IcgK.js";var z1={exports:{}};/*!
+import{cE as k,bB as $1}from"./index-Roqv-ag3.js";import{ag as Z,ah as H1,ai as U,aj as n0,ak as c0,al as i0,am as j1,an as K1,ao as Y1,ap as R}from"./index-IymdfAbv.js";var z1={exports:{}};/*!
  * clipboard.js v2.0.11
  * clipboard.js v2.0.11
  * https://clipboardjs.com/
  * https://clipboardjs.com/
  *
  *

+ 1 - 1
dist/index.html

@@ -10,7 +10,7 @@
     <meta name="description" content="ppt编辑器" />
     <meta name="description" content="ppt编辑器" />
     <meta name="keywords" content="ppt,powerpoint,office powerpoint,在线ppt,幻灯片,演示文稿,ppt在线制作,Vue3,TypeScript" />
     <meta name="keywords" content="ppt,powerpoint,office powerpoint,在线ppt,幻灯片,演示文稿,ppt在线制作,Vue3,TypeScript" />
     <title>PPT</title>
     <title>PPT</title>
-    <script type="module" crossorigin src="./assets/index-DauVgfvu.js"></script>
+    <script type="module" crossorigin src="./assets/index-Roqv-ag3.js"></script>
     <link rel="stylesheet" crossorigin href="./assets/index-AavUEaYy.css">
     <link rel="stylesheet" crossorigin href="./assets/index-AavUEaYy.css">
   </head>
   </head>
   <body>
   <body>

+ 1 - 0
src/api/aiCourseware.ts

@@ -10,6 +10,7 @@ export interface AiCoursewareGenerateQuery {
   id: string
   id: string
   fromType?: string
   fromType?: string
   outline: AiCoursewareOutline
   outline: AiCoursewareOutline
+  visualPrompt?: string
 }
 }
 
 
 export interface AiCoursewareOutline {
 export interface AiCoursewareOutline {

+ 160 - 46
src/utils/aiCoursewareToSlides.ts

@@ -1,8 +1,16 @@
 import { nanoid } from "nanoid"
 import { nanoid } from "nanoid"
-import type { Slide, PPTElement, SlideTheme, PPTTextElement, PPTImageElement } from "@/types/slides"
+import type { Slide, PPTElement, SlideTheme, PPTTextElement, PPTImageElement, PPTShapeElement, SlideBackground } from "@/types/slides"
 import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware"
 import type { AiCoursewareOutline, AiCoursewarePage } from "@/api/aiCourseware"
 
 
-export function aiCoursewareToSlides(outline: AiCoursewareOutline): { title: string; theme: Partial<SlideTheme>; slides: Slide[] } {
+interface AiCoursewareVisualOptions {
+  backgroundImage?: string
+}
+
+const SLIDE_WIDTH = 1600
+const SLIDE_HEIGHT = 900
+const FONT_NAME = "Microsoft Yahei"
+
+export function aiCoursewareToSlides(outline: AiCoursewareOutline, options: AiCoursewareVisualOptions = {}): { title: string; theme: Partial<SlideTheme>; slides: Slide[] } {
   const primary = outline.style === "clean" ? "#2D6CDF" : "#FF8A3D"
   const primary = outline.style === "clean" ? "#2D6CDF" : "#FF8A3D"
   const background = outline.style === "clean" ? "#F7F9FC" : "#FFF8EF"
   const background = outline.style === "clean" ? "#F7F9FC" : "#FFF8EF"
   const fontColor = "#263238"
   const fontColor = "#263238"
@@ -10,29 +18,55 @@ export function aiCoursewareToSlides(outline: AiCoursewareOutline): { title: str
     themeColor: primary,
     themeColor: primary,
     backgroundColor: background,
     backgroundColor: background,
     fontColor,
     fontColor,
-    fontName: "Microsoft Yahei",
+    fontName: FONT_NAME,
     fontSize: "36px"
     fontSize: "36px"
   }
   }
 
 
   return {
   return {
     title: outline.title || outline.chapterName || "AI音乐课件",
     title: outline.title || outline.chapterName || "AI音乐课件",
     theme,
     theme,
-    slides: outline.pages.map(page => buildSlide(page, outline, primary, background, fontColor))
+    slides: outline.pages.map(page => buildSlide(page, outline, primary, background, fontColor, options))
   }
   }
 }
 }
 
 
-function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, primary: string, background: string, fontColor: string): Slide {
+function buildSlide(
+  page: AiCoursewarePage,
+  outline: AiCoursewareOutline,
+  primary: string,
+  background: string,
+  fontColor: string,
+  options: AiCoursewareVisualOptions
+): Slide {
   const isCover = page.type === "cover" || page.pageNo === 1
   const isCover = page.type === "cover" || page.pageNo === 1
+  const hasCustomBackground = !!options.backgroundImage
   const elements: PPTElement[] = []
   const elements: PPTElement[] = []
+  const visual = visualConfig(page, primary, outline.style)
+  const backgroundConfig: SlideBackground = hasCustomBackground
+    ? {
+        type: "image",
+        image: options.backgroundImage,
+        imageSize: "cover"
+      }
+    : {
+        type: "solid",
+        color: background
+      }
+
+  if (hasCustomBackground) {
+    elements.push(shapeElement({ left: 0, top: 0, width: SLIDE_WIDTH, height: SLIDE_HEIGHT, fill: "rgba(255, 255, 255, 0.74)", opacity: 0.92 }))
+  }
+
+  elements.push(shapeElement({ left: 0, top: 0, width: 18, height: SLIDE_HEIGHT, fill: primary, opacity: 0.95 }))
+  elements.push(shapeElement({ left: 86, top: isCover ? 142 : 54, width: 70, height: 8, fill: primary, opacity: 0.9 }))
 
 
   elements.push(
   elements.push(
     textElement({
     textElement({
-      left: 110,
-      top: isCover ? 130 : 70,
-      width: isCover ? 980 : 1040,
-      height: isCover ? 160 : 82,
+      left: 100,
+      top: isCover ? 172 : 72,
+      width: isCover ? 850 : 980,
+      height: isCover ? 98 : 64,
       content: page.title || outline.title,
       content: page.title || outline.title,
-      fontSize: isCover ? 58 : 38,
+      fontSize: isCover ? 48 : 34,
       color: primary,
       color: primary,
       bold: true
       bold: true
     })
     })
@@ -42,12 +76,12 @@ function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, primar
   if (subtitle) {
   if (subtitle) {
     elements.push(
     elements.push(
       textElement({
       textElement({
-        left: 116,
-        top: isCover ? 312 : 150,
-        width: 920,
-        height: 48,
+        left: 104,
+        top: isCover ? 288 : 132,
+        width: 840,
+        height: 40,
         content: subtitle,
         content: subtitle,
-        fontSize: isCover ? 28 : 20,
+        fontSize: isCover ? 24 : 18,
         color: fontColor
         color: fontColor
       })
       })
     )
     )
@@ -56,56 +90,68 @@ function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, primar
   if (page.image?.url) {
   if (page.image?.url) {
     elements.push(
     elements.push(
       imageElement({
       imageElement({
-        left: isCover ? 1080 : 1180,
-        top: isCover ? 150 : 150,
-        width: isCover ? 650 : 590,
-        height: isCover ? 620 : 480,
+        left: isCover ? 980 : 1040,
+        top: isCover ? 138 : 150,
+        width: isCover ? 480 : 430,
+        height: isCover ? 520 : 360,
         src: page.image.url
         src: page.image.url
       })
       })
     )
     )
+  } else {
+    elements.push(...fallbackVisualElements(page, visual, isCover))
   }
   }
 
 
-  const bulletTop = isCover ? 430 : 250
-  const bulletWidth = page.image?.url ? 900 : 1320
+  const bulletTop = isCover ? 410 : 230
+  const bulletWidth = page.image?.url ? 820 : 960
   const bullets = normalizeBullets(page)
   const bullets = normalizeBullets(page)
   elements.push(
   elements.push(
+    shapeElement({
+      left: 92,
+      top: bulletTop - 20,
+      width: bulletWidth + 38,
+      height: Math.min(330, 78 + bullets.length * 58),
+      fill: hasCustomBackground ? "rgba(255, 255, 255, 0.82)" : "#FFFFFF",
+      opacity: 0.96
+    })
+  )
+  elements.push(
     textElement({
     textElement({
       left: 120,
       left: 120,
       top: bulletTop,
       top: bulletTop,
       width: bulletWidth,
       width: bulletWidth,
-      height: Math.min(430, 72 + bullets.length * 58),
-      content: bullets.map(item => `• ${escapeHtml(item)}`).join("<br>"),
-      fontSize: isCover ? 30 : 28,
+      height: Math.min(290, 66 + bullets.length * 50),
+      content: bullets.map(item => `<div style="margin-bottom:12px;">${visual.bulletMark} ${escapeHtml(item)}</div>`).join(""),
+      fontSize: isCover ? 26 : 24,
       color: fontColor,
       color: fontColor,
-      lineHeight: 1.45
+      lineHeight: 1.36
     })
     })
   )
   )
 
 
   if (page.interaction?.question) {
   if (page.interaction?.question) {
+    elements.push(shapeElement({ left: 104, top: 704, width: 1090, height: 88, fill: primary, opacity: 0.92 }))
     elements.push(
     elements.push(
       textElement({
       textElement({
-        left: 120,
-        top: 760,
-        width: 1200,
-        height: 105,
+        left: 134,
+        top: 722,
+        width: 1020,
+        height: 56,
         content: `互动:${escapeHtml(page.interaction.question)}`,
         content: `互动:${escapeHtml(page.interaction.question)}`,
-        fontSize: 28,
+        fontSize: 24,
         color: "#FFFFFF",
         color: "#FFFFFF",
-        fill: primary,
         lineHeight: 1.35
         lineHeight: 1.35
       })
       })
     )
     )
   } else if (page.teacherTips) {
   } else if (page.teacherTips) {
+    elements.push(shapeElement({ left: 104, top: 724, width: 1090, height: 62, fill: hasCustomBackground ? "rgba(255, 255, 255, 0.78)" : "#FFFFFF", opacity: 0.94 }))
     elements.push(
     elements.push(
       textElement({
       textElement({
-        left: 120,
-        top: 780,
-        width: 1200,
-        height: 86,
+        left: 130,
+        top: 742,
+        width: 1030,
+        height: 34,
         content: `提示:${escapeHtml(page.teacherTips)}`,
         content: `提示:${escapeHtml(page.teacherTips)}`,
-        fontSize: 22,
+        fontSize: 18,
         color: "#52616B",
         color: "#52616B",
-        fill: "#FFFFFF",
         lineHeight: 1.35
         lineHeight: 1.35
       })
       })
     )
     )
@@ -113,8 +159,8 @@ function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, primar
 
 
   elements.push(
   elements.push(
     textElement({
     textElement({
-      left: 1700,
-      top: 995,
+      left: 1450,
+      top: 838,
       width: 120,
       width: 120,
       height: 36,
       height: 36,
       content: `${page.pageNo || ""}/15`,
       content: `${page.pageNo || ""}/15`,
@@ -126,10 +172,7 @@ function buildSlide(page: AiCoursewarePage, outline: AiCoursewareOutline, primar
   return {
   return {
     id: nanoid(10),
     id: nanoid(10),
     elements,
     elements,
-    background: {
-      type: "solid",
-      color: background
-    },
+    background: backgroundConfig,
     remark: page.teacherTips || ""
     remark: page.teacherTips || ""
   }
   }
 }
 }
@@ -160,8 +203,8 @@ function textElement(params: {
     width: params.width,
     width: params.width,
     height: params.height,
     height: params.height,
     rotate: 0,
     rotate: 0,
-    content: `<div style="font-size:${params.fontSize}px;${params.bold ? "font-weight:700;" : ""}">${params.content}</div>`,
-    defaultFontName: "Microsoft Yahei",
+    content: `<div style="font-family:${FONT_NAME};font-size:${params.fontSize}px;${params.bold ? "font-weight:700;" : ""}">${params.content}</div>`,
+    defaultFontName: FONT_NAME,
     defaultColor: params.color,
     defaultColor: params.color,
     fill: params.fill,
     fill: params.fill,
     lineHeight: params.lineHeight || 1.25,
     lineHeight: params.lineHeight || 1.25,
@@ -181,7 +224,7 @@ function imageElement(params: { left: number; top: number; width: number; height
     rotate: 0,
     rotate: 0,
     src: params.src,
     src: params.src,
     fixedRatio: false,
     fixedRatio: false,
-    radius: 12,
+    radius: 8,
     shadow: {
     shadow: {
       h: 0,
       h: 0,
       v: 8,
       v: 8,
@@ -191,6 +234,77 @@ function imageElement(params: { left: number; top: number; width: number; height
   }
   }
 }
 }
 
 
+function shapeElement(params: {
+  left: number
+  top: number
+  width: number
+  height: number
+  fill: string
+  opacity?: number
+  radius?: number
+}): PPTShapeElement {
+  return {
+    id: nanoid(10),
+    type: "shape",
+    left: params.left,
+    top: params.top,
+    width: params.width,
+    height: params.height,
+    rotate: 0,
+    viewBox: [200, 200],
+    path: roundedRectPath(params.radius || 18),
+    fixedRatio: false,
+    fill: params.fill,
+    opacity: params.opacity ?? 1
+  }
+}
+
+function fallbackVisualElements(page: AiCoursewarePage, visual: ReturnType<typeof visualConfig>, isCover: boolean): PPTElement[] {
+  const left = isCover ? 1010 : 1080
+  const top = isCover ? 168 : 180
+  const size = isCover ? 360 : 280
+  return [
+    shapeElement({ left, top, width: size, height: size, fill: visual.light, opacity: 0.94, radius: 80 }),
+    shapeElement({ left: left + 34, top: top + 34, width: size - 68, height: size - 68, fill: "#FFFFFF", opacity: 0.9, radius: 70 }),
+    textElement({
+      left: left + 64,
+      top: top + (isCover ? 112 : 84),
+      width: size - 128,
+      height: 100,
+      content: visual.icon,
+      fontSize: isCover ? 76 : 62,
+      color: visual.color,
+      bold: true
+    }),
+    textElement({
+      left: left + 54,
+      top: top + (isCover ? 230 : 186),
+      width: size - 108,
+      height: 44,
+      content: visual.label,
+      fontSize: isCover ? 24 : 20,
+      color: visual.color,
+      bold: true
+    })
+  ]
+}
+
+function visualConfig(page: AiCoursewarePage, primary: string, style: string) {
+  const type = page.type || ""
+  const cartoonLight = style === "clean" ? "#E8F1FF" : "#FFF0D9"
+  if (type.includes("interaction") || type.includes("activity")) return { icon: "?", label: "课堂互动", color: "#1E88E5", light: "#EAF4FF", bulletMark: "◆" }
+  if (type.includes("summary")) return { icon: "✓", label: "课堂小结", color: "#2E7D32", light: "#EAF7EC", bulletMark: "✓" }
+  if (type.includes("goal")) return { icon: "◎", label: "学习目标", color: "#7B1FA2", light: "#F3E8FF", bulletMark: "◎" }
+  if (type.includes("listen")) return { icon: "♪", label: "听一听", color: "#00897B", light: "#E5F6F3", bulletMark: "♪" }
+  if (type.includes("homework")) return { icon: "✎", label: "课后练习", color: "#6D4C41", light: "#F3ECE8", bulletMark: "•" }
+  return { icon: "♫", label: "音乐课堂", color: primary, light: cartoonLight, bulletMark: "•" }
+}
+
+function roundedRectPath(radius: number) {
+  const r = Math.max(0, Math.min(radius, 100))
+  return `M ${r} 0 L ${200 - r} 0 Q 200 0 200 ${r} L 200 ${200 - r} Q 200 200 ${200 - r} 200 L ${r} 200 Q 0 200 0 ${200 - r} L 0 ${r} Q 0 0 ${r} 0 Z`
+}
+
 function escapeHtml(value: string) {
 function escapeHtml(value: string) {
   return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
   return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
 }
 }

+ 168 - 20
src/views/Editor/EditorHeader/AiCoursewareDialog.vue

@@ -1,7 +1,7 @@
 <template>
 <template>
   <Modal
   <Modal
     :visible="visible"
     :visible="visible"
-    :width="880"
+    :width="980"
     :close-button="true"
     :close-button="true"
     :close-on-click-mask="!working"
     :close-on-click-mask="!working"
     @update:visible="value => emit('update:visible', value)"
     @update:visible="value => emit('update:visible', value)"
@@ -12,7 +12,16 @@
           <div class="title">AI生成课件</div>
           <div class="title">AI生成课件</div>
           <div class="sub">根据当前教材、单元和章节生成15页音乐课件</div>
           <div class="sub">根据当前教材、单元和章节生成15页音乐课件</div>
         </div>
         </div>
-        <div v-if="taskInfo" class="progress">{{ taskInfo.progress || 0 }}%</div>
+      </div>
+
+      <div v-if="taskInfo" class="progress-panel">
+        <div class="progress-head">
+          <div class="progress-title">{{ taskInfo.message || "正在处理" }}</div>
+          <div class="progress-value">{{ taskInfo.progress || 0 }}%</div>
+        </div>
+        <div class="progress-track">
+          <div class="progress-bar" :style="{ width: `${taskInfo.progress || 0}%` }"></div>
+        </div>
       </div>
       </div>
 
 
       <div class="extra">
       <div class="extra">
@@ -20,18 +29,34 @@
         <TextArea v-model:value="extraPrompt" :rows="2" placeholder="可选,例如:多一些节奏互动,语言更适合小学低年级" :disabled="working" />
         <TextArea v-model:value="extraPrompt" :rows="2" placeholder="可选,例如:多一些节奏互动,语言更适合小学低年级" :disabled="working" />
       </div>
       </div>
 
 
+      <div class="background-setting">
+        <div>
+          <div class="label">课件背景图(可选)</div>
+          <div class="hint">建议 16:9,推荐 1920×1080 或 1600×900。上传后会应用到每一页。</div>
+        </div>
+        <div class="background-actions">
+          <FileInput @change="handleBackgroundUpload">
+            <button class="upload-btn" :disabled="working || uploadingBackground" type="button">
+              {{ uploadingBackground ? "上传中..." : backgroundImageUrl ? "更换背景图" : "上传背景图" }}
+            </button>
+          </FileInput>
+          <button v-if="backgroundImageUrl" class="remove-btn" :disabled="working" type="button" @click="removeBackground">移除</button>
+        </div>
+        <div v-if="backgroundImageUrl" class="background-preview" :style="{ backgroundImage: `url(${backgroundImageUrl})` }"></div>
+      </div>
+
       <div v-if="outline" class="outline">
       <div v-if="outline" class="outline">
         <div class="outline-head">
         <div class="outline-head">
           <div>
           <div>
             <div class="outline-title">{{ outline.title }}</div>
             <div class="outline-title">{{ outline.title }}</div>
-            <div class="outline-meta">{{ outline.gradeName }} · {{ outline.unitName }} · {{ outline.courseType }}</div>
+            <div class="outline-meta">{{ outlineMeta }}</div>
           </div>
           </div>
           <div class="count">{{ outline.pages.length }}页</div>
           <div class="count">{{ outline.pages.length }}页</div>
         </div>
         </div>
 
 
         <div class="page-list">
         <div class="page-list">
           <div v-for="page in outline.pages" :key="page.pageNo" class="page-item">
           <div v-for="page in outline.pages" :key="page.pageNo" class="page-item">
-            <div class="page-no">{{ page.pageNo }}</div>
+            <div class="page-no">{{ page.pageNo }}</div>
             <div class="page-content">
             <div class="page-content">
               <input v-model="page.title" class="page-title" :disabled="working" />
               <input v-model="page.title" class="page-title" :disabled="working" />
               <textarea
               <textarea
@@ -47,8 +72,6 @@
 
 
       <div v-else class="empty">点击生成大纲后,可在这里确认和微调15页结构。</div>
       <div v-else class="empty">点击生成大纲后,可在这里确认和微调15页结构。</div>
 
 
-      <div v-if="taskInfo" class="task">{{ taskInfo.message }}</div>
-
       <div class="footer">
       <div class="footer">
         <Button @click="emit('update:visible', false)" :disabled="working">取消</Button>
         <Button @click="emit('update:visible', false)" :disabled="working">取消</Button>
         <Button type="primary" @click="handleOutline" :disabled="working">{{ outline ? "重新生成大纲" : "生成大纲" }}</Button>
         <Button type="primary" @click="handleOutline" :disabled="working">{{ outline ? "重新生成大纲" : "生成大纲" }}</Button>
@@ -64,11 +87,13 @@ import { ElMessage, ElMessageBox } from "element-plus"
 import Modal from "@/components/Modal.vue"
 import Modal from "@/components/Modal.vue"
 import Button from "@/components/Button.vue"
 import Button from "@/components/Button.vue"
 import TextArea from "@/components/TextArea.vue"
 import TextArea from "@/components/TextArea.vue"
+import FileInput from "@/components/FileInput.vue"
 import queryParams from "@/queryParams"
 import queryParams from "@/queryParams"
 import usePptWork from "@/store/pptWork"
 import usePptWork from "@/store/pptWork"
 import { useSlidesStore } from "@/store"
 import { useSlidesStore } from "@/store"
 import useHistorySnapshot from "@/hooks/useHistorySnapshot"
 import useHistorySnapshot from "@/hooks/useHistorySnapshot"
 import { httpAjaxErrMsg } from "@/plugins/httpAjax"
 import { httpAjaxErrMsg } from "@/plugins/httpAjax"
+import fileUpload from "@/utils/oss-file-upload"
 import {
 import {
   aiCoursewareGenerateApi,
   aiCoursewareGenerateApi,
   aiCoursewareOutlineApi,
   aiCoursewareOutlineApi,
@@ -97,7 +122,13 @@ const extraPrompt = ref("")
 const outline = ref<AiCoursewareOutline>()
 const outline = ref<AiCoursewareOutline>()
 const taskInfo = ref<AiCoursewareTaskInfo>()
 const taskInfo = ref<AiCoursewareTaskInfo>()
 const pollTimer = ref<number>()
 const pollTimer = ref<number>()
+const backgroundImageUrl = ref("")
+const uploadingBackground = ref(false)
 const working = computed(() => taskInfo.value?.status === "PENDING" || taskInfo.value?.status === "RUNNING")
 const working = computed(() => taskInfo.value?.status === "PENDING" || taskInfo.value?.status === "RUNNING")
+const outlineMeta = computed(() => {
+  if (!outline.value) return ""
+  return [outline.value.gradeName, outline.value.unitName].filter(Boolean).join(" · ")
+})
 
 
 watch(
 watch(
   () => props.visible,
   () => props.visible,
@@ -115,7 +146,7 @@ async function handleOutline() {
   const res = await httpAjaxErrMsg(aiCoursewareOutlineApi, {
   const res = await httpAjaxErrMsg(aiCoursewareOutlineApi, {
     id: pptWork.id,
     id: pptWork.id,
     fromType: queryParams.pptResourcesType || queryParams.fromType,
     fromType: queryParams.pptResourcesType || queryParams.fromType,
-    extraPrompt: extraPrompt.value
+    extraPrompt: buildExtraPrompt()
   })
   })
   taskInfo.value = undefined
   taskInfo.value = undefined
   if (res.code === 200) {
   if (res.code === 200) {
@@ -137,7 +168,8 @@ async function handleGenerate() {
   const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
   const res = await httpAjaxErrMsg(aiCoursewareGenerateApi, {
     id: pptWork.id,
     id: pptWork.id,
     fromType: queryParams.pptResourcesType || queryParams.fromType,
     fromType: queryParams.pptResourcesType || queryParams.fromType,
-    outline: outline.value
+    outline: outline.value,
+    visualPrompt: buildVisualPrompt()
   })
   })
   if (res.code !== 200) return
   if (res.code !== 200) return
 
 
@@ -184,7 +216,7 @@ async function applyResult(taskId: string) {
   if (res.code !== 200) return
   if (res.code !== 200) return
 
 
   const result = res.data as AiCoursewareGenerateResult
   const result = res.data as AiCoursewareGenerateResult
-  const converted = aiCoursewareToSlides(result.outline)
+  const converted = aiCoursewareToSlides(result.outline, { backgroundImage: backgroundImageUrl.value })
   slidesStore.setTitle(converted.title)
   slidesStore.setTitle(converted.title)
   slidesStore.setTheme(converted.theme)
   slidesStore.setTheme(converted.theme)
   slidesStore.setSlides(converted.slides)
   slidesStore.setSlides(converted.slides)
@@ -200,11 +232,50 @@ function updateBullets(page: AiCoursewarePage, event: Event) {
     .map(item => item.trim())
     .map(item => item.trim())
     .filter(Boolean)
     .filter(Boolean)
 }
 }
+
+async function handleBackgroundUpload(files: FileList) {
+  if (working.value || uploadingBackground.value) return
+  const file = files[0]
+  if (!file) return
+  if (!file.type.startsWith("image/")) {
+    ElMessage.error("请上传图片文件")
+    return
+  }
+  if (!pptWork.id) {
+    ElMessage.error("当前课件ID为空")
+    return
+  }
+  uploadingBackground.value = true
+  try {
+    backgroundImageUrl.value = await fileUpload(file.name, file, `${pptWork.id}/ai-background/`, true, { isLoading: true, text: "背景图上传中" })
+  } catch {
+    ElMessage.error("背景图上传失败")
+  } finally {
+    uploadingBackground.value = false
+  }
+}
+
+function removeBackground() {
+  backgroundImageUrl.value = ""
+}
+
+function buildExtraPrompt() {
+  const prompts = [extraPrompt.value]
+  if (backgroundImageUrl.value) {
+    prompts.push(buildVisualPrompt())
+  }
+  return prompts.filter(Boolean).join("\n")
+}
+
+function buildVisualPrompt() {
+  if (!backgroundImageUrl.value) return ""
+  return "用户将使用自定义16:9背景图作为每页底图。请生成适合叠加在背景图上的课件内容:每页文字更精简,标题清楚,保留留白,强调高对比和清晰层次,避免生成需要大面积纯色背景才能阅读的密集内容。"
+}
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .ai-dialog {
 .ai-dialog {
-  height: 76vh;
+  height: 82vh;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
 }
 }
@@ -222,18 +293,91 @@ function updateBullets(page: AiCoursewarePage, event: Event) {
 }
 }
 .sub,
 .sub,
 .outline-meta,
 .outline-meta,
-.task {
+.hint {
   margin-top: 6px;
   margin-top: 6px;
   color: #7b8794;
   color: #7b8794;
   font-size: 13px;
   font-size: 13px;
 }
 }
-.progress {
+.progress-panel {
+  margin-top: 18px;
+  padding: 14px 16px;
+  border: 1px solid rgba($color: $themeColor, $alpha: 0.2);
+  background: rgba($color: $themeColor, $alpha: 0.06);
+  border-radius: 8px;
+}
+.progress-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 10px;
+}
+.progress-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1f2933;
+}
+.progress-value {
+  font-size: 14px;
   color: $themeColor;
   color: $themeColor;
   font-weight: 700;
   font-weight: 700;
 }
 }
+.progress-track {
+  height: 8px;
+  overflow: hidden;
+  border-radius: 999px;
+  background: #e7edf5;
+}
+.progress-bar {
+  height: 100%;
+  border-radius: inherit;
+  background: $themeColor;
+  transition: width 0.25s ease;
+}
 .extra {
 .extra {
   margin-top: 18px;
   margin-top: 18px;
 }
 }
+.background-setting {
+  position: relative;
+  margin-top: 14px;
+  padding: 14px 150px 14px 14px;
+  min-height: 74px;
+  border: 1px solid #edf1f5;
+  border-radius: 8px;
+  background: #fafcff;
+}
+.background-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-top: 10px;
+}
+.upload-btn,
+.remove-btn {
+  height: 32px;
+  padding: 0 12px;
+  border-radius: 6px;
+  border: 1px solid #d9d9d9;
+  background: #fff;
+  cursor: pointer;
+}
+.upload-btn {
+  color: $themeColor;
+  border-color: rgba($color: $themeColor, $alpha: 0.55);
+}
+.remove-btn {
+  color: #7b8794;
+}
+.background-preview {
+  position: absolute;
+  right: 14px;
+  top: 14px;
+  width: 120px;
+  height: 68px;
+  border-radius: 6px;
+  border: 1px solid #d9d9d9;
+  background-size: cover;
+  background-position: center;
+}
 .label {
 .label {
   margin-bottom: 8px;
   margin-bottom: 8px;
   font-size: 13px;
   font-size: 13px;
@@ -258,24 +402,28 @@ function updateBullets(page: AiCoursewarePage, event: Event) {
   margin-top: 12px;
   margin-top: 12px;
   overflow: auto;
   overflow: auto;
   border: 1px solid #edf1f5;
   border: 1px solid #edf1f5;
+  border-radius: 8px;
 }
 }
 .page-item {
 .page-item {
   display: flex;
   display: flex;
-  padding: 12px;
+  padding: 14px;
   border-bottom: 1px solid #edf1f5;
   border-bottom: 1px solid #edf1f5;
+  background: #fff;
   &:last-child {
   &:last-child {
     border-bottom: 0;
     border-bottom: 0;
   }
   }
 }
 }
 .page-no {
 .page-no {
-  width: 34px;
-  height: 34px;
-  line-height: 34px;
+  width: 64px;
+  height: 30px;
+  line-height: 30px;
   text-align: center;
   text-align: center;
   border-radius: 6px;
   border-radius: 6px;
   color: #fff;
   color: #fff;
   background: $themeColor;
   background: $themeColor;
   flex-shrink: 0;
   flex-shrink: 0;
+  font-size: 13px;
+  font-weight: 600;
 }
 }
 .page-content {
 .page-content {
   margin-left: 12px;
   margin-left: 12px;
@@ -291,14 +439,14 @@ function updateBullets(page: AiCoursewarePage, event: Event) {
   font-family: inherit;
   font-family: inherit;
 }
 }
 .page-title {
 .page-title {
-  height: 32px;
-  padding: 0 8px;
+  height: 36px;
+  padding: 0 10px;
   font-weight: 600;
   font-weight: 600;
 }
 }
 .page-bullets {
 .page-bullets {
   margin-top: 8px;
   margin-top: 8px;
-  height: 74px;
-  padding: 8px;
+  height: 86px;
+  padding: 10px;
   resize: vertical;
   resize: vertical;
   line-height: 1.5;
   line-height: 1.5;
 }
 }

Неке датотеке нису приказане због велике количине промена