Blockly2.vue 66 KB

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