Blockly.vue 62 KB

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