BlocklyEditor.vue 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101
  1. <template>
  2. <!-- 智能台灯 -->
  3. <div v-if="showLampPreview" class="desk-lamp-container">
  4. <!-- 标题框 -->
  5. <div class="desk-lamp-title-box">
  6. <div class="desk-lamp-box-icon" @click="goBackLab">
  7. <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
  8. 返回虚拟实验室
  9. </div>
  10. </div>
  11. <!-- 收音状态显示区域 - 添加这段代码 -->
  12. <div v-if="isRecording" class="recording-status-container">
  13. <div class="recording-text">正在收音...</div>
  14. <div class="equalizer">
  15. <div class="bar bar-1"></div>
  16. <div class="bar bar-2"></div>
  17. <div class="bar bar-3"></div>
  18. <div class="bar bar-4"></div>
  19. <div class="bar bar-5"></div>
  20. <div class="bar bar-6"></div>
  21. <div class="bar bar-7"></div>
  22. </div>
  23. <div class="recording-countdown">{{ recordingCountdown }}秒</div>
  24. </div>
  25. <img src="@/assets/images/desklamp.png" alt="智能台灯" class="full-screen-image" />
  26. <!-- 使用动态样式设置灯光遮罩 -->
  27. <div v-if="state.lamp.isLightOn" :style="{ '--lamp-color': state.lamp.color,'--lamp-opacity': state.lamp.brightness / 100 }" class="lamp-light-mask"></div>
  28. <!-- 右下角按钮组 -->
  29. <div class="button-group">
  30. <el-button class="control-button run-button" @click="toggleLight">运行</el-button>
  31. <el-button class="control-button code-button" @click="handleViewCode">代码</el-button>
  32. </div>
  33. <!-- 显示当前灯光信息 -->
  34. <div v-if="state.lamp.isLightOn" class="lamp-info">
  35. <p>颜色: {{ state.lamp.colorLog }}色</p>
  36. <p>亮度: {{ state.lamp.brightness }}%</p>
  37. <!-- 音乐播放状态显示和控制按钮 -->
  38. <div v-if="state.isMusicPlaying" class="music-info">
  39. <p>正在播放: {{ state.currentMusicName }}</p>
  40. <button class="stop-music-btn" @click="handleStopMusic">
  41. 停止播放
  42. </button>
  43. </div>
  44. </div>
  45. </div>
  46. <!-- Blockly编程界面 -->
  47. <div class="container">
  48. <!-- 返回智能台灯 -->
  49. <div class="title-box">
  50. <div class="box-icon" @click="goBack">
  51. <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
  52. 返回智能台灯
  53. </div>
  54. </div>
  55. <!-- 工具箱-->
  56. <div class="content">
  57. <div class="toolbox-section">
  58. <h2>工具箱</h2>
  59. <div id="toolbox" style="display: none;">
  60. <!-- 添加AI模块分类 -->
  61. <category name="AI模块" categorystyle="ai_category">
  62. <block type="ai_voice_input"></block>
  63. <block type="ai_text_to_image"></block>
  64. <block type="ai_text_to_video"></block>
  65. <block type="ai_text_to_text"></block>
  66. <block type="ai_smart_lamp_single_param"></block>
  67. <block type="ai_smart_lamp"></block>
  68. <block type="ai_lamp_set_brightness"></block>
  69. <block type="ai_lamp_set_color"></block>
  70. <block type="ai_music_play"></block>
  71. </category>
  72. <category name="逻辑" colour="%{BKY_LOGIC_HUE}">
  73. <block type="controls_if"></block>
  74. <block type="logic_compare"></block>
  75. <block type="logic_operation"></block>
  76. <block type="logic_negate"></block>
  77. <block type="logic_boolean"></block>
  78. </category>
  79. <category name="循环" colour="%{BKY_LOOPS_HUE}">
  80. <block type="controls_repeat_ext">
  81. <value name="TIMES">
  82. <shadow type="math_number">
  83. <field name="NUM">10</field>
  84. </shadow>
  85. </value>
  86. </block>
  87. <block type="controls_whileUntil"></block>
  88. </category>
  89. <category name="数学" colour="%{BKY_MATH_HUE}">
  90. <block type="math_number"></block>
  91. <block type="math_arithmetic"></block>
  92. <block type="math_single"></block>
  93. </category>
  94. <category name="文本" colour="%{BKY_TEXTS_HUE}">
  95. <block type="text"></block>
  96. <block type="text_length"></block>
  97. <block type="text_print"></block>
  98. </category>
  99. <category name="变量" colour="%{BKY_VARIABLES_HUE}" custom="VARIABLE"></category>
  100. </div>
  101. <div class="json-section">
  102. <h3>JSON 数据</h3>
  103. <textarea v-model="jsonData" placeholder="在此输入JSON格式的积木块数据..."></textarea>
  104. <div class="controls">
  105. <button @click="loadWorkspaceFromJson">加载JSON到工作区</button>
  106. <button @click="exportWorkspaceToJson">导出工作区为JSON</button>
  107. </div>
  108. <div v-if="statusMessage" :class="['status', statusType]">
  109. {{ statusMessage }}
  110. </div>
  111. </div>
  112. <!-- 在template部分的适当位置音频播放器组件 -->
  113. <div class="music-player-container" v-if="state.currentMusicUrl">
  114. <h5>音乐播放</h5>
  115. <audio
  116. ref="musicPlayer"
  117. :src="state.currentMusicUrl"
  118. @ended="handleMusicEnded"
  119. preload="metadata">
  120. 您的浏览器不支持音频元素
  121. </audio>
  122. <div class="music-status">
  123. <p v-if="state.isMusicPlaying">正在播放: {{ state.currentMusicName }}</p>
  124. <p v-else>准备就绪</p>
  125. </div>
  126. <!-- 停止播放按钮 - 修复点击事件 -->
  127. <el-button
  128. v-if="state.isMusicPlaying"
  129. type="danger"
  130. size="small"
  131. @click="handleStopMusic"
  132. style="margin-top: 10px;">
  133. 停止播放
  134. </el-button>
  135. </div>
  136. </div>
  137. <!-- 工作区-->
  138. <div class="workspace-section">
  139. <h2>工作区</h2>
  140. <div id="blocklyDiv"></div>
  141. <div class="controls">
  142. <button id="generateCode" @click="generateCode('javascript')">生成JavaScript代码</button>
  143. <button id="generateCode" @click="generateCode('python')">生成Python代码</button>
  144. <button id="runCode" @click="runCode">运行代码</button>
  145. <button @click="clearWorkspace">清空工作区</button>
  146. </div>
  147. </div>
  148. <!-- 输出-->
  149. <div class="output-section">
  150. <h2>输出</h2>
  151. <div class="controls">
  152. <button @click="clearOutput">清空输出</button>
  153. </div>
  154. <pre id="output">{{ output }}</pre>
  155. </div>
  156. <!-- AI结果预览模态框 -->
  157. <div v-if="state.previewVisible" class="preview-modal" @click="handleClosePreview">
  158. <div class="preview-content" @click.stop>
  159. <button class="close-button" @click="handleClosePreview">&times;</button>
  160. <div v-if="state.previewType === 'image'" class="preview-image-container">
  161. <img :src="state.previewContent" alt="AI生成图片" class="preview-image">
  162. </div>
  163. <div v-else-if="state.previewType === 'video'" class="preview-video-container">
  164. <video :src="state.previewContent" controls class="preview-video"></video>
  165. </div>
  166. <div v-else-if="state.previewType === 'text'" class="preview-text-container">
  167. {{ state.previewContent }}
  168. </div>
  169. </div>
  170. </div>
  171. </div>
  172. </div>
  173. </template>
  174. <script setup>
  175. // 仅保留必要的导入和主组件逻辑
  176. import { ref, onMounted, onUnmounted, reactive } from 'vue';
  177. import { useRouter } from 'vue-router';
  178. import { ArrowLeftBold } from '@element-plus/icons-vue';
  179. import * as Blockly from "blockly";
  180. import 'blockly/msg/zh-hans';
  181. import { javascriptGenerator } from "blockly/javascript";
  182. import { pythonGenerator } from "blockly/python";
  183. // 【文生图】文生图
  184. import {
  185. AiImageStatusEnum,
  186. CreatePainting,
  187. PaintingGetMys,
  188. CreateVideo,
  189. VideoGetMys,
  190. sendChatMessageStream,
  191. CreateDialogue
  192. } from "@/api/questions.js";
  193. import { getModelIdByType, ModelPlatformEnum } from "@/api/teachers.js";
  194. import { ModelTypeEnum } from "@/api/teachers.js";
  195. import { globalState } from "@/utils/globalState.js";
  196. //音乐
  197. import { playMusic, stopMusic, onMusicEnded } from "@/api/blockly/music.js";
  198. import {ElButton} from "element-plus";
  199. const router = useRouter();
  200. // 台灯预览显示状态
  201. const showLampPreview = ref(true);
  202. // 语音识别
  203. const isRecording = ref(false);
  204. const recordingCountdown = ref(10);
  205. let countdownInterval = null;
  206. // 返回虚拟实验室
  207. const goBackLab = () => {
  208. router.push("/virtual-laboratory");
  209. };
  210. const goBack = () => {
  211. showLampPreview.value = true;
  212. };
  213. // 切换灯光状态
  214. const toggleLight = () => {
  215. state.lamp.isLightOn = true;
  216. generateCode('javascript');
  217. // 在运行前设置为正在录音状态
  218. startRecordingStatus();
  219. runCode();
  220. };
  221. // 添加开始录音状态函数
  222. const handleMusicEnded = () => {
  223. onMusicEnded(state);
  224. };
  225. // 添加开始录音状态函数
  226. function startRecordingStatus() {
  227. isRecording.value = true;
  228. recordingCountdown.value = 10;
  229. // 清除之前的定时器
  230. if (countdownInterval) {
  231. clearInterval(countdownInterval);
  232. }
  233. // 设置新的倒计时
  234. countdownInterval = setInterval(() => {
  235. recordingCountdown.value--;
  236. if (recordingCountdown.value <= 0) {
  237. clearInterval(countdownInterval);
  238. endRecordingStatus();
  239. }
  240. }, 1000);
  241. }
  242. // 添加结束录音状态函数
  243. function endRecordingStatus() {
  244. isRecording.value = false;
  245. if (countdownInterval) {
  246. clearInterval(countdownInterval);
  247. countdownInterval = null;
  248. }
  249. }
  250. // 查看代码编程界面显示状态
  251. const handleViewCode = () => {
  252. showLampPreview.value = false;
  253. };
  254. // 响应式变量
  255. const jsonData = ref(`{
  256. "blocks": {
  257. "languageVersion": 0,
  258. "blocks": [
  259. {
  260. "type": "variables_set",
  261. "id": "kM:Fgf:wd4U3Z$j0x8oK",
  262. "x": 90,
  263. "y": 130,
  264. "fields": {
  265. "VAR": {
  266. "id": "MHW(ZbOKhL!/An\`5N@6\`"
  267. }
  268. },
  269. "inputs": {
  270. "VALUE": {
  271. "block": {
  272. "type": "ai_voice_input",
  273. "id": "l5E=g|1L+4hThQ8v})lQ",
  274. "fields": {
  275. "LANGUAGE": "zh-CN"
  276. },
  277. "inputs": {
  278. "PROMPT": {
  279. "block": {
  280. "type": "text",
  281. "id": "Q*n.c_)@7j^E2=s5/X!n",
  282. "fields": {
  283. "TEXT": "请发言:"
  284. }
  285. }
  286. }
  287. }
  288. }
  289. }
  290. },
  291. "next": {
  292. "block": {
  293. "type": "variables_set",
  294. "id": "]g.xbBe.i=a9B*Kfw@|\`",
  295. "fields": {
  296. "VAR": {
  297. "id": "zn.7{ZqbUaH1?P,R05hF"
  298. }
  299. },
  300. "inputs": {
  301. "VALUE": {
  302. "block": {
  303. "type": "ai_text_to_text",
  304. "id": "R$h+R!6#@+4=+WX1*nvh",
  305. "inputs": {
  306. "PROMPT": {
  307. "block": {
  308. "type": "variables_get",
  309. "id": "h$S$nt)3VU.=nX*W-mo~",
  310. "fields": {
  311. "VAR": {
  312. "id": "MHW(ZbOKhL!/An\`5N@6\`"
  313. }
  314. }
  315. }
  316. },
  317. "提示词": {
  318. "block": {
  319. "type": "text",
  320. "id": "7k%sgLP?i]e[,m^49P++",
  321. "fields": {
  322. "TEXT": "请只回复我指定格式:白,100,热闹"
  323. }
  324. }
  325. }
  326. }
  327. }
  328. }
  329. },
  330. "next": {
  331. "block": {
  332. "type": "ai_smart_lamp_single_param",
  333. "id": "!.0;Ktwm+Z?o8_9FRa}G",
  334. "inputs": {
  335. "PARAMS": {
  336. "block": {
  337. "type": "variables_get",
  338. "id": "d{cIJ-kEFFQcn~%A,g@g",
  339. "fields": {
  340. "VAR": {
  341. "id": "zn.7{ZqbUaH1?P,R05hF"
  342. }
  343. }
  344. }
  345. }
  346. }
  347. }
  348. }
  349. }
  350. }
  351. }
  352. ]
  353. },
  354. "variables": [
  355. {
  356. "name": "inputText",
  357. "id": "MHW(ZbOKhL!/An\`5N@6\`"
  358. },
  359. {
  360. "name": "lampConfig",
  361. "id": "zn.7{ZqbUaH1?P,R05hF"
  362. }
  363. ]
  364. }`);
  365. //输出结果
  366. const output = ref('');
  367. const statusMessage = ref('');
  368. const statusType = ref('');
  369. let workspace = null;
  370. // 创建音乐播放器引用
  371. const musicPlayer = ref(null);
  372. // 状态管理
  373. const state = reactive({
  374. workspace: null,
  375. generatedContent: {
  376. imageUrl: "",
  377. videoUrl: "",
  378. text: "",
  379. },
  380. previewVisible: false,
  381. previewType: "",
  382. previewContent: "",
  383. isProcessing: false,
  384. //年级
  385. gradeId: "",
  386. //【文生图】文生图
  387. inProgressImageMap: {},
  388. //【文生视频】文生视频
  389. inProgressVideoMap: {},
  390. // 台灯状态
  391. lamp: {
  392. isLightOn: true,// 台灯是否亮着
  393. brightness: 50, // 默认亮度50%
  394. color: "#ffffff", // 默认颜色白色
  395. colorLog: "白", // 默认颜色白色
  396. },
  397. // 【文本文】对话相关状态
  398. activeConversationId: null,
  399. conversationInAbortController: null,
  400. // 独立的音乐播放状态
  401. currentMusicUrl: '',
  402. currentMusicName: '',
  403. isMusicPlaying: false,
  404. });
  405. // 统一轮询管理器
  406. const pollingManager = {
  407. timers: {},
  408. // 启动轮询
  409. startPolling(type, callback, interval = 3000) {
  410. // 如果已有相同类型的轮询,先清除
  411. this.stopPolling(type);
  412. this.timers[type] = setInterval(async () => {
  413. try {
  414. await callback();
  415. } catch (error) {
  416. console.error(`${type}轮询失败:`, error);
  417. }
  418. }, interval);
  419. return this.timers[type];
  420. },
  421. // 停止轮询
  422. stopPolling(type) {
  423. if (this.timers[type]) {
  424. clearInterval(this.timers[type]);
  425. this.timers[type] = null;
  426. }
  427. },
  428. // 停止所有轮询
  429. stopAll() {
  430. Object.keys(this.timers).forEach(type => this.stopPolling(type));
  431. }
  432. };
  433. // 统一的错误处理包装器
  434. function withErrorHandling(operationName, fn, errorMessage = null) {
  435. return async function(...args) {
  436. try {
  437. state.isProcessing = true;
  438. return await fn.apply(this, args);
  439. } catch (error) {
  440. console.error(`${operationName}失败:`, error);
  441. showStatus(errorMessage || `${operationName}发生错误: ${error.message || '未知错误'}`);
  442. return null;
  443. } finally {
  444. state.isProcessing = false;
  445. }
  446. };
  447. }
  448. // 任务状态轮询公共函数
  449. async function pollTaskStatus(taskType, taskIds, fetchApi, onSuccess, onFailure) {
  450. if (taskIds.length === 0) {
  451. pollingManager.stopPolling(taskType);
  452. return {};
  453. }
  454. try {
  455. const list = await fetchApi(taskIds);
  456. const activeTasks = {};
  457. list.data.forEach((task) => {
  458. if (task.status === AiImageStatusEnum.IN_PROGRESS) {
  459. activeTasks[task.id] = task;
  460. } else if (task.status === AiImageStatusEnum.SUCCESS) {
  461. // 任务成功完成
  462. if (onSuccess) {
  463. onSuccess(task);
  464. }
  465. } else if (task.status === AiImageStatusEnum.FAIL) {
  466. // 任务失败
  467. if (onFailure) {
  468. onFailure(task);
  469. }
  470. }
  471. });
  472. return activeTasks;
  473. } catch (error) {
  474. console.error(`${taskType}状态轮询失败:`, error);
  475. return {};
  476. }
  477. }
  478. // AI服务模块 - 统一管理
  479. const aiService = {
  480. // 语音识别
  481. recognizeVoice: withErrorHandling('语音识别', async function(promptText = "", language = "zh-CN") {
  482. console.log("语音识别开始");
  483. // 前端语音采集
  484. const recognitionResult = await this.captureVoice(language, promptText);
  485. return recognitionResult || "";
  486. }, '语音识别失败'),
  487. // 前端语音采集
  488. captureVoice(language, promptText) {
  489. return new Promise((resolve) => {
  490. if (
  491. !"webkitSpeechRecognition" in window &&
  492. !"SpeechRecognition" in window
  493. ) {
  494. showStatus("您的浏览器不支持语音识别功能");
  495. resolve("");
  496. return;
  497. }
  498. const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
  499. const recognition = new SpeechRecognition();
  500. recognition.lang = language;
  501. recognition.interimResults = false;
  502. recognition.maxAlternatives = 1;
  503. let countdown = 10;
  504. // 固定的消息提示框
  505. const messageText = promptText ? `${promptText}\n请开始说话...(${countdown}秒)` : `请开始说话...(${countdown}秒)`;
  506. showStatus(messageText);
  507. // 倒计时
  508. const timer = setInterval(() => {
  509. countdown--;
  510. if (countdown > 0) {
  511. const newText = promptText ? `${promptText}\n请开始说话...(${countdown}秒)` : `请开始说话...(${countdown}秒)`;
  512. showStatus(newText);
  513. } else {
  514. clearInterval(timer);
  515. }
  516. }, 1000);
  517. recognition.onresult = (event) => {
  518. const speechResult = event.results[0][0].transcript;
  519. console.log("语音识别结果:", speechResult);
  520. showStatus("语音识别完成");
  521. resolve(speechResult);
  522. };
  523. recognition.onerror = (event) => {
  524. console.error("语音识别错误:", event.error);
  525. showStatus("语音识别发生错误: " + event.error, 'error');
  526. resolve("");
  527. };
  528. recognition.start();
  529. });
  530. },
  531. // 文本生成图片
  532. textToImage: withErrorHandling('AI图片生成', async function(prompt, waitForCompletion = true) {
  533. console.log("AI图片生成中,提示词:", prompt);
  534. //获取文生图-模型id
  535. const modelRes = await getModelIdByType({
  536. type: ModelTypeEnum.TEXT_TO_IMAGE,
  537. platform: ModelPlatformEnum.DOUBAO,
  538. });
  539. if (!modelRes.data) {
  540. showStatus("获取模型ID失败", 'error');
  541. return null;
  542. }
  543. // 使用CreatePainting API创建图片任务
  544. const createRes = await CreatePainting({
  545. modelId: modelRes.data,
  546. prompt: prompt,
  547. width: 1024,
  548. height: 1024,
  549. });
  550. // 记录任务ID到映射中
  551. state.inProgressImageMap[createRes.data] = {
  552. id: createRes.data,
  553. status: AiImageStatusEnum.IN_PROGRESS,
  554. };
  555. // 开始轮询任务状态
  556. this.startPollingTasks('image');
  557. // 如果需要等待完成,等待图片生成完成
  558. if (waitForCompletion) {
  559. console.log("AI图片生成中,请等待。。。:");
  560. return await this.waitForImageCompletion(createRes.data);
  561. }
  562. return createRes.data; // 返回任务ID
  563. }, '生成图片失败'),
  564. // 【文生图】等待图片生成完成
  565. waitForImageCompletion(imageId) {
  566. return new Promise((resolve, reject) => {
  567. const checkInterval = setInterval(async () => {
  568. try {
  569. const list = await PaintingGetMys([imageId]);
  570. if (list.data && list.data.length > 0) {
  571. const image = list.data[0];
  572. if (image.status === AiImageStatusEnum.SUCCESS) {
  573. clearInterval(checkInterval);
  574. resolve(image.picUrl);
  575. } else if (image.status === AiImageStatusEnum.FAIL) {
  576. clearInterval(checkInterval);
  577. reject(new Error(image.error || "图片生成失败"));
  578. }
  579. }
  580. } catch (error) {
  581. clearInterval(checkInterval);
  582. reject(error);
  583. }
  584. }, 3000);
  585. });
  586. },
  587. // 文本生成视频
  588. textToVideo: withErrorHandling('AI视频生成', async function(prompt, waitForCompletion = true) {
  589. console.log("AI视频生成中,提示词:", prompt);
  590. //获取视频生成模型id
  591. const modelRes = await getModelIdByType({
  592. type: ModelTypeEnum.IMAGE_TO_VIDEO,
  593. platform: ModelPlatformEnum.DOUBAO,
  594. });
  595. if (!modelRes.data) {
  596. showStatus("获取模型ID失败", 'error');
  597. return null;
  598. }
  599. // 使用CreateVideo API创建视频任务
  600. const createRes = await CreateVideo({
  601. modelId: modelRes.data,
  602. prompt: prompt,
  603. duration: 4,
  604. resolution: "1080P",
  605. });
  606. // 记录任务ID
  607. state.inProgressVideoMap[createRes.data] = {
  608. id: createRes.data,
  609. status: AiImageStatusEnum.IN_PROGRESS,
  610. };
  611. console.log("AI视频生成中,请等待。。。");
  612. // 启动统一的轮询机制
  613. this.startPollingTasks('video');
  614. // 如果需要等待完成,使用Promise封装结果
  615. if (waitForCompletion) {
  616. return new Promise((resolve, reject) => {
  617. // 设置一次性的状态检查
  618. const checkStatus = () => {
  619. const videoInfo = state.generatedContent.videoUrl;
  620. if (videoInfo && videoInfo.includes(createRes.data)) {
  621. resolve(videoInfo);
  622. } else if (state.inProgressVideoMap[createRes.data]?.status === AiImageStatusEnum.FAIL) {
  623. reject(new Error("视频生成失败"));
  624. } else if (!state.inProgressVideoMap[createRes.data]) {
  625. reject(new Error("视频任务已不存在"));
  626. } else {
  627. // 继续检查
  628. setTimeout(checkStatus, 1000);
  629. }
  630. };
  631. checkStatus();
  632. });
  633. }
  634. return createRes.data; // 返回任务ID
  635. }, '生成视频失败'),
  636. // 文本生成文本(如AI对话)
  637. textToText: withErrorHandling('AI大模型调用', async function(prompt, model = "default") {
  638. console.log("AI智能体请求,输入文本:", prompt);
  639. // 如果没有活跃的对话ID,创建新对话
  640. if (!state.activeConversationId) {
  641. // 使用与TextToText.vue相同的方式创建对话
  642. const res = await CreateDialogue({ roleId: 75 });
  643. state.activeConversationId = res.data;
  644. console.log("AI智能体创建成功,请等待。。。");
  645. }
  646. // 创建AbortController实例
  647. state.conversationInAbortController = new AbortController();
  648. // 使用流式API发送消息
  649. let resultText = "";
  650. let isFirstChunk = true;
  651. await sendChatMessageStream(
  652. state.activeConversationId,
  653. prompt,
  654. null,
  655. state.conversationInAbortController,
  656. true, // 启用上下文
  657. async (res) => {
  658. try {
  659. const { code, data, msg } = JSON.parse(res.data);
  660. if (code !== 0) {
  661. console.log(`对话异常! ${msg}`);
  662. return;
  663. }
  664. // 根据事件类型处理
  665. if (data.eventType === "TEXT") {
  666. // 如果内容为空,就不处理
  667. if (data.receive?.content === "") {
  668. return;
  669. }
  670. // 处理文本消息
  671. resultText += data.receive.content;
  672. // 首次返回时更新预览内容
  673. if (isFirstChunk) {
  674. isFirstChunk = false;
  675. // 设置预览内容
  676. state.generatedContent.text = resultText;
  677. state.previewType = "text";
  678. state.previewContent = resultText;
  679. } else {
  680. // 更新预览内容
  681. state.generatedContent.text = resultText;
  682. state.previewContent = resultText;
  683. }
  684. }
  685. } catch (error) {
  686. console.error("处理流式响应失败:", error);
  687. }
  688. },
  689. (error) => {
  690. console.log(`对话异常! ${error}`);
  691. this.stopTextToTextStream();
  692. throw error;
  693. },
  694. () => {
  695. // console.log(`结束对话!`);
  696. this.stopTextToTextStream();
  697. }
  698. );
  699. // 确保最终结果被设置
  700. if (resultText) {
  701. console.log("AI大模型调用成功,返回结果:", resultText);
  702. state.generatedContent.text = resultText;
  703. state.previewType = "text";
  704. state.previewContent = resultText;
  705. }
  706. return resultText;
  707. }, 'AI大模型调用失败'),
  708. // 停止文本生成流
  709. stopTextToTextStream() {
  710. if (state.conversationInAbortController) {
  711. state.conversationInAbortController.abort();
  712. }
  713. },
  714. // 设置台灯亮度
  715. setLampBrightness: withErrorHandling('设置台灯亮度', async function(brightness) {
  716. // 验证亮度值在0-100之间
  717. const validBrightness = Math.max(0, Math.min(100, parseInt(brightness) || 0));
  718. // 更新状态
  719. state.lamp.brightness = validBrightness;
  720. // 模拟API调用(实际项目中可替换为真实API)
  721. console.log(`智能台灯亮度已设置为: ${validBrightness}%`);
  722. return validBrightness;
  723. }, '设置台灯亮度失败'),
  724. // 设置台灯颜色
  725. setLampColor: withErrorHandling('设置台灯颜色', async function(color) {
  726. // 预定义的颜色映射
  727. const colorMap = {
  728. '紫': '#D886F0',
  729. '橙': '#F89E35',
  730. '黄': '#F9E67E',
  731. '青': '#6BF5E6',
  732. '白': '#ffffff',
  733. };
  734. // 获取有效的颜色值
  735. let validColor = colorMap[color] || color;
  736. // 检查是否是有效的颜色格式
  737. if (!/^#[0-9A-F]{6}$/i.test(validColor)) {
  738. validColor = "#ffffff"; // 默认白色
  739. }
  740. // 更新状态
  741. state.lamp.color = validColor;
  742. state.lamp.colorLog = color;
  743. // 模拟API调用(实际项目中可替换为真实API)
  744. console.log(`智能台灯颜色已设置为: ${color}`);
  745. return validColor;
  746. }, '设置台灯颜色失败'),
  747. /**
  748. * 播放音乐功能
  749. * @param {string} musicType - 音乐类型
  750. * @returns {Promise<void>} - 返回Promise
  751. */
  752. playMusic: withErrorHandling('播放音乐', async function(musicType) {
  753. return playMusic(musicType, state, musicPlayer);
  754. }, '播放音乐失败'),
  755. /**
  756. * 停止音乐播放
  757. * @returns {Promise<void>} - 返回Promise
  758. */
  759. stopMusic: withErrorHandling('停止音乐', async function() {
  760. return stopMusic(state, musicPlayer);
  761. }, '停止音乐失败'),
  762. // 综合控制台灯(参数格式:"颜色, 亮度, 音乐")
  763. controlLampWithSingleParam: withErrorHandling('智能台灯综合控制', async function(params) {
  764. // 解析参数字符串
  765. let color = '白'; // 默认颜色
  766. let brightness = 0; // 默认亮度
  767. let music = ''; // 音乐信息
  768. if (params && typeof params === 'string') {
  769. // 根据逗号分割参数
  770. const paramArray = params.split(',').map(p => p.trim());
  771. // 提取颜色(第一个参数)
  772. if (paramArray.length > 0 && paramArray[0]) {
  773. color = paramArray[0];
  774. }
  775. // 提取亮度(第二个参数)
  776. if (paramArray.length > 1 && paramArray[1]) {
  777. brightness = paramArray[1];
  778. }
  779. // 提取音乐(第三个参数)
  780. if (paramArray.length > 2 && paramArray[2]) {
  781. music = paramArray[2];
  782. // 调用音乐播放函数
  783. await this.playMusic(music);
  784. }
  785. }
  786. // 调用控制台灯方法
  787. return await this.controlLamp(brightness, color);
  788. }, '智能台灯综合控制失败'),
  789. // 综合控制台灯(参数格式:"颜色, 亮度")
  790. controlLamp: withErrorHandling('智能台灯控制', async function(brightness, color) {
  791. // 先设置亮度
  792. await this.setLampBrightness(brightness);
  793. // 再设置颜色
  794. await this.setLampColor(color);
  795. return { brightness: state.lamp.brightness, color: state.lamp.color };
  796. }, '智能台灯控制失败'),
  797. // 启动任务轮询
  798. startPollingTasks(type) {
  799. if (type === 'image' || type === 'all') {
  800. pollingManager.startPolling('image', async () => {
  801. const imageIds = Object.keys(state.inProgressImageMap).map(Number);
  802. state.inProgressImageMap = await pollTaskStatus(
  803. 'image',
  804. imageIds,
  805. PaintingGetMys,
  806. (image) => {
  807. state.generatedContent.imageUrl = image.picUrl;
  808. state.previewType = "image";
  809. state.previewContent = image.picUrl;
  810. state.previewVisible = true;
  811. console.log("AI图片生成完成:", image.picUrl);
  812. },
  813. (image) => {
  814. showStatus("图片生成失败: " + (image.error || "未知错误"), 'error');
  815. console.error("图片生成失败:", image.id, image.error);
  816. }
  817. );
  818. });
  819. }
  820. if (type === 'video' || type === 'all') {
  821. pollingManager.startPolling('video', async () => {
  822. const videoIds = Object.keys(state.inProgressVideoMap).map(Number);
  823. state.inProgressVideoMap = await pollTaskStatus(
  824. 'video',
  825. videoIds,
  826. VideoGetMys,
  827. (video) => {
  828. state.generatedContent.videoUrl = video.videoUrl;
  829. state.previewType = "video";
  830. state.previewContent = video.videoUrl;
  831. state.previewVisible = true;
  832. console.log("AI视频生成完成:", video.videoUrl);
  833. },
  834. (video) => {
  835. showStatus("视频生成失败: " + (video.error || "未知错误"), 'error');
  836. console.error("视频生成失败:", video.id, video.error);
  837. }
  838. );
  839. });
  840. }
  841. }
  842. };
  843. // 专门的停止音乐处理函数
  844. const handleStopMusic = () => {
  845. // 直接调用导入的stopMusic函数并传递正确的参数
  846. stopMusic(state, musicPlayer);
  847. // 提示信息
  848. showStatus('音乐已停止播放');
  849. };
  850. // 关闭预览
  851. const handleClosePreview = () => {
  852. state.previewVisible = false;
  853. state.previewContent = "";
  854. state.previewType = "";
  855. state.generatedContent.text = null;
  856. state.generatedContent.imageUrl = null;
  857. state.generatedContent.videoUrl = null;
  858. };
  859. // 组件挂载后初始化Blockly
  860. onMounted(() => {
  861. // 从全局状态初始化年级ID
  862. state.gradeId = globalState.initGradeId();
  863. // 注册AI语音识别积木
  864. Blockly.Blocks["ai_voice_input"] = {
  865. init: function () {
  866. this.appendDummyInput().appendField("语音识别");
  867. this.appendValueInput("PROMPT")
  868. .setCheck("String")
  869. .appendField("提示文字:");
  870. this.appendDummyInput()
  871. .appendField("语言:")
  872. .appendField(
  873. new Blockly.FieldDropdown([
  874. ["中文", "zh-CN"],
  875. // ["英文", "en-US"],
  876. ]),
  877. "LANGUAGE"
  878. );
  879. this.setOutput(true, "String");
  880. this.setColour(310);
  881. this.setTooltip("使用语音识别获取文本输入");
  882. this.setHelpUrl("");
  883. },
  884. };
  885. // 注册AI文本生成图片积木
  886. Blockly.Blocks["ai_text_to_image"] = {
  887. init: function () {
  888. this.appendDummyInput().appendField("AI生成图片");
  889. this.appendValueInput("PROMPT").setCheck("String").appendField("提示词:");
  890. this.appendDummyInput()
  891. .appendField("等待完成:", "WAIT_LABEL")
  892. .appendField(new Blockly.FieldCheckbox("TRUE"), "WAIT_FOR_COMPLETION");
  893. this.setInputsInline(false);
  894. this.setPreviousStatement(true, null);
  895. this.setNextStatement(true, null);
  896. this.setColour(340);
  897. this.setTooltip("使用AI将文本描述转换为图片");
  898. this.setHelpUrl("");
  899. },
  900. };
  901. // 注册AI文本生成视频积木
  902. Blockly.Blocks["ai_text_to_video"] = {
  903. init: function () {
  904. this.appendDummyInput().appendField("AI生成视频");
  905. this.appendValueInput("PROMPT").setCheck("String").appendField("提示词:");
  906. this.appendDummyInput()
  907. .appendField("等待完成:", "WAIT_LABEL")
  908. .appendField(new Blockly.FieldCheckbox("TRUE"), "WAIT_FOR_COMPLETION");
  909. this.setInputsInline(false);
  910. this.setPreviousStatement(true, null);
  911. this.setNextStatement(true, null);
  912. this.setColour(340);
  913. this.setTooltip("使用AI将文本描述转换为视频");
  914. this.setHelpUrl("");
  915. },
  916. };
  917. // 注册AI文本生成文本积木
  918. Blockly.Blocks["ai_text_to_text"] = {
  919. init: function () {
  920. this.appendDummyInput().appendField("AI大模型调用");
  921. this.appendValueInput("PROMPT")
  922. .setCheck("String")
  923. .appendField("输入文本:");
  924. this.appendValueInput("提示词")
  925. .setCheck("String")
  926. .appendField("提示词:");
  927. this.setOutput(true, "String");
  928. this.setColour(300);
  929. this.setTooltip("使用AI大模型调用并返回结果");
  930. this.setHelpUrl("");
  931. },
  932. };
  933. //AI智能台灯单参数积木
  934. Blockly.Blocks['ai_smart_lamp_single_param'] = {
  935. init: function() {
  936. this.appendDummyInput()
  937. .appendField('智能台灯控制(单参数)');
  938. this.appendValueInput('PARAMS')
  939. .setCheck('String')
  940. .appendField('参数(格式: 颜色,亮度,音乐):');
  941. this.appendDummyInput()
  942. .appendField('例如: 蓝,50,平静');
  943. this.setInputsInline(false);
  944. this.setPreviousStatement(true, null);
  945. this.setNextStatement(true, null);
  946. this.setColour(280);
  947. this.setTooltip('通过一个参数字符串控制智能台灯的亮度、颜色和音乐\n格式: 颜色,亮度,音乐\n例如: 蓝,50,平静');
  948. this.setHelpUrl('');
  949. }
  950. };
  951. // 注册AI智能台灯积木
  952. Blockly.Blocks["ai_smart_lamp"] = {
  953. init: function () {
  954. this.appendDummyInput().appendField("智能台灯控制");
  955. this.appendValueInput("BRIGHTNESS")
  956. .setCheck(["Number", "String"])
  957. .appendField("亮度 (0-100):");
  958. this.appendValueInput("COLOR").setCheck("String").appendField("颜色:");
  959. this.setInputsInline(false);
  960. this.setPreviousStatement(true, null);
  961. this.setNextStatement(true, null);
  962. this.setColour(280);
  963. this.setTooltip("控制智能台灯的亮度和颜色");
  964. this.setHelpUrl("");
  965. },
  966. };
  967. // 注册AI台灯设置亮度积木
  968. Blockly.Blocks["ai_lamp_set_brightness"] = {
  969. init: function () {
  970. this.appendDummyInput().appendField("设置台灯亮度");
  971. this.appendValueInput("BRIGHTNESS")
  972. .setCheck(["Number", "String"])
  973. .appendField("亮度 (0-100):");
  974. this.setInputsInline(false);
  975. this.setPreviousStatement(true, null);
  976. this.setNextStatement(true, null);
  977. this.setColour(270);
  978. this.setTooltip("设置智能台灯的亮度");
  979. this.setHelpUrl("");
  980. },
  981. };
  982. // 注册AI台灯设置颜色积木
  983. Blockly.Blocks["ai_lamp_set_color"] = {
  984. init: function () {
  985. this.appendDummyInput().appendField("设置台灯颜色");
  986. this.appendValueInput("COLOR").setCheck("String").appendField("颜色:");
  987. this.appendDummyInput().appendField("可选颜色: 白,黄,紫,橙,青");
  988. this.setInputsInline(false);
  989. this.setPreviousStatement(true, null);
  990. this.setNextStatement(true, null);
  991. this.setColour(275);
  992. this.setTooltip("设置智能台灯的颜色");
  993. this.setHelpUrl("");
  994. },
  995. };
  996. // 注册音乐播放积木
  997. Blockly.Blocks['ai_music_play'] = {
  998. init: function() {
  999. this.appendDummyInput()
  1000. .appendField('播放音乐');
  1001. this.appendDummyInput()
  1002. .appendField('音乐类型:')
  1003. .appendField(
  1004. new Blockly.FieldDropdown([
  1005. ['热闹', '热闹'],
  1006. ['舒缓', '舒缓'],
  1007. ]),
  1008. 'MUSIC_TYPE'
  1009. );
  1010. this.setInputsInline(false);
  1011. this.setPreviousStatement(true, null);
  1012. this.setNextStatement(true, null);
  1013. this.setColour(290);
  1014. this.setTooltip('播放指定类型的音乐');
  1015. this.setHelpUrl('');
  1016. }
  1017. };
  1018. // 注册JavaScript代码生成器
  1019. registerJavaScriptGenerators();
  1020. // 注册Python代码生成器
  1021. registerPythonGenerators();
  1022. // 初始化Blockly工作区
  1023. const blocklyDiv = document.getElementById('blocklyDiv');
  1024. const toolbox = document.getElementById('toolbox');
  1025. workspace = Blockly.inject(blocklyDiv, {
  1026. toolbox: toolbox,
  1027. collapse: true,
  1028. comments: true,
  1029. disable: false, // 设为false以允许编辑
  1030. maxBlocks: Infinity,
  1031. trashcan: true,
  1032. horizontalLayout: false,
  1033. toolboxPosition: 'start',
  1034. css: true,
  1035. media: 'https://unpkg.com/blockly/media/',
  1036. rtl: false,
  1037. scrollbars: true,
  1038. sounds: false, // 禁用声音以提高性能
  1039. oneBasedIndex: true,
  1040. grid: {
  1041. spacing: 20,
  1042. length: 3,
  1043. colour: "#ccc",
  1044. snap: true
  1045. },
  1046. zoom: {
  1047. controls: true,
  1048. wheel: true,
  1049. startScale: 1.0,
  1050. maxScale: 3,
  1051. minScale: 0.3,
  1052. scaleSpeed: 1.2
  1053. }
  1054. });
  1055. // 使用state.workspace替代workspace
  1056. state.workspace = workspace;
  1057. // 加载初始JSON数据
  1058. loadWorkspaceFromJson();
  1059. // 修改工作区变化监听器,使其包含拖拽修复逻辑
  1060. workspace.addChangeListener((event) => {
  1061. // 生成代码
  1062. generateCode("javascript");
  1063. // 拖拽修复逻辑
  1064. if (event.type === Blockly.Events.BLOCK_CREATE) {
  1065. const block = workspace.getBlockById(event.blockId);
  1066. if (block) {
  1067. block.setEditable(true);
  1068. }
  1069. }
  1070. });
  1071. // 将aiService挂载到window,以便执行生成的代码时可以访问
  1072. window.aiService = aiService;
  1073. });
  1074. // 组件卸载时清除所有资源
  1075. onUnmounted(() => {
  1076. // 清除所有定时器
  1077. if (countdownInterval) {
  1078. clearInterval(countdownInterval);
  1079. }
  1080. // 停止所有轮询
  1081. pollingManager.stopAll();
  1082. // 关闭语音识别(如果正在进行)
  1083. if (recognition) {
  1084. recognition.stop();
  1085. }
  1086. // 清理工作区
  1087. if (workspace) {
  1088. workspace.dispose();
  1089. }
  1090. // 清理音频资源
  1091. if (musicPlayer.value) {
  1092. musicPlayer.value.pause();
  1093. }
  1094. });
  1095. // 注册JavaScript代码生成器
  1096. function registerJavaScriptGenerators() {
  1097. // 语音识别
  1098. javascriptGenerator.forBlock['ai_voice_input'] = function(block, generator) {
  1099. const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
  1100. const language = block.getFieldValue('LANGUAGE');
  1101. const code = `await aiService.recognizeVoice(${prompt || "''"}, '${language}')`;
  1102. return [code, javascriptGenerator.ORDER_ATOMIC];
  1103. };
  1104. // 文本生成图片
  1105. javascriptGenerator.forBlock['ai_text_to_image'] = function(block, generator) {
  1106. const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
  1107. const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
  1108. const code = `await aiService.textToImage(${prompt}, ${waitForCompletion});`;
  1109. return code;
  1110. };
  1111. // 文本生成视频
  1112. javascriptGenerator.forBlock['ai_text_to_video'] = function(block, generator) {
  1113. const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
  1114. const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
  1115. const code = `await aiService.textToVideo(${prompt}, ${waitForCompletion});`;
  1116. return code;
  1117. };
  1118. // 文本生成文本
  1119. javascriptGenerator.forBlock['ai_text_to_text'] = function(block, generator) {
  1120. const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
  1121. const model = block.getFieldValue('MODEL');
  1122. const code = `await aiService.textToText(${prompt}, '${model}')`;
  1123. return [code, javascriptGenerator.ORDER_ATOMIC];
  1124. };
  1125. // 智能台灯控制(单参数)
  1126. javascriptGenerator.forBlock['ai_smart_lamp_single_param'] = function(block, generator) {
  1127. const params = generator.valueToCode(block, 'PARAMS', javascriptGenerator.ORDER_ATOMIC);
  1128. const code = `await aiService.controlLampWithSingleParam(${params || "'白,0,平静'"});`;
  1129. return code;
  1130. };
  1131. // 智能台灯控制(多参数)
  1132. javascriptGenerator.forBlock['ai_smart_lamp'] = function(block, generator) {
  1133. const brightness = generator.valueToCode(block, 'BRIGHTNESS', javascriptGenerator.ORDER_ATOMIC);
  1134. const color = generator.valueToCode(block, 'COLOR', javascriptGenerator.ORDER_ATOMIC);
  1135. const code = `await aiService.controlLamp(${brightness || '0'}, ${color || "'白'"});`;
  1136. return code;
  1137. };
  1138. // 设置台灯亮度
  1139. javascriptGenerator.forBlock['ai_lamp_set_brightness'] = function(block, generator) {
  1140. const brightness = generator.valueToCode(block, 'BRIGHTNESS', javascriptGenerator.ORDER_ATOMIC);
  1141. const code = `await aiService.setLampBrightness(${brightness || '0'});`;
  1142. return code;
  1143. };
  1144. // 设置台灯颜色
  1145. javascriptGenerator.forBlock['ai_lamp_set_color'] = function(block, generator) {
  1146. const color = generator.valueToCode(block, 'COLOR', javascriptGenerator.ORDER_ATOMIC);
  1147. const code = `await aiService.setLampColor(${color || "'白'"});`;
  1148. return code;
  1149. };
  1150. // 音乐播放
  1151. javascriptGenerator.forBlock['ai_music_play'] = function(block, generator) {
  1152. const musicType = block.getFieldValue('MUSIC_TYPE');
  1153. const code = `await aiService.playMusic('${musicType}');`;
  1154. return code;
  1155. };
  1156. }
  1157. // 注册Python代码生成器
  1158. function registerPythonGenerators() {
  1159. // 语音识别
  1160. pythonGenerator.forBlock['ai_voice_input'] = function(block, generator) {
  1161. const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
  1162. const language = block.getFieldValue('LANGUAGE');
  1163. const code = `ai_service.recognize_voice(${prompt || "''"}, '${language}')`;
  1164. return [code, pythonGenerator.ORDER_ATOMIC];
  1165. };
  1166. // 文本生成图片
  1167. pythonGenerator.forBlock['ai_text_to_image'] = function(block, generator) {
  1168. const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
  1169. const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
  1170. const code = `ai_service.text_to_image(${prompt}, ${waitForCompletion})\n`;
  1171. return code;
  1172. };
  1173. // 文本生成视频
  1174. pythonGenerator.forBlock['ai_text_to_video'] = function(block, generator) {
  1175. const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
  1176. const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
  1177. const code = `ai_service.text_to_video(${prompt}, ${waitForCompletion})\n`;
  1178. return code;
  1179. };
  1180. // 文本生成文本
  1181. pythonGenerator.forBlock['ai_text_to_text'] = function(block, generator) {
  1182. const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
  1183. const model = block.getFieldValue('MODEL');
  1184. const code = `ai_service.text_to_text(${prompt}, '${model}')`;
  1185. return [code, pythonGenerator.ORDER_ATOMIC];
  1186. };
  1187. // 智能台灯控制(单参数)
  1188. pythonGenerator.forBlock['ai_smart_lamp_single_param'] = function(block, generator) {
  1189. const params = generator.valueToCode(block, 'PARAMS', pythonGenerator.ORDER_ATOMIC);
  1190. const code = `ai_service.control_lamp_with_single_param(${params || "'白,0,平静'"})\n`;
  1191. return code;
  1192. };
  1193. // 智能台灯控制
  1194. pythonGenerator.forBlock['ai_smart_lamp'] = function(block, generator) {
  1195. const brightness = generator.valueToCode(block, 'BRIGHTNESS', pythonGenerator.ORDER_ATOMIC);
  1196. const color = generator.valueToCode(block, 'COLOR', pythonGenerator.ORDER_ATOMIC);
  1197. const code = `ai_service.control_lamp(${brightness || '0'}, ${color || "'白'"})\n`;
  1198. return code;
  1199. };
  1200. // 设置台灯亮度
  1201. pythonGenerator.forBlock['ai_lamp_set_brightness'] = function(block, generator) {
  1202. const brightness = generator.valueToCode(block, 'BRIGHTNESS', pythonGenerator.ORDER_ATOMIC);
  1203. const code = `ai_service.set_lamp_brightness(${brightness || '0'})\n`;
  1204. return code;
  1205. };
  1206. // 设置台灯颜色
  1207. pythonGenerator.forBlock['ai_lamp_set_color'] = function(block, generator) {
  1208. const color = generator.valueToCode(block, 'COLOR', pythonGenerator.ORDER_ATOMIC);
  1209. const code = `ai_service.set_lamp_color(${color || "'白'"})\n`;
  1210. return code;
  1211. };
  1212. // 音乐播放
  1213. pythonGenerator.forBlock['ai_music_play'] = function(block, generator) {
  1214. const musicType = block.getFieldValue('MUSIC_TYPE');
  1215. const code = `ai_service.play_music('${musicType}')\n`;
  1216. return code;
  1217. };
  1218. }
  1219. // 从JSON加载工作区
  1220. const loadWorkspaceFromJson = () => {
  1221. try {
  1222. const json = JSON.parse(jsonData.value);
  1223. Blockly.serialization.workspaces.load(json, workspace);
  1224. showStatus('工作区已成功从JSON加载!');
  1225. } catch (error) {
  1226. showStatus('JSON解析错误: ' + error.message, 'error');
  1227. console.error('JSON解析错误:', error);
  1228. }
  1229. };
  1230. // 导出工作区为JSON
  1231. const exportWorkspaceToJson = () => {
  1232. try {
  1233. const state = Blockly.serialization.workspaces.save(workspace);
  1234. jsonData.value = JSON.stringify(state, null, 2);
  1235. showStatus('工作区已成功导出为JSON!');
  1236. } catch (error) {
  1237. showStatus('导出错误: ' + error.message, 'error');
  1238. console.error('导出错误:', error);
  1239. }
  1240. };
  1241. // 生成代码
  1242. const generateCode = (language = 'javascript') => {
  1243. try {
  1244. let generator;
  1245. if (language == "javascript") {
  1246. generator = javascriptGenerator;
  1247. } else if (language == "python") {
  1248. generator = pythonGenerator;
  1249. } else {
  1250. console.error("不支持的语言类型");
  1251. return;
  1252. }
  1253. const code = generator.workspaceToCode(workspace);
  1254. output.value = code;
  1255. // 将生成的代码也输出到JSON框中
  1256. // 创建一个包含代码的JSON对象
  1257. const codeJson = {
  1258. "generated_code": {
  1259. "language": language,
  1260. "code": code
  1261. }
  1262. };
  1263. // 将JSON对象格式化为字符串并设置到JSON框
  1264. jsonData.value = JSON.stringify(codeJson, null, 2);
  1265. showStatus('代码生成成功!已同时输出到JSON框');
  1266. } catch (error) {
  1267. output.value = '// 代码生成错误: ' + error.message;
  1268. showStatus('代码生成错误: ' + error.message, 'error');
  1269. console.error('代码生成错误:', error);
  1270. }
  1271. };
  1272. // 运行代码
  1273. const runCode = async () => {
  1274. try {
  1275. const code = javascriptGenerator.workspaceToCode(workspace);
  1276. // 初始化输出区域,显示生成的代码和执行结果标题
  1277. output.value = code + '\n\n// 执行结果:\n';
  1278. // 保存原始console方法
  1279. const originalConsoleLog = console.log;
  1280. const originalConsoleError = console.error;
  1281. const originalConsoleWarn = console.warn;
  1282. // 创建输出缓冲区
  1283. let outputBuffer = "";
  1284. // 重定义console.log方法,确保所有日志都输出到output变量
  1285. console.log = (...args) => {
  1286. // 将参数转换为字符串
  1287. const message = args.map(arg => {
  1288. if (typeof arg === 'object') {
  1289. try {
  1290. return JSON.stringify(arg, null, 2);
  1291. } catch (e) {
  1292. return String(arg);
  1293. }
  1294. }
  1295. return arg;
  1296. }).join(' ');
  1297. // 过滤掉Vue警告信息
  1298. if (!message.includes('[Vue warn]')) {
  1299. outputBuffer += message + '\n';
  1300. // 实时更新输出区域内容
  1301. output.value = code + '\n\n// 执行结果:\n' + outputBuffer;
  1302. }
  1303. // 保留原始console.log功能
  1304. originalConsoleLog.apply(console, args);
  1305. };
  1306. // 重定义console.error方法
  1307. console.error = (...args) => {
  1308. const message = args.map(arg => {
  1309. if (typeof arg === 'object') {
  1310. try {
  1311. return JSON.stringify(arg, null, 2);
  1312. } catch (e) {
  1313. return String(arg);
  1314. }
  1315. }
  1316. return arg;
  1317. }).join(' ');
  1318. if (!message.includes('[Vue warn]')) {
  1319. outputBuffer += '// 错误: ' + message + '\n';
  1320. output.value = code + '\n\n// 执行结果:\n' + outputBuffer;
  1321. }
  1322. originalConsoleError.apply(console, args);
  1323. };
  1324. // 重定义console.warn方法
  1325. console.warn = (...args) => {
  1326. const message = args.map(arg => {
  1327. if (typeof arg === 'object') {
  1328. try {
  1329. return JSON.stringify(arg, null, 2);
  1330. } catch (e) {
  1331. return String(arg);
  1332. }
  1333. }
  1334. return arg;
  1335. }).join(' ');
  1336. if (!message.includes('[Vue warn]')) {
  1337. outputBuffer += '// 警告: ' + message + '\n';
  1338. output.value = code + '\n\n// 执行结果:\n' + outputBuffer;
  1339. }
  1340. originalConsoleWarn.apply(console, args);
  1341. };
  1342. try {
  1343. // 安全检查
  1344. if (code.includes('eval(') || code.includes('Function(') ||
  1345. code.includes('document.write') || code.includes('window.location')) {
  1346. throw new Error('代码包含不安全的操作');
  1347. }
  1348. // 包装代码为异步函数执行,支持await
  1349. const wrappedCode = `(async () => { ${code} })()`;
  1350. await new Function(wrappedCode)();
  1351. // 确保最终结果被正确显示
  1352. output.value = code + '\n\n// 执行结果:\n' + outputBuffer;
  1353. } catch (error) {
  1354. // 捕获并显示执行错误
  1355. outputBuffer += '\n// 执行错误: ' + error.message;
  1356. output.value = code + '\n\n// 执行结果:\n' + outputBuffer;
  1357. } finally {
  1358. // 恢复原始console方法
  1359. // console.log = originalConsoleLog;
  1360. // console.error = originalConsoleError;
  1361. // console.warn = originalConsoleWarn;
  1362. }
  1363. // 显示状态消息
  1364. showStatus('代码执行成功!');
  1365. } catch (error) {
  1366. // 处理runCode函数本身的错误
  1367. output.value += '\n// 执行错误: ' + error.message;
  1368. showStatus('代码执行错误: ' + error.message, 'error');
  1369. console.error('代码执行错误:', error);
  1370. }
  1371. };
  1372. // 清空工作区
  1373. const clearWorkspace = () => {
  1374. workspace.clear();
  1375. showStatus('工作区已清空!');
  1376. };
  1377. // 清空输出
  1378. const clearOutput = () => {
  1379. output.value = '// 输出已清空\n';
  1380. };
  1381. // 显示状态消息
  1382. const showStatus = (message, type = 'success') => {
  1383. statusMessage.value = message;
  1384. statusType.value = type;
  1385. // 3秒后自动清除状态消息
  1386. // setTimeout(() => {
  1387. // statusMessage.value = '';
  1388. // }, 3000);
  1389. };
  1390. </script>
  1391. <style scoped lang="scss">
  1392. @use "sass:math";
  1393. @function rpx($px) {
  1394. @return math.div($px, 750) * 100vw;
  1395. }
  1396. * {
  1397. margin: 0;
  1398. padding: 0;
  1399. box-sizing: border-box;
  1400. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  1401. }
  1402. .container {
  1403. position: fixed;
  1404. top: 0;
  1405. left: 0;
  1406. right: 0;
  1407. bottom: 0;
  1408. background: rgba(255, 255, 255, 0.95);
  1409. overflow-y: auto;
  1410. }
  1411. /* 自定义滚动条样式 */
  1412. .container::-webkit-scrollbar {
  1413. width: rpx(2); /* 滚动条宽度 */
  1414. }
  1415. .container::-webkit-scrollbar-track {
  1416. background: #f1effd; /* 滚动条轨道背景色 */
  1417. border-radius: rpx(4);
  1418. }
  1419. .container::-webkit-scrollbar-thumb {
  1420. background: #e2ddfc; /* 滚动条滑块颜色 */
  1421. border-radius: rpx(4);
  1422. }
  1423. .container::-webkit-scrollbar-thumb:hover {
  1424. background: #e2ddfc; /* 滚动条滑块 hover 状态颜色 */
  1425. }
  1426. .content {
  1427. display: flex;
  1428. flex-wrap: wrap;
  1429. min-height: 600px;
  1430. }
  1431. .toolbox-section {
  1432. flex: 1;
  1433. min-width: 250px;
  1434. background: #f8f9fa;
  1435. padding: 15px;
  1436. border-right: 1px solid #e0e0e0;
  1437. display: flex;
  1438. flex-direction: column;
  1439. }
  1440. .workspace-section {
  1441. flex: 3;
  1442. min-width: 400px;
  1443. padding: 15px;
  1444. position: relative;
  1445. height: 70%;
  1446. }
  1447. .output-section {
  1448. margin-top: 15px;
  1449. background: #2c3e50;
  1450. color: white;
  1451. padding: 15px;
  1452. border-radius: 8px;
  1453. border: 1px solid #34495e;
  1454. width: 100%;
  1455. }
  1456. /* AI模块样式 */
  1457. [categorystyle="ai_category"] > .blocklyTreeRow {
  1458. background-color: #9c27b0 !important;
  1459. }
  1460. /* 预览样式 */
  1461. .preview-modal {
  1462. position: fixed;
  1463. top: 0;
  1464. left: 0;
  1465. width: 100%;
  1466. height: 100%;
  1467. background-color: rgba(0, 0, 0, 0.7);
  1468. display: flex;
  1469. justify-content: center;
  1470. align-items: center;
  1471. z-index: 1000;
  1472. }
  1473. .preview-content {
  1474. background-color: white;
  1475. padding: 20px;
  1476. border-radius: 8px;
  1477. max-width: 80%;
  1478. max-height: 80%;
  1479. overflow: auto;
  1480. position: relative;
  1481. }
  1482. .close-button {
  1483. position: absolute;
  1484. top: 10px;
  1485. right: 10px;
  1486. font-size: 24px;
  1487. background: none;
  1488. border: none;
  1489. cursor: pointer;
  1490. color: #333;
  1491. }
  1492. .preview-image-container,
  1493. .preview-video-container {
  1494. display: flex;
  1495. justify-content: center;
  1496. }
  1497. .preview-image,
  1498. .preview-video {
  1499. max-width: 100%;
  1500. max-height: 60vh;
  1501. border-radius: 4px;
  1502. }
  1503. .preview-text-container {
  1504. max-height: 60vh;
  1505. overflow-y: auto;
  1506. padding: 10px;
  1507. background-color: #f5f5f5;
  1508. border-radius: 4px;
  1509. color: #333;
  1510. }
  1511. /* 文生图预览 */
  1512. .extra-image-preview {
  1513. color: black;
  1514. margin-top: 10px;
  1515. padding: 10px;
  1516. border: 1px solid #ddd;
  1517. border-radius: 5px;
  1518. background-color: #f9f9f9;
  1519. }
  1520. .extra-preview-image {
  1521. max-width: 100%;
  1522. max-height: 400px;
  1523. border-radius: 4px;
  1524. }
  1525. /* 音乐播放器 */
  1526. .music-player-container {
  1527. margin-top: 20px;
  1528. padding: 15px;
  1529. background-color: #f9f9f9;
  1530. border-radius: 8px;
  1531. border: 1px solid #e0e0e0;
  1532. color: #333;
  1533. }
  1534. .music-player-container h5 {
  1535. margin-top: 0;
  1536. margin-bottom: 10px;
  1537. color: #333;
  1538. font-size: 16px;
  1539. }
  1540. .music-player-container audio {
  1541. width: 100%;
  1542. margin-bottom: 10px;
  1543. }
  1544. .music-status {
  1545. font-size: 14px;
  1546. color: #666;
  1547. padding: 5px 0;
  1548. }
  1549. h2 {
  1550. margin-bottom: 15px;
  1551. color: #2c3e50;
  1552. border-bottom: 2px solid #3498db;
  1553. padding-bottom: 8px;
  1554. }
  1555. .output-section h2 {
  1556. color: white;
  1557. border-bottom: 2px solid #1abc9c;
  1558. }
  1559. #blocklyDiv {
  1560. height: 500px;
  1561. width: 100%;
  1562. background: white;
  1563. border: 1px solid #ddd;
  1564. border-radius: 8px;
  1565. }
  1566. .controls {
  1567. display: flex;
  1568. gap: 10px;
  1569. margin-top: 15px;
  1570. flex-wrap: wrap;
  1571. }
  1572. button {
  1573. padding: 10px 20px;
  1574. border: none;
  1575. border-radius: 5px;
  1576. background: #3498db;
  1577. color: white;
  1578. font-weight: bold;
  1579. cursor: pointer;
  1580. transition: all 0.3s ease;
  1581. }
  1582. button:hover {
  1583. background: #2980b9;
  1584. transform: translateY(-2px);
  1585. box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
  1586. }
  1587. #generateCode {
  1588. background: #2ecc71;
  1589. }
  1590. #generateCode:hover {
  1591. background: #27ae60;
  1592. }
  1593. #runCode {
  1594. background: #e74c3c;
  1595. }
  1596. #runCode:hover {
  1597. background: #c0392b;
  1598. }
  1599. #output {
  1600. background: #1a2530;
  1601. padding: 10px;
  1602. border-radius: 5px;
  1603. min-height: 100px;
  1604. max-height: 300px;
  1605. margin-top: 10px;
  1606. font-family: 'Courier New', monospace;
  1607. white-space: pre-wrap;
  1608. overflow-y: auto;
  1609. font-size: 12px;
  1610. }
  1611. .json-section {
  1612. margin-top: 15px;
  1613. padding: 15px;
  1614. background: #f1f8ff;
  1615. border-radius: 8px;
  1616. border: 1px solid #d1e7ff;
  1617. }
  1618. .json-section h3 {
  1619. margin-bottom: 10px;
  1620. color: #2c3e50;
  1621. }
  1622. textarea {
  1623. width: 100%;
  1624. min-height: 120px;
  1625. padding: 10px;
  1626. border: 1px solid #ddd;
  1627. border-radius: 5px;
  1628. font-family: 'Courier New', monospace;
  1629. resize: vertical;
  1630. }
  1631. .status {
  1632. margin-top: 10px;
  1633. padding: 8px 12px;
  1634. border-radius: 4px;
  1635. font-weight: bold;
  1636. }
  1637. .success {
  1638. background: #d4edda;
  1639. color: #155724;
  1640. border: 1px solid #c3e6cb;
  1641. }
  1642. .error {
  1643. background: #f8d7da;
  1644. color: #721c24;
  1645. border: 1px solid #f5c6cb;
  1646. }
  1647. /* 智能台灯 */
  1648. .desk-lamp-container {
  1649. position: fixed;
  1650. top: 0;
  1651. left: 0;
  1652. right: 0;
  1653. bottom: 0;
  1654. overflow: hidden;
  1655. display: flex;
  1656. justify-content: center;
  1657. align-items: center;
  1658. z-index: 1000;
  1659. }
  1660. .full-screen-image {
  1661. width: 100%;
  1662. height: 100%;
  1663. object-fit: cover;
  1664. }
  1665. /* 台灯灯光样式 - 使用动态颜色 */
  1666. .lamp-light-mask {
  1667. position: absolute;
  1668. width: rpx(78);
  1669. height: rpx(250);
  1670. margin-top: rpx(49);
  1671. margin-left: rpx(192);
  1672. background: linear-gradient(to bottom, var(--lamp-color) 0%, var(--lamp-color) 10%, rgba(255, 255, 255, 0) 90%);
  1673. filter: blur(60px);
  1674. transform: rotate(9deg);
  1675. transform-origin: top center;
  1676. opacity: var(--lamp-opacity, 0.6);
  1677. /* 创建扇形效果 */
  1678. clip-path: polygon(0% 0%, 100% 0%, 250% 100%, -150% 100%);
  1679. }
  1680. /* 灯光信息显示 */
  1681. .lamp-info {
  1682. position: absolute;
  1683. bottom: 100px;
  1684. right: 30px;
  1685. background-color: rgba(255, 255, 255, 0.2);
  1686. padding: 10px 20px;
  1687. border-radius: 8px;
  1688. backdrop-filter: blur(10px);
  1689. color: white;
  1690. font-size: 14px;
  1691. z-index: 1000;
  1692. }
  1693. /* 标题框样式 */
  1694. .desk-lamp-title-box {
  1695. position: absolute;
  1696. top: 20px;
  1697. left: 20px;
  1698. z-index: 1000;
  1699. }
  1700. .desk-lamp-box-icon {
  1701. display: flex;
  1702. align-items: center;
  1703. gap: 10px;
  1704. padding: 10px 20px;
  1705. background-color: rgba(255, 255, 255, 0.2);
  1706. border-radius: 30px;
  1707. backdrop-filter: blur(10px);
  1708. cursor: pointer;
  1709. transition: all 0.3s ease;
  1710. font-size: 16px;
  1711. color: white;
  1712. font-weight: 500;
  1713. }
  1714. .desk-lamp-box-icon:hover {
  1715. background-color: rgba(255, 255, 255, 0.3);
  1716. transform: translateX(-3px);
  1717. }
  1718. .left-icon {
  1719. font-size: 18px;
  1720. }
  1721. /* 右下角按钮组样式 */
  1722. .button-group {
  1723. position: absolute;
  1724. bottom: 30px;
  1725. right: 30px;
  1726. display: flex;
  1727. gap: 15px;
  1728. z-index: 1000;
  1729. }
  1730. .control-button {
  1731. padding: 12px 24px;
  1732. border-radius: 8px;
  1733. font-size: 16px;
  1734. font-weight: 500;
  1735. transition: all 0.3s ease;
  1736. backdrop-filter: blur(5px);
  1737. }
  1738. .run-button {
  1739. background-color: rgba(64, 169, 255, 0.8);
  1740. color: white;
  1741. border: none;
  1742. }
  1743. .run-button:hover {
  1744. background-color: rgba(64, 169, 255, 1);
  1745. transform: translateY(-2px);
  1746. }
  1747. .code-button {
  1748. background-color: rgba(132, 94, 255, 0.8);
  1749. color: white;
  1750. border: none;
  1751. }
  1752. .code-button:hover {
  1753. background-color: rgba(132, 94, 255, 1);
  1754. transform: translateY(-2px);
  1755. }
  1756. /* 返回按钮样式 */
  1757. .title-box {
  1758. position: relative;
  1759. top: 10px;
  1760. left: 20px;
  1761. margin-bottom: 20px;
  1762. z-index: 10;
  1763. }
  1764. .box-icon {
  1765. display: flex;
  1766. align-items: center;
  1767. gap: 10px;
  1768. padding: 10px 20px;
  1769. background-color: rgba(255, 255, 255, 0.2);
  1770. border-radius: 30px;
  1771. backdrop-filter: blur(10px);
  1772. cursor: pointer;
  1773. transition: all 0.3s ease;
  1774. font-size: 16px;
  1775. color: #333;
  1776. font-weight: 500;
  1777. width: fit-content;
  1778. }
  1779. .box-icon:hover {
  1780. background-color: rgba(255, 255, 255, 0.3);
  1781. transform: translateX(-3px);
  1782. }
  1783. .left-icon {
  1784. font-size: 18px;
  1785. }
  1786. /* 语音识别-录音状态容器 */
  1787. .recording-status-container {
  1788. position: absolute;
  1789. top: 20%;
  1790. left: 50%;
  1791. transform: translateX(-50%);
  1792. background: rgba(0, 0, 0, 0.7);
  1793. color: white;
  1794. padding: 20px 40px;
  1795. border-radius: 10px;
  1796. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
  1797. text-align: center;
  1798. z-index: 1000;
  1799. backdrop-filter: blur(5px);
  1800. }
  1801. .recording-text {
  1802. font-size: 18px;
  1803. margin-bottom: 15px;
  1804. font-weight: 500;
  1805. }
  1806. .recording-countdown {
  1807. font-size: 16px;
  1808. color: #4CAF50;
  1809. margin-top: 15px;
  1810. font-weight: bold;
  1811. }
  1812. .equalizer {
  1813. display: flex;
  1814. justify-content: center;
  1815. align-items: flex-end;
  1816. height: 60px;
  1817. gap: 4px;
  1818. }
  1819. .bar {
  1820. width: 8px;
  1821. background: linear-gradient(180deg, #4CAF50 0%, #8BC34A 100%);
  1822. border-radius: 4px;
  1823. animation: equalize 1s infinite ease-in-out;
  1824. }
  1825. .bar-1 { animation-delay: 0s; }
  1826. .bar-2 { animation-delay: 0.1s; }
  1827. .bar-3 { animation-delay: 0.2s; }
  1828. .bar-4 { animation-delay: 0.3s; }
  1829. .bar-5 { animation-delay: 0.4s; }
  1830. .bar-6 { animation-delay: 0.5s; }
  1831. .bar-7 { animation-delay: 0.6s; }
  1832. @keyframes equalize {
  1833. 0%, 100% { height: 10px; }
  1834. 25% { height: 40px; }
  1835. 50% { height: 60px; }
  1836. 75% { height: 25px; }
  1837. }
  1838. //台灯播放音乐
  1839. .music-info {
  1840. margin-top: 10px;
  1841. padding: 8px;
  1842. background-color: rgba(255, 255, 255, 0.1);
  1843. border-radius: 6px;
  1844. }
  1845. .music-info p {
  1846. margin: 0 0 8px 0;
  1847. color: #ffffff;
  1848. font-size: 14px;
  1849. }
  1850. .stop-music-btn {
  1851. background-color: #ff4d4f;
  1852. color: white;
  1853. border: none;
  1854. padding: 6px 12px;
  1855. border-radius: 4px;
  1856. cursor: pointer;
  1857. font-size: 12px;
  1858. transition: background-color 0.3s;
  1859. }
  1860. .stop-music-btn:hover {
  1861. background-color: #ff7875;
  1862. }
  1863. </style>