Blockly.vue 81 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697
  1. <template>
  2. <!-- 智能台灯 -->
  3. <div v-if="showLampPreview" class="desk-lamp-container">
  4. <!-- 标题框 -->
  5. <div class="desk-lamp-title-box">
  6. <div class="desk-lamp-box-icon" @click="goBackLab">
  7. <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
  8. 返回虚拟实验室
  9. </div>
  10. </div>
  11. <img src="@/assets/images/desklamp.png" alt="智能台灯" class="full-screen-image" />
  12. <!-- 使用动态样式设置灯光遮罩 -->
  13. <div v-if="state.lamp.isLightOn" :style="{ '--lamp-color': state.lamp.color,'--lamp-opacity': state.lamp.brightness / 100 }" class="lamp-light-mask"></div>
  14. <!-- 右下角按钮组 -->
  15. <div class="button-group">
  16. <el-button class="control-button run-button" @click="toggleLight">运行</el-button>
  17. <el-button class="control-button code-button" @click="handleViewCode">智能台灯</el-button>
  18. </div>
  19. <!-- 显示当前灯光信息 -->
  20. <div v-if="state.lamp.isLightOn" class="lamp-info">
  21. <p>颜色: {{ state.lamp.colorLog }}色</p>
  22. <p>亮度: {{ state.lamp.brightness }}%</p>
  23. </div>
  24. </div>
  25. <!-- Blockly -->
  26. <div class="page-container">
  27. <!-- 返回 -->
  28. <div class="title-box">
  29. <div class="box-icon" @click="goBack">
  30. <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
  31. 返回智能台灯
  32. </div>
  33. </div>
  34. <div class="home-container">
  35. <div class="box-blockly">
  36. <div
  37. id="blocklyDiv"
  38. style="height: 100%; width: 100%; background-color: #cccccc"
  39. ></div>
  40. <!-- blockly工具栏 -->
  41. <template>
  42. <xml id="toolbox" style="display: none">
  43. <!-- AI模块分类 - 扩展更多功能 -->
  44. <category name="AI模块" categorystyle="ai_category">
  45. <block type="ai_voice_input"></block>
  46. <block type="ai_text_to_image"></block>
  47. <block type="ai_text_to_video"></block>
  48. <block type="ai_text_to_text"></block>
  49. <block type="ai_smart_lamp_single_param"></block>
  50. <block type="ai_smart_lamp"></block>
  51. <block type="ai_lamp_set_brightness"></block>
  52. <block type="ai_lamp_set_color"></block>
  53. <block type="ai_music_play"></block>
  54. </category>
  55. <!-- 其他原有分类保持不变 -->
  56. <category name="逻辑" categorystyle="logic_category">
  57. <block type="controls_if"></block>
  58. <block type="logic_compare"></block>
  59. <block type="logic_operation"></block>
  60. <block type="logic_negate"></block>
  61. <block type="logic_boolean"></block>
  62. <block type="logic_null" disabled="true"></block>
  63. <block type="logic_ternary"></block>
  64. </category>
  65. <category name="循环" categorystyle="loop_category">
  66. <block type="controls_repeat_ext">
  67. <value name="TIMES">
  68. <shadow type="math_number">
  69. <field name="NUM">10</field>
  70. </shadow>
  71. </value>
  72. </block>
  73. <block type="controls_repeat" disabled="true"></block>
  74. <block type="controls_whileUntil"></block>
  75. <block type="controls_for">
  76. <value name="FROM">
  77. <shadow type="math_number">
  78. <field name="NUM">1</field>
  79. </shadow>
  80. </value>
  81. <value name="TO">
  82. <shadow type="math_number">
  83. <field name="NUM">10</field>
  84. </shadow>
  85. </value>
  86. <value name="BY">
  87. <shadow type="math_number">
  88. <field name="NUM">1</field>
  89. </shadow>
  90. </value>
  91. </block>
  92. <block type="controls_forEach"></block>
  93. <block type="controls_flow_statements"></block>
  94. </category>
  95. <category name="数值" categorystyle="math_category">
  96. <block type="math_number" gap="32">
  97. <field name="NUM">123</field>
  98. </block>
  99. <block type="math_arithmetic">
  100. <value name="A">
  101. <shadow type="math_number">
  102. <field name="NUM">1</field>
  103. </shadow>
  104. </value>
  105. <value name="B">
  106. <shadow type="math_number">
  107. <field name="NUM">1</field>
  108. </shadow>
  109. </value>
  110. </block>
  111. <block type="math_single">
  112. <value name="NUM">
  113. <shadow type="math_number">
  114. <field name="NUM">9</field>
  115. </shadow>
  116. </value>
  117. </block>
  118. <block type="math_trig">
  119. <value name="NUM">
  120. <shadow type="math_number">
  121. <field name="NUM">45</field>
  122. </shadow>
  123. </value>
  124. </block>
  125. <block type="math_constant"></block>
  126. <block type="math_number_property">
  127. <value name="NUMBER_TO_CHECK">
  128. <shadow type="math_number">
  129. <field name="NUM">0</field>
  130. </shadow>
  131. </value>
  132. </block>
  133. <block type="math_round">
  134. <value name="NUM">
  135. <shadow type="math_number">
  136. <field name="NUM">3.1</field>
  137. </shadow>
  138. </value>
  139. </block>
  140. <block type="math_on_list"></block>
  141. <block type="math_modulo">
  142. <value name="DIVIDEND">
  143. <shadow type="math_number">
  144. <field name="NUM">64</field>
  145. </shadow>
  146. </value>
  147. <value name="DIVISOR">
  148. <shadow type="math_number">
  149. <field name="NUM">10</field>
  150. </shadow>
  151. </value>
  152. </block>
  153. <block type="math_constrain">
  154. <value name="VALUE">
  155. <shadow type="math_number">
  156. <field name="NUM">50</field>
  157. </shadow>
  158. </value>
  159. <value name="LOW">
  160. <shadow type="math_number">
  161. <field name="NUM">1</field>
  162. </shadow>
  163. </value>
  164. <value name="HIGH">
  165. <shadow type="math_number">
  166. <field name="NUM">100</field>
  167. </shadow>
  168. </value>
  169. </block>
  170. <block type="math_random_int">
  171. <value name="FROM">
  172. <shadow type="math_number">
  173. <field name="NUM">1</field>
  174. </shadow>
  175. </value>
  176. <value name="TO">
  177. <shadow type="math_number">
  178. <field name="NUM">100</field>
  179. </shadow>
  180. </value>
  181. </block>
  182. <block type="math_random_float"></block>
  183. <block type="math_atan2">
  184. <value name="X">
  185. <shadow type="math_number">
  186. <field name="NUM">1</field>
  187. </shadow>
  188. </value>
  189. <value name="Y">
  190. <shadow type="math_number">
  191. <field name="NUM">1</field>
  192. </shadow>
  193. </value>
  194. </block>
  195. </category>
  196. <category name="文本" categorystyle="text_category">
  197. <block type="text"></block>
  198. <block type="text_join"></block>
  199. <block type="text_append">
  200. <value name="TEXT">
  201. <shadow type="text"></shadow>
  202. </value>
  203. </block>
  204. <block type="text_length">
  205. <value name="VALUE">
  206. <shadow type="text">
  207. <field name="TEXT">abc</field>
  208. </shadow>
  209. </value>
  210. </block>
  211. <block type="text_isEmpty">
  212. <value name="VALUE">
  213. <shadow type="text">
  214. <field name="TEXT"></field>
  215. </shadow>
  216. </value>
  217. </block>
  218. <block type="text_indexOf">
  219. <value name="VALUE">
  220. <block type="variables_get">
  221. <field name="VAR">text</field>
  222. </block>
  223. </value>
  224. <value name="FIND">
  225. <shadow type="text">
  226. <field name="TEXT">abc</field>
  227. </shadow>
  228. </value>
  229. </block>
  230. <block type="text_charAt">
  231. <value name="VALUE">
  232. <block type="variables_get">
  233. <field name="VAR">text</field>
  234. </block>
  235. </value>
  236. </block>
  237. <block type="text_getSubstring">
  238. <value name="STRING">
  239. <block type="variables_get">
  240. <field name="VAR">text</field>
  241. </block>
  242. </value>
  243. </block>
  244. <block type="text_changeCase">
  245. <value name="TEXT">
  246. <shadow type="text">
  247. <field name="TEXT">abc</field>
  248. </shadow>
  249. </value>
  250. </block>
  251. <block type="text_trim">
  252. <value name="TEXT">
  253. <shadow type="text">
  254. <field name="TEXT">abc</field>
  255. </shadow>
  256. </value>
  257. </block>
  258. <block type="text_count">
  259. <value name="SUB">
  260. <shadow type="text"></shadow>
  261. </value>
  262. <value name="TEXT">
  263. <shadow type="text"></shadow>
  264. </value>
  265. </block>
  266. <block type="text_replace">
  267. <value name="FROM">
  268. <shadow type="text"></shadow>
  269. </value>
  270. <value name="TO">
  271. <shadow type="text"></shadow>
  272. </value>
  273. <value name="TEXT">
  274. <shadow type="text"></shadow>
  275. </value>
  276. </block>
  277. <block type="text_reverse">
  278. <value name="TEXT">
  279. <shadow type="text"></shadow>
  280. </value>
  281. </block>
  282. <label text="Input/Output:" web-class="ioLabel"></label>
  283. <block type="text_print">
  284. <value name="TEXT">
  285. <shadow type="text">
  286. <field name="TEXT">abc</field>
  287. </shadow>
  288. </value>
  289. </block>
  290. <block type="text_prompt_ext">
  291. <value name="TEXT">
  292. <shadow type="text">
  293. <field name="TEXT">abc</field>
  294. </shadow>
  295. </value>
  296. </block>
  297. </category>
  298. <category name="列表" categorystyle="list_category">
  299. <block type="lists_create_with">
  300. <mutation items="0"></mutation>
  301. </block>
  302. <block type="lists_create_with"></block>
  303. <block type="lists_repeat">
  304. <value name="NUM">
  305. <shadow type="math_number">
  306. <field name="NUM">5</field>
  307. </shadow>
  308. </value>
  309. </block>
  310. <block type="lists_length"></block>
  311. <block type="lists_isEmpty"></block>
  312. <block type="lists_indexOf">
  313. <value name="VALUE">
  314. <block type="variables_get">
  315. <field name="VAR">list</field>
  316. </block>
  317. </value>
  318. </block>
  319. <block type="lists_getIndex">
  320. <value name="VALUE">
  321. <block type="variables_get">
  322. <field name="VAR">list</field>
  323. </block>
  324. </value>
  325. </block>
  326. <block type="lists_setIndex">
  327. <value name="LIST">
  328. <block type="variables_get">
  329. <field name="VAR">list</field>
  330. </block>
  331. </value>
  332. </block>
  333. <block type="lists_getSublist">
  334. <value name="LIST">
  335. <block type="variables_get">
  336. <field name="VAR">list</field>
  337. </block>
  338. </value>
  339. </block>
  340. <block type="lists_split">
  341. <value name="DELIM">
  342. <shadow type="text">
  343. <field name="TEXT">,</field>
  344. </shadow>
  345. </value>
  346. </block>
  347. <block type="lists_sort"></block>
  348. <block type="lists_reverse"></block>
  349. </category>
  350. <sep></sep>
  351. <category
  352. name="Variables"
  353. categorystyle="variable_category"
  354. custom="VARIABLE"
  355. ></category>
  356. <category
  357. name="Functions"
  358. categorystyle="procedure_category"
  359. custom="PROCEDURE"
  360. ></category>
  361. </xml>
  362. </template>
  363. </div>
  364. <div class="box-code">
  365. <div class="button-container">
  366. <el-button type="danger" plain id="run-code" @click="runCode()"
  367. >运行代码</el-button
  368. >
  369. <el-button type="warning" plain id="import" @click="load()"
  370. >重新加载</el-button
  371. >
  372. </div>
  373. <br />
  374. <div class="button-container">
  375. <el-button
  376. type="primary"
  377. plain
  378. id="to-code-js"
  379. @click="generateCode('javascript')"
  380. >生成 JavaScript</el-button
  381. >
  382. <el-button
  383. type="success"
  384. plain
  385. id="to-code-py"
  386. @click="generateCode('python')"
  387. >生成 Python</el-button
  388. >
  389. <el-button type="info" plain id="save-json" @click="saveJson()"
  390. >保存 JSON</el-button
  391. >
  392. <el-button type="info" plain id="save-xml" @click="saveXml()"
  393. >保存 XML</el-button
  394. >
  395. </div>
  396. <div class="json-section">
  397. <h3>JSON 数据</h3>
  398. <textarea v-model="jsonData" placeholder="在此输入JSON格式的积木块数据..."></textarea>
  399. <div class="controls">
  400. <button @click="loadWorkspaceFromJson">加载JSON到工作区</button>
  401. <button @click="exportWorkspaceToJson">导出工作区为JSON</button>
  402. </div>
  403. <div v-if="statusMessage" :class="['status', statusType]" style="color: #1a1a1a">
  404. {{ statusMessage }}
  405. </div>
  406. </div>
  407. <!-- 代码显示区 -->
  408. <div class="code-section">
  409. <h4>生成的代码</h4>
  410. <textarea name="" id="textarea" class="box-code-textarea"></textarea>
  411. </div>
  412. <!-- 运行结果区 -->
  413. <div class="result-section">
  414. <h4>运行结果</h4>
  415. <div id="run-result" class="run-result-content"></div>
  416. <!-- 额外的图片预览区域 -->
  417. <div
  418. v-if="state.generatedContent.imageUrl"
  419. class="extra-image-preview"
  420. >
  421. <h5>生成的图片:</h5>
  422. <img
  423. :src="state.generatedContent.imageUrl"
  424. class="extra-preview-image"
  425. alt="AI生成图片"
  426. />
  427. </div>
  428. <div
  429. v-if="state.generatedContent.videoUrl"
  430. class="extra-image-preview"
  431. >
  432. <h5>生成的视频:</h5>
  433. <video
  434. :src="state.generatedContent.videoUrl"
  435. controls
  436. class="preview-video"
  437. alt="AI生成视频"
  438. ></video>
  439. </div>
  440. <!-- 在template部分的适当位置音频播放器组件 -->
  441. <div class="music-player-container" v-if="state.currentMusicUrl">
  442. <h5>音乐播放</h5>
  443. <audio
  444. ref="musicPlayer"
  445. :src="state.currentMusicUrl"
  446. @ended="handleMusicEnded"
  447. preload="metadata">
  448. 您的浏览器不支持音频元素
  449. </audio>
  450. <div class="music-status">
  451. <p v-if="state.isMusicPlaying">正在播放: {{ state.currentMusicName }}</p>
  452. <p v-else>准备就绪</p>
  453. </div>
  454. <!-- 停止播放按钮 - 修复点击事件 -->
  455. <el-button
  456. v-if="state.isMusicPlaying"
  457. type="danger"
  458. size="small"
  459. @click="handleStopMusic"
  460. style="margin-top: 10px;">
  461. 停止播放
  462. </el-button>
  463. </div>
  464. </div>
  465. </div>
  466. <!-- AI结果预览模态框 -->
  467. <el-dialog
  468. v-if="state.previewConten"
  469. title="AI生成结果"
  470. width="80%"
  471. :before-close="handleClosePreview"
  472. >
  473. <div
  474. v-if="state.previewType === 'image'"
  475. class="preview-image-container"
  476. >
  477. <img
  478. :src="state.previewContent"
  479. class="preview-image"
  480. alt="生成的图片"
  481. />
  482. </div>
  483. <div
  484. v-if="state.previewType === 'video'"
  485. class="preview-video-container"
  486. >
  487. <video
  488. :src="state.previewContent"
  489. controls
  490. class="preview-video"
  491. ></video>
  492. </div>
  493. <div v-if="state.previewType === 'text'" class="preview-text-container">
  494. <p>{{ state.previewContent }}</p>
  495. </div>
  496. <span slot="footer" class="dialog-footer">
  497. <el-button @click="handleClosePreview">关闭</el-button>
  498. </span>
  499. </el-dialog>
  500. </div>
  501. </div>
  502. </template>
  503. <script setup>
  504. import { ref, onMounted, onUnmounted, reactive } from "vue";
  505. import { useRouter } from "vue-router";
  506. import { ArrowLeftBold } from "@element-plus/icons-vue";
  507. import * as Blockly from "blockly";
  508. import { javascriptGenerator } from "blockly/javascript";
  509. import { pythonGenerator } from "blockly/python";
  510. import * as hans from "blockly/msg/zh-hans";
  511. import { ElDialog, ElButton, ElMessage } from "element-plus";
  512. //【文生图】文生图
  513. import {
  514. AiImageStatusEnum,
  515. CreatePainting,
  516. PaintingGetMys,
  517. CreateVideo,
  518. VideoGetMys,
  519. sendChatMessageStream,
  520. CreateDialogue,
  521. } from "@/api/questions.js";
  522. import { getModelIdByType, ModelPlatformEnum } from "@/api/teachers.js";
  523. import { ModelTypeEnum } from "@/api/teachers.js";
  524. import { globalState } from "@/utils/globalState.js";
  525. //音乐
  526. import { playMusic, stopMusic, onMusicEnded } from "@/api/blockly/music.js";
  527. const router = useRouter();
  528. // 台灯预览显示状态
  529. const showLampPreview = ref(false)
  530. // 返回虚拟实验室
  531. const goBackLab = () => {
  532. router.push("/virtual-laboratory");
  533. };
  534. const goBack = () => {
  535. showLampPreview.value = true
  536. };
  537. // 切换灯光状态
  538. const toggleLight = () => {
  539. state.lamp.isLightOn = true;
  540. generateCode('javascript');
  541. runCode()
  542. };
  543. // 查看代码编程界面显示状态
  544. const handleViewCode = () =>{
  545. showLampPreview.value = false
  546. };
  547. Blockly.setLocale(hans);
  548. // 状态管理
  549. const state = reactive({
  550. workspace: null,
  551. generatedContent: {
  552. imageUrl: "",
  553. videoUrl: "",
  554. text: "",
  555. },
  556. previewVisible: false,
  557. previewType: "",
  558. previewContent: "",
  559. isProcessing: false,
  560. //年级
  561. gradeId: "",
  562. //【文生图】文生图
  563. inProgressImageMap: {},
  564. //【文生视频】文生视频
  565. inProgressVideoMap: {},
  566. // 台灯状态
  567. lamp: {
  568. isLightOn: false,// 台灯是否亮着
  569. brightness: 0, // 默认亮度50%
  570. color: "#ffffff", // 默认颜色白色
  571. colorLog: "白", // 默认颜色白色
  572. },
  573. // 【文本文】对话相关状态
  574. activeConversationId: null,
  575. conversationInAbortController: null,
  576. // 独立的音乐播放状态
  577. currentMusicUrl: '',
  578. currentMusicName: '',
  579. isMusicPlaying: false,
  580. });
  581. // 创建音乐播放器引用
  582. const musicPlayer = ref(null);
  583. // 音乐相关的处理函数
  584. const handleMusicEnded = () => {
  585. onMusicEnded(state);
  586. };
  587. // 专门的停止音乐处理函数
  588. const handleStopMusic = () => {
  589. // 直接调用导入的stopMusic函数并传递正确的参数
  590. stopMusic(state, musicPlayer);
  591. // 提示信息
  592. ElMessage.success('音乐已停止播放');
  593. };
  594. // 统一轮询管理器
  595. const pollingManager = {
  596. timers: {},
  597. // 启动轮询
  598. startPolling(type, callback, interval = 3000) {
  599. // 如果已有相同类型的轮询,先清除
  600. this.stopPolling(type);
  601. this.timers[type] = setInterval(async () => {
  602. try {
  603. await callback();
  604. } catch (error) {
  605. console.error(`${type}轮询失败:`, error);
  606. }
  607. }, interval);
  608. return this.timers[type];
  609. },
  610. // 停止轮询
  611. stopPolling(type) {
  612. if (this.timers[type]) {
  613. clearInterval(this.timers[type]);
  614. this.timers[type] = null;
  615. }
  616. },
  617. // 停止所有轮询
  618. stopAll() {
  619. Object.keys(this.timers).forEach(type => this.stopPolling(type));
  620. }
  621. };
  622. // 统一的错误处理包装器
  623. function withErrorHandling(operationName, fn, errorMessage = null) {
  624. return async function(...args) {
  625. try {
  626. state.isProcessing = true;
  627. return await fn.apply(this, args);
  628. } catch (error) {
  629. console.error(`${operationName}失败:`, error);
  630. ElMessage.error(errorMessage || `${operationName}发生错误: ${error.message || '未知错误'}`);
  631. return null;
  632. } finally {
  633. state.isProcessing = false;
  634. }
  635. };
  636. }
  637. // 任务状态轮询公共函数
  638. async function pollTaskStatus(taskType, taskIds, fetchApi, onSuccess, onFailure) {
  639. if (taskIds.length === 0) {
  640. pollingManager.stopPolling(taskType);
  641. return {};
  642. }
  643. try {
  644. const list = await fetchApi(taskIds);
  645. const activeTasks = {};
  646. list.data.forEach((task) => {
  647. if (task.status === AiImageStatusEnum.IN_PROGRESS) {
  648. activeTasks[task.id] = task;
  649. } else if (task.status === AiImageStatusEnum.SUCCESS) {
  650. // 任务成功完成
  651. if (onSuccess) {
  652. onSuccess(task);
  653. }
  654. } else if (task.status === AiImageStatusEnum.FAIL) {
  655. // 任务失败
  656. if (onFailure) {
  657. onFailure(task);
  658. }
  659. }
  660. });
  661. return activeTasks;
  662. } catch (error) {
  663. console.error(`${taskType}状态轮询失败:`, error);
  664. return {};
  665. }
  666. }
  667. // AI服务模块 - 统一管理
  668. const aiService = {
  669. // 语音识别
  670. recognizeVoice: withErrorHandling('语音识别', async function(promptText = "", language = "zh-CN") {
  671. console.log("语音识别开始");
  672. // 前端语音采集
  673. const recognitionResult = await this.captureVoice(language, promptText);
  674. return recognitionResult || "";
  675. }, '语音识别失败'),
  676. // 前端语音采集
  677. captureVoice(language, promptText) {
  678. return new Promise((resolve) => {
  679. if (
  680. !"webkitSpeechRecognition" in window &&
  681. !"SpeechRecognition" in window
  682. ) {
  683. ElMessage.warning("您的浏览器不支持语音识别功能");
  684. resolve("");
  685. return;
  686. }
  687. const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
  688. const recognition = new SpeechRecognition();
  689. recognition.lang = language;
  690. recognition.interimResults = false;
  691. recognition.maxAlternatives = 1;
  692. let countdown = 10;
  693. // 固定的消息提示框
  694. const messageText = promptText ? `${promptText}\n请开始说话...(${countdown}秒)` : `请开始说话...(${countdown}秒)`;
  695. const messageInstance = ElMessage.info({
  696. message: messageText,
  697. duration: 0 // 0表示不自动关闭
  698. });
  699. // 隐藏的div来更新倒计时显示
  700. const timer = setInterval(() => {
  701. countdown--;
  702. if (countdown > 0) {
  703. // 找到消息提示框中的文本元素并更新
  704. const messageElement = document.querySelector('.el-message__content');
  705. if (messageElement) {
  706. const newText = promptText ? `${promptText}\n请开始说话...(${countdown}秒)` : `请开始说话...(${countdown}秒)`;
  707. messageElement.textContent = newText;
  708. }
  709. } else {
  710. clearInterval(timer);
  711. // 倒计时结束后关闭提示框
  712. if (messageInstance && messageInstance.close) {
  713. messageInstance.close();
  714. }
  715. }
  716. }, 1000);
  717. recognition.onresult = (event) => {
  718. const speechResult = event.results[0][0].transcript;
  719. console.log("语音识别结果:", speechResult);
  720. resolve(speechResult);
  721. };
  722. recognition.onerror = (event) => {
  723. console.error("语音识别错误:", event.error);
  724. ElMessage.error("语音识别发生错误: " + event.error);
  725. resolve("");
  726. };
  727. // recognition.onend = () => {
  728. // console.log("语音识别已结束");
  729. // };
  730. recognition.start();
  731. });
  732. },
  733. // 文本生成图片
  734. textToImage: withErrorHandling('AI图片生成', async function(prompt, waitForCompletion = true) {
  735. console.log("AI图片生成中,提示词:", prompt);
  736. //获取文生图-模型id
  737. const modelRes = await getModelIdByType({
  738. type: ModelTypeEnum.TEXT_TO_IMAGE,
  739. platform: ModelPlatformEnum.DOUBAO,
  740. });
  741. if (!modelRes.data) {
  742. ElMessage.error("获取模型ID失败");
  743. return null;
  744. }
  745. // 使用CreatePainting API创建图片任务
  746. const createRes = await CreatePainting({
  747. modelId: modelRes.data,
  748. prompt: prompt,
  749. width: 1024,
  750. height: 1024,
  751. });
  752. // 记录任务ID到映射中
  753. state.inProgressImageMap[createRes.data] = {
  754. id: createRes.data,
  755. status: AiImageStatusEnum.IN_PROGRESS,
  756. };
  757. // 开始轮询任务状态
  758. this.startPollingTasks('image');
  759. // 如果需要等待完成,等待图片生成完成
  760. if (waitForCompletion) {
  761. console.log("AI图片生成中,请等待。。。:");
  762. return await this.waitForImageCompletion(createRes.data);
  763. }
  764. return createRes.data; // 返回任务ID
  765. }, '生成图片失败'),
  766. // 【文生图】等待图片生成完成
  767. waitForImageCompletion(imageId) {
  768. return new Promise((resolve, reject) => {
  769. const checkInterval = setInterval(async () => {
  770. try {
  771. const list = await PaintingGetMys([imageId]);
  772. if (list.data && list.data.length > 0) {
  773. const image = list.data[0];
  774. if (image.status === AiImageStatusEnum.SUCCESS) {
  775. clearInterval(checkInterval);
  776. resolve(image.picUrl);
  777. } else if (image.status === AiImageStatusEnum.FAIL) {
  778. clearInterval(checkInterval);
  779. reject(new Error(image.error || "图片生成失败"));
  780. }
  781. }
  782. } catch (error) {
  783. clearInterval(checkInterval);
  784. reject(error);
  785. }
  786. }, 3000);
  787. });
  788. },
  789. // 文本生成视频
  790. textToVideo: withErrorHandling('AI视频生成', async function(prompt, waitForCompletion = true) {
  791. console.log("AI视频生成中,提示词:", prompt);
  792. //获取视频生成模型id
  793. const modelRes = await getModelIdByType({
  794. type: ModelTypeEnum.IMAGE_TO_VIDEO,
  795. platform: ModelPlatformEnum.DOUBAO,
  796. });
  797. if (!modelRes.data) {
  798. ElMessage.error("获取模型ID失败");
  799. return null;
  800. }
  801. // 使用CreateVideo API创建视频任务
  802. const createRes = await CreateVideo({
  803. modelId: modelRes.data,
  804. prompt: prompt,
  805. duration: 4,
  806. resolution: "1080P",
  807. });
  808. // 记录任务ID
  809. state.inProgressVideoMap[createRes.data] = {
  810. id: createRes.data,
  811. status: AiImageStatusEnum.IN_PROGRESS,
  812. };
  813. console.log("AI视频生成中,请等待。。。");
  814. // 启动统一的轮询机制
  815. this.startPollingTasks('video');
  816. // 如果需要等待完成,使用Promise封装结果
  817. if (waitForCompletion) {
  818. return new Promise((resolve, reject) => {
  819. // 设置一次性的状态检查
  820. const checkStatus = () => {
  821. const videoInfo = state.generatedContent.videoUrl;
  822. if (videoInfo && videoInfo.includes(createRes.data)) {
  823. resolve(videoInfo);
  824. } else if (state.inProgressVideoMap[createRes.data]?.status === AiImageStatusEnum.FAIL) {
  825. reject(new Error("视频生成失败"));
  826. } else if (!state.inProgressVideoMap[createRes.data]) {
  827. reject(new Error("视频任务已不存在"));
  828. } else {
  829. // 继续检查
  830. setTimeout(checkStatus, 1000);
  831. }
  832. };
  833. checkStatus();
  834. });
  835. }
  836. return createRes.data; // 返回任务ID
  837. }, '生成视频失败'),
  838. // 文本生成文本(如AI对话)
  839. textToText: withErrorHandling('AI大模型调用', async function(prompt, model = "default") {
  840. console.log("AI智能体请求,输入文本:", prompt);
  841. // 如果没有活跃的对话ID,创建新对话
  842. if (!state.activeConversationId) {
  843. // 使用与TextToText.vue相同的方式创建对话
  844. const res = await CreateDialogue({ roleId: 75 });
  845. state.activeConversationId = res.data;
  846. console.log("AI智能体创建成功,请等待。。。");
  847. }
  848. // 创建AbortController实例
  849. state.conversationInAbortController = new AbortController();
  850. // 使用流式API发送消息
  851. let resultText = "";
  852. let isFirstChunk = true;
  853. await sendChatMessageStream(
  854. state.activeConversationId,
  855. prompt,
  856. null,
  857. state.conversationInAbortController,
  858. true, // 启用上下文
  859. async (res) => {
  860. try {
  861. const { code, data, msg } = JSON.parse(res.data);
  862. if (code !== 0) {
  863. console.log(`对话异常! ${msg}`);
  864. return;
  865. }
  866. // 根据事件类型处理
  867. if (data.eventType === "TEXT") {
  868. // 如果内容为空,就不处理
  869. if (data.receive?.content === "") {
  870. return;
  871. }
  872. // 处理文本消息
  873. resultText += data.receive.content;
  874. // 首次返回时更新预览内容
  875. if (isFirstChunk) {
  876. isFirstChunk = false;
  877. // 设置预览内容
  878. state.generatedContent.text = resultText;
  879. state.previewType = "text";
  880. state.previewContent = resultText;
  881. if (!state.previewVisible) {
  882. state.previewVisible = true;
  883. }
  884. } else {
  885. // 更新预览内容
  886. state.generatedContent.text = resultText;
  887. state.previewContent = resultText;
  888. }
  889. }
  890. } catch (error) {
  891. console.error("处理流式响应失败:", error);
  892. }
  893. },
  894. (error) => {
  895. console.log(`对话异常! ${error}`);
  896. this.stopTextToTextStream();
  897. throw error;
  898. },
  899. () => {
  900. // console.log(`结束对话!`);
  901. this.stopTextToTextStream();
  902. }
  903. );
  904. // 确保最终结果被设置
  905. if (resultText) {
  906. console.log("AI大模型调用成功,返回结果:", resultText);
  907. state.generatedContent.text = resultText;
  908. state.previewType = "text";
  909. state.previewContent = resultText;
  910. if (!state.previewVisible) {
  911. state.previewVisible = true;
  912. }
  913. }
  914. return resultText;
  915. }, 'AI大模型调用失败'),
  916. // 停止文本生成流
  917. stopTextToTextStream() {
  918. if (state.conversationInAbortController) {
  919. state.conversationInAbortController.abort();
  920. }
  921. },
  922. // 设置台灯亮度
  923. setLampBrightness: withErrorHandling('设置台灯亮度', async function(brightness) {
  924. // 验证亮度值在0-100之间
  925. const validBrightness = Math.max(0, Math.min(100, parseInt(brightness) || 0));
  926. // 更新状态
  927. state.lamp.brightness = validBrightness;
  928. // 模拟API调用(实际项目中可替换为真实API)
  929. console.log(`智能台灯亮度已设置为: ${validBrightness}%`);
  930. return validBrightness;
  931. }, '设置台灯亮度失败'),
  932. // 设置台灯颜色
  933. setLampColor: withErrorHandling('设置台灯颜色', async function(color) {
  934. // 预定义的颜色映射
  935. const colorMap = {
  936. '紫': '#D886F0',
  937. '橙': '#F89E35',
  938. '黄': '#F9E67E',
  939. '青': '#6BF5E6',
  940. '白': '#ffffff',
  941. };
  942. // 获取有效的颜色值
  943. let validColor = colorMap[color] || color;
  944. // 检查是否是有效的颜色格式
  945. if (!/^#[0-9A-F]{6}$/i.test(validColor)) {
  946. validColor = "#ffffff"; // 默认白色
  947. }
  948. // 更新状态
  949. state.lamp.color = validColor;
  950. state.lamp.colorLog = color;
  951. // 模拟API调用(实际项目中可替换为真实API)
  952. console.log(`智能台灯颜色已设置为: ${color}`);
  953. return validColor;
  954. }, '设置台灯颜色失败'),
  955. // 音乐播放相关方法
  956. playMusic: withErrorHandling('播放音乐', async function(musicType) {
  957. return playMusic(musicType, state, musicPlayer);
  958. }, '播放音乐失败'),
  959. stopMusic: withErrorHandling('停止音乐', async function() {
  960. return stopMusic(state, musicPlayer);
  961. }, '停止音乐失败'),
  962. // 综合控制台灯(参数格式:"颜色, 亮度, 音乐")
  963. controlLampWithSingleParam: withErrorHandling('智能台灯综合控制', async function(params) {
  964. // 解析参数字符串
  965. let color = '白'; // 默认颜色
  966. let brightness = 0; // 默认亮度
  967. let music = ''; // 音乐信息
  968. if (params && typeof params === 'string') {
  969. // 根据逗号分割参数
  970. const paramArray = params.split(',').map(p => p.trim());
  971. // 提取颜色(第一个参数)
  972. if (paramArray.length > 0 && paramArray[0]) {
  973. color = paramArray[0];
  974. }
  975. // 提取亮度(第二个参数)
  976. if (paramArray.length > 1 && paramArray[1]) {
  977. brightness = paramArray[1];
  978. }
  979. // 提取音乐(第三个参数)
  980. if (paramArray.length > 2 && paramArray[2]) {
  981. music = paramArray[2];
  982. // 调用音乐播放函数
  983. await this.playMusic(music);
  984. }
  985. }
  986. // 调用控制台灯方法
  987. return await this.controlLamp(brightness, color);
  988. }, '智能台灯综合控制失败'),
  989. // 综合控制台灯(参数格式:"颜色, 亮度")
  990. controlLamp: withErrorHandling('智能台灯控制', async function(brightness, color) {
  991. // 先设置亮度
  992. await this.setLampBrightness(brightness);
  993. // 再设置颜色
  994. await this.setLampColor(color);
  995. return { brightness: state.lamp.brightness, color: state.lamp.color };
  996. }, '智能台灯控制失败'),
  997. // 启动任务轮询
  998. startPollingTasks(type) {
  999. if (type === 'image' || type === 'all') {
  1000. pollingManager.startPolling('image', async () => {
  1001. const imageIds = Object.keys(state.inProgressImageMap).map(Number);
  1002. state.inProgressImageMap = await pollTaskStatus(
  1003. 'image',
  1004. imageIds,
  1005. PaintingGetMys,
  1006. (image) => {
  1007. state.generatedContent.imageUrl = image.picUrl;
  1008. state.previewType = "image";
  1009. state.previewContent = image.picUrl;
  1010. state.previewVisible = true;
  1011. console.log("AI图片生成完成:", image.picUrl);
  1012. },
  1013. (image) => {
  1014. ElMessage.error("图片生成失败: " + (image.error || "未知错误"));
  1015. console.error("图片生成失败:", image.id, image.error);
  1016. }
  1017. );
  1018. });
  1019. }
  1020. if (type === 'video' || type === 'all') {
  1021. pollingManager.startPolling('video', async () => {
  1022. const videoIds = Object.keys(state.inProgressVideoMap).map(Number);
  1023. state.inProgressVideoMap = await pollTaskStatus(
  1024. 'video',
  1025. videoIds,
  1026. VideoGetMys,
  1027. (video) => {
  1028. state.generatedContent.videoUrl = video.videoUrl;
  1029. state.previewType = "video";
  1030. state.previewContent = video.videoUrl;
  1031. state.previewVisible = true;
  1032. console.log("AI视频生成完成:", video.videoUrl);
  1033. },
  1034. (video) => {
  1035. ElMessage.error("视频生成失败: " + (video.error || "未知错误"));
  1036. console.error("视频生成失败:", video.id, video.error);
  1037. }
  1038. );
  1039. });
  1040. }
  1041. }
  1042. };
  1043. // 初始化Blockly工作区和自定义积木
  1044. onMounted(async () => {
  1045. // 从全局状态初始化年级ID
  1046. state.gradeId = globalState.initGradeId();
  1047. // 注册AI语音输入积木
  1048. Blockly.Blocks["ai_voice_input"] = {
  1049. init: function () {
  1050. this.appendDummyInput().appendField("语音输入");
  1051. this.appendValueInput("PROMPT")
  1052. .setCheck("String")
  1053. .appendField("提示文字:");
  1054. this.appendDummyInput()
  1055. .appendField("语言:")
  1056. .appendField(
  1057. new Blockly.FieldDropdown([
  1058. ["中文", "zh-CN"],
  1059. ["英文", "en-US"],
  1060. ]),
  1061. "LANGUAGE"
  1062. );
  1063. this.setOutput(true, "String");
  1064. this.setColour(310);
  1065. this.setTooltip("使用语音识别获取文本输入");
  1066. this.setHelpUrl("");
  1067. },
  1068. };
  1069. // 注册AI文本生成图片积木
  1070. Blockly.Blocks["ai_text_to_image"] = {
  1071. init: function () {
  1072. this.appendDummyInput().appendField("AI生成图片");
  1073. this.appendValueInput("PROMPT").setCheck("String").appendField("提示词:");
  1074. this.appendDummyInput()
  1075. .appendField("等待完成:", "WAIT_LABEL")
  1076. .appendField(new Blockly.FieldCheckbox("TRUE"), "WAIT_FOR_COMPLETION");
  1077. this.setInputsInline(false);
  1078. this.setPreviousStatement(true, null);
  1079. this.setNextStatement(true, null);
  1080. this.setColour(340);
  1081. this.setTooltip("使用AI将文本描述转换为图片");
  1082. this.setHelpUrl("");
  1083. },
  1084. };
  1085. // 注册AI文本生成视频积木
  1086. Blockly.Blocks["ai_text_to_video"] = {
  1087. init: function () {
  1088. this.appendDummyInput().appendField("AI生成视频");
  1089. this.appendValueInput("PROMPT").setCheck("String").appendField("提示词:");
  1090. this.appendDummyInput()
  1091. .appendField("等待完成:", "WAIT_LABEL")
  1092. .appendField(new Blockly.FieldCheckbox("TRUE"), "WAIT_FOR_COMPLETION");
  1093. this.setInputsInline(false);
  1094. this.setPreviousStatement(true, null);
  1095. this.setNextStatement(true, null);
  1096. this.setColour(340);
  1097. this.setTooltip("使用AI将文本描述转换为视频");
  1098. this.setHelpUrl("");
  1099. },
  1100. };
  1101. // 注册AI文本生成文本积木
  1102. Blockly.Blocks["ai_text_to_text"] = {
  1103. init: function () {
  1104. this.appendDummyInput().appendField("AI大模型调用");
  1105. this.appendValueInput("PROMPT")
  1106. .setCheck("String")
  1107. .appendField("输入文本:");
  1108. this.appendValueInput("提示词")
  1109. .setCheck("String")
  1110. .appendField("提示词:");
  1111. this.setOutput(true, "String");
  1112. this.setColour(300);
  1113. this.setTooltip("使用AI大模型调用并返回结果");
  1114. this.setHelpUrl("");
  1115. },
  1116. };
  1117. //AI智能台灯单参数积木
  1118. Blockly.Blocks['ai_smart_lamp_single_param'] = {
  1119. init: function() {
  1120. this.appendDummyInput()
  1121. .appendField('智能台灯控制(单参数)');
  1122. this.appendValueInput('PARAMS')
  1123. .setCheck('String')
  1124. .appendField('参数(格式: 颜色,亮度,音乐):');
  1125. this.appendDummyInput()
  1126. .appendField('例如: 蓝,50,平静');
  1127. this.setInputsInline(false);
  1128. this.setPreviousStatement(true, null);
  1129. this.setNextStatement(true, null);
  1130. this.setColour(280);
  1131. this.setTooltip('通过一个参数字符串控制智能台灯的亮度、颜色和音乐\n格式: 颜色,亮度,音乐\n例如: 蓝,50,平静');
  1132. this.setHelpUrl('');
  1133. }
  1134. };
  1135. // 注册AI智能台灯积木
  1136. Blockly.Blocks["ai_smart_lamp"] = {
  1137. init: function () {
  1138. this.appendDummyInput().appendField("智能台灯控制");
  1139. this.appendValueInput("BRIGHTNESS")
  1140. .setCheck(["Number", "String"])
  1141. .appendField("亮度 (0-100):");
  1142. this.appendValueInput("COLOR").setCheck("String").appendField("颜色:");
  1143. this.setInputsInline(false);
  1144. this.setPreviousStatement(true, null);
  1145. this.setNextStatement(true, null);
  1146. this.setColour(280);
  1147. this.setTooltip("控制智能台灯的亮度和颜色");
  1148. this.setHelpUrl("");
  1149. },
  1150. };
  1151. // 注册AI台灯设置亮度积木
  1152. Blockly.Blocks["ai_lamp_set_brightness"] = {
  1153. init: function () {
  1154. this.appendDummyInput().appendField("设置台灯亮度");
  1155. this.appendValueInput("BRIGHTNESS")
  1156. .setCheck(["Number", "String"])
  1157. .appendField("亮度 (0-100):");
  1158. this.setInputsInline(false);
  1159. this.setPreviousStatement(true, null);
  1160. this.setNextStatement(true, null);
  1161. this.setColour(270);
  1162. this.setTooltip("设置智能台灯的亮度");
  1163. this.setHelpUrl("");
  1164. },
  1165. };
  1166. // 注册AI台灯设置颜色积木
  1167. Blockly.Blocks["ai_lamp_set_color"] = {
  1168. init: function () {
  1169. this.appendDummyInput().appendField("设置台灯颜色");
  1170. this.appendValueInput("COLOR").setCheck("String").appendField("颜色:");
  1171. this.appendDummyInput().appendField("可选颜色: 红,蓝,绿,黄,青,靛,紫");
  1172. this.setInputsInline(false);
  1173. this.setPreviousStatement(true, null);
  1174. this.setNextStatement(true, null);
  1175. this.setColour(275);
  1176. this.setTooltip("设置智能台灯的颜色");
  1177. this.setHelpUrl("");
  1178. },
  1179. };
  1180. // 注册音乐播放积木
  1181. Blockly.Blocks['ai_music_play'] = {
  1182. init: function() {
  1183. this.appendDummyInput()
  1184. .appendField('播放音乐');
  1185. this.appendDummyInput()
  1186. .appendField('音乐类型:')
  1187. .appendField(
  1188. new Blockly.FieldDropdown([
  1189. ['热闹', '热闹'],
  1190. ['舒缓', '舒缓'],
  1191. ]),
  1192. 'MUSIC_TYPE'
  1193. );
  1194. this.setInputsInline(false);
  1195. this.setPreviousStatement(true, null);
  1196. this.setNextStatement(true, null);
  1197. this.setColour(290);
  1198. this.setTooltip('播放指定类型的音乐');
  1199. this.setHelpUrl('');
  1200. }
  1201. };
  1202. // 注册JavaScript代码生成器
  1203. registerJavaScriptGenerators();
  1204. // 注册Python代码生成器
  1205. registerPythonGenerators();
  1206. // 初始化工作区 - 简化且标准化的实现
  1207. try {
  1208. // 使用标准方式初始化Blockly工作区,参考BlocklyEditor_2.vue的配置
  1209. const blocklyDiv = document.getElementById('blocklyDiv');
  1210. const toolbox = document.getElementById('toolbox');
  1211. state.workspace = Blockly.inject(blocklyDiv, {
  1212. toolbox: toolbox,
  1213. collapse: true,
  1214. comments: true,
  1215. disable: false, // 设为false以允许编辑
  1216. maxBlocks: Infinity,
  1217. trashcan: true,
  1218. horizontalLayout: false,
  1219. toolboxPosition: 'start',
  1220. css: true,
  1221. media: 'https://unpkg.com/blockly/media/',
  1222. rtl: false,
  1223. scrollbars: true,
  1224. sounds: false, // 禁用声音以提高性能
  1225. oneBasedIndex: true,
  1226. grid: {
  1227. spacing: 20,
  1228. length: 3,
  1229. colour: "#ccc",
  1230. snap: true
  1231. },
  1232. zoom: {
  1233. controls: true,
  1234. wheel: true,
  1235. startScale: 1.0,
  1236. maxScale: 3,
  1237. minScale: 0.3,
  1238. scaleSpeed: 1.2
  1239. }
  1240. });
  1241. // 使用合并的监听器函数
  1242. addCombinedChangeListener();
  1243. // 使用当前Blockly版本支持的标准方法加载初始内容
  1244. /*const initialXml = "<xml xmlns=\"https://developers.google.com/blockly/xml\">\n" +
  1245. " <variables>\n" +
  1246. " <variable id=\"kAVG*zJLw/q)l/(/eIMM\">inputText</variable>\n" +
  1247. " <variable id=\"[7F(niLz{.fvWvY.VT/N\">lampConfig</variable>\n" +
  1248. " </variables>\n" +
  1249. " <block type=\"variables_set\" id=\"XIsa=sV/8ImVP5P3:ol4\" x=\"90\" y=\"90\">\n" +
  1250. " <field name=\"VAR\" id=\"kAVG*zJLw/q)l/(/eIMM\">inputText</field>\n" +
  1251. " <value name=\"VALUE\">\n" +
  1252. " <block type=\"ai_voice_input\" id=\"Ql).^+/uT(P~$aMg2OEA\">\n" +
  1253. " <field name=\"LANGUAGE\">zh-CN</field>\n" +
  1254. " <value name=\"PROMPT\">\n" +
  1255. " <block type=\"text\" id=\"kXjnnUncUu7p$a[zz)da\">\n" +
  1256. " <field name=\"TEXT\">请说话:</field>\n" +
  1257. " </block>\n" +
  1258. " </value>\n" +
  1259. " </block>\n" +
  1260. " </value>\n" +
  1261. " <next>\n" +
  1262. " <block type=\"variables_set\" id=\"AP0CO$VPeM0*PS*CKT,V\">\n" +
  1263. " <field name=\"VAR\" id=\"[7F(niLz{.fvWvY.VT/N\">lampConfig</field>\n" +
  1264. " <value name=\"VALUE\">\n" +
  1265. " <block type=\"ai_text_to_text\" id=\"XYG.cN_=CX5:Vd_)wdZ7\" x=\"190\" y=\"250\">\n" +
  1266. " <value name=\"PROMPT\">\n" +
  1267. " <block type=\"variables_get\" id=\"(3Sd)lTX],5xho{p_=s!\">\n" +
  1268. " <field name=\"VAR\" id=\"8(,gJ5{2y*8olrD{^G.P\">inputText</field>\n" +
  1269. " </block>\n" +
  1270. " </value>\n" +
  1271. " <value name=\"提示词\">\n" +
  1272. " <block type=\"text\" id=\"ubD*WPRnwBC|BxGraKX3\">\n" +
  1273. " <field name=\"TEXT\">请只回复我指定格式:白,100,热闹</field>\n" +
  1274. " </block>\n" +
  1275. " </value>\n" +
  1276. " </block>\n" +
  1277. " </value>\n" +
  1278. " <next>\n" +
  1279. " <block type=\"ai_smart_lamp_single_param\" id=\"ZTAwt~MMm(t5w:$$Azt`\">\n" +
  1280. " <value name=\"PARAMS\">\n" +
  1281. " <block type=\"variables_get\" id=\"X):w|X4hYE~ru1r9`W8f\">\n" +
  1282. " <field name=\"VAR\" id=\"[7F(niLz{.fvWvY.VT/N\">lampConfig</field>\n" +
  1283. " </block>\n" +
  1284. " </value>\n" +
  1285. " </block>\n" +
  1286. " </next>\n" +
  1287. " </block>\n" +
  1288. " </next>\n" +
  1289. " </block>\n" +
  1290. " </xml>";
  1291. // const initialXml = "<xml xmlns=\"https://developers.google.com/blockly/xml\">\n" +
  1292. // " </xml>";
  1293. try {
  1294. // 尝试使用现代API加载工作区
  1295. const parsedXml = Blockly.utils.xml.textToDom(initialXml);
  1296. Blockly.Xml.domToWorkspace(parsedXml, state.workspace);
  1297. console.log('XML加载成功');
  1298. } catch (xmlError) {
  1299. console.warn('XML加载失败,尝试清空工作区:', xmlError);
  1300. // 如果XML加载失败,清空工作区
  1301. state.workspace.clear();
  1302. }*/
  1303. // 添加拖拽修复
  1304. if (state.workspace) {
  1305. // 确保所有块都可以正确拖拽
  1306. state.workspace.addChangeListener((event) => {
  1307. if (event.type === Blockly.Events.BLOCK_CREATE) {
  1308. // 对于新创建的块,确保它们是可拖拽的
  1309. const block = state.workspace.getBlockById(event.blockId);
  1310. if (block && block.editable) {
  1311. // 确保块是可编辑的
  1312. block.setEditable(true);
  1313. }
  1314. }
  1315. });
  1316. }
  1317. // 初始化完成后手动生成一次代码
  1318. generateCode("javascript");
  1319. } catch (error) {
  1320. console.error('初始化Blockly工作区失败:', error);
  1321. ElMessage.error('Blockly初始化失败,请刷新页面重试');
  1322. }
  1323. // 修改工作区变化监听器,使其包含拖拽修复逻辑
  1324. state.workspace.addChangeListener((event) => {
  1325. // 生成代码
  1326. generateCode("javascript");
  1327. // 拖拽修复逻辑
  1328. if (event.type === Blockly.Events.BLOCK_CREATE) {
  1329. const block = state.workspace.getBlockById(event.blockId);
  1330. if (block) {
  1331. block.setEditable(true);
  1332. }
  1333. }
  1334. });
  1335. // 加载初始JSON数据
  1336. // workspace = state.workspace;
  1337. // loadWorkspaceFromJson();
  1338. // 添加工作区变化监听器
  1339. // state.workspace.addChangeListener(() => generateCode("javascript"));
  1340. });
  1341. // 组件卸载时清除所有资源
  1342. onUnmounted(() => {
  1343. // 统一的轮询管理器停止所有轮询
  1344. pollingManager.stopAll();
  1345. // 停止音乐播放
  1346. aiService.stopMusic();
  1347. // 停止文本生成流
  1348. aiService.stopTextToTextStream();
  1349. // 释放工作区资源
  1350. if (state.workspace) {
  1351. state.workspace.dispose();
  1352. }
  1353. });
  1354. // 响应式变量
  1355. const jsonData = ref(`{
  1356. "blocks": {
  1357. "languageVersion": 0,
  1358. "blocks": [
  1359. {
  1360. "type": "text_print",
  1361. "x": 100,
  1362. "y": 100,
  1363. "inputs": {
  1364. "TEXT": {
  1365. "block": {
  1366. "type": "text",
  1367. "fields": {
  1368. "TEXT": "Hello, Blockly!"
  1369. }
  1370. }
  1371. }
  1372. }
  1373. },
  1374. {
  1375. "type": "controls_if",
  1376. "x": 100,
  1377. "y": 200,
  1378. "inputs": {
  1379. "IF0": {
  1380. "block": {
  1381. "type": "logic_compare",
  1382. "fields": {
  1383. "OP": "EQ"
  1384. },
  1385. "inputs": {
  1386. "A": {
  1387. "block": {
  1388. "type": "math_number",
  1389. "fields": {
  1390. "NUM": 5
  1391. }
  1392. }
  1393. },
  1394. "B": {
  1395. "block": {
  1396. "type": "math_number",
  1397. "fields": {
  1398. "NUM": 5
  1399. }
  1400. }
  1401. }
  1402. }
  1403. }
  1404. },
  1405. "DO0": {
  1406. "block": {
  1407. "type": "text_print",
  1408. "inputs": {
  1409. "TEXT": {
  1410. "block": {
  1411. "type": "text",
  1412. "fields": {
  1413. "TEXT": "条件成立!"
  1414. }
  1415. }
  1416. }
  1417. }
  1418. }
  1419. }
  1420. }
  1421. }
  1422. ]
  1423. }
  1424. }`);
  1425. const statusMessage = ref('');
  1426. const statusType = ref('');
  1427. // let workspace = null;
  1428. // 从JSON加载工作区 - 改进版
  1429. const loadWorkspaceFromJson = () => {
  1430. try {
  1431. const json = JSON.parse(jsonData.value);
  1432. // 1. 确保工作区存在
  1433. if (!state.workspace) {
  1434. showStatus('工作区未初始化', 'error');
  1435. console.error('工作区未初始化');
  1436. return;
  1437. }
  1438. console.log('开始加载JSON数据:', JSON.stringify(json).substring(0, 200) + '...');
  1439. // 无论使用哪种策略加载成功后,确保所有块可拖拽
  1440. const ensureBlocksDraggable = () => {
  1441. const allBlocks = state.workspace.getAllBlocks();
  1442. allBlocks.forEach(block => {
  1443. // 强制设置为可编辑状态,不做任何条件检查
  1444. block.setEditable(true);
  1445. // 添加调试日志,确认每个块都被设置为可编辑
  1446. console.log(`设置块${block.id}为可编辑状态`);
  1447. });
  1448. return allBlocks.length;
  1449. };
  1450. // 2. 尝试多种加载策略
  1451. try {
  1452. // 策略1: 先清空工作区再加载
  1453. state.workspace.clear();
  1454. Blockly.serialization.workspaces.load(json, state.workspace);
  1455. const blockCount = ensureBlocksDraggable(); // 调用ensureBlocksDraggable确保所有块可拖拽
  1456. showStatus(`工作区已成功从JSON加载!共加载${blockCount}个积木。`, 'success');
  1457. console.log(`策略1成功: 加载了${blockCount}个积木`);
  1458. return;
  1459. } catch (error1) {
  1460. console.warn('策略1失败,尝试策略2:', error1);
  1461. try {
  1462. // 策略2: 清理JSON后再加载
  1463. state.workspace.clear();
  1464. const cleanedJson = deepCleanJsonForConnectionIssues(json);
  1465. Blockly.serialization.workspaces.load(cleanedJson, state.workspace);
  1466. const blockCount = ensureBlocksDraggable(); // 调用ensureBlocksDraggable确保所有块可拖拽
  1467. showStatus(`工作区已通过清理后的JSON成功加载!共加载${blockCount}个积木。`, 'success');
  1468. console.log(`策略2成功: 加载了${blockCount}个积木`);
  1469. return;
  1470. } catch (error2) {
  1471. console.warn('策略2失败,尝试策略3:', error2);
  1472. try {
  1473. // 策略3: 直接使用XML序列化API作为中间格式
  1474. state.workspace.clear();
  1475. // 先将JSON转换为XML字符串
  1476. const xmlText = jsonToXml(json);
  1477. console.log('生成的XML:', xmlText.substring(0, 200) + '...');
  1478. // 加载XML到工作区
  1479. const xmlDom = Blockly.utils.xml.textToDom(xmlText);
  1480. Blockly.Xml.domToWorkspace(xmlDom, state.workspace);
  1481. const blockCount = ensureBlocksDraggable(); // 调用ensureBlocksDraggable确保所有块可拖拽
  1482. showStatus(`工作区已通过XML中间格式成功加载!共加载${blockCount}个积木。`, 'success');
  1483. console.log(`策略3成功: 加载了${blockCount}个积木`);
  1484. return;
  1485. } catch (error3) {
  1486. console.warn('策略3失败,尝试策略4:', error3);
  1487. try {
  1488. // 策略4: 创建新的工作区实例并使用XML导入
  1489. // 先释放旧工作区资源
  1490. if (state.workspace) {
  1491. state.workspace.dispose();
  1492. }
  1493. // 创建新的工作区
  1494. const blocklyDiv = document.getElementById('blocklyDiv');
  1495. const toolbox = document.getElementById('toolbox');
  1496. state.workspace = Blockly.inject(blocklyDiv, {
  1497. toolbox: toolbox,
  1498. collapse: true,
  1499. comments: true,
  1500. disable: false,
  1501. maxBlocks: Infinity,
  1502. trashcan: true,
  1503. horizontalLayout: false,
  1504. toolboxPosition: 'start',
  1505. css: true,
  1506. media: 'https://unpkg.com/blockly/media/',
  1507. rtl: false,
  1508. scrollbars: true,
  1509. sounds: false,
  1510. oneBasedIndex: true,
  1511. grid: {
  1512. spacing: 20,
  1513. length: 3,
  1514. colour: "#ccc",
  1515. snap: true
  1516. },
  1517. zoom: {
  1518. controls: true,
  1519. wheel: true,
  1520. startScale: 1.0,
  1521. maxScale: 3,
  1522. minScale: 0.3,
  1523. scaleSpeed: 1.2
  1524. }
  1525. });
  1526. // 重新添加工作区变化监听器 - 使用合并后的监听器
  1527. addCombinedChangeListener();
  1528. // 使用XML作为中间格式导入
  1529. const xmlText = jsonToXml(json);
  1530. console.log('创建新工作区,生成的XML:', xmlText.substring(0, 200) + '...');
  1531. const xmlDom = Blockly.utils.xml.textToDom(xmlText);
  1532. Blockly.Xml.domToWorkspace(xmlDom, state.workspace);
  1533. const blockCount = ensureBlocksDraggable(); // 调用ensureBlocksDraggable确保所有块可拖拽
  1534. showStatus(`工作区已通过创建新实例并使用XML格式成功加载!共加载${blockCount}个积木。`, 'success');
  1535. console.log(`策略4成功: 加载了${blockCount}个积木`);
  1536. return;
  1537. } catch (error4) {
  1538. // 所有策略都失败
  1539. showStatus('JSON解析错误: ' + error4.message, 'error');
  1540. console.error('所有加载策略均失败:', error4);
  1541. }
  1542. }
  1543. }
  1544. }
  1545. } catch (error) {
  1546. showStatus('JSON解析错误: ' + error.message, 'error');
  1547. console.error('JSON解析错误:', error);
  1548. }
  1549. };
  1550. // 2. 创建一个合并的工作区变化监听器函数
  1551. function addCombinedChangeListener() {
  1552. if (!state.workspace) return;
  1553. // 移除旧的监听器(如果有)
  1554. state.workspace.removeAllChangeListeners();
  1555. // 添加新的合并监听器
  1556. state.workspace.addChangeListener((event) => {
  1557. // 生成代码
  1558. generateCode("javascript");
  1559. // 拖拽修复逻辑
  1560. if (event.type === Blockly.Events.BLOCK_CREATE || event.type === Blockly.Events.BLOCK_ADD)
  1561. {
  1562. const block = state.workspace.getBlockById(event.blockId);
  1563. if (block) {
  1564. // 强制设置为可编辑状态
  1565. block.setEditable(true);
  1566. console.log(`监听器设置块${block.id}为可编辑状态`);
  1567. }
  1568. }
  1569. });
  1570. }
  1571. // 导出工作区为JSON - 增强版
  1572. const exportWorkspaceToJson = () => {
  1573. try {
  1574. if (!state.workspace) {
  1575. showStatus('工作区未初始化', 'error');
  1576. return;
  1577. }
  1578. // 使用state.workspace替代workspace
  1579. const stateJson = Blockly.serialization.workspaces.save(state.workspace);
  1580. jsonData.value = JSON.stringify(stateJson, null, 2);
  1581. // 显示导出的积木数量
  1582. const blockCount = state.workspace.getAllBlocks().length;
  1583. showStatus(`工作区已成功导出为JSON!共导出${blockCount}个积木。`, 'success');
  1584. console.log(`导出JSON成功: 导出了${blockCount}个积木`);
  1585. } catch (error) {
  1586. showStatus('导出错误: ' + error.message, 'error');
  1587. console.error('导出错误:', error);
  1588. }
  1589. };
  1590. // 深度清理JSON中可能导致连接问题的部分
  1591. function deepCleanJsonForConnectionIssues(json) {
  1592. const cleanedJson = JSON.parse(JSON.stringify(json));
  1593. // 1. 清理blocks结构
  1594. if (cleanedJson.blocks && cleanedJson.blocks.blocks) {
  1595. cleanedJson.blocks.blocks.forEach(block => {
  1596. // 2. 移除inputs中的connection引用
  1597. if (block.inputs) {
  1598. Object.keys(block.inputs).forEach(inputKey => {
  1599. const input = block.inputs[inputKey];
  1600. if (input.connection) {
  1601. delete input.connection;
  1602. }
  1603. // 3. 递归清理嵌套的block
  1604. if (input.block) {
  1605. cleanBlockConnections(input.block);
  1606. }
  1607. });
  1608. }
  1609. // 4. 清理next和previous连接
  1610. if (block.next) {
  1611. if (block.next.connection) {
  1612. delete block.next.connection;
  1613. }
  1614. if (block.next.block) {
  1615. cleanBlockConnections(block.next.block);
  1616. }
  1617. }
  1618. if (block.previous) {
  1619. if (block.previous.connection) {
  1620. delete block.previous.connection;
  1621. }
  1622. }
  1623. });
  1624. }
  1625. return cleanedJson;
  1626. }
  1627. // 递归清理单个block的连接信息
  1628. function cleanBlockConnections(block) {
  1629. if (!block) return;
  1630. // 清理inputs中的连接
  1631. if (block.inputs) {
  1632. Object.keys(block.inputs).forEach(inputKey => {
  1633. const input = block.inputs[inputKey];
  1634. if (input.connection) {
  1635. delete input.connection;
  1636. }
  1637. if (input.block) {
  1638. cleanBlockConnections(input.block);
  1639. }
  1640. });
  1641. }
  1642. // 清理next和previous连接
  1643. if (block.next) {
  1644. if (block.next.connection) {
  1645. delete block.next.connection;
  1646. }
  1647. if (block.next.block) {
  1648. cleanBlockConnections(block.next.block);
  1649. }
  1650. }
  1651. if (block.previous) {
  1652. if (block.previous.connection) {
  1653. delete block.previous.connection;
  1654. }
  1655. }
  1656. }
  1657. // 将JSON转换为XML(增强版)
  1658. function jsonToXml(json) {
  1659. // 先检查JSON是否有效
  1660. if (!json || typeof json !== 'object') {
  1661. console.error('无效的JSON数据');
  1662. return '<xml xmlns="https://developers.google.com/blockly/xml"></xml>';
  1663. }
  1664. let xml = '<xml xmlns="https://developers.google.com/blockly/xml">';
  1665. // 1. 处理变量
  1666. if (json.variables && json.variables.variables) {
  1667. xml += '<variables>';
  1668. json.variables.variables.forEach(variable => {
  1669. xml += `<variable id="${escapeXml(variable.id)}">${escapeXml(variable.name)}</variable>`;
  1670. });
  1671. xml += '</variables>';
  1672. }
  1673. // 2. 处理积木块 - 这是关键部分
  1674. if (json.blocks && json.blocks.blocks) {
  1675. // 找出所有根节点积木(没有previous连接的积木)
  1676. const rootBlocks = json.blocks.blocks.filter(block => !block.previous);
  1677. console.log(`找到${rootBlocks.length}个根积木块`);
  1678. // 递归生成每个根积木块的XML
  1679. rootBlocks.forEach(block => {
  1680. xml += generateBlockXml(block);
  1681. });
  1682. } else {
  1683. console.warn('JSON中没有找到blocks数据');
  1684. }
  1685. xml += '</xml>';
  1686. return xml;
  1687. }
  1688. // 递归生成单个积木块的XML
  1689. function generateBlockXml(block) {
  1690. if (!block) return '';
  1691. let blockXml = `<block type="${escapeXml(block.type)}" id="${escapeXml(block.id)}" x="${block.x || 0}" y="${block.y || 0}">`;
  1692. // 处理fields
  1693. if (block.fields) {
  1694. Object.keys(block.fields).forEach(fieldName => {
  1695. const fieldValue = block.fields[fieldName];
  1696. blockXml += `<field name="${escapeXml(fieldName)}">${escapeXml(String(fieldValue))}</field>`;
  1697. });
  1698. }
  1699. // 处理inputs
  1700. if (block.inputs) {
  1701. Object.keys(block.inputs).forEach(inputName => {
  1702. const input = block.inputs[inputName];
  1703. if (input.block) {
  1704. blockXml += `<value name="${escapeXml(inputName)}">`;
  1705. blockXml += generateBlockXml(input.block);
  1706. blockXml += '</value>';
  1707. }
  1708. });
  1709. }
  1710. // 处理嵌套的next积木
  1711. if (block.next && block.next.block) {
  1712. blockXml += '<next>';
  1713. blockXml += generateBlockXml(block.next.block);
  1714. blockXml += '</next>';
  1715. }
  1716. blockXml += '</block>';
  1717. return blockXml;
  1718. }
  1719. // XML转义函数
  1720. function escapeXml(text) {
  1721. if (typeof text !== 'string') {
  1722. text = String(text);
  1723. }
  1724. return text
  1725. .replace(/&/g, '&amp;')
  1726. .replace(/</g, '&lt;')
  1727. .replace(/>/g, '&gt;')
  1728. .replace(/"/g, '&quot;')
  1729. .replace(/'/g, '&apos;');
  1730. }
  1731. // 显示状态消息
  1732. const showStatus = (message, type) => {
  1733. statusMessage.value = message;
  1734. statusType.value = type;
  1735. // 3秒后自动清除状态消息
  1736. setTimeout(() => {
  1737. statusMessage.value = '';
  1738. }, 3000);
  1739. };
  1740. // 注册JavaScript代码生成器
  1741. function registerJavaScriptGenerators() {
  1742. // 语音输入
  1743. javascriptGenerator.forBlock['ai_voice_input'] = function(block, generator) {
  1744. const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
  1745. const language = block.getFieldValue('LANGUAGE');
  1746. const code = `await aiService.recognizeVoice(${prompt || "''"}, '${language}')`;
  1747. return [code, javascriptGenerator.ORDER_ATOMIC];
  1748. };
  1749. // 文本生成图片
  1750. javascriptGenerator.forBlock['ai_text_to_image'] = function(block, generator) {
  1751. const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
  1752. const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
  1753. const code = `await aiService.textToImage(${prompt}, ${waitForCompletion});`;
  1754. return code;
  1755. };
  1756. // 文本生成视频
  1757. javascriptGenerator.forBlock['ai_text_to_video'] = function(block, generator) {
  1758. const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
  1759. const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
  1760. const code = `await aiService.textToVideo(${prompt}, ${waitForCompletion});`;
  1761. return code;
  1762. };
  1763. // 文本生成文本
  1764. javascriptGenerator.forBlock['ai_text_to_text'] = function(block, generator) {
  1765. const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
  1766. const model = block.getFieldValue('MODEL');
  1767. const code = `await aiService.textToText(${prompt}, '${model}')`;
  1768. return [code, javascriptGenerator.ORDER_ATOMIC];
  1769. };
  1770. // 智能台灯控制(单参数)
  1771. javascriptGenerator.forBlock['ai_smart_lamp_single_param'] = function(block, generator) {
  1772. const params = generator.valueToCode(block, 'PARAMS', javascriptGenerator.ORDER_ATOMIC);
  1773. const code = `await aiService.controlLampWithSingleParam(${params || "'白,0,平静'"});`;
  1774. return code;
  1775. };
  1776. // 智能台灯控制(多参数)
  1777. javascriptGenerator.forBlock['ai_smart_lamp'] = function(block, generator) {
  1778. const brightness = generator.valueToCode(block, 'BRIGHTNESS', javascriptGenerator.ORDER_ATOMIC);
  1779. const color = generator.valueToCode(block, 'COLOR', javascriptGenerator.ORDER_ATOMIC);
  1780. const code = `await aiService.controlLamp(${brightness || '0'}, ${color || "'白'"});`;
  1781. return code;
  1782. };
  1783. // 设置台灯亮度
  1784. javascriptGenerator.forBlock['ai_lamp_set_brightness'] = function(block, generator) {
  1785. const brightness = generator.valueToCode(block, 'BRIGHTNESS', javascriptGenerator.ORDER_ATOMIC);
  1786. const code = `await aiService.setLampBrightness(${brightness || '0'});`;
  1787. return code;
  1788. };
  1789. // 设置台灯颜色
  1790. javascriptGenerator.forBlock['ai_lamp_set_color'] = function(block, generator) {
  1791. const color = generator.valueToCode(block, 'COLOR', javascriptGenerator.ORDER_ATOMIC);
  1792. const code = `await aiService.setLampColor(${color || "'白'"});`;
  1793. return code;
  1794. };
  1795. // 音乐播放
  1796. javascriptGenerator.forBlock['ai_music_play'] = function(block, generator) {
  1797. const musicType = block.getFieldValue('MUSIC_TYPE');
  1798. const code = `await aiService.playMusic('${musicType}');`;
  1799. return code;
  1800. };
  1801. }
  1802. // 注册Python代码生成器
  1803. function registerPythonGenerators() {
  1804. // 语音输入
  1805. pythonGenerator.forBlock['ai_voice_input'] = function(block, generator) {
  1806. const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
  1807. const language = block.getFieldValue('LANGUAGE');
  1808. const code = `ai_service.recognize_voice(${prompt || "''"}, '${language}')`;
  1809. return [code, pythonGenerator.ORDER_ATOMIC];
  1810. };
  1811. // 文本生成图片
  1812. pythonGenerator.forBlock['ai_text_to_image'] = function(block, generator) {
  1813. const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
  1814. const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
  1815. const code = `ai_service.text_to_image(${prompt}, ${waitForCompletion})\n`;
  1816. return code;
  1817. };
  1818. // 文本生成视频
  1819. pythonGenerator.forBlock['ai_text_to_video'] = function(block, generator) {
  1820. const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
  1821. const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
  1822. const code = `ai_service.text_to_video(${prompt}, ${waitForCompletion})\n`;
  1823. return code;
  1824. };
  1825. // 文本生成文本
  1826. pythonGenerator.forBlock['ai_text_to_text'] = function(block, generator) {
  1827. const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
  1828. const model = block.getFieldValue('MODEL');
  1829. const code = `ai_service.text_to_text(${prompt}, '${model}')`;
  1830. return [code, pythonGenerator.ORDER_ATOMIC];
  1831. };
  1832. // 智能台灯控制(单参数)
  1833. pythonGenerator.forBlock['ai_smart_lamp_single_param'] = function(block, generator) {
  1834. const params = generator.valueToCode(block, 'PARAMS', pythonGenerator.ORDER_ATOMIC);
  1835. const code = `ai_service.control_lamp_with_single_param(${params || "'白,0,平静'"})\n`;
  1836. return code;
  1837. };
  1838. // 智能台灯控制
  1839. pythonGenerator.forBlock['ai_smart_lamp'] = function(block, generator) {
  1840. const brightness = generator.valueToCode(block, 'BRIGHTNESS', pythonGenerator.ORDER_ATOMIC);
  1841. const color = generator.valueToCode(block, 'COLOR', pythonGenerator.ORDER_ATOMIC);
  1842. const code = `ai_service.control_lamp(${brightness || '0'}, ${color || "'白'"})\n`;
  1843. return code;
  1844. };
  1845. // 设置台灯亮度
  1846. pythonGenerator.forBlock['ai_lamp_set_brightness'] = function(block, generator) {
  1847. const brightness = generator.valueToCode(block, 'BRIGHTNESS', pythonGenerator.ORDER_ATOMIC);
  1848. const code = `ai_service.set_lamp_brightness(${brightness || '0'})\n`;
  1849. return code;
  1850. };
  1851. // 设置台灯颜色
  1852. pythonGenerator.forBlock['ai_lamp_set_color'] = function(block, generator) {
  1853. const color = generator.valueToCode(block, 'COLOR', pythonGenerator.ORDER_ATOMIC);
  1854. const code = `ai_service.set_lamp_color(${color || "'白'"})\n`;
  1855. return code;
  1856. };
  1857. // 音乐播放
  1858. pythonGenerator.forBlock['ai_music_play'] = function(block, generator) {
  1859. const musicType = block.getFieldValue('MUSIC_TYPE');
  1860. const code = `ai_service.play_music('${musicType}')\n`;
  1861. return code;
  1862. };
  1863. }
  1864. // 生成代码
  1865. const generateCode = (language) => {
  1866. if (!state.workspace) {
  1867. console.error("workspace 未正确初始化");
  1868. return;
  1869. }
  1870. let generator;
  1871. if (language === "javascript") {
  1872. generator = javascriptGenerator;
  1873. } else if (language === "python") {
  1874. generator = pythonGenerator;
  1875. } else {
  1876. console.error("不支持的语言类型");
  1877. return;
  1878. }
  1879. const code = generator.workspaceToCode(state.workspace);
  1880. document.getElementById("textarea").value = code;
  1881. };
  1882. // 运行代码
  1883. async function runCode() {
  1884. try {
  1885. const codeTextarea = document.getElementById("textarea");
  1886. const code = codeTextarea.value;
  1887. const resultElement = document.getElementById("run-result");
  1888. // 清空之前的结果
  1889. resultElement.innerHTML = '<div class="running-indicator">代码运行中...</div>';
  1890. // 保存原始console方法
  1891. const originalConsoleLog = console.log;
  1892. const originalConsoleError = console.error;
  1893. const originalConsoleWarn = console.warn;
  1894. // 创建输出缓冲区
  1895. let outputBuffer = "";
  1896. // 重定义console方法 - 修复console.log不更新DOM的问题
  1897. console.log = (...args) => {
  1898. // 过滤掉包含Vue警告的日志
  1899. const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' ');
  1900. // 只显示不包含Vue warn的日志
  1901. if (!message.includes('[Vue warn]')) {
  1902. outputBuffer += '<div class="log-message">' + message + '</div>';
  1903. resultElement.innerHTML = outputBuffer; // 关键修复:立即更新DOM
  1904. }
  1905. originalConsoleLog.apply(console, args);
  1906. };
  1907. console.error = (...args) => {
  1908. // 过滤掉包含Vue警告的日志
  1909. const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' ');
  1910. // 只显示不包含Vue warn的日志
  1911. if (!message.includes('[Vue warn]')) {
  1912. outputBuffer += '<div class="error-message">' + message + '</div>';
  1913. resultElement.innerHTML = outputBuffer;
  1914. }
  1915. originalConsoleError.apply(console, args);
  1916. };
  1917. console.warn = (...args) => {
  1918. // 过滤掉包含Vue警告的日志
  1919. const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' ');
  1920. // 只显示不包含Vue warn的日志
  1921. if (!message.includes('[Vue warn]')) {
  1922. outputBuffer += '<div class="warn-message">' + message + '</div>';
  1923. resultElement.innerHTML = outputBuffer;
  1924. }
  1925. originalConsoleWarn.apply(console, args);
  1926. };
  1927. try {
  1928. // 添加安全检查
  1929. if (code.includes('eval(') || code.includes('Function(') ||
  1930. code.includes('document.write') || code.includes('window.location')) {
  1931. throw new Error('代码包含不安全的操作');
  1932. }
  1933. // 包装代码为异步函数执行,支持await
  1934. const wrappedCode = `(async () => { ${code} })()`;
  1935. await new Function(wrappedCode)();
  1936. // outputBuffer += '<div class="success-message">代码执行成功</div>';
  1937. resultElement.innerHTML = outputBuffer;
  1938. } catch (error) {
  1939. outputBuffer += `<div class="error-message">执行错误: ${error.message}</div>`;
  1940. resultElement.innerHTML = outputBuffer;
  1941. } finally {
  1942. // 恢复原始console方法
  1943. // console.log = originalConsoleLog;
  1944. // console.error = originalConsoleError;
  1945. // console.warn = originalConsoleWarn;
  1946. }
  1947. } catch (err) {
  1948. console.error('运行代码时发生错误:', err);
  1949. document.getElementById('run-result').innerHTML =
  1950. `<div class="error-message">运行代码时发生错误: ${err.message}</div>`;
  1951. }
  1952. }
  1953. // 保存JSON
  1954. function saveJson() {
  1955. try {
  1956. const output = document.getElementById('textarea');
  1957. const json = Blockly.serialization.workspaces.save(state.workspace);
  1958. output.value = JSON.stringify(json, null, 2);
  1959. output.focus();
  1960. output.select();
  1961. taChange();
  1962. ElMessage.success('JSON保存成功');
  1963. } catch (error) {
  1964. console.error('保存JSON失败:', error);
  1965. ElMessage.error('保存JSON失败: ' + error.message);
  1966. }
  1967. }
  1968. // 保存XML
  1969. function saveXml() {
  1970. try {
  1971. const output = document.getElementById('textarea');
  1972. const xml = Blockly.Xml.workspaceToDom(state.workspace);
  1973. output.value = Blockly.Xml.domToPrettyText(xml);
  1974. output.focus();
  1975. output.select();
  1976. taChange();
  1977. ElMessage.success('XML保存成功');
  1978. } catch (error) {
  1979. console.error('保存XML失败:', error);
  1980. ElMessage.error('保存XML失败: ' + error.message);
  1981. }
  1982. }
  1983. // 重新加载
  1984. function load() {
  1985. const input = document.getElementById('textarea');
  1986. if (!input.value) return;
  1987. try {
  1988. //关闭预览
  1989. handleClosePreview();
  1990. const valid = saveIsValid(input.value);
  1991. if (valid.json) {
  1992. const parsedState = JSON.parse(input.value);
  1993. Blockly.serialization.workspaces.load(parsedState, state.workspace);
  1994. } else if (valid.xml) {
  1995. const xml = Blockly.utils.xml.textToDom(input.value);
  1996. Blockly.Xml.domToWorkspace(xml, state.workspace);
  1997. }
  1998. taChange();
  1999. } catch (error) {
  2000. console.error("加载失败:", error);
  2001. ElMessage.error("加载失败: " + error.message);
  2002. }
  2003. }
  2004. // 文本域变化时触发
  2005. function taChange() {
  2006. const textarea = document.getElementById("textarea");
  2007. if (sessionStorage) {
  2008. sessionStorage.setItem("textarea", textarea.value);
  2009. }
  2010. const valid = saveIsValid(textarea.value);
  2011. document.getElementById("import").disabled = !valid.json && !valid.xml;
  2012. }
  2013. // 检查保存内容是否为有效JSON或XML
  2014. function saveIsValid(save) {
  2015. let validJson = true;
  2016. try {
  2017. JSON.parse(save);
  2018. } catch (e) {
  2019. validJson = false;
  2020. }
  2021. let validXml = true;
  2022. try {
  2023. Blockly.utils.xml.textToDom(save);
  2024. } catch (e) {
  2025. validXml = false;
  2026. }
  2027. return { json: validJson, xml: validXml };
  2028. }
  2029. // 关闭预览
  2030. function handleClosePreview() {
  2031. state.previewVisible = false;
  2032. state.previewContent = "";
  2033. state.previewType = "";
  2034. state.generatedContent.text = null;
  2035. state.generatedContent.imageUrl = null;
  2036. state.generatedContent.videoUrl = null;
  2037. }
  2038. // 将aiService挂载到window,以便执行生成的代码时可以访问
  2039. window.aiService = aiService;
  2040. </script>
  2041. <style scoped lang="scss">
  2042. /* 原有样式保持不变 */
  2043. @use "sass:math";
  2044. @function rpx($px) {
  2045. @return math.div($px, 750) * 100vw;
  2046. }
  2047. // 智能台灯
  2048. .desk-lamp-container {
  2049. position: fixed;
  2050. top: 0;
  2051. left: 0;
  2052. right: 0;
  2053. bottom: 0;
  2054. overflow: hidden;
  2055. display: flex;
  2056. justify-content: center;
  2057. align-items: center;
  2058. z-index: 1000;
  2059. }
  2060. .full-screen-image {
  2061. width: 100%;
  2062. height: 100%;
  2063. object-fit: cover;
  2064. }
  2065. /* 台灯灯光样式 - 使用动态颜色 */
  2066. .lamp-light-mask {
  2067. position: absolute;
  2068. width: rpx(78);
  2069. height: rpx(250);
  2070. margin-top: rpx(49);
  2071. margin-left: rpx(192);
  2072. background: linear-gradient(to bottom, var(--lamp-color) 0%, var(--lamp-color) 10%, rgba(255, 255, 255, 0) 90%);
  2073. filter: blur(60px);
  2074. transform: rotate(9deg);
  2075. transform-origin: top center;
  2076. opacity: var(--lamp-opacity, 0.6);
  2077. /* 创建扇形效果 */
  2078. clip-path: polygon(0% 0%, 100% 0%, 250% 100%, -150% 100%);
  2079. }
  2080. /* 灯光信息显示 */
  2081. .lamp-info {
  2082. position: absolute;
  2083. bottom: 100px;
  2084. right: 30px;
  2085. background-color: rgba(255, 255, 255, 0.2);
  2086. padding: 10px 20px;
  2087. border-radius: 8px;
  2088. backdrop-filter: blur(10px);
  2089. color: white;
  2090. font-size: 14px;
  2091. z-index: 1000;
  2092. }
  2093. /* 标题框样式 */
  2094. .desk-lamp-title-box {
  2095. position: absolute;
  2096. top: 20px;
  2097. left: 20px;
  2098. z-index: 1000;
  2099. }
  2100. .desk-lamp-box-icon {
  2101. display: flex;
  2102. align-items: center;
  2103. gap: 10px;
  2104. padding: 10px 20px;
  2105. background-color: rgba(255, 255, 255, 0.2);
  2106. border-radius: 30px;
  2107. backdrop-filter: blur(10px);
  2108. cursor: pointer;
  2109. transition: all 0.3s ease;
  2110. font-size: 16px;
  2111. color: white;
  2112. font-weight: 500;
  2113. }
  2114. .desk-lamp-box-icon:hover {
  2115. background-color: rgba(255, 255, 255, 0.3);
  2116. transform: translateX(-3px);
  2117. }
  2118. .left-icon {
  2119. font-size: 18px;
  2120. }
  2121. /* 右下角按钮组样式 */
  2122. .button-group {
  2123. position: absolute;
  2124. bottom: 30px;
  2125. right: 30px;
  2126. display: flex;
  2127. gap: 15px;
  2128. z-index: 1000;
  2129. }
  2130. .control-button {
  2131. padding: 12px 24px;
  2132. border-radius: 8px;
  2133. font-size: 16px;
  2134. font-weight: 500;
  2135. transition: all 0.3s ease;
  2136. backdrop-filter: blur(5px);
  2137. }
  2138. .run-button {
  2139. background-color: rgba(64, 169, 255, 0.8);
  2140. color: white;
  2141. border: none;
  2142. }
  2143. .run-button:hover {
  2144. background-color: rgba(64, 169, 255, 1);
  2145. transform: translateY(-2px);
  2146. }
  2147. .code-button {
  2148. background-color: rgba(132, 94, 255, 0.8);
  2149. color: white;
  2150. border: none;
  2151. }
  2152. .code-button:hover {
  2153. background-color: rgba(132, 94, 255, 1);
  2154. transform: translateY(-2px);
  2155. }
  2156. // 页面主容器样式
  2157. .page-container {
  2158. position: fixed;
  2159. top: 0;
  2160. left: 0;
  2161. right: 0;
  2162. bottom: 0;
  2163. background: linear-gradient(to bottom, #001169, #b4a8e1);
  2164. display: flex;
  2165. flex-direction: column;
  2166. }
  2167. // 标题样式
  2168. .title-box {
  2169. height: rpx(30);
  2170. }
  2171. .box-icon {
  2172. width: 100%;
  2173. height: 100%;
  2174. display: flex;
  2175. align-items: center;
  2176. color: white;
  2177. padding-left: rpx(15);
  2178. font-size: rpx(10);
  2179. cursor: pointer;
  2180. }
  2181. .box-icon .left-icon {
  2182. margin-left: rpx(10);
  2183. margin-right: rpx(5);
  2184. }
  2185. .home-container {
  2186. flex: 1;
  2187. display: flex;
  2188. // padding-top: rpx(10);
  2189. }
  2190. .box-blockly {
  2191. width: 60%;
  2192. height: 90%;
  2193. padding: rpx(10) rpx(10);
  2194. float: left;
  2195. }
  2196. .box-code {
  2197. color: white;
  2198. width: 35%;
  2199. height: 90%;
  2200. padding: rpx(10) rpx(10);
  2201. display: flex;
  2202. flex-direction: column;
  2203. .button-container {
  2204. display: flex;
  2205. flex-wrap: wrap;
  2206. gap: 8px;
  2207. align-items: flex-start;
  2208. }
  2209. .button-container :deep(.el-button) {
  2210. flex-shrink: 0;
  2211. }
  2212. .code-section {
  2213. flex: 1;
  2214. display: flex;
  2215. flex-direction: column;
  2216. margin-bottom: 10px;
  2217. .box-code-textarea {
  2218. flex: 1;
  2219. resize: none;
  2220. background-color: #f8f9fa;
  2221. border: 1px solid #ddd;
  2222. border-radius: 4px;
  2223. padding: 10px;
  2224. overflow-y: auto;
  2225. font-family: monospace;
  2226. font-size: 14px;
  2227. }
  2228. }
  2229. //运行结果区域样式
  2230. .result-section {
  2231. flex: 1;
  2232. display: flex;
  2233. flex-direction: column;
  2234. .run-result-content {
  2235. color: black;
  2236. // 固定高度为300px,可根据需要调整
  2237. height: 180px;
  2238. background-color: #f8f9fa;
  2239. border: 1px solid #ddd;
  2240. border-radius: 4px;
  2241. padding: 10px;
  2242. overflow-y: auto;
  2243. font-family: monospace;
  2244. font-size: 14px;
  2245. // 优化滚动条样式 - 适用于Webkit浏览器(Chrome, Safari)
  2246. &::-webkit-scrollbar {
  2247. width: 8px;
  2248. height: 8px;
  2249. }
  2250. &::-webkit-scrollbar-track {
  2251. background: #f1f1f1;
  2252. border-radius: 4px;
  2253. }
  2254. &::-webkit-scrollbar-thumb {
  2255. background: #c1c1c1;
  2256. border-radius: 4px;
  2257. }
  2258. &::-webkit-scrollbar-thumb:hover {
  2259. background: #a8a8a8;
  2260. }
  2261. // Firefox滚动条样式
  2262. scrollbar-width: thin;
  2263. scrollbar-color: #c1c1c1 #f1f1f1;
  2264. }
  2265. }
  2266. }
  2267. // AI模块样式
  2268. [categorystyle="ai_category"] > .blocklyTreeRow {
  2269. background-color: #9c27b0 !important;
  2270. }
  2271. // 运行结果样式
  2272. .running-indicator {
  2273. color: #666;
  2274. }
  2275. .log-message {
  2276. color: #333;
  2277. }
  2278. .error-message {
  2279. color: #dc3545;
  2280. }
  2281. .warn-message {
  2282. color: #ffc107;
  2283. }
  2284. .success-message {
  2285. color: #28a745;
  2286. }
  2287. // 预览样式
  2288. .preview-image-container,
  2289. .preview-video-container {
  2290. display: flex;
  2291. justify-content: center;
  2292. }
  2293. .preview-image,
  2294. .preview-video {
  2295. max-width: 100%;
  2296. max-height: 60vh;
  2297. border-radius: 4px;
  2298. }
  2299. .preview-text-container {
  2300. max-height: 60vh;
  2301. overflow-y: auto;
  2302. padding: 10px;
  2303. background-color: #f5f5f5;
  2304. border-radius: 4px;
  2305. }
  2306. //【文生图预览】
  2307. .extra-image-preview {
  2308. color: black;
  2309. margin-top: 10px;
  2310. padding: 10px;
  2311. border: 1px solid #ddd;
  2312. border-radius: 5px;
  2313. background-color: #f9f9f9;
  2314. }
  2315. .extra-preview-image {
  2316. max-width: 100%;
  2317. max-height: 400px;
  2318. border-radius: 4px;
  2319. }
  2320. //台灯
  2321. .lamp-preview-container {
  2322. margin-top: 20px;
  2323. padding: 10px;
  2324. background-color: rgba(255, 255, 255, 0.1);
  2325. border-radius: 8px;
  2326. text-align: center;
  2327. }
  2328. .lamp-display {
  2329. margin: 15px auto;
  2330. position: relative;
  2331. transition: all 0.3s ease;
  2332. }
  2333. .lamp-image {
  2334. width: 100px;
  2335. height: 150px;
  2336. margin: 0 auto;
  2337. background-color: #e0e0e0;
  2338. border-radius: 10px 10px 0 0;
  2339. position: relative;
  2340. box-shadow: 0 0 40px 20px #ffffff80;
  2341. transition: box-shadow 0.5s ease;
  2342. &::before {
  2343. content: "";
  2344. position: absolute;
  2345. top: 10px;
  2346. left: 10px;
  2347. right: 10px;
  2348. bottom: 40px;
  2349. background-color: #f5f5f5;
  2350. border-radius: 5px;
  2351. }
  2352. &::after {
  2353. content: "";
  2354. position: absolute;
  2355. bottom: 10px;
  2356. left: 40px;
  2357. width: 20px;
  2358. height: 30px;
  2359. background-color: #888888;
  2360. }
  2361. }
  2362. .lamp-info {
  2363. margin-top: 10px;
  2364. font-size: 14px;
  2365. color: #ffffff;
  2366. }
  2367. //音乐播放器
  2368. .music-player-container {
  2369. margin-top: 20px;
  2370. padding: 15px;
  2371. background-color: #f9f9f9;
  2372. border-radius: 8px;
  2373. border: 1px solid #e0e0e0;
  2374. }
  2375. .music-player-container h5 {
  2376. margin-top: 0;
  2377. margin-bottom: 10px;
  2378. color: #333;
  2379. font-size: 16px;
  2380. }
  2381. .music-player-container audio {
  2382. width: 100%;
  2383. margin-bottom: 10px;
  2384. }
  2385. .music-status {
  2386. font-size: 14px;
  2387. color: #666;
  2388. padding: 5px 0;
  2389. }
  2390. .json-section {
  2391. margin-top: 15px;
  2392. padding: 15px;
  2393. background: #f1f8ff;
  2394. border-radius: 8px;
  2395. border: 1px solid #d1e7ff;
  2396. }
  2397. .json-section h3 {
  2398. margin-bottom: 10px;
  2399. color: #2c3e50;
  2400. }
  2401. textarea {
  2402. width: 100%;
  2403. min-height: 120px;
  2404. padding: 10px;
  2405. border: 1px solid #ddd;
  2406. border-radius: 5px;
  2407. font-family: 'Courier New', monospace;
  2408. resize: vertical;
  2409. }
  2410. </style>