Blockly2.vue 65 KB

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