Blockly.vue 66 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192
  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. <img src="@/assets/images/desklamp.png" alt="智能台灯" class="full-screen-image" />
  12. <!-- 使用动态样式设置灯光遮罩 -->
  13. <div v-if="state.lamp.isLightOn" :style="{ '--lamp-color': state.lamp.color,'--lamp-opacity': state.lamp.brightness / 100 }" class="lamp-light-mask"></div>
  14. <!-- 右下角按钮组 -->
  15. <div class="button-group">
  16. <el-button class="control-button run-button" @click="toggleLight">运行</el-button>
  17. <el-button class="control-button code-button" @click="handleViewCode">智能台灯</el-button>
  18. </div>
  19. <!-- 显示当前灯光信息 -->
  20. <div v-if="state.lamp.isLightOn" class="lamp-info">
  21. <p>颜色: {{ state.lamp.colorLog }}色</p>
  22. <p>亮度: {{ state.lamp.brightness }}%</p>
  23. </div>
  24. </div>
  25. <!-- Blockly -->
  26. <div class="page-container">
  27. <!-- 返回 -->
  28. <div class="title-box">
  29. <div class="box-icon" @click="goBack">
  30. <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
  31. 返回虚拟实验室
  32. </div>
  33. </div>
  34. <div class="home-container">
  35. <div class="box-blockly">
  36. <div
  37. id="blocklyDiv"
  38. style="height: 100%; width: 100%; background-color: #cccccc"
  39. ></div>
  40. <!-- blockly工具栏 -->
  41. <template>
  42. <xml id="toolbox" style="display: none">
  43. <!-- AI模块分类 - 扩展更多功能 -->
  44. <category name="AI模块" categorystyle="ai_category">
  45. <block type="ai_voice_input"></block>
  46. <block type="ai_text_to_image"></block>
  47. <block type="ai_text_to_video"></block>
  48. <block type="ai_text_to_text"></block>
  49. <block type="ai_smart_lamp_single_param"></block>
  50. <block type="ai_smart_lamp"></block>
  51. <block type="ai_lamp_set_brightness"></block>
  52. <block type="ai_lamp_set_color"></block>
  53. <block type="ai_music_play"></block>
  54. </category>
  55. <!-- 其他原有分类保持不变 -->
  56. <category name="逻辑" categorystyle="logic_category">
  57. <block type="controls_if"></block>
  58. <block type="logic_compare"></block>
  59. <block type="logic_operation"></block>
  60. <block type="logic_negate"></block>
  61. <block type="logic_boolean"></block>
  62. <block type="logic_null" disabled="true"></block>
  63. <block type="logic_ternary"></block>
  64. </category>
  65. <category name="循环" categorystyle="loop_category">
  66. <block type="controls_repeat_ext">
  67. <value name="TIMES">
  68. <shadow type="math_number">
  69. <field name="NUM">10</field>
  70. </shadow>
  71. </value>
  72. </block>
  73. <block type="controls_repeat" disabled="true"></block>
  74. <block type="controls_whileUntil"></block>
  75. <block type="controls_for">
  76. <value name="FROM">
  77. <shadow type="math_number">
  78. <field name="NUM">1</field>
  79. </shadow>
  80. </value>
  81. <value name="TO">
  82. <shadow type="math_number">
  83. <field name="NUM">10</field>
  84. </shadow>
  85. </value>
  86. <value name="BY">
  87. <shadow type="math_number">
  88. <field name="NUM">1</field>
  89. </shadow>
  90. </value>
  91. </block>
  92. <block type="controls_forEach"></block>
  93. <block type="controls_flow_statements"></block>
  94. </category>
  95. <category name="数值" categorystyle="math_category">
  96. <block type="math_number" gap="32">
  97. <field name="NUM">123</field>
  98. </block>
  99. <block type="math_arithmetic">
  100. <value name="A">
  101. <shadow type="math_number">
  102. <field name="NUM">1</field>
  103. </shadow>
  104. </value>
  105. <value name="B">
  106. <shadow type="math_number">
  107. <field name="NUM">1</field>
  108. </shadow>
  109. </value>
  110. </block>
  111. <block type="math_single">
  112. <value name="NUM">
  113. <shadow type="math_number">
  114. <field name="NUM">9</field>
  115. </shadow>
  116. </value>
  117. </block>
  118. <block type="math_trig">
  119. <value name="NUM">
  120. <shadow type="math_number">
  121. <field name="NUM">45</field>
  122. </shadow>
  123. </value>
  124. </block>
  125. <block type="math_constant"></block>
  126. <block type="math_number_property">
  127. <value name="NUMBER_TO_CHECK">
  128. <shadow type="math_number">
  129. <field name="NUM">0</field>
  130. </shadow>
  131. </value>
  132. </block>
  133. <block type="math_round">
  134. <value name="NUM">
  135. <shadow type="math_number">
  136. <field name="NUM">3.1</field>
  137. </shadow>
  138. </value>
  139. </block>
  140. <block type="math_on_list"></block>
  141. <block type="math_modulo">
  142. <value name="DIVIDEND">
  143. <shadow type="math_number">
  144. <field name="NUM">64</field>
  145. </shadow>
  146. </value>
  147. <value name="DIVISOR">
  148. <shadow type="math_number">
  149. <field name="NUM">10</field>
  150. </shadow>
  151. </value>
  152. </block>
  153. <block type="math_constrain">
  154. <value name="VALUE">
  155. <shadow type="math_number">
  156. <field name="NUM">50</field>
  157. </shadow>
  158. </value>
  159. <value name="LOW">
  160. <shadow type="math_number">
  161. <field name="NUM">1</field>
  162. </shadow>
  163. </value>
  164. <value name="HIGH">
  165. <shadow type="math_number">
  166. <field name="NUM">100</field>
  167. </shadow>
  168. </value>
  169. </block>
  170. <block type="math_random_int">
  171. <value name="FROM">
  172. <shadow type="math_number">
  173. <field name="NUM">1</field>
  174. </shadow>
  175. </value>
  176. <value name="TO">
  177. <shadow type="math_number">
  178. <field name="NUM">100</field>
  179. </shadow>
  180. </value>
  181. </block>
  182. <block type="math_random_float"></block>
  183. <block type="math_atan2">
  184. <value name="X">
  185. <shadow type="math_number">
  186. <field name="NUM">1</field>
  187. </shadow>
  188. </value>
  189. <value name="Y">
  190. <shadow type="math_number">
  191. <field name="NUM">1</field>
  192. </shadow>
  193. </value>
  194. </block>
  195. </category>
  196. <category name="文本" categorystyle="text_category">
  197. <block type="text"></block>
  198. <block type="text_join"></block>
  199. <block type="text_append">
  200. <value name="TEXT">
  201. <shadow type="text"></shadow>
  202. </value>
  203. </block>
  204. <block type="text_length">
  205. <value name="VALUE">
  206. <shadow type="text">
  207. <field name="TEXT">abc</field>
  208. </shadow>
  209. </value>
  210. </block>
  211. <block type="text_isEmpty">
  212. <value name="VALUE">
  213. <shadow type="text">
  214. <field name="TEXT"></field>
  215. </shadow>
  216. </value>
  217. </block>
  218. <block type="text_indexOf">
  219. <value name="VALUE">
  220. <block type="variables_get">
  221. <field name="VAR">text</field>
  222. </block>
  223. </value>
  224. <value name="FIND">
  225. <shadow type="text">
  226. <field name="TEXT">abc</field>
  227. </shadow>
  228. </value>
  229. </block>
  230. <block type="text_charAt">
  231. <value name="VALUE">
  232. <block type="variables_get">
  233. <field name="VAR">text</field>
  234. </block>
  235. </value>
  236. </block>
  237. <block type="text_getSubstring">
  238. <value name="STRING">
  239. <block type="variables_get">
  240. <field name="VAR">text</field>
  241. </block>
  242. </value>
  243. </block>
  244. <block type="text_changeCase">
  245. <value name="TEXT">
  246. <shadow type="text">
  247. <field name="TEXT">abc</field>
  248. </shadow>
  249. </value>
  250. </block>
  251. <block type="text_trim">
  252. <value name="TEXT">
  253. <shadow type="text">
  254. <field name="TEXT">abc</field>
  255. </shadow>
  256. </value>
  257. </block>
  258. <block type="text_count">
  259. <value name="SUB">
  260. <shadow type="text"></shadow>
  261. </value>
  262. <value name="TEXT">
  263. <shadow type="text"></shadow>
  264. </value>
  265. </block>
  266. <block type="text_replace">
  267. <value name="FROM">
  268. <shadow type="text"></shadow>
  269. </value>
  270. <value name="TO">
  271. <shadow type="text"></shadow>
  272. </value>
  273. <value name="TEXT">
  274. <shadow type="text"></shadow>
  275. </value>
  276. </block>
  277. <block type="text_reverse">
  278. <value name="TEXT">
  279. <shadow type="text"></shadow>
  280. </value>
  281. </block>
  282. <label text="Input/Output:" web-class="ioLabel"></label>
  283. <block type="text_print">
  284. <value name="TEXT">
  285. <shadow type="text">
  286. <field name="TEXT">abc</field>
  287. </shadow>
  288. </value>
  289. </block>
  290. <block type="text_prompt_ext">
  291. <value name="TEXT">
  292. <shadow type="text">
  293. <field name="TEXT">abc</field>
  294. </shadow>
  295. </value>
  296. </block>
  297. </category>
  298. <category name="列表" categorystyle="list_category">
  299. <block type="lists_create_with">
  300. <mutation items="0"></mutation>
  301. </block>
  302. <block type="lists_create_with"></block>
  303. <block type="lists_repeat">
  304. <value name="NUM">
  305. <shadow type="math_number">
  306. <field name="NUM">5</field>
  307. </shadow>
  308. </value>
  309. </block>
  310. <block type="lists_length"></block>
  311. <block type="lists_isEmpty"></block>
  312. <block type="lists_indexOf">
  313. <value name="VALUE">
  314. <block type="variables_get">
  315. <field name="VAR">list</field>
  316. </block>
  317. </value>
  318. </block>
  319. <block type="lists_getIndex">
  320. <value name="VALUE">
  321. <block type="variables_get">
  322. <field name="VAR">list</field>
  323. </block>
  324. </value>
  325. </block>
  326. <block type="lists_setIndex">
  327. <value name="LIST">
  328. <block type="variables_get">
  329. <field name="VAR">list</field>
  330. </block>
  331. </value>
  332. </block>
  333. <block type="lists_getSublist">
  334. <value name="LIST">
  335. <block type="variables_get">
  336. <field name="VAR">list</field>
  337. </block>
  338. </value>
  339. </block>
  340. <block type="lists_split">
  341. <value name="DELIM">
  342. <shadow type="text">
  343. <field name="TEXT">,</field>
  344. </shadow>
  345. </value>
  346. </block>
  347. <block type="lists_sort"></block>
  348. <block type="lists_reverse"></block>
  349. </category>
  350. <sep></sep>
  351. <category
  352. name="Variables"
  353. categorystyle="variable_category"
  354. custom="VARIABLE"
  355. ></category>
  356. <category
  357. name="Functions"
  358. categorystyle="procedure_category"
  359. custom="PROCEDURE"
  360. ></category>
  361. </xml>
  362. </template>
  363. </div>
  364. <div class="box-code">
  365. <div class="button-container">
  366. <el-button type="danger" plain id="run-code" @click="runCode()"
  367. >运行代码</el-button
  368. >
  369. <el-button type="warning" plain id="import" @click="load()"
  370. >重新加载</el-button
  371. >
  372. </div>
  373. <br />
  374. <div class="button-container">
  375. <el-button
  376. type="primary"
  377. plain
  378. id="to-code-js"
  379. @click="generateCode('javascript')"
  380. >生成 JavaScript</el-button
  381. >
  382. <el-button
  383. type="success"
  384. plain
  385. id="to-code-py"
  386. @click="generateCode('python')"
  387. >生成 Python</el-button
  388. >
  389. <el-button type="info" plain id="save-json" @click="saveJson()"
  390. >保存 JSON</el-button
  391. >
  392. <el-button type="info" plain id="save-xml" @click="saveXml()"
  393. >保存 XML</el-button
  394. >
  395. </div>
  396. <!-- 代码显示区 -->
  397. <div class="code-section">
  398. <h4>生成的代码</h4>
  399. <textarea name="" id="textarea" class="box-code-textarea"></textarea>
  400. </div>
  401. <!-- 运行结果区 -->
  402. <div class="result-section">
  403. <h4>运行结果</h4>
  404. <div id="run-result" class="run-result-content"></div>
  405. <!-- 额外的图片预览区域 -->
  406. <div
  407. v-if="state.generatedContent.imageUrl"
  408. class="extra-image-preview"
  409. >
  410. <h5>生成的图片:</h5>
  411. <img
  412. :src="state.generatedContent.imageUrl"
  413. class="extra-preview-image"
  414. alt="AI生成图片"
  415. />
  416. </div>
  417. <div
  418. v-if="state.generatedContent.videoUrl"
  419. class="extra-image-preview"
  420. >
  421. <h5>生成的视频:</h5>
  422. <video
  423. :src="state.generatedContent.videoUrl"
  424. controls
  425. class="preview-video"
  426. alt="AI生成视频"
  427. ></video>
  428. </div>
  429. <!-- 在template部分的适当位置音频播放器组件 -->
  430. <div class="music-player-container" v-if="state.currentMusicUrl">
  431. <h5>音乐播放</h5>
  432. <audio
  433. ref="musicPlayer"
  434. :src="state.currentMusicUrl"
  435. @ended="handleMusicEnded"
  436. preload="metadata">
  437. 您的浏览器不支持音频元素
  438. </audio>
  439. <div class="music-status">
  440. <p v-if="state.isMusicPlaying">正在播放: {{ state.currentMusicName }}</p>
  441. <p v-else>准备就绪</p>
  442. </div>
  443. <!-- 停止播放按钮 - 修复点击事件 -->
  444. <el-button
  445. v-if="state.isMusicPlaying"
  446. type="danger"
  447. size="small"
  448. @click="handleStopMusic"
  449. style="margin-top: 10px;">
  450. 停止播放
  451. </el-button>
  452. </div>
  453. </div>
  454. </div>
  455. <!-- AI结果预览模态框 -->
  456. <el-dialog
  457. v-if="state.previewConten"
  458. title="AI生成结果"
  459. width="80%"
  460. :before-close="handleClosePreview"
  461. >
  462. <div
  463. v-if="state.previewType === 'image'"
  464. class="preview-image-container"
  465. >
  466. <img
  467. :src="state.previewContent"
  468. class="preview-image"
  469. alt="生成的图片"
  470. />
  471. </div>
  472. <div
  473. v-if="state.previewType === 'video'"
  474. class="preview-video-container"
  475. >
  476. <video
  477. :src="state.previewContent"
  478. controls
  479. class="preview-video"
  480. ></video>
  481. </div>
  482. <div v-if="state.previewType === 'text'" class="preview-text-container">
  483. <p>{{ state.previewContent }}</p>
  484. </div>
  485. <span slot="footer" class="dialog-footer">
  486. <el-button @click="handleClosePreview">关闭</el-button>
  487. </span>
  488. </el-dialog>
  489. </div>
  490. </div>
  491. </template>
  492. <script setup>
  493. import { ref, onMounted, onUnmounted, reactive } from "vue";
  494. import { useRouter } from "vue-router";
  495. import { ArrowLeftBold } from "@element-plus/icons-vue";
  496. import * as Blockly from "blockly";
  497. import { javascriptGenerator } from "blockly/javascript";
  498. import { pythonGenerator } from "blockly/python";
  499. import * as hans from "blockly/msg/zh-hans";
  500. import { ElDialog, ElButton, ElMessage } from "element-plus";
  501. //【文生图】文生图
  502. import {
  503. AiImageStatusEnum,
  504. CreatePainting,
  505. PaintingGetMys,
  506. CreateVideo,
  507. VideoGetMys,
  508. sendChatMessageStream,
  509. CreateDialogue,
  510. } from "@/api/questions.js";
  511. import { getModelIdByType, ModelPlatformEnum } from "@/api/teachers.js";
  512. import { ModelTypeEnum } from "@/api/teachers.js";
  513. import { globalState } from "@/utils/globalState.js";
  514. //音乐
  515. import { playMusic, stopMusic, onMusicEnded } from "@/api/blockly/music.js";
  516. const router = useRouter();
  517. // 台灯预览显示状态
  518. const showLampPreview = ref(true)
  519. // 返回虚拟实验室
  520. const goBackLab = () => {
  521. router.push("/virtual-laboratory");
  522. };
  523. const goBack = () => {
  524. showLampPreview.value = true
  525. };
  526. // 切换灯光状态
  527. const toggleLight = () => {
  528. state.lamp.isLightOn = true;
  529. generateCode('javascript');
  530. runCode()
  531. };
  532. // 查看代码编程界面显示状态
  533. const handleViewCode = () =>{
  534. showLampPreview.value = false
  535. };
  536. Blockly.setLocale(hans);
  537. // 状态管理
  538. const state = reactive({
  539. workspace: null,
  540. generatedContent: {
  541. imageUrl: "",
  542. videoUrl: "",
  543. text: "",
  544. },
  545. previewVisible: false,
  546. previewType: "",
  547. previewContent: "",
  548. isProcessing: false,
  549. //年级
  550. gradeId: "",
  551. //【文生图】文生图
  552. inProgressImageMap: {},
  553. //【文生视频】文生视频
  554. inProgressVideoMap: {},
  555. // 台灯状态
  556. lamp: {
  557. isLightOn: false,// 台灯是否亮着
  558. brightness: 0, // 默认亮度50%
  559. color: "#ffffff", // 默认颜色白色
  560. colorLog: "白", // 默认颜色白色
  561. },
  562. // 【文本文】对话相关状态
  563. activeConversationId: null,
  564. conversationInAbortController: null,
  565. // 独立的音乐播放状态
  566. currentMusicUrl: '',
  567. currentMusicName: '',
  568. isMusicPlaying: false,
  569. });
  570. // 创建音乐播放器引用
  571. const musicPlayer = ref(null);
  572. // 音乐相关的处理函数
  573. const handleMusicEnded = () => {
  574. onMusicEnded(state);
  575. };
  576. // 专门的停止音乐处理函数
  577. const handleStopMusic = () => {
  578. // 直接调用导入的stopMusic函数并传递正确的参数
  579. stopMusic(state, musicPlayer);
  580. // 提示信息
  581. ElMessage.success('音乐已停止播放');
  582. };
  583. // 统一轮询管理器
  584. const pollingManager = {
  585. timers: {},
  586. // 启动轮询
  587. startPolling(type, callback, interval = 3000) {
  588. // 如果已有相同类型的轮询,先清除
  589. this.stopPolling(type);
  590. this.timers[type] = setInterval(async () => {
  591. try {
  592. await callback();
  593. } catch (error) {
  594. console.error(`${type}轮询失败:`, error);
  595. }
  596. }, interval);
  597. return this.timers[type];
  598. },
  599. // 停止轮询
  600. stopPolling(type) {
  601. if (this.timers[type]) {
  602. clearInterval(this.timers[type]);
  603. this.timers[type] = null;
  604. }
  605. },
  606. // 停止所有轮询
  607. stopAll() {
  608. Object.keys(this.timers).forEach(type => this.stopPolling(type));
  609. }
  610. };
  611. // 统一的错误处理包装器
  612. function withErrorHandling(operationName, fn, errorMessage = null) {
  613. return async function(...args) {
  614. try {
  615. state.isProcessing = true;
  616. return await fn.apply(this, args);
  617. } catch (error) {
  618. console.error(`${operationName}失败:`, error);
  619. ElMessage.error(errorMessage || `${operationName}发生错误: ${error.message || '未知错误'}`);
  620. return null;
  621. } finally {
  622. state.isProcessing = false;
  623. }
  624. };
  625. }
  626. // 任务状态轮询公共函数
  627. async function pollTaskStatus(taskType, taskIds, fetchApi, onSuccess, onFailure) {
  628. if (taskIds.length === 0) {
  629. pollingManager.stopPolling(taskType);
  630. return {};
  631. }
  632. try {
  633. const list = await fetchApi(taskIds);
  634. const activeTasks = {};
  635. list.data.forEach((task) => {
  636. if (task.status === AiImageStatusEnum.IN_PROGRESS) {
  637. activeTasks[task.id] = task;
  638. } else if (task.status === AiImageStatusEnum.SUCCESS) {
  639. // 任务成功完成
  640. if (onSuccess) {
  641. onSuccess(task);
  642. }
  643. } else if (task.status === AiImageStatusEnum.FAIL) {
  644. // 任务失败
  645. if (onFailure) {
  646. onFailure(task);
  647. }
  648. }
  649. });
  650. return activeTasks;
  651. } catch (error) {
  652. console.error(`${taskType}状态轮询失败:`, error);
  653. return {};
  654. }
  655. }
  656. // AI服务模块 - 统一管理
  657. const aiService = {
  658. // 语音识别
  659. recognizeVoice: withErrorHandling('语音识别', async function(promptText = "", language = "zh-CN") {
  660. console.log("语音识别开始");
  661. // 前端语音采集
  662. const recognitionResult = await this.captureVoice(language, promptText);
  663. return recognitionResult || "";
  664. }, '语音识别失败'),
  665. // 前端语音采集
  666. captureVoice(language, promptText) {
  667. return new Promise((resolve) => {
  668. if (
  669. !"webkitSpeechRecognition" in window &&
  670. !"SpeechRecognition" in window
  671. ) {
  672. ElMessage.warning("您的浏览器不支持语音识别功能");
  673. resolve("");
  674. return;
  675. }
  676. const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
  677. const recognition = new SpeechRecognition();
  678. recognition.lang = language;
  679. recognition.interimResults = false;
  680. recognition.maxAlternatives = 1;
  681. let countdown = 10;
  682. // 固定的消息提示框
  683. const messageText = promptText ? `${promptText}\n请开始说话...(${countdown}秒)` : `请开始说话...(${countdown}秒)`;
  684. const messageInstance = ElMessage.info({
  685. message: messageText,
  686. duration: 0 // 0表示不自动关闭
  687. });
  688. // 隐藏的div来更新倒计时显示
  689. const timer = setInterval(() => {
  690. countdown--;
  691. if (countdown > 0) {
  692. // 找到消息提示框中的文本元素并更新
  693. const messageElement = document.querySelector('.el-message__content');
  694. if (messageElement) {
  695. const newText = promptText ? `${promptText}\n请开始说话...(${countdown}秒)` : `请开始说话...(${countdown}秒)`;
  696. messageElement.textContent = newText;
  697. }
  698. } else {
  699. clearInterval(timer);
  700. // 倒计时结束后关闭提示框
  701. if (messageInstance && messageInstance.close) {
  702. messageInstance.close();
  703. }
  704. }
  705. }, 1000);
  706. recognition.onresult = (event) => {
  707. const speechResult = event.results[0][0].transcript;
  708. console.log("语音识别结果:", speechResult);
  709. resolve(speechResult);
  710. };
  711. recognition.onerror = (event) => {
  712. console.error("语音识别错误:", event.error);
  713. ElMessage.error("语音识别发生错误: " + event.error);
  714. resolve("");
  715. };
  716. // recognition.onend = () => {
  717. // console.log("语音识别已结束");
  718. // };
  719. recognition.start();
  720. });
  721. },
  722. // 文本生成图片
  723. textToImage: withErrorHandling('AI图片生成', async function(prompt, waitForCompletion = true) {
  724. console.log("AI图片生成中,提示词:", prompt);
  725. //获取文生图-模型id
  726. const modelRes = await getModelIdByType({
  727. type: ModelTypeEnum.TEXT_TO_IMAGE,
  728. platform: ModelPlatformEnum.DOUBAO,
  729. });
  730. if (!modelRes.data) {
  731. ElMessage.error("获取模型ID失败");
  732. return null;
  733. }
  734. // 使用CreatePainting API创建图片任务
  735. const createRes = await CreatePainting({
  736. modelId: modelRes.data,
  737. prompt: prompt,
  738. width: 1024,
  739. height: 1024,
  740. });
  741. // 记录任务ID到映射中
  742. state.inProgressImageMap[createRes.data] = {
  743. id: createRes.data,
  744. status: AiImageStatusEnum.IN_PROGRESS,
  745. };
  746. // 开始轮询任务状态
  747. this.startPollingTasks('image');
  748. // 如果需要等待完成,等待图片生成完成
  749. if (waitForCompletion) {
  750. console.log("AI图片生成中,请等待。。。:");
  751. return await this.waitForImageCompletion(createRes.data);
  752. }
  753. return createRes.data; // 返回任务ID
  754. }, '生成图片失败'),
  755. // 【文生图】等待图片生成完成
  756. waitForImageCompletion(imageId) {
  757. return new Promise((resolve, reject) => {
  758. const checkInterval = setInterval(async () => {
  759. try {
  760. const list = await PaintingGetMys([imageId]);
  761. if (list.data && list.data.length > 0) {
  762. const image = list.data[0];
  763. if (image.status === AiImageStatusEnum.SUCCESS) {
  764. clearInterval(checkInterval);
  765. resolve(image.picUrl);
  766. } else if (image.status === AiImageStatusEnum.FAIL) {
  767. clearInterval(checkInterval);
  768. reject(new Error(image.error || "图片生成失败"));
  769. }
  770. }
  771. } catch (error) {
  772. clearInterval(checkInterval);
  773. reject(error);
  774. }
  775. }, 3000);
  776. });
  777. },
  778. // 文本生成视频
  779. textToVideo: withErrorHandling('AI视频生成', async function(prompt, waitForCompletion = true) {
  780. console.log("AI视频生成中,提示词:", prompt);
  781. //获取视频生成模型id
  782. const modelRes = await getModelIdByType({
  783. type: ModelTypeEnum.IMAGE_TO_VIDEO,
  784. platform: ModelPlatformEnum.DOUBAO,
  785. });
  786. if (!modelRes.data) {
  787. ElMessage.error("获取模型ID失败");
  788. return null;
  789. }
  790. // 使用CreateVideo API创建视频任务
  791. const createRes = await CreateVideo({
  792. modelId: modelRes.data,
  793. prompt: prompt,
  794. duration: 4,
  795. resolution: "1080P",
  796. });
  797. // 记录任务ID
  798. state.inProgressVideoMap[createRes.data] = {
  799. id: createRes.data,
  800. status: AiImageStatusEnum.IN_PROGRESS,
  801. };
  802. console.log("AI视频生成中,请等待。。。");
  803. // 启动统一的轮询机制
  804. this.startPollingTasks('video');
  805. // 如果需要等待完成,使用Promise封装结果
  806. if (waitForCompletion) {
  807. return new Promise((resolve, reject) => {
  808. // 设置一次性的状态检查
  809. const checkStatus = () => {
  810. const videoInfo = state.generatedContent.videoUrl;
  811. if (videoInfo && videoInfo.includes(createRes.data)) {
  812. resolve(videoInfo);
  813. } else if (state.inProgressVideoMap[createRes.data]?.status === AiImageStatusEnum.FAIL) {
  814. reject(new Error("视频生成失败"));
  815. } else if (!state.inProgressVideoMap[createRes.data]) {
  816. reject(new Error("视频任务已不存在"));
  817. } else {
  818. // 继续检查
  819. setTimeout(checkStatus, 1000);
  820. }
  821. };
  822. checkStatus();
  823. });
  824. }
  825. return createRes.data; // 返回任务ID
  826. }, '生成视频失败'),
  827. // 文本生成文本(如AI对话)
  828. textToText: withErrorHandling('AI大模型调用', async function(prompt, model = "default") {
  829. console.log("AI智能体请求,提示词:", prompt);
  830. // 如果没有活跃的对话ID,创建新对话
  831. if (!state.activeConversationId) {
  832. // 使用与TextToText.vue相同的方式创建对话
  833. const res = await CreateDialogue({ roleId: 75 });
  834. state.activeConversationId = res.data;
  835. console.log("AI智能体创建成功,请等待。。。");
  836. }
  837. // 创建AbortController实例
  838. state.conversationInAbortController = new AbortController();
  839. // 使用流式API发送消息
  840. let resultText = "";
  841. let isFirstChunk = true;
  842. await sendChatMessageStream(
  843. state.activeConversationId,
  844. prompt,
  845. null,
  846. state.conversationInAbortController,
  847. true, // 启用上下文
  848. async (res) => {
  849. try {
  850. const { code, data, msg } = JSON.parse(res.data);
  851. if (code !== 0) {
  852. console.log(`对话异常! ${msg}`);
  853. return;
  854. }
  855. // 根据事件类型处理
  856. if (data.eventType === "TEXT") {
  857. // 如果内容为空,就不处理
  858. if (data.receive?.content === "") {
  859. return;
  860. }
  861. // 处理文本消息
  862. resultText += data.receive.content;
  863. // 首次返回时更新预览内容
  864. if (isFirstChunk) {
  865. isFirstChunk = false;
  866. // 设置预览内容
  867. state.generatedContent.text = resultText;
  868. state.previewType = "text";
  869. state.previewContent = resultText;
  870. if (!state.previewVisible) {
  871. state.previewVisible = true;
  872. }
  873. } else {
  874. // 更新预览内容
  875. state.generatedContent.text = resultText;
  876. state.previewContent = resultText;
  877. }
  878. }
  879. } catch (error) {
  880. console.error("处理流式响应失败:", error);
  881. }
  882. },
  883. (error) => {
  884. console.log(`对话异常! ${error}`);
  885. this.stopTextToTextStream();
  886. throw error;
  887. },
  888. () => {
  889. // console.log(`结束对话!`);
  890. this.stopTextToTextStream();
  891. }
  892. );
  893. // 确保最终结果被设置
  894. if (resultText) {
  895. console.log("AI大模型调用成功,返回结果:", resultText);
  896. state.generatedContent.text = resultText;
  897. state.previewType = "text";
  898. state.previewContent = resultText;
  899. if (!state.previewVisible) {
  900. state.previewVisible = true;
  901. }
  902. }
  903. return resultText;
  904. }, 'AI大模型调用失败'),
  905. // 停止文本生成流
  906. stopTextToTextStream() {
  907. if (state.conversationInAbortController) {
  908. state.conversationInAbortController.abort();
  909. }
  910. },
  911. // 设置台灯亮度
  912. setLampBrightness: withErrorHandling('设置台灯亮度', async function(brightness) {
  913. // 验证亮度值在0-100之间
  914. const validBrightness = Math.max(0, Math.min(100, parseInt(brightness) || 0));
  915. // 更新状态
  916. state.lamp.brightness = validBrightness;
  917. // 模拟API调用(实际项目中可替换为真实API)
  918. console.log(`智能台灯亮度已设置为: ${validBrightness}%`);
  919. return validBrightness;
  920. }, '设置台灯亮度失败'),
  921. // 设置台灯颜色
  922. setLampColor: withErrorHandling('设置台灯颜色', async function(color) {
  923. // 预定义的颜色映射
  924. const colorMap = {
  925. '紫': '#D886F0',
  926. '橙': '#F89E35',
  927. '黄': '#F9E67E',
  928. '青': '#6BF5E6',
  929. '白': '#ffffff',
  930. };
  931. // 获取有效的颜色值
  932. let validColor = colorMap[color] || color;
  933. // 检查是否是有效的颜色格式
  934. if (!/^#[0-9A-F]{6}$/i.test(validColor)) {
  935. validColor = "#ffffff"; // 默认白色
  936. }
  937. // 更新状态
  938. state.lamp.color = validColor;
  939. state.lamp.colorLog = color;
  940. // 模拟API调用(实际项目中可替换为真实API)
  941. console.log(`智能台灯颜色已设置为: ${color}`);
  942. return validColor;
  943. }, '设置台灯颜色失败'),
  944. // 音乐播放相关方法
  945. playMusic: withErrorHandling('播放音乐', async function(musicType) {
  946. return playMusic(musicType, state, musicPlayer);
  947. }, '播放音乐失败'),
  948. stopMusic: withErrorHandling('停止音乐', async function() {
  949. return stopMusic(state, musicPlayer);
  950. }, '停止音乐失败'),
  951. // 综合控制台灯(参数格式:"颜色, 亮度, 音乐")
  952. controlLampWithSingleParam: withErrorHandling('智能台灯综合控制', async function(params) {
  953. // 解析参数字符串
  954. let color = '白'; // 默认颜色
  955. let brightness = 0; // 默认亮度
  956. let music = ''; // 音乐信息
  957. if (params && typeof params === 'string') {
  958. // 根据逗号分割参数
  959. const paramArray = params.split(',').map(p => p.trim());
  960. // 提取颜色(第一个参数)
  961. if (paramArray.length > 0 && paramArray[0]) {
  962. color = paramArray[0];
  963. }
  964. // 提取亮度(第二个参数)
  965. if (paramArray.length > 1 && paramArray[1]) {
  966. brightness = paramArray[1];
  967. }
  968. // 提取音乐(第三个参数)
  969. if (paramArray.length > 2 && paramArray[2]) {
  970. music = paramArray[2];
  971. // 调用音乐播放函数
  972. await this.playMusic(music);
  973. }
  974. }
  975. // 调用控制台灯方法
  976. return await this.controlLamp(brightness, color);
  977. }, '智能台灯综合控制失败'),
  978. // 综合控制台灯(参数格式:"颜色, 亮度")
  979. controlLamp: withErrorHandling('智能台灯控制', async function(brightness, color) {
  980. // 先设置亮度
  981. await this.setLampBrightness(brightness);
  982. // 再设置颜色
  983. await this.setLampColor(color);
  984. return { brightness: state.lamp.brightness, color: state.lamp.color };
  985. }, '智能台灯控制失败'),
  986. // 启动任务轮询
  987. startPollingTasks(type) {
  988. if (type === 'image' || type === 'all') {
  989. pollingManager.startPolling('image', async () => {
  990. const imageIds = Object.keys(state.inProgressImageMap).map(Number);
  991. state.inProgressImageMap = await pollTaskStatus(
  992. 'image',
  993. imageIds,
  994. PaintingGetMys,
  995. (image) => {
  996. state.generatedContent.imageUrl = image.picUrl;
  997. state.previewType = "image";
  998. state.previewContent = image.picUrl;
  999. state.previewVisible = true;
  1000. console.log("AI图片生成完成:", image.picUrl);
  1001. },
  1002. (image) => {
  1003. ElMessage.error("图片生成失败: " + (image.error || "未知错误"));
  1004. console.error("图片生成失败:", image.id, image.error);
  1005. }
  1006. );
  1007. });
  1008. }
  1009. if (type === 'video' || type === 'all') {
  1010. pollingManager.startPolling('video', async () => {
  1011. const videoIds = Object.keys(state.inProgressVideoMap).map(Number);
  1012. state.inProgressVideoMap = await pollTaskStatus(
  1013. 'video',
  1014. videoIds,
  1015. VideoGetMys,
  1016. (video) => {
  1017. state.generatedContent.videoUrl = video.videoUrl;
  1018. state.previewType = "video";
  1019. state.previewContent = video.videoUrl;
  1020. state.previewVisible = true;
  1021. console.log("AI视频生成完成:", video.videoUrl);
  1022. },
  1023. (video) => {
  1024. ElMessage.error("视频生成失败: " + (video.error || "未知错误"));
  1025. console.error("视频生成失败:", video.id, video.error);
  1026. }
  1027. );
  1028. });
  1029. }
  1030. }
  1031. };
  1032. // 初始化Blockly工作区和自定义积木
  1033. onMounted(async () => {
  1034. // 从全局状态初始化年级ID
  1035. state.gradeId = globalState.initGradeId();
  1036. // 注册AI语音输入积木
  1037. Blockly.Blocks["ai_voice_input"] = {
  1038. init: function () {
  1039. this.appendDummyInput().appendField("语音输入");
  1040. this.appendValueInput("PROMPT")
  1041. .setCheck("String")
  1042. .appendField("提示文字:");
  1043. this.appendDummyInput()
  1044. .appendField("语言:")
  1045. .appendField(
  1046. new Blockly.FieldDropdown([
  1047. ["中文", "zh-CN"],
  1048. ["英文", "en-US"],
  1049. ]),
  1050. "LANGUAGE"
  1051. );
  1052. this.setOutput(true, "String");
  1053. this.setColour(310);
  1054. this.setTooltip("使用语音识别获取文本输入");
  1055. this.setHelpUrl("");
  1056. },
  1057. };
  1058. // 注册AI文本生成图片积木
  1059. Blockly.Blocks["ai_text_to_image"] = {
  1060. init: function () {
  1061. this.appendDummyInput().appendField("AI生成图片");
  1062. this.appendValueInput("PROMPT").setCheck("String").appendField("提示词:");
  1063. this.appendDummyInput()
  1064. .appendField("等待完成:", "WAIT_LABEL")
  1065. .appendField(new Blockly.FieldCheckbox("TRUE"), "WAIT_FOR_COMPLETION");
  1066. this.setInputsInline(false);
  1067. this.setPreviousStatement(true, null);
  1068. this.setNextStatement(true, null);
  1069. this.setColour(340);
  1070. this.setTooltip("使用AI将文本描述转换为图片");
  1071. this.setHelpUrl("");
  1072. },
  1073. };
  1074. // 注册AI文本生成视频积木
  1075. Blockly.Blocks["ai_text_to_video"] = {
  1076. init: function () {
  1077. this.appendDummyInput().appendField("AI生成视频");
  1078. this.appendValueInput("PROMPT").setCheck("String").appendField("提示词:");
  1079. this.appendDummyInput()
  1080. .appendField("等待完成:", "WAIT_LABEL")
  1081. .appendField(new Blockly.FieldCheckbox("TRUE"), "WAIT_FOR_COMPLETION");
  1082. this.setInputsInline(false);
  1083. this.setPreviousStatement(true, null);
  1084. this.setNextStatement(true, null);
  1085. this.setColour(340);
  1086. this.setTooltip("使用AI将文本描述转换为视频");
  1087. this.setHelpUrl("");
  1088. },
  1089. };
  1090. // 注册AI文本生成文本积木
  1091. Blockly.Blocks["ai_text_to_text"] = {
  1092. init: function () {
  1093. this.appendDummyInput().appendField("AI大模型调用");
  1094. this.appendValueInput("PROMPT")
  1095. .setCheck("String")
  1096. .appendField("输入文本:");
  1097. this.appendDummyInput()
  1098. .appendField("模型:")
  1099. .appendField(
  1100. new Blockly.FieldDropdown([
  1101. ["默认", "default"],
  1102. ["对话", "chat"],
  1103. ["分析", "analysis"],
  1104. ]),
  1105. "MODEL"
  1106. );
  1107. this.setOutput(true, "String");
  1108. this.setColour(300);
  1109. this.setTooltip("使用AI大模型调用并返回结果");
  1110. this.setHelpUrl("");
  1111. },
  1112. };
  1113. //AI智能台灯单参数积木
  1114. Blockly.Blocks['ai_smart_lamp_single_param'] = {
  1115. init: function() {
  1116. this.appendDummyInput()
  1117. .appendField('智能台灯控制(单参数)');
  1118. this.appendValueInput('PARAMS')
  1119. .setCheck('String')
  1120. .appendField('参数(格式: 颜色,亮度,音乐):');
  1121. this.appendDummyInput()
  1122. .appendField('例如: 蓝,50,平静');
  1123. this.setInputsInline(false);
  1124. this.setPreviousStatement(true, null);
  1125. this.setNextStatement(true, null);
  1126. this.setColour(280);
  1127. this.setTooltip('通过一个参数字符串控制智能台灯的亮度、颜色和音乐\n格式: 颜色,亮度,音乐\n例如: 蓝,50,平静');
  1128. this.setHelpUrl('');
  1129. }
  1130. };
  1131. // 注册AI智能台灯积木
  1132. Blockly.Blocks["ai_smart_lamp"] = {
  1133. init: function () {
  1134. this.appendDummyInput().appendField("智能台灯控制");
  1135. this.appendValueInput("BRIGHTNESS")
  1136. .setCheck(["Number", "String"])
  1137. .appendField("亮度 (0-100):");
  1138. this.appendValueInput("COLOR").setCheck("String").appendField("颜色:");
  1139. this.setInputsInline(false);
  1140. this.setPreviousStatement(true, null);
  1141. this.setNextStatement(true, null);
  1142. this.setColour(280);
  1143. this.setTooltip("控制智能台灯的亮度和颜色");
  1144. this.setHelpUrl("");
  1145. },
  1146. };
  1147. // 注册AI台灯设置亮度积木
  1148. Blockly.Blocks["ai_lamp_set_brightness"] = {
  1149. init: function () {
  1150. this.appendDummyInput().appendField("设置台灯亮度");
  1151. this.appendValueInput("BRIGHTNESS")
  1152. .setCheck(["Number", "String"])
  1153. .appendField("亮度 (0-100):");
  1154. this.setInputsInline(false);
  1155. this.setPreviousStatement(true, null);
  1156. this.setNextStatement(true, null);
  1157. this.setColour(270);
  1158. this.setTooltip("设置智能台灯的亮度");
  1159. this.setHelpUrl("");
  1160. },
  1161. };
  1162. // 注册AI台灯设置颜色积木
  1163. Blockly.Blocks["ai_lamp_set_color"] = {
  1164. init: function () {
  1165. this.appendDummyInput().appendField("设置台灯颜色");
  1166. this.appendValueInput("COLOR").setCheck("String").appendField("颜色:");
  1167. this.appendDummyInput().appendField("可选颜色: 红,蓝,绿,黄,青,靛,紫");
  1168. this.setInputsInline(false);
  1169. this.setPreviousStatement(true, null);
  1170. this.setNextStatement(true, null);
  1171. this.setColour(275);
  1172. this.setTooltip("设置智能台灯的颜色");
  1173. this.setHelpUrl("");
  1174. },
  1175. };
  1176. // 注册音乐播放积木
  1177. Blockly.Blocks['ai_music_play'] = {
  1178. init: function() {
  1179. this.appendDummyInput()
  1180. .appendField('播放音乐');
  1181. this.appendDummyInput()
  1182. .appendField('音乐类型:')
  1183. .appendField(
  1184. new Blockly.FieldDropdown([
  1185. ['平静', '平静'],
  1186. ['欢快', '欢快'],
  1187. ['舒缓', '舒缓'],
  1188. ['激情', '激情'],
  1189. ['冥想', '冥想'],
  1190. ['自然', '自然']
  1191. ]),
  1192. 'MUSIC_TYPE'
  1193. );
  1194. this.setInputsInline(false);
  1195. this.setPreviousStatement(true, null);
  1196. this.setNextStatement(true, null);
  1197. this.setColour(290);
  1198. this.setTooltip('播放指定类型的音乐');
  1199. this.setHelpUrl('');
  1200. }
  1201. };
  1202. // 注册JavaScript代码生成器
  1203. registerJavaScriptGenerators();
  1204. // 注册Python代码生成器
  1205. registerPythonGenerators();
  1206. // 在初始化工作区后添加以下代码来恢复初始加载功能并修复拖拽问题
  1207. // 初始化工作区
  1208. state.workspace = Blockly.inject("blocklyDiv", {
  1209. toolbox: document.getElementById("toolbox"),
  1210. grid: { spacing: 20, length: 3, colour: "#ccc", snap: true },
  1211. trashcan: true,
  1212. });
  1213. // 恢复并优化初始加载组件逻辑 - 与当前Blockly版本兼容的修复版
  1214. try {
  1215. // 使用字符串拼接定义初始XML内容
  1216. const initialXml = "<xml xmlns=\"https://developers.google.com/blockly/xml\">\n" +
  1217. " <variables>\n" +
  1218. " <variable id=\"kAVG*zJLw/q)l/(/eIMM\">inputText</variable>\n" +
  1219. " <variable id=\"[7F(niLz{.fvWvY.VT/N\">lampConfig</variable>\n" +
  1220. " </variables>\n" +
  1221. " <block type=\"variables_set\" id=\"XIsa=sV/8ImVP5P3:ol4\" x=\"90\" y=\"90\">\n" +
  1222. " <field name=\"VAR\" id=\"kAVG*zJLw/q)l/(/eIMM\">inputText</field>\n" +
  1223. " <value name=\"VALUE\">\n" +
  1224. " <block type=\"ai_voice_input\" id=\"Ql).^+/uT(P~$aMg2OEA\">\n" +
  1225. " <field name=\"LANGUAGE\">zh-CN</field>\n" +
  1226. " <value name=\"PROMPT\">\n" +
  1227. " <block type=\"text\" id=\"kXjnnUncUu7p$a[zz)da\">\n" +
  1228. " <field name=\"TEXT\">请说话:</field>\n" +
  1229. " </block>\n" +
  1230. " </value>\n" +
  1231. " </block>\n" +
  1232. " </value>\n" +
  1233. " <next>\n" +
  1234. " <block type=\"variables_set\" id=\"AP0CO$VPeM0*PS*CKT,V\">\n" +
  1235. " <field name=\"VAR\" id=\"[7F(niLz{.fvWvY.VT/N\">lampConfig</field>\n" +
  1236. " <value name=\"VALUE\">\n" +
  1237. " <block type=\"ai_text_to_text\" id=\"lZTz/FN3b(*_p?1zug7F\">\n" +
  1238. " <field name=\"MODEL\">default</field>\n" +
  1239. " <value name=\"PROMPT\">\n" +
  1240. " <block type=\"variables_get\" id=\"si~IQyn|#1H(BKV4Gq;X\">\n" +
  1241. " <field name=\"VAR\" id=\"kAVG*zJLw/q)l/(/eIMM\">inputText</field>\n" +
  1242. " </block>\n" +
  1243. " </value>\n" +
  1244. " </block>\n" +
  1245. " </value>\n" +
  1246. " <next>\n" +
  1247. " <block type=\"ai_smart_lamp_single_param\" id=\"ZTAwt~MMm(t5w:$$Azt`\">\n" +
  1248. " <value name=\"PARAMS\">\n" +
  1249. " <block type=\"variables_get\" id=\"X):w|X4hYE~ru1r9`W8f\">\n" +
  1250. " <field name=\"VAR\" id=\"[7F(niLz{.fvWvY.VT/N\">lampConfig</field>\n" +
  1251. " </block>\n" +
  1252. " </value>\n" +
  1253. " </block>\n" +
  1254. " </next>\n" +
  1255. " </block>\n" +
  1256. " </next>\n" +
  1257. " </block>\n" +
  1258. "</xml>";
  1259. // 使用安全的方式解析XML
  1260. const xml = Blockly.utils.xml.textToDom(initialXml);
  1261. // 清空工作区确保没有遗留节点
  1262. state.workspace.clear();
  1263. // 使用当前Blockly版本支持的标准方法加载XML
  1264. Blockly.Xml.domToWorkspace(xml, state.workspace);
  1265. // 使用setTimeout确保DOM完全加载和节点注册完成
  1266. setTimeout(() => {
  1267. // 确保所有块都可移动和可删除
  1268. const allBlocks = state.workspace.getAllBlocks();
  1269. allBlocks.forEach(block => {
  1270. // 重置拖拽状态
  1271. block.draggable_ = true;
  1272. // 强制设置可移动和可删除属性
  1273. block.setMovable(true);
  1274. block.setDeletable(true);
  1275. // 关键修复:确保SVG节点正确注册到工作区
  1276. if (block.svgGroup_ && block.svgGroup_.parentNode) {
  1277. // 重新附加到DOM以触发正确的注册
  1278. const parent = block.svgGroup_.parentNode;
  1279. parent.removeChild(block.svgGroup_);
  1280. parent.appendChild(block.svgGroup_);
  1281. // 强制重新渲染块
  1282. block.render();
  1283. }
  1284. });
  1285. // 修复FocusManager问题:采用兼容方式
  1286. const canvas = state.workspace.getCanvas();
  1287. if (canvas && canvas.dispatchEvent) {
  1288. // 模拟鼠标移动事件,触发Blockly内部重新扫描DOM
  1289. const tempEvent = new MouseEvent('mousemove', {
  1290. bubbles: true,
  1291. cancelable: true,
  1292. view: window
  1293. });
  1294. canvas.dispatchEvent(tempEvent);
  1295. }
  1296. // 强制重新渲染整个工作区
  1297. state.workspace.render();
  1298. // 为初始块添加自定义的拖拽处理器
  1299. const initialBlocks = state.workspace.getAllBlocks();
  1300. initialBlocks.forEach(block => {
  1301. // 为初始块添加错误处理包装器
  1302. if (block.onMouseDown) {
  1303. const originalMouseDown = block.onMouseDown;
  1304. block.onMouseDown = function(e) {
  1305. try {
  1306. // 调用原始的鼠标按下处理函数
  1307. originalMouseDown.call(this, e);
  1308. } catch (error) {
  1309. console.warn('处理块鼠标事件失败:', error);
  1310. // 防止错误传播,影响其他块
  1311. if (e && e.stopPropagation) {
  1312. e.stopPropagation();
  1313. }
  1314. }
  1315. };
  1316. }
  1317. });
  1318. // 初始化完成后手动生成一次代码
  1319. generateCode("javascript");
  1320. }, 1000); // 增加延迟时间确保DOM完全就绪
  1321. } catch (error) {
  1322. console.error('加载初始组件失败:', error);
  1323. }
  1324. // 添加工作区变化监听器
  1325. state.workspace.addChangeListener(() => generateCode("javascript"));
  1326. });
  1327. // 组件卸载时清除所有资源
  1328. onUnmounted(() => {
  1329. // 统一的轮询管理器停止所有轮询
  1330. pollingManager.stopAll();
  1331. // 停止音乐播放
  1332. aiService.stopMusic();
  1333. // 停止文本生成流
  1334. aiService.stopTextToTextStream();
  1335. // 释放工作区资源
  1336. if (state.workspace) {
  1337. state.workspace.dispose();
  1338. }
  1339. });
  1340. // 注册JavaScript代码生成器
  1341. function registerJavaScriptGenerators() {
  1342. // 语音输入
  1343. javascriptGenerator.forBlock['ai_voice_input'] = function(block, generator) {
  1344. const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
  1345. const language = block.getFieldValue('LANGUAGE');
  1346. const code = `await aiService.recognizeVoice(${prompt || "''"}, '${language}')`;
  1347. return [code, javascriptGenerator.ORDER_ATOMIC];
  1348. };
  1349. // 文本生成图片
  1350. javascriptGenerator.forBlock['ai_text_to_image'] = function(block, generator) {
  1351. const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
  1352. const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
  1353. const code = `await aiService.textToImage(${prompt}, ${waitForCompletion});`;
  1354. return code;
  1355. };
  1356. // 文本生成视频
  1357. javascriptGenerator.forBlock['ai_text_to_video'] = function(block, generator) {
  1358. const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
  1359. const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
  1360. const code = `await aiService.textToVideo(${prompt}, ${waitForCompletion});`;
  1361. return code;
  1362. };
  1363. // 文本生成文本
  1364. javascriptGenerator.forBlock['ai_text_to_text'] = function(block, generator) {
  1365. const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
  1366. const model = block.getFieldValue('MODEL');
  1367. const code = `await aiService.textToText(${prompt}, '${model}')`;
  1368. return [code, javascriptGenerator.ORDER_ATOMIC];
  1369. };
  1370. // 智能台灯控制(单参数)
  1371. javascriptGenerator.forBlock['ai_smart_lamp_single_param'] = function(block, generator) {
  1372. const params = generator.valueToCode(block, 'PARAMS', javascriptGenerator.ORDER_ATOMIC);
  1373. const code = `await aiService.controlLampWithSingleParam(${params || "'白,0,平静'"});`;
  1374. return code;
  1375. };
  1376. // 智能台灯控制(多参数)
  1377. javascriptGenerator.forBlock['ai_smart_lamp'] = function(block, generator) {
  1378. const brightness = generator.valueToCode(block, 'BRIGHTNESS', javascriptGenerator.ORDER_ATOMIC);
  1379. const color = generator.valueToCode(block, 'COLOR', javascriptGenerator.ORDER_ATOMIC);
  1380. const code = `await aiService.controlLamp(${brightness || '0'}, ${color || "'白'"});`;
  1381. return code;
  1382. };
  1383. // 设置台灯亮度
  1384. javascriptGenerator.forBlock['ai_lamp_set_brightness'] = function(block, generator) {
  1385. const brightness = generator.valueToCode(block, 'BRIGHTNESS', javascriptGenerator.ORDER_ATOMIC);
  1386. const code = `await aiService.setLampBrightness(${brightness || '0'});`;
  1387. return code;
  1388. };
  1389. // 设置台灯颜色
  1390. javascriptGenerator.forBlock['ai_lamp_set_color'] = function(block, generator) {
  1391. const color = generator.valueToCode(block, 'COLOR', javascriptGenerator.ORDER_ATOMIC);
  1392. const code = `await aiService.setLampColor(${color || "'白'"});`;
  1393. return code;
  1394. };
  1395. // 音乐播放
  1396. javascriptGenerator.forBlock['ai_music_play'] = function(block, generator) {
  1397. const musicType = block.getFieldValue('MUSIC_TYPE');
  1398. const code = `await aiService.playMusic('${musicType}');`;
  1399. return code;
  1400. };
  1401. }
  1402. // 注册Python代码生成器
  1403. function registerPythonGenerators() {
  1404. // 语音输入
  1405. pythonGenerator.forBlock['ai_voice_input'] = function(block, generator) {
  1406. const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
  1407. const language = block.getFieldValue('LANGUAGE');
  1408. const code = `ai_service.recognize_voice(${prompt || "''"}, '${language}')`;
  1409. return [code, pythonGenerator.ORDER_ATOMIC];
  1410. };
  1411. // 文本生成图片
  1412. pythonGenerator.forBlock['ai_text_to_image'] = function(block, generator) {
  1413. const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
  1414. const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
  1415. const code = `ai_service.text_to_image(${prompt}, ${waitForCompletion})\n`;
  1416. return code;
  1417. };
  1418. // 文本生成视频
  1419. pythonGenerator.forBlock['ai_text_to_video'] = function(block, generator) {
  1420. const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
  1421. const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
  1422. const code = `ai_service.text_to_video(${prompt}, ${waitForCompletion})\n`;
  1423. return code;
  1424. };
  1425. // 文本生成文本
  1426. pythonGenerator.forBlock['ai_text_to_text'] = function(block, generator) {
  1427. const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
  1428. const model = block.getFieldValue('MODEL');
  1429. const code = `ai_service.text_to_text(${prompt}, '${model}')`;
  1430. return [code, pythonGenerator.ORDER_ATOMIC];
  1431. };
  1432. // 智能台灯控制(单参数)
  1433. pythonGenerator.forBlock['ai_smart_lamp_single_param'] = function(block, generator) {
  1434. const params = generator.valueToCode(block, 'PARAMS', pythonGenerator.ORDER_ATOMIC);
  1435. const code = `ai_service.control_lamp_with_single_param(${params || "'白,0,平静'"})\n`;
  1436. return code;
  1437. };
  1438. // 智能台灯控制
  1439. pythonGenerator.forBlock['ai_smart_lamp'] = function(block, generator) {
  1440. const brightness = generator.valueToCode(block, 'BRIGHTNESS', pythonGenerator.ORDER_ATOMIC);
  1441. const color = generator.valueToCode(block, 'COLOR', pythonGenerator.ORDER_ATOMIC);
  1442. const code = `ai_service.control_lamp(${brightness || '0'}, ${color || "'白'"})\n`;
  1443. return code;
  1444. };
  1445. // 设置台灯亮度
  1446. pythonGenerator.forBlock['ai_lamp_set_brightness'] = function(block, generator) {
  1447. const brightness = generator.valueToCode(block, 'BRIGHTNESS', pythonGenerator.ORDER_ATOMIC);
  1448. const code = `ai_service.set_lamp_brightness(${brightness || '0'})\n`;
  1449. return code;
  1450. };
  1451. // 设置台灯颜色
  1452. pythonGenerator.forBlock['ai_lamp_set_color'] = function(block, generator) {
  1453. const color = generator.valueToCode(block, 'COLOR', pythonGenerator.ORDER_ATOMIC);
  1454. const code = `ai_service.set_lamp_color(${color || "'白'"})\n`;
  1455. return code;
  1456. };
  1457. // 音乐播放
  1458. pythonGenerator.forBlock['ai_music_play'] = function(block, generator) {
  1459. const musicType = block.getFieldValue('MUSIC_TYPE');
  1460. const code = `ai_service.play_music('${musicType}')\n`;
  1461. return code;
  1462. };
  1463. }
  1464. // 生成代码
  1465. const generateCode = (language) => {
  1466. if (!state.workspace) {
  1467. console.error("workspace 未正确初始化");
  1468. return;
  1469. }
  1470. let generator;
  1471. if (language === "javascript") {
  1472. generator = javascriptGenerator;
  1473. } else if (language === "python") {
  1474. generator = pythonGenerator;
  1475. } else {
  1476. console.error("不支持的语言类型");
  1477. return;
  1478. }
  1479. const code = generator.workspaceToCode(state.workspace);
  1480. document.getElementById("textarea").value = code;
  1481. };
  1482. // 运行代码
  1483. async function runCode() {
  1484. try {
  1485. const codeTextarea = document.getElementById("textarea");
  1486. const code = codeTextarea.value;
  1487. const resultElement = document.getElementById("run-result");
  1488. // 清空之前的结果
  1489. resultElement.innerHTML = '<div class="running-indicator">代码运行中...</div>';
  1490. // 保存原始console方法
  1491. const originalConsoleLog = console.log;
  1492. const originalConsoleError = console.error;
  1493. const originalConsoleWarn = console.warn;
  1494. // 创建输出缓冲区
  1495. let outputBuffer = "";
  1496. // 重定义console方法 - 修复console.log不更新DOM的问题
  1497. console.log = (...args) => {
  1498. // 过滤掉包含Vue警告的日志
  1499. const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' ');
  1500. // 只显示不包含Vue warn的日志
  1501. if (!message.includes('[Vue warn]')) {
  1502. outputBuffer += '<div class="log-message">' + message + '</div>';
  1503. resultElement.innerHTML = outputBuffer; // 关键修复:立即更新DOM
  1504. }
  1505. originalConsoleLog.apply(console, args);
  1506. };
  1507. console.error = (...args) => {
  1508. // 过滤掉包含Vue警告的日志
  1509. const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' ');
  1510. // 只显示不包含Vue warn的日志
  1511. if (!message.includes('[Vue warn]')) {
  1512. outputBuffer += '<div class="error-message">' + message + '</div>';
  1513. resultElement.innerHTML = outputBuffer;
  1514. }
  1515. originalConsoleError.apply(console, args);
  1516. };
  1517. console.warn = (...args) => {
  1518. // 过滤掉包含Vue警告的日志
  1519. const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' ');
  1520. // 只显示不包含Vue warn的日志
  1521. if (!message.includes('[Vue warn]')) {
  1522. outputBuffer += '<div class="warn-message">' + message + '</div>';
  1523. resultElement.innerHTML = outputBuffer;
  1524. }
  1525. originalConsoleWarn.apply(console, args);
  1526. };
  1527. try {
  1528. // 添加安全检查
  1529. if (code.includes('eval(') || code.includes('Function(') ||
  1530. code.includes('document.write') || code.includes('window.location')) {
  1531. throw new Error('代码包含不安全的操作');
  1532. }
  1533. // 包装代码为异步函数执行,支持await
  1534. const wrappedCode = `(async () => { ${code} })()`;
  1535. await new Function(wrappedCode)();
  1536. // outputBuffer += '<div class="success-message">代码执行成功</div>';
  1537. resultElement.innerHTML = outputBuffer;
  1538. } catch (error) {
  1539. outputBuffer += `<div class="error-message">执行错误: ${error.message}</div>`;
  1540. resultElement.innerHTML = outputBuffer;
  1541. } finally {
  1542. // 恢复原始console方法
  1543. // console.log = originalConsoleLog;
  1544. // console.error = originalConsoleError;
  1545. // console.warn = originalConsoleWarn;
  1546. }
  1547. } catch (err) {
  1548. console.error('运行代码时发生错误:', err);
  1549. document.getElementById('run-result').innerHTML =
  1550. `<div class="error-message">运行代码时发生错误: ${err.message}</div>`;
  1551. }
  1552. }
  1553. // 保存JSON
  1554. function saveJson() {
  1555. try {
  1556. const output = document.getElementById('textarea');
  1557. const json = Blockly.serialization.workspaces.save(state.workspace);
  1558. output.value = JSON.stringify(json, null, 2);
  1559. output.focus();
  1560. output.select();
  1561. taChange();
  1562. ElMessage.success('JSON保存成功');
  1563. } catch (error) {
  1564. console.error('保存JSON失败:', error);
  1565. ElMessage.error('保存JSON失败: ' + error.message);
  1566. }
  1567. }
  1568. // 保存XML
  1569. function saveXml() {
  1570. try {
  1571. const output = document.getElementById('textarea');
  1572. const xml = Blockly.Xml.workspaceToDom(state.workspace);
  1573. output.value = Blockly.Xml.domToPrettyText(xml);
  1574. output.focus();
  1575. output.select();
  1576. taChange();
  1577. ElMessage.success('XML保存成功');
  1578. } catch (error) {
  1579. console.error('保存XML失败:', error);
  1580. ElMessage.error('保存XML失败: ' + error.message);
  1581. }
  1582. }
  1583. // 重新加载
  1584. function load() {
  1585. const input = document.getElementById('textarea');
  1586. if (!input.value) return;
  1587. try {
  1588. //关闭预览
  1589. handleClosePreview();
  1590. const valid = saveIsValid(input.value);
  1591. if (valid.json) {
  1592. const parsedState = JSON.parse(input.value);
  1593. Blockly.serialization.workspaces.load(parsedState, state.workspace);
  1594. } else if (valid.xml) {
  1595. const xml = Blockly.utils.xml.textToDom(input.value);
  1596. Blockly.Xml.domToWorkspace(xml, state.workspace);
  1597. }
  1598. taChange();
  1599. } catch (error) {
  1600. console.error("加载失败:", error);
  1601. ElMessage.error("加载失败: " + error.message);
  1602. }
  1603. }
  1604. // 文本域变化时触发
  1605. function taChange() {
  1606. const textarea = document.getElementById("textarea");
  1607. if (sessionStorage) {
  1608. sessionStorage.setItem("textarea", textarea.value);
  1609. }
  1610. const valid = saveIsValid(textarea.value);
  1611. document.getElementById("import").disabled = !valid.json && !valid.xml;
  1612. }
  1613. // 检查保存内容是否为有效JSON或XML
  1614. function saveIsValid(save) {
  1615. let validJson = true;
  1616. try {
  1617. JSON.parse(save);
  1618. } catch (e) {
  1619. validJson = false;
  1620. }
  1621. let validXml = true;
  1622. try {
  1623. Blockly.utils.xml.textToDom(save);
  1624. } catch (e) {
  1625. validXml = false;
  1626. }
  1627. return { json: validJson, xml: validXml };
  1628. }
  1629. // 关闭预览
  1630. function handleClosePreview() {
  1631. state.previewVisible = false;
  1632. state.previewContent = "";
  1633. state.previewType = "";
  1634. state.generatedContent.text = null;
  1635. state.generatedContent.imageUrl = null;
  1636. state.generatedContent.videoUrl = null;
  1637. }
  1638. // 将aiService挂载到window,以便执行生成的代码时可以访问
  1639. window.aiService = aiService;
  1640. </script>
  1641. <style scoped lang="scss">
  1642. /* 原有样式保持不变 */
  1643. @use "sass:math";
  1644. @function rpx($px) {
  1645. @return math.div($px, 750) * 100vw;
  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. .page-container {
  1758. position: fixed;
  1759. top: 0;
  1760. left: 0;
  1761. right: 0;
  1762. bottom: 0;
  1763. background: linear-gradient(to bottom, #001169, #b4a8e1);
  1764. display: flex;
  1765. flex-direction: column;
  1766. }
  1767. // 标题样式
  1768. .title-box {
  1769. height: rpx(30);
  1770. }
  1771. .box-icon {
  1772. width: 100%;
  1773. height: 100%;
  1774. display: flex;
  1775. align-items: center;
  1776. color: white;
  1777. padding-left: rpx(15);
  1778. font-size: rpx(10);
  1779. cursor: pointer;
  1780. }
  1781. .box-icon .left-icon {
  1782. margin-left: rpx(10);
  1783. margin-right: rpx(5);
  1784. }
  1785. .home-container {
  1786. flex: 1;
  1787. display: flex;
  1788. // padding-top: rpx(10);
  1789. }
  1790. .box-blockly {
  1791. width: 60%;
  1792. height: 90%;
  1793. padding: rpx(10) rpx(10);
  1794. float: left;
  1795. }
  1796. .box-code {
  1797. color: white;
  1798. width: 35%;
  1799. height: 90%;
  1800. padding: rpx(10) rpx(10);
  1801. display: flex;
  1802. flex-direction: column;
  1803. .button-container {
  1804. display: flex;
  1805. flex-wrap: wrap;
  1806. gap: 8px;
  1807. align-items: flex-start;
  1808. }
  1809. .button-container :deep(.el-button) {
  1810. flex-shrink: 0;
  1811. }
  1812. .code-section {
  1813. flex: 1;
  1814. display: flex;
  1815. flex-direction: column;
  1816. margin-bottom: 10px;
  1817. .box-code-textarea {
  1818. flex: 1;
  1819. resize: none;
  1820. background-color: #f8f9fa;
  1821. border: 1px solid #ddd;
  1822. border-radius: 4px;
  1823. padding: 10px;
  1824. overflow-y: auto;
  1825. font-family: monospace;
  1826. font-size: 14px;
  1827. }
  1828. }
  1829. .result-section {
  1830. flex: 1;
  1831. display: flex;
  1832. flex-direction: column;
  1833. .run-result-content {
  1834. color: black;
  1835. flex: 1;
  1836. background-color: #f8f9fa;
  1837. border: 1px solid #ddd;
  1838. border-radius: 4px;
  1839. padding: 10px;
  1840. overflow-y: auto;
  1841. font-family: monospace;
  1842. font-size: 14px;
  1843. }
  1844. }
  1845. }
  1846. // AI模块样式
  1847. [categorystyle="ai_category"] > .blocklyTreeRow {
  1848. background-color: #9c27b0 !important;
  1849. }
  1850. // 运行结果样式
  1851. .running-indicator {
  1852. color: #666;
  1853. }
  1854. .log-message {
  1855. color: #333;
  1856. }
  1857. .error-message {
  1858. color: #dc3545;
  1859. }
  1860. .warn-message {
  1861. color: #ffc107;
  1862. }
  1863. .success-message {
  1864. color: #28a745;
  1865. }
  1866. // 预览样式
  1867. .preview-image-container,
  1868. .preview-video-container {
  1869. display: flex;
  1870. justify-content: center;
  1871. }
  1872. .preview-image,
  1873. .preview-video {
  1874. max-width: 100%;
  1875. max-height: 60vh;
  1876. border-radius: 4px;
  1877. }
  1878. .preview-text-container {
  1879. max-height: 60vh;
  1880. overflow-y: auto;
  1881. padding: 10px;
  1882. background-color: #f5f5f5;
  1883. border-radius: 4px;
  1884. }
  1885. //【文生图预览】
  1886. .extra-image-preview {
  1887. color: black;
  1888. margin-top: 10px;
  1889. padding: 10px;
  1890. border: 1px solid #ddd;
  1891. border-radius: 5px;
  1892. background-color: #f9f9f9;
  1893. }
  1894. .extra-preview-image {
  1895. max-width: 100%;
  1896. max-height: 400px;
  1897. border-radius: 4px;
  1898. }
  1899. //台灯
  1900. .lamp-preview-container {
  1901. margin-top: 20px;
  1902. padding: 10px;
  1903. background-color: rgba(255, 255, 255, 0.1);
  1904. border-radius: 8px;
  1905. text-align: center;
  1906. }
  1907. .lamp-display {
  1908. margin: 15px auto;
  1909. position: relative;
  1910. transition: all 0.3s ease;
  1911. }
  1912. .lamp-image {
  1913. width: 100px;
  1914. height: 150px;
  1915. margin: 0 auto;
  1916. background-color: #e0e0e0;
  1917. border-radius: 10px 10px 0 0;
  1918. position: relative;
  1919. box-shadow: 0 0 40px 20px #ffffff80;
  1920. transition: box-shadow 0.5s ease;
  1921. &::before {
  1922. content: "";
  1923. position: absolute;
  1924. top: 10px;
  1925. left: 10px;
  1926. right: 10px;
  1927. bottom: 40px;
  1928. background-color: #f5f5f5;
  1929. border-radius: 5px;
  1930. }
  1931. &::after {
  1932. content: "";
  1933. position: absolute;
  1934. bottom: 10px;
  1935. left: 40px;
  1936. width: 20px;
  1937. height: 30px;
  1938. background-color: #888888;
  1939. }
  1940. }
  1941. .lamp-info {
  1942. margin-top: 10px;
  1943. font-size: 14px;
  1944. color: #ffffff;
  1945. }
  1946. //音乐播放器
  1947. .music-player-container {
  1948. margin-top: 20px;
  1949. padding: 15px;
  1950. background-color: #f9f9f9;
  1951. border-radius: 8px;
  1952. border: 1px solid #e0e0e0;
  1953. }
  1954. .music-player-container h5 {
  1955. margin-top: 0;
  1956. margin-bottom: 10px;
  1957. color: #333;
  1958. font-size: 16px;
  1959. }
  1960. .music-player-container audio {
  1961. width: 100%;
  1962. margin-bottom: 10px;
  1963. }
  1964. .music-status {
  1965. font-size: 14px;
  1966. color: #666;
  1967. padding: 5px 0;
  1968. }
  1969. </style>