SelfDirectedLearning.vue 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618
  1. <!-- AI自主学习 -->
  2. <template>
  3. <div class="self-directed-learning">
  4. <div class="title-section">
  5. <h3>和AI一起,学你想学!</h3>
  6. </div>
  7. <div class="input-section">
  8. <!-- 发送消息输入框 -->
  9. <div class="dialogue-card user-input-card">
  10. <div class="dialogue-content">
  11. <textarea
  12. v-model="prompt"
  13. class="user-input-textarea"
  14. placeholder=""
  15. @keyup.enter.exact="doSendMessage(prompt.trim())"
  16. ></textarea>
  17. <div class="dropdowns-container">
  18. <el-select
  19. v-for="dropdown in dropdowns"
  20. :key="dropdown.key"
  21. v-model="dropdown.value"
  22. @change="(value) => handleSelect(dropdown.key, value)"
  23. filterable
  24. class="custom-select"
  25. :popper-append-to-body="false"
  26. :teleported="false"
  27. >
  28. <template #prefix>
  29. <el-icon><component :is="dropdown.icon" /></el-icon>
  30. </template>
  31. <el-option
  32. v-for="item in dropdown.options"
  33. :key="item.value"
  34. :label="item.label"
  35. :value="item.value"
  36. />
  37. </el-select>
  38. </div>
  39. <button class="send-button" :class="{ 'active': prompt.trim().length > 0 && (!showNewContainer || allStepsCompleted) }" @click="doSendMessage(prompt.trim())" :disabled="!prompt.trim().length || (showNewContainer && !allStepsCompleted)">
  40. <Top />
  41. </button>
  42. </div>
  43. </div>
  44. <!-- 生成/进度 -->
  45. <div class="new-container" v-if="showNewContainer">
  46. <!-- 全部完成时显示成功状态 -->
  47. <div v-if="allStepsCompleted" class="success-container">
  48. <div class="success-checkmark">✓</div>
  49. <div class="success-text">完成</div>
  50. </div>
  51. <!-- 未完成时显示进度 -->
  52. <template v-else>
  53. <div class="left-box">
  54. <div class="loading-container">
  55. <div class="loading-spinner"></div>
  56. <div class="loading-text">正在生成中</div>
  57. </div>
  58. </div>
  59. <div class="right-box">
  60. <div class="progress-container">
  61. <div class="progress-title">正在生成课程</div>
  62. <div class="progress-steps">
  63. <div class="progress-step" v-for="(step, index) in visibleProgressSteps" :key="index" :style="{ animationDelay: `${index * 0.2}s` }">
  64. <div class="step-header">
  65. <span class="step-text">{{ step.text }}</span>
  66. <span class="step-status" :class="{ 'completed': step.completed, 'active': step.active }">
  67. <span v-if="step.completed" class="checkmark">✓</span>
  68. <span v-else-if="step.active" class="loading-dot"></span>
  69. <span v-else class="step-placeholder"></span>
  70. </span>
  71. </div>
  72. <div class="step-progress" v-if="(index === 2 || index === 3) && step.visible">
  73. <div class="progress-bar">
  74. <div class="progress-fill" :style="{ width: `${step.progress}%` }"></div>
  75. </div>
  76. <span class="progress-text">{{ step.progress }}%</span>
  77. </div>
  78. </div>
  79. </div>
  80. </div>
  81. </div>
  82. </template>
  83. </div>
  84. </div>
  85. </div>
  86. <el-divider></el-divider>
  87. </template>
  88. <script setup>
  89. import {ref, onMounted, computed, reactive, defineEmits} from "vue";
  90. // 定义事件
  91. const emit = defineEmits(['refreshData']);
  92. import { Top, Search, User,Memo,ArrowUpBold,ArrowDownBold } from '@element-plus/icons-vue'
  93. import {
  94. aIGenerateCourse,
  95. AiImageStatusEnum,
  96. CreateDialogue,
  97. CreatePainting,
  98. PaintingGetMys,
  99. sendChatMessageStream, textToSpeech
  100. } from "@/api/questions.js";
  101. import {Message} from "@/utils/message/Message.js";
  102. import {teacherList} from "@/api/teachers.js";
  103. // 本地数字人列表
  104. const localScriptRoles = ref([])
  105. // 发送消息输入框
  106. const prompt = ref(""); // prompt
  107. // 对话相关
  108. const activeConversationId = ref(null) // 选中的对话编号
  109. const conversationInAbortController = ref() // 对话进行中 abort 控制器(控制 stream 对话)
  110. const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作
  111. const enableContext = ref(true) // 是否开启上下文
  112. // 接收 Stream 消息
  113. const receiveMessageFullText = ref('')
  114. const scriptDataTemp = ref(null)
  115. // 生成状态
  116. const isGenerating = ref(false)// 是否正在生成脚本
  117. const isGeneratingImages = ref(false)// 是否正在生成图片
  118. const isGeneratingVoiceovers = ref(false)// 是否正在生成语音
  119. const generatingImageUrl = ref('')
  120. // 图片生成相关状态
  121. const inProgressImageMap = ref({}) // 监听的图片映射,key 为 image 编号,value 为 { sectionIndex, type: 'image'|'audio' }
  122. const inProgressTimer = ref(null) // 生成中的图片定时器,轮询生成进展
  123. // 小节列表
  124. const lessonList = ref([
  125. {id: '1', name: '课程引入'},
  126. {id: '2', name: '知识讲解'},
  127. {id: '3', name: '课程总结'}
  128. ])
  129. //主讲人、助讲人
  130. const selectedMainTeacher = ref('')
  131. const selectedAssistants = ref([''])
  132. //脚本数据
  133. const scriptData = reactive(
  134. {
  135. courseName: '', // 课程名称
  136. coverImage: { // 封面图
  137. prompt: '',
  138. url: '',
  139. generating: false
  140. },
  141. lessons: [ // 课程小节
  142. {
  143. lessonName: '课程引入', // 小节课程名称
  144. sections: [ // 环节
  145. {
  146. sectionName: '环节一', // 环节名称
  147. backgroundImage: {
  148. prompt: '',
  149. url: '',
  150. generating: false
  151. },
  152. backgroundAudio: {
  153. type: '',
  154. url: ''
  155. },
  156. dialogues: [
  157. {
  158. type: 'digital',
  159. roleName: '',
  160. content: '',
  161. voiceoverUrl: '',
  162. generatingVoiceover: false
  163. },
  164. {
  165. type: 'user',
  166. roleName: '',
  167. content: ''
  168. }
  169. ]
  170. }
  171. ]
  172. },
  173. {
  174. lessonName: '知识讲解', // 小节课程名称
  175. sections: [ // 环节
  176. {
  177. sectionName: '环节一', // 环节名称
  178. backgroundImage: {
  179. prompt: '',
  180. url: '',
  181. generating: false
  182. },
  183. backgroundAudio: {
  184. type: '',
  185. url: ''
  186. },
  187. dialogues: [
  188. {
  189. type: 'digital',
  190. roleName: '',
  191. content: '',
  192. voiceoverUrl: '',
  193. generatingVoiceover: false
  194. },
  195. {
  196. type: 'user',
  197. roleName: '',
  198. content: ''
  199. }
  200. ]
  201. }
  202. ]
  203. },
  204. {
  205. lessonName: '课程总结', // 小节课程名称
  206. sections: [ // 环节
  207. {
  208. sectionName: '环节一', // 环节名称
  209. backgroundImage: {
  210. prompt: '',
  211. url: '',
  212. generating: false
  213. },
  214. backgroundAudio: {
  215. type: '',
  216. url: ''
  217. },
  218. dialogues: [
  219. {
  220. type: 'digital',
  221. roleName: '',
  222. content: '',
  223. voiceoverUrl: '',
  224. generatingVoiceover: false
  225. },
  226. {
  227. type: 'user',
  228. roleName: '',
  229. content: ''
  230. }
  231. ]
  232. }
  233. ]
  234. }
  235. ]
  236. }
  237. )
  238. // 生成脚本
  239. const generateScript = async () => {
  240. try {
  241. isGenerating.value = true
  242. const role = localScriptRoles.value.find((r) => r.name === selectedMainTeacher.value)
  243. let content =
  244. prompt.value +
  245. '(主讲人:' + role.name +
  246. ',主讲人角色定位:' + role.description +
  247. ';助讲有:'
  248. let zhujiang = []
  249. selectedAssistants.value.forEach((rName) => {
  250. const assistantRole = localScriptRoles.value.find((r) => r.name === rName)
  251. zhujiang.push(assistantRole.name + '[' + assistantRole.description + ']')
  252. })
  253. content += zhujiang.join(',') + ');注意:给我的背景图提示词不允许出现主讲人和助讲人物形象。'
  254. // 1. 创建对话
  255. await createAiRoleIdConversation(13)
  256. //2、生成课程信息
  257. content += "注意:我需要让你生成"+lessonList.value.length+"节课程,";
  258. for (let i = 1; i <= lessonList.value.length; i++) {
  259. content+="第"+i+"节课程是"+lessonList.value[i-1].name+";"
  260. }
  261. //3、生成课程小节
  262. progressSteps.value[0].visible = true;
  263. progressSteps.value[0].active = true;
  264. let courseLessons = [];//存储课程小节信息
  265. scriptData.lessons = []
  266. for (let i = 0; i < lessonList.value.length; i++) {
  267. let lesson = lessonList.value[i]
  268. progressSteps.value[0].next = "生成课程小节【" + lesson.name + "】..."
  269. content = i === 0 ? content + `现在开始生成第${i+1}节课:${lesson.name}` : content
  270. let lessonInfo = {courseName: lesson.name, courseLabel: lesson.id};
  271. await doSendMessageStream(activeConversationId.value, content, lessonInfo)
  272. console.log("生成课程小节信息:", lessonInfo)
  273. courseLessons.push(lessonInfo)
  274. }
  275. scriptData.lessons = courseLessons;
  276. progressSteps.value[0].completed = true;
  277. progressSteps.value[0].active = false;
  278. content = "最后请给我围绕着这几节课的课程封面图描述词(coverImage)和课程名称(courseName),课程名字不能超过10个字。"
  279. progressSteps.value[1].visible = true;
  280. progressSteps.value[1].active = true;
  281. let courseInfo = {}//存储课程信息
  282. await doSendMessageStream(activeConversationId.value, content, courseInfo)
  283. console.log("生成课程信息:", courseInfo)
  284. scriptData.courseName = courseInfo.courseName
  285. scriptData.coverImage = courseInfo.coverImage
  286. progressSteps.value[1].completed = true;
  287. progressSteps.value[1].active = false;
  288. //4、生成全部图片
  289. progressSteps.value[2].visible = true;
  290. progressSteps.value[2].active = true;
  291. await generateAllImages()
  292. progressSteps.value[2].completed = true;
  293. progressSteps.value[2].active = false;
  294. //5、生成全部配音
  295. progressSteps.value[3].visible = true;
  296. progressSteps.value[3].active = true;
  297. await generateAllVoiceovers()
  298. progressSteps.value[3].completed = true;
  299. progressSteps.value[3].active = false;
  300. localStorage.setItem('scriptData', JSON.stringify(scriptData))
  301. console.log("【最终数据:】", scriptData)
  302. // 调用完成方法,封装数据并存储到后台
  303. await completeGenerate()
  304. } catch (error) {
  305. console.error('生成脚本失败:', error)
  306. } finally {
  307. isGenerating.value = false
  308. }
  309. }
  310. // 完成生成,封装数据并存储到后台
  311. const completeGenerate = async () => {
  312. try {
  313. console.log('开始封装数据并存储到后台...')
  314. const courses = scriptData.lessons.map(lesson => ({
  315. courseName: lesson.courseName,
  316. courseLabel: lesson.courseLabel,
  317. courseContent: JSON.stringify(lesson)
  318. }))
  319. // 封装数据
  320. const dataToStore = {
  321. grade: localStorage.getItem('selectedGradeId'),
  322. courseName: scriptData.courseName,
  323. coverImage: scriptData.coverImage.url,
  324. courseList: courses
  325. }
  326. console.log('封装的数据:', dataToStore)
  327. // 调用后端API将文本转成语音
  328. const res = await aIGenerateCourse(dataToStore)
  329. if (res.code !== 0) {
  330. console.error('生成课程失败:', res.msg)
  331. Message().error('生成课程失败:' + res.msg)
  332. return
  333. }
  334. // 所有步骤完成后,3秒后隐藏容器
  335. setTimeout(() => {
  336. showNewContainer.value = false;
  337. // 通知父组件刷新数据
  338. emit('refreshData');
  339. }, 2000);
  340. } catch (error) {
  341. console.error('存储数据失败:', error)
  342. }
  343. }
  344. /** 新建聊天对话 */
  345. const createAiRoleIdConversation = async (roleId) => {
  346. // 智能问答
  347. await CreateDialogue({ roleId: roleId })
  348. .then((res) => {
  349. console.log("创建会话:", res.data);
  350. activeConversationId.value = res.data;
  351. })
  352. .catch((error) => {
  353. console.error("请求出错:", error);
  354. });
  355. }
  356. /** 调取数字人生成脚本 */
  357. const doSendMessageStream = async (conversationId, content, outputContent) => {
  358. conversationInAbortController.value = new AbortController()
  359. conversationInProgress.value = true
  360. receiveMessageFullText.value = '' // 清空之前的文本
  361. try {
  362. let isFirstChunk = true
  363. await sendChatMessageStream(
  364. conversationId,
  365. content,
  366. undefined, // contentAnswer 参数
  367. conversationInAbortController.value,
  368. enableContext.value,
  369. async (res) => {
  370. const { code, data, msg } = JSON.parse(res.data)
  371. if (code !== 0) {
  372. console.error(`对话异常! ${msg}`)
  373. return
  374. }
  375. if (data.eventType === 'TEXT') {
  376. if (data.receive?.content === '') {
  377. return
  378. }
  379. if (isFirstChunk) {
  380. isFirstChunk = false
  381. }
  382. receiveMessageFullText.value += data.receive.content
  383. console.log('数据加载中..')
  384. try {
  385. const parsedData = JSON.parse(receiveMessageFullText.value)
  386. // 将解析后的数据填充到outputContent对象中
  387. console.log('zuizhon..', parsedData)
  388. Object.assign(outputContent, parsedData)
  389. // updateScriptData(parsedData)
  390. } catch (e) {
  391. // 解析失败,说明数据还不完整,继续等待
  392. }
  393. }
  394. },
  395. (error) => {
  396. console.error(`对话异常! ${error}`)
  397. throw error
  398. },
  399. () => {
  400. try {
  401. if (receiveMessageFullText.value) {
  402. console.log('最终数据:', receiveMessageFullText.value)
  403. const parsedData = JSON.parse(receiveMessageFullText.value)
  404. console.log('最终数据json:', parsedData)
  405. // 将解析后的数据填充到outputContent对象中
  406. // updateScriptData(parsedData)
  407. console.log('zuizhon..', parsedData)
  408. Object.assign(outputContent, parsedData)
  409. }
  410. } catch (e) {
  411. console.error('最终数据解析失败:', e)
  412. // 清洗规则:移除```json、```、** 等非JSON标记,只保留中间的JSON内容
  413. try {
  414. let cleanJsonStr = receiveMessageFullText.value
  415. const parsedData = JSON.parse(
  416. cleanJsonStr
  417. .replace(/^```json\s*/, '')
  418. .replace(/\s*```$/, '')
  419. .replace(/\*\*/g, '')
  420. .trim()
  421. )
  422. console.log('最终清洗后数据json:', parsedData)
  423. // 将解析后的数据填充到outputContent对象中
  424. console.log('zuizhon..', parsedData)
  425. Object.assign(outputContent, parsedData)
  426. // updateScriptData(parsedData)
  427. } catch (cleanError) {
  428. console.error('清洗后数据解析失败:', cleanError)
  429. }
  430. }
  431. }
  432. )
  433. } catch (error) {
  434. console.error('发送消息流失败:', error)
  435. } finally {
  436. conversationInProgress.value = false
  437. }
  438. }
  439. // 生成所有背景图
  440. const generateAllImages = async () => {
  441. return new Promise(async (resolve, reject) => {
  442. isGeneratingImages.value = true
  443. try {
  444. // 计算总图片数
  445. progressCounters.value.images.total = 0
  446. if (scriptData.coverImage && scriptData.coverImage.prompt && !scriptData.coverImage.url) {
  447. progressCounters.value.images.total++
  448. }
  449. if (scriptData.lessons && scriptData.lessons.length > 0) {
  450. for (let lessonIndex = 0; lessonIndex < scriptData.lessons.length; lessonIndex++) {
  451. const lesson = scriptData.lessons[lessonIndex]
  452. if (lesson && lesson.sections) {
  453. for (let sectionIndex = 0; sectionIndex < lesson.sections.length; sectionIndex++) {
  454. const section = lesson.sections[sectionIndex]
  455. if (section && section.backgroundImage && section.backgroundImage.prompt && !section.backgroundImage.url) {
  456. progressCounters.value.images.total++
  457. }
  458. }
  459. }
  460. }
  461. }
  462. progressCounters.value.images.current = 0
  463. // 确保scriptData和coverImage存在
  464. if (!scriptData.coverImage) {
  465. scriptData.coverImage = { prompt: '', url: '', generating: false }
  466. }
  467. // 生成封面图
  468. if (scriptData.coverImage.prompt && !scriptData.coverImage.url) {
  469. await generateCoverImage()
  470. }
  471. // 生成所有环节的背景图
  472. if (scriptData.lessons && scriptData.lessons.length > 0) {
  473. for (let lessonIndex = 0; lessonIndex < scriptData.lessons.length; lessonIndex++) {
  474. const lesson = scriptData.lessons[lessonIndex]
  475. if (lesson && lesson.sections) {
  476. for (let sectionIndex = 0; sectionIndex < lesson.sections.length; sectionIndex++) {
  477. const section = lesson.sections[sectionIndex]
  478. // 确保section和backgroundImage存在
  479. if (section) {
  480. if (!section.backgroundImage) {
  481. section.backgroundImage = { prompt: '', url: '', generating: false }
  482. }
  483. // 只处理没有背景图URL的环节
  484. if (section.backgroundImage.prompt && !section.backgroundImage.url) {
  485. await generateImage(lessonIndex, sectionIndex)
  486. }
  487. }
  488. }
  489. }
  490. }
  491. }
  492. // 检查是否有图片正在生成
  493. if (Object.keys(inProgressImageMap.value).length === 0) {
  494. // 没有图片需要生成,直接resolve
  495. progressSteps.value[2].progress = 100
  496. isGeneratingImages.value = false
  497. resolve()
  498. } else {
  499. // 启动图片轮询
  500. startImagePolling()
  501. // 检查是否有图片正在生成
  502. const checkImagesComplete = setInterval(() => {
  503. if (Object.keys(inProgressImageMap.value).length === 0) {
  504. progressSteps.value[2].progress = 100
  505. isGeneratingImages.value = false
  506. clearInterval(checkImagesComplete)
  507. stopImagePolling()
  508. resolve()
  509. }
  510. }, 1000)
  511. }
  512. } catch (error) {
  513. console.error('生成所有背景图失败:', error)
  514. isGeneratingImages.value = false
  515. reject(error)
  516. }
  517. })
  518. }
  519. // 生成封面图
  520. const generateCoverImage = async () => {
  521. scriptData.coverImage.generating = true
  522. try {
  523. const response = await CreatePainting({
  524. "modelId": 56,
  525. "prompt": scriptData.coverImage.prompt,
  526. "width": 1024,
  527. "height": 1024
  528. })
  529. console.log("生成封面图", response)
  530. let imageId = response.data
  531. // 处理不同格式的响应
  532. if (typeof imageId === 'object' && imageId !== null) {
  533. // 如果是对象,尝试获取id属性
  534. imageId = imageId.id || imageId.imageId || imageId.data
  535. }
  536. // 转换为数字
  537. imageId = Number(imageId)
  538. if (!isNaN(imageId)) {
  539. inProgressImageMap.value[imageId] = {
  540. type: 'cover'
  541. }
  542. } else {
  543. console.error('无效的图片ID:', response.data)
  544. scriptData.coverImage.generating = false
  545. }
  546. } catch (error) {
  547. console.error('生成封面图失败:', error)
  548. scriptData.coverImage.generating = false
  549. }
  550. }
  551. // 生成图片
  552. const generateImage = async (lessonIndex, sectionIndex) => {
  553. const lesson = scriptData.lessons[lessonIndex]
  554. if (!lesson) return
  555. const section = lesson.sections[sectionIndex]
  556. if (!section) return
  557. const media = section.backgroundImage
  558. media.generating = true
  559. try {
  560. const res = await CreatePainting({
  561. "modelId": 56,
  562. "prompt": media.prompt,
  563. "width": 1024,
  564. "height": 1024
  565. })
  566. console.log("生成图片", res)
  567. let imageId = res.data
  568. // 处理不同格式的响应
  569. if (typeof imageId === 'object' && imageId !== null) {
  570. // 如果是对象,尝试获取id属性
  571. imageId = imageId.id || imageId.imageId || imageId.data
  572. }
  573. // 转换为数字
  574. imageId = Number(imageId)
  575. if (!isNaN(imageId)) {
  576. inProgressImageMap.value[imageId] = {
  577. lessonIndex: lessonIndex,
  578. sectionIndex: sectionIndex,
  579. type: 'image'
  580. }
  581. } else {
  582. console.error('无效的图片ID:', res.data)
  583. media.generating = false
  584. }
  585. } catch (error) {
  586. console.error('生成图片失败:', error);
  587. media.generating = false
  588. }
  589. }
  590. // 启动图片轮询
  591. const startImagePolling = () => {
  592. if (inProgressTimer.value) {
  593. clearInterval(inProgressTimer.value)
  594. }
  595. inProgressTimer.value = setInterval(refreshWatchImages, 3000)
  596. }
  597. // 停止图片轮询
  598. const stopImagePolling = () => {
  599. if (inProgressTimer.value) {
  600. clearInterval(inProgressTimer.value)
  601. inProgressTimer.value = null
  602. }
  603. }
  604. // 轮询生成中的图片列表
  605. const refreshWatchImages = async () => {
  606. const imageIds = Object.keys(inProgressImageMap.value)
  607. .map(Number)
  608. .filter(id => !isNaN(id))
  609. if (imageIds.length === 0) {
  610. return
  611. }
  612. try {
  613. const list = await PaintingGetMys(imageIds)
  614. const newWatchImages = {}
  615. let completedCount = 0
  616. // 遍历所有正在生成的图片
  617. Object.keys(inProgressImageMap.value).forEach((key) => {
  618. const imageId = Number(key)
  619. const info = inProgressImageMap.value[key]
  620. // 查找对应的图片状态
  621. const image = list.data.find(item => item.id === imageId)
  622. if (image) {
  623. if (image.status === AiImageStatusEnum.IN_PROGRESS) {
  624. // 图片正在生成中,保留在map中
  625. newWatchImages[key] = info
  626. } else if (image.status === AiImageStatusEnum.SUCCESS && image.picUrl) {
  627. // 图片生成成功
  628. completedCount++
  629. if (info.type === 'cover') {
  630. // 处理封面图
  631. scriptData.coverImage.url = image.picUrl
  632. scriptData.coverImage.generating = false
  633. } else {
  634. // 处理课程小节背景图
  635. const lesson = scriptData.lessons[info.lessonIndex]
  636. if (lesson) {
  637. const section = lesson.sections[info.sectionIndex]
  638. if (section) {
  639. section.backgroundImage.url = image.picUrl
  640. section.backgroundImage.generating = false
  641. }
  642. }
  643. }
  644. // 更新生成中的图片预览
  645. generatingImageUrl.value = image.picUrl
  646. localStorage.setItem('scriptData', JSON.stringify(scriptData))
  647. console.log("【最终数据,生成图片成功】", scriptData)
  648. } else if (image.status === AiImageStatusEnum.FAIL) {
  649. // 图片生成失败
  650. completedCount++
  651. if (info.type === 'cover') {
  652. // 处理封面图失败
  653. scriptData.coverImage.generating = false
  654. } else {
  655. // 处理课程小节背景图失败
  656. const lesson = scriptData.lessons[info.lessonIndex]
  657. if (lesson) {
  658. const section = lesson.sections[info.sectionIndex]
  659. if (section) {
  660. section.backgroundImage.generating = false
  661. }
  662. }
  663. }
  664. }
  665. } else {
  666. // 图片不存在于返回列表中,可能已经处理完成或超时,从map中移除
  667. completedCount++
  668. if (info.type === 'cover') {
  669. scriptData.coverImage.generating = false
  670. } else {
  671. const lesson = scriptData.lessons[info.lessonIndex]
  672. if (lesson) {
  673. const section = lesson.sections[info.sectionIndex]
  674. if (section) {
  675. section.backgroundImage.generating = false
  676. }
  677. }
  678. }
  679. }
  680. })
  681. inProgressImageMap.value = newWatchImages
  682. // 更新进度
  683. progressCounters.value.images.current = completedCount
  684. if (progressCounters.value.images.total > 0) {
  685. progressSteps.value[2].progress = Math.round((completedCount / progressCounters.value.images.total) * 100)
  686. }
  687. } catch (error) {
  688. console.error('轮询图片状态失败:', error)
  689. }
  690. }
  691. // 生成所有语音
  692. const generateAllVoiceovers = async () => {
  693. isGeneratingVoiceovers.value = true
  694. try {
  695. // 计算总语音数
  696. progressCounters.value.voiceovers.total = 0
  697. if (scriptData.lessons && scriptData.lessons.length > 0) {
  698. for (let lessonIndex = 0; lessonIndex < scriptData.lessons.length; lessonIndex++) {
  699. const lesson = scriptData.lessons[lessonIndex]
  700. if (lesson && lesson.sections) {
  701. for (let sectionIndex = 0; sectionIndex < lesson.sections.length; sectionIndex++) {
  702. const section = lesson.sections[sectionIndex]
  703. if (section && section.dialogues) {
  704. for (let dialogueIndex = 0; dialogueIndex < section.dialogues.length; dialogueIndex++) {
  705. const dialogue = section.dialogues[dialogueIndex]
  706. if (dialogue && dialogue.type !== 'user' && !dialogue.voiceoverUrl && dialogue.content) {
  707. progressCounters.value.voiceovers.total++
  708. }
  709. }
  710. }
  711. }
  712. }
  713. }
  714. }
  715. progressCounters.value.voiceovers.current = 0
  716. // 确保scriptData和lessons存在
  717. if (scriptData.lessons && scriptData.lessons.length > 0) {
  718. for (let lessonIndex = 0; lessonIndex < scriptData.lessons.length; lessonIndex++) {
  719. const lesson = scriptData.lessons[lessonIndex]
  720. if (lesson && lesson.sections) {
  721. for (let sectionIndex = 0; sectionIndex < lesson.sections.length; sectionIndex++) {
  722. const section = lesson.sections[sectionIndex]
  723. if (section && section.dialogues) {
  724. for (let dialogueIndex = 0; dialogueIndex < section.dialogues.length; dialogueIndex++) {
  725. const dialogue = section.dialogues[dialogueIndex]
  726. // 只处理没有语音URL且有内容的对话
  727. if (dialogue && dialogue.type !== 'user' && !dialogue.voiceoverUrl && dialogue.content) {
  728. await generateVoiceover(lessonIndex, sectionIndex, dialogueIndex)
  729. // 更新进度
  730. progressCounters.value.voiceovers.current++
  731. if (progressCounters.value.voiceovers.total > 0) {
  732. progressSteps.value[3].progress = Math.round((progressCounters.value.voiceovers.current / progressCounters.value.voiceovers.total) * 100)
  733. }
  734. }
  735. }
  736. }
  737. }
  738. }
  739. }
  740. }
  741. // 完成后设置为100%
  742. progressSteps.value[3].progress = 100
  743. } catch (error) {
  744. console.error('生成所有语音失败:', error)
  745. } finally {
  746. isGeneratingVoiceovers.value = false
  747. }
  748. }
  749. // 文本转语音API调用
  750. // 生成单个语音
  751. const generateVoiceover = async (lessonIndex, sectionIndex, dialogueIndex) => {
  752. const lesson = scriptData.lessons[lessonIndex]
  753. if (!lesson) return
  754. const section = lesson.sections[sectionIndex]
  755. if (!section) return
  756. const dialogue = section.dialogues[dialogueIndex]
  757. // 只处理数字人对话和提问
  758. if (dialogue.type === 'user') {
  759. return
  760. }
  761. dialogue.generatingVoiceover = true
  762. let role = localScriptRoles.value.find((r) => r.name === dialogue.roleName)
  763. if (!role) {
  764. console.error('未找到角色:', dialogue.roleName)
  765. dialogue.generatingVoiceover = false
  766. return
  767. }
  768. try {
  769. // 调用后端API将文本转成语音
  770. const response = await textToSpeech({
  771. roleId: role.id,
  772. content: dialogue.content
  773. })
  774. // 将返回的URL赋值给对话
  775. dialogue.voiceoverUrl = response.data
  776. localStorage.setItem('scriptData', JSON.stringify(scriptData))
  777. localStorage.setItem('【最终数据,生成语音成功】', scriptData)
  778. } catch (error) {
  779. console.error('生成语音失败:', error)
  780. } finally {
  781. dialogue.generatingVoiceover = false
  782. }
  783. }
  784. // 控制生成进度显示状态
  785. const showNewContainer = ref(false);
  786. // 生成进度步骤
  787. const progressSteps = ref([
  788. { text: '生成课程脚本...', completed: false, active: false, visible: false, progress: 0 },
  789. { text: '生成课程小节...', completed: false, active: false, visible: false, progress: 0 },
  790. { text: '生成背景图...', completed: false, active: false, visible: false, progress: 0 },
  791. { text: '生成配音...', completed: false, active: false, visible: false, progress: 0 }
  792. ]);
  793. // 进度计数器
  794. const progressCounters = ref({
  795. images: { total: 0, current: 0 },
  796. voiceovers: { total: 0, current: 0 }
  797. });
  798. // 计算主讲下拉框选项
  799. const teacherOptions = computed(() => {
  800. const roles = localScriptRoles.value.length > 0 ? localScriptRoles.value : localScriptRoles.value;
  801. return roles.map(role => ({
  802. label: role.name,
  803. value: role.name
  804. }));
  805. });
  806. // 计算助教下拉框选项
  807. const assistantOptions = computed(() => {
  808. const roles = localScriptRoles.value.length > 0 ? localScriptRoles.value : localScriptRoles.value;
  809. return roles.map(role => ({
  810. label: role.name,
  811. value: role.name
  812. }));
  813. });
  814. // 计算生成主题下拉框选项,基于 lessonList 动态生成
  815. const courseOptions = computed(() => {
  816. // 提取 lessonList 中的课程名称
  817. const lessonNames = lessonList.value.map(lesson => lesson.name).join('、');
  818. return [
  819. {
  820. label: `诗词课(${lessonNames})`,
  821. value: '诗词课'
  822. }
  823. ];
  824. });
  825. /**
  826. * 获取角色列表
  827. * @returns {Promise<void>}
  828. */
  829. const getRoleList = async () => {
  830. try {
  831. let grade = localStorage.getItem('selectedGrade')
  832. grade = grade ? grade : '小学低年级'
  833. // 获取小学低年级AI数据
  834. const listAiRes = await teacherList({ category: grade + 'AI' })
  835. const listRes = await teacherList({ category: grade })
  836. const listAi = listAiRes.data.list || []
  837. const list = listRes.data.list || []
  838. listAi.push(...list)
  839. localScriptRoles.value = listAi
  840. } catch (error) {
  841. console.error('获取角色数据失败:', error)
  842. }
  843. }
  844. // 下拉框配置
  845. const dropdowns = ref([
  846. {
  847. key: 'course',
  848. value: '生成主题',
  849. visible: false,
  850. icon: Search,
  851. options: courseOptions.value
  852. },
  853. {
  854. key: 'teacher',
  855. value: '主讲',
  856. visible: false,
  857. icon: User,
  858. options: teacherOptions.value
  859. },
  860. {
  861. key: 'assistant',
  862. value: '助教',
  863. visible: false,
  864. icon: Memo,
  865. options: assistantOptions.value
  866. }
  867. ]);
  868. // 下拉框配置映射,用于生成内容和正则表达式
  869. const dropdownConfig = {
  870. course: {
  871. prefix: '诗词课',
  872. default: '诗词课',
  873. regex: /诗词课[^,]+/g
  874. },
  875. teacher: {
  876. prefix: '主讲为',
  877. default: '主讲',
  878. regex: /主讲为[^,]+/g
  879. },
  880. assistant: {
  881. prefix: '助教为',
  882. default: '助教',
  883. regex: /助教为[^,]+/g
  884. }
  885. };
  886. // 处理下拉框选择
  887. const handleSelect = (key, command) => {
  888. const dropdown = dropdowns.value.find(item => item.key === key);
  889. if (dropdown && dropdownConfig[key]) {
  890. // 更新下拉框显示值
  891. dropdown.value = command;
  892. // 如果选择的是主讲,更新selectedMainTeacher
  893. if (key === 'teacher') {
  894. selectedMainTeacher.value = command;
  895. }
  896. // 如果选择的是助教,更新selectedAssistants
  897. else if (key === 'assistant') {
  898. selectedAssistants.value = [command];
  899. }
  900. // const config = dropdownConfig[key];
  901. // // 生成新内容,仅当选择的值与默认值不同时添加
  902. // const newContent = command === config.default
  903. // ? config.prefix
  904. // : `${config.prefix}${command}`;
  905. // // 检查输入框中是否已存在该类型的内容
  906. // if (config.regex.test(prompt.value)) {
  907. // // 替换已存在的内容
  908. // prompt.value = prompt.value.replace(config.regex, newContent);
  909. // } else {
  910. // // 添加到输入框,用逗号隔开
  911. // if (prompt.value.trim()) {
  912. // prompt.value += `, ${newContent}`;
  913. // } else {
  914. // prompt.value = newContent;
  915. // }
  916. // }
  917. }
  918. };
  919. // 处理下拉框可见性变化
  920. const handleVisibleChange = (key, visible) => {
  921. const dropdown = dropdowns.value.find(item => item.key === key);
  922. if (dropdown) {
  923. dropdown.visible = visible;
  924. }
  925. };
  926. /** 处理来自 keydown 的发送消息 */
  927. const handleSendByKeydown = async (event) => {
  928. const content = prompt.value?.trim();
  929. if (event.key === "Enter") {
  930. if (event.shiftKey) {
  931. // 插入换行
  932. prompt.value += "\r\n";
  933. event.preventDefault(); // 防止默认的换行行为
  934. } else {
  935. // 发送消息
  936. await doSendMessage(content);
  937. event.preventDefault(); // 防止默认的提交行为
  938. }
  939. }
  940. };
  941. /** 生成操作 */
  942. const doSendMessage = async (content) => {
  943. // 校验
  944. if (content.length < 1) {
  945. console.error("发送失败,原因:内容为空!");
  946. return;
  947. }
  948. showNewContainer.value = true;
  949. // 调用生成脚本方法
  950. await generateScript();
  951. };
  952. // 计算属性:返回可见的进度步骤
  953. const visibleProgressSteps = computed(() => {
  954. return progressSteps.value.filter(step => step.visible);
  955. });
  956. // 计算属性:检查是否所有步骤都已完成
  957. const allStepsCompleted = computed(() => {
  958. return progressSteps.value.every(step => step.completed);
  959. });
  960. // 组件挂载时
  961. onMounted(async () => {
  962. // 初始状态下不显示生成进度
  963. showNewContainer.value = false;
  964. await getRoleList();
  965. // 初始化生成主题下拉框选项
  966. dropdowns.value[0].options = courseOptions.value;
  967. // 初始化主讲下拉框选项
  968. dropdowns.value[1].options = teacherOptions.value;
  969. // 初始化助教下拉框选项
  970. dropdowns.value[2].options = assistantOptions.value;
  971. });
  972. </script>
  973. <style scoped lang="scss">
  974. @use "sass:math";
  975. // 定义rpx转换函数
  976. @function rpx($px) {
  977. @return math.div($px, 750) * 100vw;
  978. }
  979. .self-directed-learning {
  980. width: 100%;
  981. display: flex;
  982. flex-direction: column;
  983. flex-grow: 1;
  984. }
  985. .title-section {
  986. display: flex;
  987. justify-content: center;
  988. align-items: center;
  989. h3 {
  990. font-size: rpx(12);
  991. color: black;
  992. font-weight: bold;
  993. letter-spacing: rpx(5);
  994. }
  995. }
  996. .input-section {
  997. display: flex;
  998. flex-direction: column;
  999. justify-content: flex-start;
  1000. margin-left: rpx(10);
  1001. width: calc(100% - rpx(10));
  1002. }
  1003. .dialogue-card {
  1004. background-color: #f1f0ff;
  1005. border: rpx(1) solid #a7a4ed;
  1006. border-radius: rpx(6);
  1007. padding: rpx(8);
  1008. max-width: 50%;
  1009. min-width: rpx(200);
  1010. width: auto;
  1011. display: inline-block;
  1012. z-index: 2;
  1013. }
  1014. .user-input-card {
  1015. width: 93%;
  1016. margin-left: rpx(20);
  1017. max-width: none;
  1018. animation: dialogueEnterCenter 0.6s ease forwards;
  1019. box-sizing: border-box;
  1020. }
  1021. .user-input-card::before {
  1022. display: none;
  1023. }
  1024. @keyframes dialogueEnterCenter {
  1025. from {
  1026. opacity: 0;
  1027. transform: translateY(rpx(20));
  1028. }
  1029. to {
  1030. opacity: 1;
  1031. transform: translateY(0);
  1032. }
  1033. }
  1034. .dialogue-content {
  1035. font-size: rpx(12);
  1036. line-height: 1.2;
  1037. color: #333;
  1038. text-align: left;
  1039. display: flex;
  1040. flex-direction: column;
  1041. position: relative;
  1042. width: 100%;
  1043. }
  1044. .user-input-textarea {
  1045. width: 100%;
  1046. min-height: rpx(70);
  1047. max-height: rpx(150);
  1048. border: none;
  1049. outline: none;
  1050. background: transparent;
  1051. font-size: rpx(8);
  1052. line-height: 1.2;
  1053. color: #333;
  1054. resize: none;
  1055. font-family: inherit;
  1056. overflow-y: auto;
  1057. text-align: left;
  1058. padding: rpx(5) rpx(0);
  1059. }
  1060. .dropdowns-container {
  1061. display: flex;
  1062. gap: rpx(5);
  1063. margin-top: rpx(5);
  1064. align-self: flex-start;
  1065. }
  1066. .dropdowns-container .custom-select {
  1067. width: auto;
  1068. height: rpx(18);
  1069. font-size: rpx(8);
  1070. max-width: rpx(80);
  1071. }
  1072. .dropdowns-container .custom-select :deep(.el-select__wrapper) {
  1073. background-color: #f1f0ff !important;
  1074. border: rpx(1) solid #a7a4ed !important;
  1075. border-radius: rpx(4) !important;
  1076. height: rpx(18) !important;
  1077. }
  1078. .dropdowns-container .custom-select .el-select__input {
  1079. font-size: rpx(8);
  1080. color: black;
  1081. height: rpx(18);
  1082. line-height: rpx(18);
  1083. }
  1084. .dropdowns-container .custom-select .el-select__prefix {
  1085. color: black;
  1086. }
  1087. .dropdowns-container .custom-select .el-select__caret {
  1088. color: black;
  1089. }
  1090. /* 下拉菜单样式 */
  1091. .dropdowns-container .custom-select :deep(.el-select-dropdown) {
  1092. min-width: rpx(50);
  1093. width: auto;
  1094. max-height: rpx(160);
  1095. overflow-y: auto;
  1096. border-radius: rpx(3);
  1097. border: 1px white solid;
  1098. background-color: rgb(255, 255, 255, 0.8);
  1099. backdrop-filter: blur(rpx(5));
  1100. // box-shadow: 0 4px 8px rgba(202, 52, 52, 0.1);
  1101. }
  1102. .dropdowns-container .custom-select :deep(.el-select-dropdown::-webkit-scrollbar) {
  1103. width: rpx(2);
  1104. }
  1105. .dropdowns-container .custom-select :deep(.el-select-dropdown::-webkit-scrollbar-track) {
  1106. background: #f1f1f1;
  1107. border-radius: rpx(2);
  1108. }
  1109. .dropdowns-container .custom-select :deep(.el-select-dropdown::-webkit-scrollbar-thumb) {
  1110. background: #c1c1c1;
  1111. border-radius: rpx(2);
  1112. }
  1113. .dropdowns-container .custom-select :deep(.el-select-dropdown::-webkit-scrollbar-thumb:hover) {
  1114. background: #a8a8a8;
  1115. }
  1116. .dropdowns-container .custom-select :deep(.el-select-dropdown__item) {
  1117. font-size: rpx(8) !important;
  1118. color: black !important;
  1119. border-radius: rpx(5) !important;
  1120. width: auto !important;
  1121. min-width: rpx(35) !important;
  1122. height: rpx(20) !important;
  1123. margin-bottom: rpx(8) !important;
  1124. display: flex !important;
  1125. align-items: center !important;
  1126. justify-content: center !important;
  1127. padding: 0 rpx(8) !important;
  1128. }
  1129. .dropdowns-container .custom-select :deep(.el-select-dropdown__item:hover),
  1130. .dropdowns-container .custom-select :deep(.el-select-dropdown__item:focus),
  1131. .dropdowns-container .custom-select :deep(.el-select-dropdown__item.is-active) {
  1132. background: linear-gradient(
  1133. to bottom,
  1134. #fee78a,
  1135. #ffce1b
  1136. ) !important;
  1137. border: none !important;
  1138. outline: none !important;
  1139. color: black !important;
  1140. }
  1141. /* 确保下拉框点击时也没有边框 */
  1142. .dropdowns-container .custom-select:focus,
  1143. .dropdowns-container .custom-select:active {
  1144. outline: none;
  1145. box-shadow: none;
  1146. }
  1147. /* 搜索输入框样式 */
  1148. .dropdowns-container .custom-select :deep(.el-select__input.is-focus) {
  1149. color: black;
  1150. }
  1151. /* 下拉框清空按钮样式 */
  1152. .dropdowns-container .custom-select .el-select__clear {
  1153. color: black;
  1154. }
  1155. /* 滚动条样式 */
  1156. .user-input-textarea::-webkit-scrollbar {
  1157. width: rpx(0);
  1158. }
  1159. .user-input-textarea::-webkit-scrollbar-track {
  1160. background: rgba(0, 0, 0, 0.05);
  1161. border-radius: rpx(3);
  1162. }
  1163. .user-input-textarea::-webkit-scrollbar-thumb {
  1164. background: rgba(64, 158, 255, 0.5);
  1165. border-radius: rpx(3);
  1166. }
  1167. .user-input-textarea::-webkit-scrollbar-thumb:hover {
  1168. background: rgba(64, 158, 255, 0.8);
  1169. }
  1170. .send-button {
  1171. position: absolute;
  1172. bottom: rpx(0);
  1173. right: rpx(0);
  1174. width: rpx(18);
  1175. height: rpx(18);
  1176. border-radius: 50%;
  1177. background: transparent;
  1178. color: #a7a4ed;
  1179. border: rpx(1) solid #a7a4ed;
  1180. font-size: rpx(2);
  1181. font-weight: bold;
  1182. display: flex;
  1183. align-items: center;
  1184. justify-content: center;
  1185. cursor: not-allowed;
  1186. transition: all 0.3s ease;
  1187. outline: none;
  1188. box-shadow: none;
  1189. opacity: 0.6;
  1190. }
  1191. .send-button.active {
  1192. background: linear-gradient(
  1193. to bottom,
  1194. #ffefb0,
  1195. #ffcc00
  1196. );
  1197. color: white;
  1198. border: none;
  1199. cursor: pointer;
  1200. opacity: 1;
  1201. }
  1202. .send-button:hover:not(.active) {
  1203. border-color: #a7a4ed;
  1204. color: #a7a4ed;
  1205. transform: none;
  1206. outline: none;
  1207. box-shadow: none;
  1208. }
  1209. .send-button:hover.active {
  1210. transform: scale(1.1);
  1211. outline: none;
  1212. box-shadow: 0 rpx(2) rpx(5) rgba(255, 204, 0, 0.3);
  1213. }
  1214. .send-button:active.active {
  1215. transform: scale(0.95);
  1216. outline: none;
  1217. box-shadow: none;
  1218. }
  1219. .send-button:focus {
  1220. outline: none;
  1221. box-shadow: none;
  1222. }
  1223. .send-button span {
  1224. display: block;
  1225. }
  1226. .new-container {
  1227. width: 93%;
  1228. margin-left: rpx(20);
  1229. margin-top: rpx(10);
  1230. display: flex;
  1231. gap: rpx(5);
  1232. background-color: #f1f0ff;
  1233. border: rpx(1) solid #a7a4ed;
  1234. border-radius: rpx(6);
  1235. padding: rpx(2);
  1236. max-width: none;
  1237. align-self: flex-start;
  1238. box-sizing: border-box;
  1239. min-height: rpx(50);
  1240. }
  1241. .success-container {
  1242. width: 100%;
  1243. display: flex;
  1244. flex-direction: column;
  1245. align-items: center;
  1246. justify-content: center;
  1247. padding: rpx(10);
  1248. gap: rpx(5);
  1249. }
  1250. .success-checkmark {
  1251. width: rpx(20);
  1252. height: rpx(20);
  1253. border-radius: 50%;
  1254. background-color: #4caf50;
  1255. color: white;
  1256. display: flex;
  1257. align-items: center;
  1258. justify-content: center;
  1259. font-size: rpx(12);
  1260. font-weight: bold;
  1261. animation: successScale 0.5s ease-out;
  1262. }
  1263. .success-text {
  1264. font-size: rpx(10);
  1265. color: #666;
  1266. font-weight: bold;
  1267. animation: successFadeIn 0.5s ease-out 0.2s both;
  1268. }
  1269. @keyframes successScale {
  1270. 0% {
  1271. transform: scale(0);
  1272. opacity: 0;
  1273. }
  1274. 50% {
  1275. transform: scale(1.2);
  1276. opacity: 1;
  1277. }
  1278. 100% {
  1279. transform: scale(1);
  1280. opacity: 1;
  1281. }
  1282. }
  1283. @keyframes successFadeIn {
  1284. from {
  1285. opacity: 0;
  1286. transform: translateY(rpx(5));
  1287. }
  1288. to {
  1289. opacity: 1;
  1290. transform: translateY(0);
  1291. }
  1292. }
  1293. .left-box {
  1294. flex: 0.5;
  1295. // background-color: rgba(241, 240, 255, 0.8);
  1296. backdrop-filter: blur(rpx(5));
  1297. // border: rpx(1) solid rgba(167, 164, 237);
  1298. border-radius: rpx(6);
  1299. padding: rpx(2);
  1300. min-height: rpx(50);
  1301. display: flex;
  1302. align-items: center;
  1303. justify-content: center;
  1304. }
  1305. .loading-container {
  1306. display: flex;
  1307. flex-direction: column;
  1308. align-items: center;
  1309. justify-content: center;
  1310. gap: rpx(5);
  1311. }
  1312. .loading-spinner {
  1313. width: rpx(12);
  1314. height: rpx(12);
  1315. border: rpx(2) solid rgba(167, 164, 237, 0.3);
  1316. border-top: rpx(2) solid #a7a4ed;
  1317. border-radius: 50%;
  1318. animation: spin 1s linear infinite;
  1319. }
  1320. .loading-text {
  1321. font-size: rpx(8);
  1322. color: #666;
  1323. animation: pulse 1.5s ease-in-out infinite;
  1324. }
  1325. @keyframes spin {
  1326. 0% { transform: rotate(0deg); }
  1327. 100% { transform: rotate(360deg); }
  1328. }
  1329. @keyframes pulse {
  1330. 0%, 100% { opacity: 1; }
  1331. 50% { opacity: 0.6; }
  1332. }
  1333. .right-box {
  1334. flex: 1;
  1335. // background-color: rgba(241, 240, 255, 0.8);
  1336. backdrop-filter: blur(rpx(5));
  1337. // border: rpx(1) solid rgba(167, 164, 237, 0.5);
  1338. border-radius: rpx(6);
  1339. padding: rpx(8);
  1340. min-height: rpx(50);
  1341. display: flex;
  1342. align-items: flex-start;
  1343. justify-content: flex-start;
  1344. text-align: left;
  1345. }
  1346. .progress-container {
  1347. width: 100%;
  1348. display: flex;
  1349. flex-direction: column;
  1350. gap: rpx(5);
  1351. }
  1352. .progress-title {
  1353. font-size: rpx(10);
  1354. font-weight: bold;
  1355. color: #666;
  1356. margin-bottom: rpx(3);
  1357. }
  1358. .progress-steps {
  1359. display: flex;
  1360. flex-direction: column;
  1361. gap: rpx(2);
  1362. }
  1363. .progress-step {
  1364. margin-bottom: rpx(5);
  1365. opacity: 0;
  1366. transform: translateY(rpx(5));
  1367. animation: stepEnter 0.5s ease forwards;
  1368. }
  1369. @keyframes stepEnter {
  1370. to {
  1371. opacity: 1;
  1372. transform: translateY(0);
  1373. }
  1374. }
  1375. .progress-step.active .step-text {
  1376. color: #a7a4ed;
  1377. font-weight: bold;
  1378. }
  1379. .step-header {
  1380. display: flex;
  1381. justify-content: flex-start;
  1382. align-items: center;
  1383. text-align: left;
  1384. font-size: rpx(8);
  1385. color: #666;
  1386. padding: rpx(1) 0;
  1387. transition: all 0.3s ease;
  1388. margin-bottom: rpx(2);
  1389. }
  1390. .step-text {
  1391. flex: 1;
  1392. overflow: hidden;
  1393. text-overflow: ellipsis;
  1394. white-space: nowrap;
  1395. transition: color 0.3s ease;
  1396. }
  1397. .step-status {
  1398. width: rpx(8);
  1399. height: rpx(8);
  1400. display: flex;
  1401. align-items: center;
  1402. justify-content: center;
  1403. border-radius: 50%;
  1404. margin-left: rpx(10);
  1405. transition: all 0.3s ease;
  1406. }
  1407. .step-placeholder {
  1408. width: rpx(4);
  1409. height: rpx(4);
  1410. border-radius: 50%;
  1411. background-color: rgba(167, 164, 237, 0.2);
  1412. }
  1413. .step-status.completed {
  1414. background-color: #4caf50;
  1415. color: white;
  1416. }
  1417. .step-status.active {
  1418. background-color: rgba(167, 164, 237, 0.3);
  1419. }
  1420. .checkmark {
  1421. font-size: rpx(6);
  1422. font-weight: bold;
  1423. }
  1424. .loading-dot {
  1425. width: rpx(4);
  1426. height: rpx(4);
  1427. background-color: #a7a4ed;
  1428. border-radius: 50%;
  1429. animation: pulse 1.5s ease-in-out infinite;
  1430. }
  1431. .step-progress {
  1432. display: flex;
  1433. align-items: center;
  1434. gap: rpx(5);
  1435. margin-left: rpx(0);
  1436. }
  1437. .progress-bar {
  1438. flex: 1;
  1439. height: rpx(2);
  1440. background-color: rgba(167, 164, 237, 0.2);
  1441. border-radius: rpx(1);
  1442. overflow: hidden;
  1443. }
  1444. .progress-fill {
  1445. height: 100%;
  1446. background-color: #a7a4ed;
  1447. transition: width 0.3s ease;
  1448. }
  1449. .progress-text {
  1450. font-size: rpx(6);
  1451. color: #666;
  1452. min-width: rpx(20);
  1453. text-align: right;
  1454. }
  1455. @keyframes pulse {
  1456. 0%, 100% { opacity: 1; transform: scale(1); }
  1457. 50% { opacity: 0.6; transform: scale(1.2); }
  1458. }
  1459. </style>