Blockly.vue 61 KB

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