|
|
@@ -1,2697 +0,0 @@
|
|
|
-<template>
|
|
|
- <!-- 智能台灯 -->
|
|
|
- <div v-if="showLampPreview" class="desk-lamp-container">
|
|
|
- <!-- 标题框 -->
|
|
|
- <div class="desk-lamp-title-box">
|
|
|
- <div class="desk-lamp-box-icon" @click="goBackLab">
|
|
|
- <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
|
|
|
- 返回虚拟实验室
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <img src="@/assets/images/desklamp.png" alt="智能台灯" class="full-screen-image" />
|
|
|
- <!-- 使用动态样式设置灯光遮罩 -->
|
|
|
- <div v-if="state.lamp.isLightOn" :style="{ '--lamp-color': state.lamp.color,'--lamp-opacity': state.lamp.brightness / 100 }" class="lamp-light-mask"></div>
|
|
|
-
|
|
|
- <!-- 右下角按钮组 -->
|
|
|
- <div class="button-group">
|
|
|
- <el-button class="control-button run-button" @click="toggleLight">运行</el-button>
|
|
|
- <el-button class="control-button code-button" @click="handleViewCode">智能台灯</el-button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 显示当前灯光信息 -->
|
|
|
- <div v-if="state.lamp.isLightOn" class="lamp-info">
|
|
|
- <p>颜色: {{ state.lamp.colorLog }}色</p>
|
|
|
- <p>亮度: {{ state.lamp.brightness }}%</p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- Blockly -->
|
|
|
- <div class="page-container">
|
|
|
- <!-- 返回 -->
|
|
|
- <div class="title-box">
|
|
|
- <div class="box-icon" @click="goBack">
|
|
|
- <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
|
|
|
- 返回智能台灯
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="home-container">
|
|
|
- <div class="box-blockly">
|
|
|
- <div
|
|
|
- id="blocklyDiv"
|
|
|
- style="height: 100%; width: 100%; background-color: #cccccc"
|
|
|
- ></div>
|
|
|
-
|
|
|
- <!-- blockly工具栏 -->
|
|
|
- <template>
|
|
|
- <xml id="toolbox" style="display: none">
|
|
|
- <!-- AI模块分类 - 扩展更多功能 -->
|
|
|
- <category name="AI模块" categorystyle="ai_category">
|
|
|
- <block type="ai_voice_input"></block>
|
|
|
- <block type="ai_text_to_image"></block>
|
|
|
- <block type="ai_text_to_video"></block>
|
|
|
- <block type="ai_text_to_text"></block>
|
|
|
- <block type="ai_smart_lamp_single_param"></block>
|
|
|
- <block type="ai_smart_lamp"></block>
|
|
|
- <block type="ai_lamp_set_brightness"></block>
|
|
|
- <block type="ai_lamp_set_color"></block>
|
|
|
- <block type="ai_music_play"></block>
|
|
|
- </category>
|
|
|
-
|
|
|
- <!-- 其他原有分类保持不变 -->
|
|
|
- <category name="逻辑" categorystyle="logic_category">
|
|
|
- <block type="controls_if"></block>
|
|
|
- <block type="logic_compare"></block>
|
|
|
- <block type="logic_operation"></block>
|
|
|
- <block type="logic_negate"></block>
|
|
|
- <block type="logic_boolean"></block>
|
|
|
- <block type="logic_null" disabled="true"></block>
|
|
|
- <block type="logic_ternary"></block>
|
|
|
- </category>
|
|
|
- <category name="循环" categorystyle="loop_category">
|
|
|
- <block type="controls_repeat_ext">
|
|
|
- <value name="TIMES">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">10</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="controls_repeat" disabled="true"></block>
|
|
|
- <block type="controls_whileUntil"></block>
|
|
|
- <block type="controls_for">
|
|
|
- <value name="FROM">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">1</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- <value name="TO">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">10</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- <value name="BY">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">1</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="controls_forEach"></block>
|
|
|
- <block type="controls_flow_statements"></block>
|
|
|
- </category>
|
|
|
- <category name="数值" categorystyle="math_category">
|
|
|
- <block type="math_number" gap="32">
|
|
|
- <field name="NUM">123</field>
|
|
|
- </block>
|
|
|
- <block type="math_arithmetic">
|
|
|
- <value name="A">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">1</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- <value name="B">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">1</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="math_single">
|
|
|
- <value name="NUM">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">9</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="math_trig">
|
|
|
- <value name="NUM">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">45</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="math_constant"></block>
|
|
|
- <block type="math_number_property">
|
|
|
- <value name="NUMBER_TO_CHECK">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">0</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="math_round">
|
|
|
- <value name="NUM">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">3.1</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="math_on_list"></block>
|
|
|
- <block type="math_modulo">
|
|
|
- <value name="DIVIDEND">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">64</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- <value name="DIVISOR">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">10</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="math_constrain">
|
|
|
- <value name="VALUE">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">50</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- <value name="LOW">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">1</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- <value name="HIGH">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">100</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="math_random_int">
|
|
|
- <value name="FROM">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">1</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- <value name="TO">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">100</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="math_random_float"></block>
|
|
|
- <block type="math_atan2">
|
|
|
- <value name="X">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">1</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- <value name="Y">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">1</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- </category>
|
|
|
- <category name="文本" categorystyle="text_category">
|
|
|
- <block type="text"></block>
|
|
|
- <block type="text_join"></block>
|
|
|
- <block type="text_append">
|
|
|
- <value name="TEXT">
|
|
|
- <shadow type="text"></shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="text_length">
|
|
|
- <value name="VALUE">
|
|
|
- <shadow type="text">
|
|
|
- <field name="TEXT">abc</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="text_isEmpty">
|
|
|
- <value name="VALUE">
|
|
|
- <shadow type="text">
|
|
|
- <field name="TEXT"></field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="text_indexOf">
|
|
|
- <value name="VALUE">
|
|
|
- <block type="variables_get">
|
|
|
- <field name="VAR">text</field>
|
|
|
- </block>
|
|
|
- </value>
|
|
|
- <value name="FIND">
|
|
|
- <shadow type="text">
|
|
|
- <field name="TEXT">abc</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="text_charAt">
|
|
|
- <value name="VALUE">
|
|
|
- <block type="variables_get">
|
|
|
- <field name="VAR">text</field>
|
|
|
- </block>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="text_getSubstring">
|
|
|
- <value name="STRING">
|
|
|
- <block type="variables_get">
|
|
|
- <field name="VAR">text</field>
|
|
|
- </block>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="text_changeCase">
|
|
|
- <value name="TEXT">
|
|
|
- <shadow type="text">
|
|
|
- <field name="TEXT">abc</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="text_trim">
|
|
|
- <value name="TEXT">
|
|
|
- <shadow type="text">
|
|
|
- <field name="TEXT">abc</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="text_count">
|
|
|
- <value name="SUB">
|
|
|
- <shadow type="text"></shadow>
|
|
|
- </value>
|
|
|
- <value name="TEXT">
|
|
|
- <shadow type="text"></shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="text_replace">
|
|
|
- <value name="FROM">
|
|
|
- <shadow type="text"></shadow>
|
|
|
- </value>
|
|
|
- <value name="TO">
|
|
|
- <shadow type="text"></shadow>
|
|
|
- </value>
|
|
|
- <value name="TEXT">
|
|
|
- <shadow type="text"></shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="text_reverse">
|
|
|
- <value name="TEXT">
|
|
|
- <shadow type="text"></shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <label text="Input/Output:" web-class="ioLabel"></label>
|
|
|
- <block type="text_print">
|
|
|
- <value name="TEXT">
|
|
|
- <shadow type="text">
|
|
|
- <field name="TEXT">abc</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="text_prompt_ext">
|
|
|
- <value name="TEXT">
|
|
|
- <shadow type="text">
|
|
|
- <field name="TEXT">abc</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- </category>
|
|
|
- <category name="列表" categorystyle="list_category">
|
|
|
- <block type="lists_create_with">
|
|
|
- <mutation items="0"></mutation>
|
|
|
- </block>
|
|
|
- <block type="lists_create_with"></block>
|
|
|
- <block type="lists_repeat">
|
|
|
- <value name="NUM">
|
|
|
- <shadow type="math_number">
|
|
|
- <field name="NUM">5</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="lists_length"></block>
|
|
|
- <block type="lists_isEmpty"></block>
|
|
|
- <block type="lists_indexOf">
|
|
|
- <value name="VALUE">
|
|
|
- <block type="variables_get">
|
|
|
- <field name="VAR">list</field>
|
|
|
- </block>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="lists_getIndex">
|
|
|
- <value name="VALUE">
|
|
|
- <block type="variables_get">
|
|
|
- <field name="VAR">list</field>
|
|
|
- </block>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="lists_setIndex">
|
|
|
- <value name="LIST">
|
|
|
- <block type="variables_get">
|
|
|
- <field name="VAR">list</field>
|
|
|
- </block>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="lists_getSublist">
|
|
|
- <value name="LIST">
|
|
|
- <block type="variables_get">
|
|
|
- <field name="VAR">list</field>
|
|
|
- </block>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="lists_split">
|
|
|
- <value name="DELIM">
|
|
|
- <shadow type="text">
|
|
|
- <field name="TEXT">,</field>
|
|
|
- </shadow>
|
|
|
- </value>
|
|
|
- </block>
|
|
|
- <block type="lists_sort"></block>
|
|
|
- <block type="lists_reverse"></block>
|
|
|
- </category>
|
|
|
- <sep></sep>
|
|
|
- <category
|
|
|
- name="Variables"
|
|
|
- categorystyle="variable_category"
|
|
|
- custom="VARIABLE"
|
|
|
- ></category>
|
|
|
- <category
|
|
|
- name="Functions"
|
|
|
- categorystyle="procedure_category"
|
|
|
- custom="PROCEDURE"
|
|
|
- ></category>
|
|
|
- </xml>
|
|
|
- </template>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="box-code">
|
|
|
- <div class="button-container">
|
|
|
- <el-button type="danger" plain id="run-code" @click="runCode()"
|
|
|
- >运行代码</el-button
|
|
|
- >
|
|
|
- <el-button type="warning" plain id="import" @click="load()"
|
|
|
- >重新加载</el-button
|
|
|
- >
|
|
|
- </div>
|
|
|
- <br />
|
|
|
-
|
|
|
- <div class="button-container">
|
|
|
- <el-button
|
|
|
- type="primary"
|
|
|
- plain
|
|
|
- id="to-code-js"
|
|
|
- @click="generateCode('javascript')"
|
|
|
- >生成 JavaScript</el-button
|
|
|
- >
|
|
|
- <el-button
|
|
|
- type="success"
|
|
|
- plain
|
|
|
- id="to-code-py"
|
|
|
- @click="generateCode('python')"
|
|
|
- >生成 Python</el-button
|
|
|
- >
|
|
|
- <el-button type="info" plain id="save-json" @click="saveJson()"
|
|
|
- >保存 JSON</el-button
|
|
|
- >
|
|
|
- <el-button type="info" plain id="save-xml" @click="saveXml()"
|
|
|
- >保存 XML</el-button
|
|
|
- >
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="json-section">
|
|
|
- <h3>JSON 数据</h3>
|
|
|
- <textarea v-model="jsonData" placeholder="在此输入JSON格式的积木块数据..."></textarea>
|
|
|
- <div class="controls">
|
|
|
- <button @click="loadWorkspaceFromJson">加载JSON到工作区</button>
|
|
|
- <button @click="exportWorkspaceToJson">导出工作区为JSON</button>
|
|
|
- </div>
|
|
|
- <div v-if="statusMessage" :class="['status', statusType]" style="color: #1a1a1a">
|
|
|
- {{ statusMessage }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 代码显示区 -->
|
|
|
- <div class="code-section">
|
|
|
- <h4>生成的代码</h4>
|
|
|
- <textarea name="" id="textarea" class="box-code-textarea"></textarea>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 运行结果区 -->
|
|
|
- <div class="result-section">
|
|
|
- <h4>运行结果</h4>
|
|
|
- <div id="run-result" class="run-result-content"></div>
|
|
|
-
|
|
|
- <!-- 额外的图片预览区域 -->
|
|
|
- <div
|
|
|
- v-if="state.generatedContent.imageUrl"
|
|
|
- class="extra-image-preview"
|
|
|
- >
|
|
|
- <h5>生成的图片:</h5>
|
|
|
- <img
|
|
|
- :src="state.generatedContent.imageUrl"
|
|
|
- class="extra-preview-image"
|
|
|
- alt="AI生成图片"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div
|
|
|
- v-if="state.generatedContent.videoUrl"
|
|
|
- class="extra-image-preview"
|
|
|
- >
|
|
|
- <h5>生成的视频:</h5>
|
|
|
- <video
|
|
|
- :src="state.generatedContent.videoUrl"
|
|
|
- controls
|
|
|
- class="preview-video"
|
|
|
- alt="AI生成视频"
|
|
|
- ></video>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 在template部分的适当位置音频播放器组件 -->
|
|
|
- <div class="music-player-container" v-if="state.currentMusicUrl">
|
|
|
- <h5>音乐播放</h5>
|
|
|
- <audio
|
|
|
- ref="musicPlayer"
|
|
|
- :src="state.currentMusicUrl"
|
|
|
- @ended="handleMusicEnded"
|
|
|
- preload="metadata">
|
|
|
- 您的浏览器不支持音频元素
|
|
|
- </audio>
|
|
|
- <div class="music-status">
|
|
|
- <p v-if="state.isMusicPlaying">正在播放: {{ state.currentMusicName }}</p>
|
|
|
- <p v-else>准备就绪</p>
|
|
|
- </div>
|
|
|
- <!-- 停止播放按钮 - 修复点击事件 -->
|
|
|
- <el-button
|
|
|
- v-if="state.isMusicPlaying"
|
|
|
- type="danger"
|
|
|
- size="small"
|
|
|
- @click="handleStopMusic"
|
|
|
- style="margin-top: 10px;">
|
|
|
- 停止播放
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
-
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- AI结果预览模态框 -->
|
|
|
- <el-dialog
|
|
|
- v-if="state.previewConten"
|
|
|
- title="AI生成结果"
|
|
|
- width="80%"
|
|
|
- :before-close="handleClosePreview"
|
|
|
- >
|
|
|
- <div
|
|
|
- v-if="state.previewType === 'image'"
|
|
|
- class="preview-image-container"
|
|
|
- >
|
|
|
- <img
|
|
|
- :src="state.previewContent"
|
|
|
- class="preview-image"
|
|
|
- alt="生成的图片"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div
|
|
|
- v-if="state.previewType === 'video'"
|
|
|
- class="preview-video-container"
|
|
|
- >
|
|
|
- <video
|
|
|
- :src="state.previewContent"
|
|
|
- controls
|
|
|
- class="preview-video"
|
|
|
- ></video>
|
|
|
- </div>
|
|
|
- <div v-if="state.previewType === 'text'" class="preview-text-container">
|
|
|
- <p>{{ state.previewContent }}</p>
|
|
|
- </div>
|
|
|
- <span slot="footer" class="dialog-footer">
|
|
|
- <el-button @click="handleClosePreview">关闭</el-button>
|
|
|
- </span>
|
|
|
- </el-dialog>
|
|
|
- </div>
|
|
|
-
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup>
|
|
|
-import { ref, onMounted, onUnmounted, reactive } from "vue";
|
|
|
-import { useRouter } from "vue-router";
|
|
|
-import { ArrowLeftBold } from "@element-plus/icons-vue";
|
|
|
-import * as Blockly from "blockly";
|
|
|
-import { javascriptGenerator } from "blockly/javascript";
|
|
|
-import { pythonGenerator } from "blockly/python";
|
|
|
-import * as hans from "blockly/msg/zh-hans";
|
|
|
-import { ElDialog, ElButton, ElMessage } from "element-plus";
|
|
|
-
|
|
|
-//【文生图】文生图
|
|
|
-import {
|
|
|
- AiImageStatusEnum,
|
|
|
- CreatePainting,
|
|
|
- PaintingGetMys,
|
|
|
- CreateVideo,
|
|
|
- VideoGetMys,
|
|
|
- sendChatMessageStream,
|
|
|
- CreateDialogue,
|
|
|
-} from "@/api/questions.js";
|
|
|
-import { getModelIdByType, ModelPlatformEnum } from "@/api/teachers.js";
|
|
|
-import { ModelTypeEnum } from "@/api/teachers.js";
|
|
|
-import { globalState } from "@/utils/globalState.js";
|
|
|
-//音乐
|
|
|
-import { playMusic, stopMusic, onMusicEnded } from "@/api/blockly/music.js";
|
|
|
-
|
|
|
-
|
|
|
-const router = useRouter();
|
|
|
-// 台灯预览显示状态
|
|
|
-const showLampPreview = ref(false)
|
|
|
-
|
|
|
-// 返回虚拟实验室
|
|
|
-const goBackLab = () => {
|
|
|
- router.push("/virtual-laboratory");
|
|
|
-};
|
|
|
-const goBack = () => {
|
|
|
- showLampPreview.value = true
|
|
|
-};
|
|
|
-// 切换灯光状态
|
|
|
-const toggleLight = () => {
|
|
|
- state.lamp.isLightOn = true;
|
|
|
- generateCode('javascript');
|
|
|
- runCode()
|
|
|
-};
|
|
|
-// 查看代码编程界面显示状态
|
|
|
-const handleViewCode = () =>{
|
|
|
- showLampPreview.value = false
|
|
|
-};
|
|
|
-
|
|
|
-Blockly.setLocale(hans);
|
|
|
-
|
|
|
-// 状态管理
|
|
|
-const state = reactive({
|
|
|
- workspace: null,
|
|
|
- generatedContent: {
|
|
|
- imageUrl: "",
|
|
|
- videoUrl: "",
|
|
|
- text: "",
|
|
|
- },
|
|
|
- previewVisible: false,
|
|
|
- previewType: "",
|
|
|
- previewContent: "",
|
|
|
- isProcessing: false,
|
|
|
-
|
|
|
- //年级
|
|
|
- gradeId: "",
|
|
|
-
|
|
|
- //【文生图】文生图
|
|
|
- inProgressImageMap: {},
|
|
|
-
|
|
|
- //【文生视频】文生视频
|
|
|
- inProgressVideoMap: {},
|
|
|
-
|
|
|
- // 台灯状态
|
|
|
- lamp: {
|
|
|
- isLightOn: false,// 台灯是否亮着
|
|
|
- brightness: 0, // 默认亮度50%
|
|
|
- color: "#ffffff", // 默认颜色白色
|
|
|
- colorLog: "白", // 默认颜色白色
|
|
|
- },
|
|
|
-
|
|
|
- // 【文本文】对话相关状态
|
|
|
- activeConversationId: null,
|
|
|
- conversationInAbortController: null,
|
|
|
-
|
|
|
- // 独立的音乐播放状态
|
|
|
- currentMusicUrl: '',
|
|
|
- currentMusicName: '',
|
|
|
- isMusicPlaying: false,
|
|
|
-});
|
|
|
-
|
|
|
-// 创建音乐播放器引用
|
|
|
-const musicPlayer = ref(null);
|
|
|
-// 音乐相关的处理函数
|
|
|
-const handleMusicEnded = () => {
|
|
|
- onMusicEnded(state);
|
|
|
-};
|
|
|
-
|
|
|
-// 专门的停止音乐处理函数
|
|
|
-const handleStopMusic = () => {
|
|
|
- // 直接调用导入的stopMusic函数并传递正确的参数
|
|
|
- stopMusic(state, musicPlayer);
|
|
|
- // 提示信息
|
|
|
- ElMessage.success('音乐已停止播放');
|
|
|
-};
|
|
|
-
|
|
|
-// 统一轮询管理器
|
|
|
-const pollingManager = {
|
|
|
- timers: {},
|
|
|
-
|
|
|
- // 启动轮询
|
|
|
- startPolling(type, callback, interval = 3000) {
|
|
|
- // 如果已有相同类型的轮询,先清除
|
|
|
- this.stopPolling(type);
|
|
|
-
|
|
|
- this.timers[type] = setInterval(async () => {
|
|
|
- try {
|
|
|
- await callback();
|
|
|
- } catch (error) {
|
|
|
- console.error(`${type}轮询失败:`, error);
|
|
|
- }
|
|
|
- }, interval);
|
|
|
-
|
|
|
- return this.timers[type];
|
|
|
- },
|
|
|
-
|
|
|
- // 停止轮询
|
|
|
- stopPolling(type) {
|
|
|
- if (this.timers[type]) {
|
|
|
- clearInterval(this.timers[type]);
|
|
|
- this.timers[type] = null;
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- // 停止所有轮询
|
|
|
- stopAll() {
|
|
|
- Object.keys(this.timers).forEach(type => this.stopPolling(type));
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-// 统一的错误处理包装器
|
|
|
-function withErrorHandling(operationName, fn, errorMessage = null) {
|
|
|
- return async function(...args) {
|
|
|
- try {
|
|
|
- state.isProcessing = true;
|
|
|
- return await fn.apply(this, args);
|
|
|
- } catch (error) {
|
|
|
- console.error(`${operationName}失败:`, error);
|
|
|
- ElMessage.error(errorMessage || `${operationName}发生错误: ${error.message || '未知错误'}`);
|
|
|
- return null;
|
|
|
- } finally {
|
|
|
- state.isProcessing = false;
|
|
|
- }
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-// 任务状态轮询公共函数
|
|
|
-async function pollTaskStatus(taskType, taskIds, fetchApi, onSuccess, onFailure) {
|
|
|
- if (taskIds.length === 0) {
|
|
|
- pollingManager.stopPolling(taskType);
|
|
|
- return {};
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- const list = await fetchApi(taskIds);
|
|
|
- const activeTasks = {};
|
|
|
-
|
|
|
- list.data.forEach((task) => {
|
|
|
- if (task.status === AiImageStatusEnum.IN_PROGRESS) {
|
|
|
- activeTasks[task.id] = task;
|
|
|
- } else if (task.status === AiImageStatusEnum.SUCCESS) {
|
|
|
- // 任务成功完成
|
|
|
- if (onSuccess) {
|
|
|
- onSuccess(task);
|
|
|
- }
|
|
|
- } else if (task.status === AiImageStatusEnum.FAIL) {
|
|
|
- // 任务失败
|
|
|
- if (onFailure) {
|
|
|
- onFailure(task);
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- return activeTasks;
|
|
|
- } catch (error) {
|
|
|
- console.error(`${taskType}状态轮询失败:`, error);
|
|
|
- return {};
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// AI服务模块 - 统一管理
|
|
|
-const aiService = {
|
|
|
- // 语音识别
|
|
|
- recognizeVoice: withErrorHandling('语音识别', async function(promptText = "", language = "zh-CN") {
|
|
|
- console.log("语音识别开始");
|
|
|
- // 前端语音采集
|
|
|
- const recognitionResult = await this.captureVoice(language, promptText);
|
|
|
- return recognitionResult || "";
|
|
|
- }, '语音识别失败'),
|
|
|
-
|
|
|
- // 前端语音采集
|
|
|
- captureVoice(language, promptText) {
|
|
|
- return new Promise((resolve) => {
|
|
|
- if (
|
|
|
- !"webkitSpeechRecognition" in window &&
|
|
|
- !"SpeechRecognition" in window
|
|
|
- ) {
|
|
|
- ElMessage.warning("您的浏览器不支持语音识别功能");
|
|
|
- resolve("");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
|
- const recognition = new SpeechRecognition();
|
|
|
-
|
|
|
- recognition.lang = language;
|
|
|
- recognition.interimResults = false;
|
|
|
- recognition.maxAlternatives = 1;
|
|
|
-
|
|
|
- let countdown = 10;
|
|
|
-
|
|
|
- // 固定的消息提示框
|
|
|
- const messageText = promptText ? `${promptText}\n请开始说话...(${countdown}秒)` : `请开始说话...(${countdown}秒)`;
|
|
|
- const messageInstance = ElMessage.info({
|
|
|
- message: messageText,
|
|
|
- duration: 0 // 0表示不自动关闭
|
|
|
- });
|
|
|
-
|
|
|
- // 隐藏的div来更新倒计时显示
|
|
|
- const timer = setInterval(() => {
|
|
|
- countdown--;
|
|
|
- if (countdown > 0) {
|
|
|
- // 找到消息提示框中的文本元素并更新
|
|
|
- const messageElement = document.querySelector('.el-message__content');
|
|
|
- if (messageElement) {
|
|
|
- const newText = promptText ? `${promptText}\n请开始说话...(${countdown}秒)` : `请开始说话...(${countdown}秒)`;
|
|
|
- messageElement.textContent = newText;
|
|
|
- }
|
|
|
- } else {
|
|
|
- clearInterval(timer);
|
|
|
- // 倒计时结束后关闭提示框
|
|
|
- if (messageInstance && messageInstance.close) {
|
|
|
- messageInstance.close();
|
|
|
- }
|
|
|
- }
|
|
|
- }, 1000);
|
|
|
-
|
|
|
-
|
|
|
- recognition.onresult = (event) => {
|
|
|
- const speechResult = event.results[0][0].transcript;
|
|
|
- console.log("语音识别结果:", speechResult);
|
|
|
- resolve(speechResult);
|
|
|
- };
|
|
|
-
|
|
|
- recognition.onerror = (event) => {
|
|
|
- console.error("语音识别错误:", event.error);
|
|
|
- ElMessage.error("语音识别发生错误: " + event.error);
|
|
|
- resolve("");
|
|
|
- };
|
|
|
-
|
|
|
- // recognition.onend = () => {
|
|
|
- // console.log("语音识别已结束");
|
|
|
- // };
|
|
|
-
|
|
|
- recognition.start();
|
|
|
- });
|
|
|
- },
|
|
|
-
|
|
|
- // 文本生成图片
|
|
|
- textToImage: withErrorHandling('AI图片生成', async function(prompt, waitForCompletion = true) {
|
|
|
- console.log("AI图片生成中,提示词:", prompt);
|
|
|
-
|
|
|
- //获取文生图-模型id
|
|
|
- const modelRes = await getModelIdByType({
|
|
|
- type: ModelTypeEnum.TEXT_TO_IMAGE,
|
|
|
- platform: ModelPlatformEnum.DOUBAO,
|
|
|
- });
|
|
|
- if (!modelRes.data) {
|
|
|
- ElMessage.error("获取模型ID失败");
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- // 使用CreatePainting API创建图片任务
|
|
|
- const createRes = await CreatePainting({
|
|
|
- modelId: modelRes.data,
|
|
|
- prompt: prompt,
|
|
|
- width: 1024,
|
|
|
- height: 1024,
|
|
|
- });
|
|
|
-
|
|
|
- // 记录任务ID到映射中
|
|
|
- state.inProgressImageMap[createRes.data] = {
|
|
|
- id: createRes.data,
|
|
|
- status: AiImageStatusEnum.IN_PROGRESS,
|
|
|
- };
|
|
|
-
|
|
|
- // 开始轮询任务状态
|
|
|
- this.startPollingTasks('image');
|
|
|
-
|
|
|
- // 如果需要等待完成,等待图片生成完成
|
|
|
- if (waitForCompletion) {
|
|
|
- console.log("AI图片生成中,请等待。。。:");
|
|
|
- return await this.waitForImageCompletion(createRes.data);
|
|
|
- }
|
|
|
-
|
|
|
- return createRes.data; // 返回任务ID
|
|
|
- }, '生成图片失败'),
|
|
|
-
|
|
|
- // 【文生图】等待图片生成完成
|
|
|
- waitForImageCompletion(imageId) {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const checkInterval = setInterval(async () => {
|
|
|
- try {
|
|
|
- const list = await PaintingGetMys([imageId]);
|
|
|
- if (list.data && list.data.length > 0) {
|
|
|
- const image = list.data[0];
|
|
|
- if (image.status === AiImageStatusEnum.SUCCESS) {
|
|
|
- clearInterval(checkInterval);
|
|
|
- resolve(image.picUrl);
|
|
|
- } else if (image.status === AiImageStatusEnum.FAIL) {
|
|
|
- clearInterval(checkInterval);
|
|
|
- reject(new Error(image.error || "图片生成失败"));
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- clearInterval(checkInterval);
|
|
|
- reject(error);
|
|
|
- }
|
|
|
- }, 3000);
|
|
|
- });
|
|
|
- },
|
|
|
-
|
|
|
- // 文本生成视频
|
|
|
- textToVideo: withErrorHandling('AI视频生成', async function(prompt, waitForCompletion = true) {
|
|
|
- console.log("AI视频生成中,提示词:", prompt);
|
|
|
-
|
|
|
- //获取视频生成模型id
|
|
|
- const modelRes = await getModelIdByType({
|
|
|
- type: ModelTypeEnum.IMAGE_TO_VIDEO,
|
|
|
- platform: ModelPlatformEnum.DOUBAO,
|
|
|
- });
|
|
|
-
|
|
|
- if (!modelRes.data) {
|
|
|
- ElMessage.error("获取模型ID失败");
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- // 使用CreateVideo API创建视频任务
|
|
|
- const createRes = await CreateVideo({
|
|
|
- modelId: modelRes.data,
|
|
|
- prompt: prompt,
|
|
|
- duration: 4,
|
|
|
- resolution: "1080P",
|
|
|
- });
|
|
|
-
|
|
|
- // 记录任务ID
|
|
|
- state.inProgressVideoMap[createRes.data] = {
|
|
|
- id: createRes.data,
|
|
|
- status: AiImageStatusEnum.IN_PROGRESS,
|
|
|
- };
|
|
|
-
|
|
|
- console.log("AI视频生成中,请等待。。。");
|
|
|
- // 启动统一的轮询机制
|
|
|
- this.startPollingTasks('video');
|
|
|
-
|
|
|
- // 如果需要等待完成,使用Promise封装结果
|
|
|
- if (waitForCompletion) {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- // 设置一次性的状态检查
|
|
|
- const checkStatus = () => {
|
|
|
- const videoInfo = state.generatedContent.videoUrl;
|
|
|
- if (videoInfo && videoInfo.includes(createRes.data)) {
|
|
|
- resolve(videoInfo);
|
|
|
- } else if (state.inProgressVideoMap[createRes.data]?.status === AiImageStatusEnum.FAIL) {
|
|
|
- reject(new Error("视频生成失败"));
|
|
|
- } else if (!state.inProgressVideoMap[createRes.data]) {
|
|
|
- reject(new Error("视频任务已不存在"));
|
|
|
- } else {
|
|
|
- // 继续检查
|
|
|
- setTimeout(checkStatus, 1000);
|
|
|
- }
|
|
|
- };
|
|
|
- checkStatus();
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- return createRes.data; // 返回任务ID
|
|
|
- }, '生成视频失败'),
|
|
|
-
|
|
|
- // 文本生成文本(如AI对话)
|
|
|
- textToText: withErrorHandling('AI大模型调用', async function(prompt, model = "default") {
|
|
|
- console.log("AI智能体请求,输入文本:", prompt);
|
|
|
-
|
|
|
- // 如果没有活跃的对话ID,创建新对话
|
|
|
- if (!state.activeConversationId) {
|
|
|
- // 使用与TextToText.vue相同的方式创建对话
|
|
|
- const res = await CreateDialogue({ roleId: 75 });
|
|
|
- state.activeConversationId = res.data;
|
|
|
- console.log("AI智能体创建成功,请等待。。。");
|
|
|
- }
|
|
|
-
|
|
|
- // 创建AbortController实例
|
|
|
- state.conversationInAbortController = new AbortController();
|
|
|
-
|
|
|
- // 使用流式API发送消息
|
|
|
- let resultText = "";
|
|
|
- let isFirstChunk = true;
|
|
|
-
|
|
|
- await sendChatMessageStream(
|
|
|
- state.activeConversationId,
|
|
|
- prompt,
|
|
|
- null,
|
|
|
- state.conversationInAbortController,
|
|
|
- true, // 启用上下文
|
|
|
- async (res) => {
|
|
|
- try {
|
|
|
- const { code, data, msg } = JSON.parse(res.data);
|
|
|
- if (code !== 0) {
|
|
|
- console.log(`对话异常! ${msg}`);
|
|
|
- return;
|
|
|
- }
|
|
|
- // 根据事件类型处理
|
|
|
- if (data.eventType === "TEXT") {
|
|
|
- // 如果内容为空,就不处理
|
|
|
- if (data.receive?.content === "") {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 处理文本消息
|
|
|
- resultText += data.receive.content;
|
|
|
-
|
|
|
- // 首次返回时更新预览内容
|
|
|
- if (isFirstChunk) {
|
|
|
- isFirstChunk = false;
|
|
|
- // 设置预览内容
|
|
|
- state.generatedContent.text = resultText;
|
|
|
- state.previewType = "text";
|
|
|
- state.previewContent = resultText;
|
|
|
- if (!state.previewVisible) {
|
|
|
- state.previewVisible = true;
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 更新预览内容
|
|
|
- state.generatedContent.text = resultText;
|
|
|
- state.previewContent = resultText;
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- console.error("处理流式响应失败:", error);
|
|
|
- }
|
|
|
- },
|
|
|
- (error) => {
|
|
|
- console.log(`对话异常! ${error}`);
|
|
|
- this.stopTextToTextStream();
|
|
|
- throw error;
|
|
|
- },
|
|
|
- () => {
|
|
|
- // console.log(`结束对话!`);
|
|
|
- this.stopTextToTextStream();
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- // 确保最终结果被设置
|
|
|
- if (resultText) {
|
|
|
- console.log("AI大模型调用成功,返回结果:", resultText);
|
|
|
- state.generatedContent.text = resultText;
|
|
|
- state.previewType = "text";
|
|
|
- state.previewContent = resultText;
|
|
|
- if (!state.previewVisible) {
|
|
|
- state.previewVisible = true;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return resultText;
|
|
|
- }, 'AI大模型调用失败'),
|
|
|
-
|
|
|
- // 停止文本生成流
|
|
|
- stopTextToTextStream() {
|
|
|
- if (state.conversationInAbortController) {
|
|
|
- state.conversationInAbortController.abort();
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- // 设置台灯亮度
|
|
|
- setLampBrightness: withErrorHandling('设置台灯亮度', async function(brightness) {
|
|
|
- // 验证亮度值在0-100之间
|
|
|
- const validBrightness = Math.max(0, Math.min(100, parseInt(brightness) || 0));
|
|
|
-
|
|
|
- // 更新状态
|
|
|
- state.lamp.brightness = validBrightness;
|
|
|
-
|
|
|
- // 模拟API调用(实际项目中可替换为真实API)
|
|
|
- console.log(`智能台灯亮度已设置为: ${validBrightness}%`);
|
|
|
-
|
|
|
- return validBrightness;
|
|
|
- }, '设置台灯亮度失败'),
|
|
|
-
|
|
|
- // 设置台灯颜色
|
|
|
- setLampColor: withErrorHandling('设置台灯颜色', async function(color) {
|
|
|
- // 预定义的颜色映射
|
|
|
- const colorMap = {
|
|
|
- '紫': '#D886F0',
|
|
|
- '橙': '#F89E35',
|
|
|
- '黄': '#F9E67E',
|
|
|
- '青': '#6BF5E6',
|
|
|
- '白': '#ffffff',
|
|
|
- };
|
|
|
-
|
|
|
- // 获取有效的颜色值
|
|
|
- let validColor = colorMap[color] || color;
|
|
|
-
|
|
|
- // 检查是否是有效的颜色格式
|
|
|
- if (!/^#[0-9A-F]{6}$/i.test(validColor)) {
|
|
|
- validColor = "#ffffff"; // 默认白色
|
|
|
- }
|
|
|
-
|
|
|
- // 更新状态
|
|
|
- state.lamp.color = validColor;
|
|
|
- state.lamp.colorLog = color;
|
|
|
-
|
|
|
- // 模拟API调用(实际项目中可替换为真实API)
|
|
|
- console.log(`智能台灯颜色已设置为: ${color}`);
|
|
|
-
|
|
|
- return validColor;
|
|
|
- }, '设置台灯颜色失败'),
|
|
|
-
|
|
|
- // 音乐播放相关方法
|
|
|
- playMusic: withErrorHandling('播放音乐', async function(musicType) {
|
|
|
- return playMusic(musicType, state, musicPlayer);
|
|
|
- }, '播放音乐失败'),
|
|
|
-
|
|
|
- stopMusic: withErrorHandling('停止音乐', async function() {
|
|
|
- return stopMusic(state, musicPlayer);
|
|
|
- }, '停止音乐失败'),
|
|
|
-
|
|
|
- // 综合控制台灯(参数格式:"颜色, 亮度, 音乐")
|
|
|
- controlLampWithSingleParam: withErrorHandling('智能台灯综合控制', async function(params) {
|
|
|
- // 解析参数字符串
|
|
|
- let color = '白'; // 默认颜色
|
|
|
- let brightness = 0; // 默认亮度
|
|
|
- let music = ''; // 音乐信息
|
|
|
-
|
|
|
- if (params && typeof params === 'string') {
|
|
|
- // 根据逗号分割参数
|
|
|
- const paramArray = params.split(',').map(p => p.trim());
|
|
|
-
|
|
|
- // 提取颜色(第一个参数)
|
|
|
- if (paramArray.length > 0 && paramArray[0]) {
|
|
|
- color = paramArray[0];
|
|
|
- }
|
|
|
-
|
|
|
- // 提取亮度(第二个参数)
|
|
|
- if (paramArray.length > 1 && paramArray[1]) {
|
|
|
- brightness = paramArray[1];
|
|
|
- }
|
|
|
-
|
|
|
- // 提取音乐(第三个参数)
|
|
|
- if (paramArray.length > 2 && paramArray[2]) {
|
|
|
- music = paramArray[2];
|
|
|
-
|
|
|
- // 调用音乐播放函数
|
|
|
- await this.playMusic(music);
|
|
|
- }
|
|
|
- }
|
|
|
- // 调用控制台灯方法
|
|
|
- return await this.controlLamp(brightness, color);
|
|
|
- }, '智能台灯综合控制失败'),
|
|
|
-
|
|
|
- // 综合控制台灯(参数格式:"颜色, 亮度")
|
|
|
- controlLamp: withErrorHandling('智能台灯控制', async function(brightness, color) {
|
|
|
- // 先设置亮度
|
|
|
- await this.setLampBrightness(brightness);
|
|
|
-
|
|
|
- // 再设置颜色
|
|
|
- await this.setLampColor(color);
|
|
|
-
|
|
|
- return { brightness: state.lamp.brightness, color: state.lamp.color };
|
|
|
- }, '智能台灯控制失败'),
|
|
|
-
|
|
|
- // 启动任务轮询
|
|
|
- startPollingTasks(type) {
|
|
|
- if (type === 'image' || type === 'all') {
|
|
|
- pollingManager.startPolling('image', async () => {
|
|
|
- const imageIds = Object.keys(state.inProgressImageMap).map(Number);
|
|
|
- state.inProgressImageMap = await pollTaskStatus(
|
|
|
- 'image',
|
|
|
- imageIds,
|
|
|
- PaintingGetMys,
|
|
|
- (image) => {
|
|
|
- state.generatedContent.imageUrl = image.picUrl;
|
|
|
- state.previewType = "image";
|
|
|
- state.previewContent = image.picUrl;
|
|
|
- state.previewVisible = true;
|
|
|
- console.log("AI图片生成完成:", image.picUrl);
|
|
|
- },
|
|
|
- (image) => {
|
|
|
- ElMessage.error("图片生成失败: " + (image.error || "未知错误"));
|
|
|
- console.error("图片生成失败:", image.id, image.error);
|
|
|
- }
|
|
|
- );
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- if (type === 'video' || type === 'all') {
|
|
|
- pollingManager.startPolling('video', async () => {
|
|
|
- const videoIds = Object.keys(state.inProgressVideoMap).map(Number);
|
|
|
- state.inProgressVideoMap = await pollTaskStatus(
|
|
|
- 'video',
|
|
|
- videoIds,
|
|
|
- VideoGetMys,
|
|
|
- (video) => {
|
|
|
- state.generatedContent.videoUrl = video.videoUrl;
|
|
|
- state.previewType = "video";
|
|
|
- state.previewContent = video.videoUrl;
|
|
|
- state.previewVisible = true;
|
|
|
- console.log("AI视频生成完成:", video.videoUrl);
|
|
|
- },
|
|
|
- (video) => {
|
|
|
- ElMessage.error("视频生成失败: " + (video.error || "未知错误"));
|
|
|
- console.error("视频生成失败:", video.id, video.error);
|
|
|
- }
|
|
|
- );
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-// 初始化Blockly工作区和自定义积木
|
|
|
-onMounted(async () => {
|
|
|
- // 从全局状态初始化年级ID
|
|
|
- state.gradeId = globalState.initGradeId();
|
|
|
-
|
|
|
- // 注册AI语音输入积木
|
|
|
- Blockly.Blocks["ai_voice_input"] = {
|
|
|
- init: function () {
|
|
|
- this.appendDummyInput().appendField("语音输入");
|
|
|
- this.appendValueInput("PROMPT")
|
|
|
- .setCheck("String")
|
|
|
- .appendField("提示文字:");
|
|
|
- this.appendDummyInput()
|
|
|
- .appendField("语言:")
|
|
|
- .appendField(
|
|
|
- new Blockly.FieldDropdown([
|
|
|
- ["中文", "zh-CN"],
|
|
|
- ["英文", "en-US"],
|
|
|
- ]),
|
|
|
- "LANGUAGE"
|
|
|
- );
|
|
|
- this.setOutput(true, "String");
|
|
|
- this.setColour(310);
|
|
|
- this.setTooltip("使用语音识别获取文本输入");
|
|
|
- this.setHelpUrl("");
|
|
|
- },
|
|
|
- };
|
|
|
-
|
|
|
- // 注册AI文本生成图片积木
|
|
|
- Blockly.Blocks["ai_text_to_image"] = {
|
|
|
- init: function () {
|
|
|
- this.appendDummyInput().appendField("AI生成图片");
|
|
|
- this.appendValueInput("PROMPT").setCheck("String").appendField("提示词:");
|
|
|
- this.appendDummyInput()
|
|
|
- .appendField("等待完成:", "WAIT_LABEL")
|
|
|
- .appendField(new Blockly.FieldCheckbox("TRUE"), "WAIT_FOR_COMPLETION");
|
|
|
- this.setInputsInline(false);
|
|
|
- this.setPreviousStatement(true, null);
|
|
|
- this.setNextStatement(true, null);
|
|
|
- this.setColour(340);
|
|
|
- this.setTooltip("使用AI将文本描述转换为图片");
|
|
|
- this.setHelpUrl("");
|
|
|
- },
|
|
|
- };
|
|
|
-
|
|
|
- // 注册AI文本生成视频积木
|
|
|
- Blockly.Blocks["ai_text_to_video"] = {
|
|
|
- init: function () {
|
|
|
- this.appendDummyInput().appendField("AI生成视频");
|
|
|
- this.appendValueInput("PROMPT").setCheck("String").appendField("提示词:");
|
|
|
- this.appendDummyInput()
|
|
|
- .appendField("等待完成:", "WAIT_LABEL")
|
|
|
- .appendField(new Blockly.FieldCheckbox("TRUE"), "WAIT_FOR_COMPLETION");
|
|
|
- this.setInputsInline(false);
|
|
|
- this.setPreviousStatement(true, null);
|
|
|
- this.setNextStatement(true, null);
|
|
|
- this.setColour(340);
|
|
|
- this.setTooltip("使用AI将文本描述转换为视频");
|
|
|
- this.setHelpUrl("");
|
|
|
- },
|
|
|
- };
|
|
|
-
|
|
|
- // 注册AI文本生成文本积木
|
|
|
- Blockly.Blocks["ai_text_to_text"] = {
|
|
|
- init: function () {
|
|
|
- this.appendDummyInput().appendField("AI大模型调用");
|
|
|
- this.appendValueInput("PROMPT")
|
|
|
- .setCheck("String")
|
|
|
- .appendField("输入文本:");
|
|
|
- this.appendValueInput("提示词")
|
|
|
- .setCheck("String")
|
|
|
- .appendField("提示词:");
|
|
|
- this.setOutput(true, "String");
|
|
|
- this.setColour(300);
|
|
|
- this.setTooltip("使用AI大模型调用并返回结果");
|
|
|
- this.setHelpUrl("");
|
|
|
- },
|
|
|
- };
|
|
|
-
|
|
|
- //AI智能台灯单参数积木
|
|
|
- Blockly.Blocks['ai_smart_lamp_single_param'] = {
|
|
|
- init: function() {
|
|
|
- this.appendDummyInput()
|
|
|
- .appendField('智能台灯控制(单参数)');
|
|
|
- this.appendValueInput('PARAMS')
|
|
|
- .setCheck('String')
|
|
|
- .appendField('参数(格式: 颜色,亮度,音乐):');
|
|
|
- this.appendDummyInput()
|
|
|
- .appendField('例如: 蓝,50,平静');
|
|
|
- this.setInputsInline(false);
|
|
|
- this.setPreviousStatement(true, null);
|
|
|
- this.setNextStatement(true, null);
|
|
|
- this.setColour(280);
|
|
|
- this.setTooltip('通过一个参数字符串控制智能台灯的亮度、颜色和音乐\n格式: 颜色,亮度,音乐\n例如: 蓝,50,平静');
|
|
|
- this.setHelpUrl('');
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 注册AI智能台灯积木
|
|
|
- Blockly.Blocks["ai_smart_lamp"] = {
|
|
|
- init: function () {
|
|
|
- this.appendDummyInput().appendField("智能台灯控制");
|
|
|
- this.appendValueInput("BRIGHTNESS")
|
|
|
- .setCheck(["Number", "String"])
|
|
|
- .appendField("亮度 (0-100):");
|
|
|
- this.appendValueInput("COLOR").setCheck("String").appendField("颜色:");
|
|
|
- this.setInputsInline(false);
|
|
|
- this.setPreviousStatement(true, null);
|
|
|
- this.setNextStatement(true, null);
|
|
|
- this.setColour(280);
|
|
|
- this.setTooltip("控制智能台灯的亮度和颜色");
|
|
|
- this.setHelpUrl("");
|
|
|
- },
|
|
|
- };
|
|
|
-
|
|
|
- // 注册AI台灯设置亮度积木
|
|
|
- Blockly.Blocks["ai_lamp_set_brightness"] = {
|
|
|
- init: function () {
|
|
|
- this.appendDummyInput().appendField("设置台灯亮度");
|
|
|
- this.appendValueInput("BRIGHTNESS")
|
|
|
- .setCheck(["Number", "String"])
|
|
|
- .appendField("亮度 (0-100):");
|
|
|
- this.setInputsInline(false);
|
|
|
- this.setPreviousStatement(true, null);
|
|
|
- this.setNextStatement(true, null);
|
|
|
- this.setColour(270);
|
|
|
- this.setTooltip("设置智能台灯的亮度");
|
|
|
- this.setHelpUrl("");
|
|
|
- },
|
|
|
- };
|
|
|
-
|
|
|
- // 注册AI台灯设置颜色积木
|
|
|
- Blockly.Blocks["ai_lamp_set_color"] = {
|
|
|
- init: function () {
|
|
|
- this.appendDummyInput().appendField("设置台灯颜色");
|
|
|
- this.appendValueInput("COLOR").setCheck("String").appendField("颜色:");
|
|
|
- this.appendDummyInput().appendField("可选颜色: 红,蓝,绿,黄,青,靛,紫");
|
|
|
- this.setInputsInline(false);
|
|
|
- this.setPreviousStatement(true, null);
|
|
|
- this.setNextStatement(true, null);
|
|
|
- this.setColour(275);
|
|
|
- this.setTooltip("设置智能台灯的颜色");
|
|
|
- this.setHelpUrl("");
|
|
|
- },
|
|
|
- };
|
|
|
-
|
|
|
- // 注册音乐播放积木
|
|
|
- Blockly.Blocks['ai_music_play'] = {
|
|
|
- init: function() {
|
|
|
- this.appendDummyInput()
|
|
|
- .appendField('播放音乐');
|
|
|
- this.appendDummyInput()
|
|
|
- .appendField('音乐类型:')
|
|
|
- .appendField(
|
|
|
- new Blockly.FieldDropdown([
|
|
|
- ['热闹', '热闹'],
|
|
|
- ['舒缓', '舒缓'],
|
|
|
- ]),
|
|
|
- 'MUSIC_TYPE'
|
|
|
- );
|
|
|
- this.setInputsInline(false);
|
|
|
- this.setPreviousStatement(true, null);
|
|
|
- this.setNextStatement(true, null);
|
|
|
- this.setColour(290);
|
|
|
- this.setTooltip('播放指定类型的音乐');
|
|
|
- this.setHelpUrl('');
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 注册JavaScript代码生成器
|
|
|
- registerJavaScriptGenerators();
|
|
|
-
|
|
|
- // 注册Python代码生成器
|
|
|
- registerPythonGenerators();
|
|
|
-
|
|
|
- // 初始化工作区 - 简化且标准化的实现
|
|
|
- try {
|
|
|
- // 使用标准方式初始化Blockly工作区,参考BlocklyEditor_2.vue的配置
|
|
|
- const blocklyDiv = document.getElementById('blocklyDiv');
|
|
|
- const toolbox = document.getElementById('toolbox');
|
|
|
-
|
|
|
- state.workspace = Blockly.inject(blocklyDiv, {
|
|
|
- toolbox: toolbox,
|
|
|
- collapse: true,
|
|
|
- comments: true,
|
|
|
- disable: false, // 设为false以允许编辑
|
|
|
- maxBlocks: Infinity,
|
|
|
- trashcan: true,
|
|
|
- horizontalLayout: false,
|
|
|
- toolboxPosition: 'start',
|
|
|
- css: true,
|
|
|
- media: 'https://unpkg.com/blockly/media/',
|
|
|
- rtl: false,
|
|
|
- scrollbars: true,
|
|
|
- sounds: false, // 禁用声音以提高性能
|
|
|
- oneBasedIndex: true,
|
|
|
- grid: {
|
|
|
- spacing: 20,
|
|
|
- length: 3,
|
|
|
- colour: "#ccc",
|
|
|
- snap: true
|
|
|
- },
|
|
|
- zoom: {
|
|
|
- controls: true,
|
|
|
- wheel: true,
|
|
|
- startScale: 1.0,
|
|
|
- maxScale: 3,
|
|
|
- minScale: 0.3,
|
|
|
- scaleSpeed: 1.2
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 使用合并的监听器函数
|
|
|
- addCombinedChangeListener();
|
|
|
-
|
|
|
- // 使用当前Blockly版本支持的标准方法加载初始内容
|
|
|
- /*const initialXml = "<xml xmlns=\"https://developers.google.com/blockly/xml\">\n" +
|
|
|
- " <variables>\n" +
|
|
|
- " <variable id=\"kAVG*zJLw/q)l/(/eIMM\">inputText</variable>\n" +
|
|
|
- " <variable id=\"[7F(niLz{.fvWvY.VT/N\">lampConfig</variable>\n" +
|
|
|
- " </variables>\n" +
|
|
|
- " <block type=\"variables_set\" id=\"XIsa=sV/8ImVP5P3:ol4\" x=\"90\" y=\"90\">\n" +
|
|
|
- " <field name=\"VAR\" id=\"kAVG*zJLw/q)l/(/eIMM\">inputText</field>\n" +
|
|
|
- " <value name=\"VALUE\">\n" +
|
|
|
- " <block type=\"ai_voice_input\" id=\"Ql).^+/uT(P~$aMg2OEA\">\n" +
|
|
|
- " <field name=\"LANGUAGE\">zh-CN</field>\n" +
|
|
|
- " <value name=\"PROMPT\">\n" +
|
|
|
- " <block type=\"text\" id=\"kXjnnUncUu7p$a[zz)da\">\n" +
|
|
|
- " <field name=\"TEXT\">请说话:</field>\n" +
|
|
|
- " </block>\n" +
|
|
|
- " </value>\n" +
|
|
|
- " </block>\n" +
|
|
|
- " </value>\n" +
|
|
|
- " <next>\n" +
|
|
|
- " <block type=\"variables_set\" id=\"AP0CO$VPeM0*PS*CKT,V\">\n" +
|
|
|
- " <field name=\"VAR\" id=\"[7F(niLz{.fvWvY.VT/N\">lampConfig</field>\n" +
|
|
|
- " <value name=\"VALUE\">\n" +
|
|
|
- " <block type=\"ai_text_to_text\" id=\"XYG.cN_=CX5:Vd_)wdZ7\" x=\"190\" y=\"250\">\n" +
|
|
|
- " <value name=\"PROMPT\">\n" +
|
|
|
- " <block type=\"variables_get\" id=\"(3Sd)lTX],5xho{p_=s!\">\n" +
|
|
|
- " <field name=\"VAR\" id=\"8(,gJ5{2y*8olrD{^G.P\">inputText</field>\n" +
|
|
|
- " </block>\n" +
|
|
|
- " </value>\n" +
|
|
|
- " <value name=\"提示词\">\n" +
|
|
|
- " <block type=\"text\" id=\"ubD*WPRnwBC|BxGraKX3\">\n" +
|
|
|
- " <field name=\"TEXT\">请只回复我指定格式:白,100,热闹</field>\n" +
|
|
|
- " </block>\n" +
|
|
|
- " </value>\n" +
|
|
|
- " </block>\n" +
|
|
|
- " </value>\n" +
|
|
|
- " <next>\n" +
|
|
|
- " <block type=\"ai_smart_lamp_single_param\" id=\"ZTAwt~MMm(t5w:$$Azt`\">\n" +
|
|
|
- " <value name=\"PARAMS\">\n" +
|
|
|
- " <block type=\"variables_get\" id=\"X):w|X4hYE~ru1r9`W8f\">\n" +
|
|
|
- " <field name=\"VAR\" id=\"[7F(niLz{.fvWvY.VT/N\">lampConfig</field>\n" +
|
|
|
- " </block>\n" +
|
|
|
- " </value>\n" +
|
|
|
- " </block>\n" +
|
|
|
- " </next>\n" +
|
|
|
- " </block>\n" +
|
|
|
- " </next>\n" +
|
|
|
- " </block>\n" +
|
|
|
- " </xml>";
|
|
|
-
|
|
|
-
|
|
|
- // const initialXml = "<xml xmlns=\"https://developers.google.com/blockly/xml\">\n" +
|
|
|
- // " </xml>";
|
|
|
-
|
|
|
- try {
|
|
|
- // 尝试使用现代API加载工作区
|
|
|
- const parsedXml = Blockly.utils.xml.textToDom(initialXml);
|
|
|
- Blockly.Xml.domToWorkspace(parsedXml, state.workspace);
|
|
|
- console.log('XML加载成功');
|
|
|
- } catch (xmlError) {
|
|
|
- console.warn('XML加载失败,尝试清空工作区:', xmlError);
|
|
|
- // 如果XML加载失败,清空工作区
|
|
|
- state.workspace.clear();
|
|
|
- }*/
|
|
|
-
|
|
|
- // 添加拖拽修复
|
|
|
- if (state.workspace) {
|
|
|
- // 确保所有块都可以正确拖拽
|
|
|
- state.workspace.addChangeListener((event) => {
|
|
|
- if (event.type === Blockly.Events.BLOCK_CREATE) {
|
|
|
- // 对于新创建的块,确保它们是可拖拽的
|
|
|
- const block = state.workspace.getBlockById(event.blockId);
|
|
|
- if (block && block.editable) {
|
|
|
- // 确保块是可编辑的
|
|
|
- block.setEditable(true);
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 初始化完成后手动生成一次代码
|
|
|
- generateCode("javascript");
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('初始化Blockly工作区失败:', error);
|
|
|
- ElMessage.error('Blockly初始化失败,请刷新页面重试');
|
|
|
- }
|
|
|
-
|
|
|
- // 修改工作区变化监听器,使其包含拖拽修复逻辑
|
|
|
- state.workspace.addChangeListener((event) => {
|
|
|
- // 生成代码
|
|
|
- generateCode("javascript");
|
|
|
-
|
|
|
- // 拖拽修复逻辑
|
|
|
- if (event.type === Blockly.Events.BLOCK_CREATE) {
|
|
|
- const block = state.workspace.getBlockById(event.blockId);
|
|
|
- if (block) {
|
|
|
- block.setEditable(true);
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 加载初始JSON数据
|
|
|
- // workspace = state.workspace;
|
|
|
- // loadWorkspaceFromJson();
|
|
|
-
|
|
|
- // 添加工作区变化监听器
|
|
|
- // state.workspace.addChangeListener(() => generateCode("javascript"));
|
|
|
-});
|
|
|
-
|
|
|
-// 组件卸载时清除所有资源
|
|
|
-onUnmounted(() => {
|
|
|
- // 统一的轮询管理器停止所有轮询
|
|
|
- pollingManager.stopAll();
|
|
|
-
|
|
|
- // 停止音乐播放
|
|
|
- aiService.stopMusic();
|
|
|
-
|
|
|
- // 停止文本生成流
|
|
|
- aiService.stopTextToTextStream();
|
|
|
-
|
|
|
- // 释放工作区资源
|
|
|
- if (state.workspace) {
|
|
|
- state.workspace.dispose();
|
|
|
- }
|
|
|
-});
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-// 响应式变量
|
|
|
-const jsonData = ref(`{
|
|
|
- "blocks": {
|
|
|
- "languageVersion": 0,
|
|
|
- "blocks": [
|
|
|
- {
|
|
|
- "type": "text_print",
|
|
|
- "x": 100,
|
|
|
- "y": 100,
|
|
|
- "inputs": {
|
|
|
- "TEXT": {
|
|
|
- "block": {
|
|
|
- "type": "text",
|
|
|
- "fields": {
|
|
|
- "TEXT": "Hello, Blockly!"
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- {
|
|
|
- "type": "controls_if",
|
|
|
- "x": 100,
|
|
|
- "y": 200,
|
|
|
- "inputs": {
|
|
|
- "IF0": {
|
|
|
- "block": {
|
|
|
- "type": "logic_compare",
|
|
|
- "fields": {
|
|
|
- "OP": "EQ"
|
|
|
- },
|
|
|
- "inputs": {
|
|
|
- "A": {
|
|
|
- "block": {
|
|
|
- "type": "math_number",
|
|
|
- "fields": {
|
|
|
- "NUM": 5
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- "B": {
|
|
|
- "block": {
|
|
|
- "type": "math_number",
|
|
|
- "fields": {
|
|
|
- "NUM": 5
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- "DO0": {
|
|
|
- "block": {
|
|
|
- "type": "text_print",
|
|
|
- "inputs": {
|
|
|
- "TEXT": {
|
|
|
- "block": {
|
|
|
- "type": "text",
|
|
|
- "fields": {
|
|
|
- "TEXT": "条件成立!"
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- ]
|
|
|
- }
|
|
|
-}`);
|
|
|
-const statusMessage = ref('');
|
|
|
-const statusType = ref('');
|
|
|
-// let workspace = null;
|
|
|
-
|
|
|
-// 从JSON加载工作区 - 改进版
|
|
|
-const loadWorkspaceFromJson = () => {
|
|
|
- try {
|
|
|
- const json = JSON.parse(jsonData.value);
|
|
|
-
|
|
|
- // 1. 确保工作区存在
|
|
|
- if (!state.workspace) {
|
|
|
- showStatus('工作区未初始化', 'error');
|
|
|
- console.error('工作区未初始化');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- console.log('开始加载JSON数据:', JSON.stringify(json).substring(0, 200) + '...');
|
|
|
-
|
|
|
- // 无论使用哪种策略加载成功后,确保所有块可拖拽
|
|
|
- const ensureBlocksDraggable = () => {
|
|
|
- const allBlocks = state.workspace.getAllBlocks();
|
|
|
- allBlocks.forEach(block => {
|
|
|
- // 强制设置为可编辑状态,不做任何条件检查
|
|
|
- block.setEditable(true);
|
|
|
- // 添加调试日志,确认每个块都被设置为可编辑
|
|
|
- console.log(`设置块${block.id}为可编辑状态`);
|
|
|
- });
|
|
|
- return allBlocks.length;
|
|
|
- };
|
|
|
-
|
|
|
- // 2. 尝试多种加载策略
|
|
|
- try {
|
|
|
- // 策略1: 先清空工作区再加载
|
|
|
- state.workspace.clear();
|
|
|
- Blockly.serialization.workspaces.load(json, state.workspace);
|
|
|
- const blockCount = ensureBlocksDraggable(); // 调用ensureBlocksDraggable确保所有块可拖拽
|
|
|
- showStatus(`工作区已成功从JSON加载!共加载${blockCount}个积木。`, 'success');
|
|
|
- console.log(`策略1成功: 加载了${blockCount}个积木`);
|
|
|
- return;
|
|
|
- } catch (error1) {
|
|
|
- console.warn('策略1失败,尝试策略2:', error1);
|
|
|
-
|
|
|
- try {
|
|
|
- // 策略2: 清理JSON后再加载
|
|
|
- state.workspace.clear();
|
|
|
- const cleanedJson = deepCleanJsonForConnectionIssues(json);
|
|
|
- Blockly.serialization.workspaces.load(cleanedJson, state.workspace);
|
|
|
- const blockCount = ensureBlocksDraggable(); // 调用ensureBlocksDraggable确保所有块可拖拽
|
|
|
- showStatus(`工作区已通过清理后的JSON成功加载!共加载${blockCount}个积木。`, 'success');
|
|
|
- console.log(`策略2成功: 加载了${blockCount}个积木`);
|
|
|
- return;
|
|
|
- } catch (error2) {
|
|
|
- console.warn('策略2失败,尝试策略3:', error2);
|
|
|
-
|
|
|
- try {
|
|
|
- // 策略3: 直接使用XML序列化API作为中间格式
|
|
|
- state.workspace.clear();
|
|
|
-
|
|
|
- // 先将JSON转换为XML字符串
|
|
|
- const xmlText = jsonToXml(json);
|
|
|
- console.log('生成的XML:', xmlText.substring(0, 200) + '...');
|
|
|
-
|
|
|
- // 加载XML到工作区
|
|
|
- const xmlDom = Blockly.utils.xml.textToDom(xmlText);
|
|
|
- Blockly.Xml.domToWorkspace(xmlDom, state.workspace);
|
|
|
-
|
|
|
- const blockCount = ensureBlocksDraggable(); // 调用ensureBlocksDraggable确保所有块可拖拽
|
|
|
- showStatus(`工作区已通过XML中间格式成功加载!共加载${blockCount}个积木。`, 'success');
|
|
|
- console.log(`策略3成功: 加载了${blockCount}个积木`);
|
|
|
- return;
|
|
|
- } catch (error3) {
|
|
|
- console.warn('策略3失败,尝试策略4:', error3);
|
|
|
-
|
|
|
- try {
|
|
|
- // 策略4: 创建新的工作区实例并使用XML导入
|
|
|
- // 先释放旧工作区资源
|
|
|
- if (state.workspace) {
|
|
|
- state.workspace.dispose();
|
|
|
- }
|
|
|
-
|
|
|
- // 创建新的工作区
|
|
|
- const blocklyDiv = document.getElementById('blocklyDiv');
|
|
|
- const toolbox = document.getElementById('toolbox');
|
|
|
-
|
|
|
- state.workspace = Blockly.inject(blocklyDiv, {
|
|
|
- toolbox: toolbox,
|
|
|
- collapse: true,
|
|
|
- comments: true,
|
|
|
- disable: false,
|
|
|
- maxBlocks: Infinity,
|
|
|
- trashcan: true,
|
|
|
- horizontalLayout: false,
|
|
|
- toolboxPosition: 'start',
|
|
|
- css: true,
|
|
|
- media: 'https://unpkg.com/blockly/media/',
|
|
|
- rtl: false,
|
|
|
- scrollbars: true,
|
|
|
- sounds: false,
|
|
|
- oneBasedIndex: true,
|
|
|
- grid: {
|
|
|
- spacing: 20,
|
|
|
- length: 3,
|
|
|
- colour: "#ccc",
|
|
|
- snap: true
|
|
|
- },
|
|
|
- zoom: {
|
|
|
- controls: true,
|
|
|
- wheel: true,
|
|
|
- startScale: 1.0,
|
|
|
- maxScale: 3,
|
|
|
- minScale: 0.3,
|
|
|
- scaleSpeed: 1.2
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 重新添加工作区变化监听器 - 使用合并后的监听器
|
|
|
- addCombinedChangeListener();
|
|
|
-
|
|
|
- // 使用XML作为中间格式导入
|
|
|
- const xmlText = jsonToXml(json);
|
|
|
- console.log('创建新工作区,生成的XML:', xmlText.substring(0, 200) + '...');
|
|
|
- const xmlDom = Blockly.utils.xml.textToDom(xmlText);
|
|
|
- Blockly.Xml.domToWorkspace(xmlDom, state.workspace);
|
|
|
-
|
|
|
- const blockCount = ensureBlocksDraggable(); // 调用ensureBlocksDraggable确保所有块可拖拽
|
|
|
- showStatus(`工作区已通过创建新实例并使用XML格式成功加载!共加载${blockCount}个积木。`, 'success');
|
|
|
- console.log(`策略4成功: 加载了${blockCount}个积木`);
|
|
|
- return;
|
|
|
- } catch (error4) {
|
|
|
- // 所有策略都失败
|
|
|
- showStatus('JSON解析错误: ' + error4.message, 'error');
|
|
|
- console.error('所有加载策略均失败:', error4);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- showStatus('JSON解析错误: ' + error.message, 'error');
|
|
|
- console.error('JSON解析错误:', error);
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-// 2. 创建一个合并的工作区变化监听器函数
|
|
|
-function addCombinedChangeListener() {
|
|
|
- if (!state.workspace) return;
|
|
|
-
|
|
|
- // 移除旧的监听器(如果有)
|
|
|
- state.workspace.removeAllChangeListeners();
|
|
|
-
|
|
|
- // 添加新的合并监听器
|
|
|
- state.workspace.addChangeListener((event) => {
|
|
|
- // 生成代码
|
|
|
- generateCode("javascript");
|
|
|
-
|
|
|
- // 拖拽修复逻辑
|
|
|
- if (event.type === Blockly.Events.BLOCK_CREATE || event.type === Blockly.Events.BLOCK_ADD)
|
|
|
- {
|
|
|
- const block = state.workspace.getBlockById(event.blockId);
|
|
|
- if (block) {
|
|
|
- // 强制设置为可编辑状态
|
|
|
- block.setEditable(true);
|
|
|
- console.log(`监听器设置块${block.id}为可编辑状态`);
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-// 导出工作区为JSON - 增强版
|
|
|
-const exportWorkspaceToJson = () => {
|
|
|
- try {
|
|
|
- if (!state.workspace) {
|
|
|
- showStatus('工作区未初始化', 'error');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 使用state.workspace替代workspace
|
|
|
- const stateJson = Blockly.serialization.workspaces.save(state.workspace);
|
|
|
- jsonData.value = JSON.stringify(stateJson, null, 2);
|
|
|
-
|
|
|
- // 显示导出的积木数量
|
|
|
- const blockCount = state.workspace.getAllBlocks().length;
|
|
|
- showStatus(`工作区已成功导出为JSON!共导出${blockCount}个积木。`, 'success');
|
|
|
- console.log(`导出JSON成功: 导出了${blockCount}个积木`);
|
|
|
- } catch (error) {
|
|
|
- showStatus('导出错误: ' + error.message, 'error');
|
|
|
- console.error('导出错误:', error);
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-// 深度清理JSON中可能导致连接问题的部分
|
|
|
-function deepCleanJsonForConnectionIssues(json) {
|
|
|
- const cleanedJson = JSON.parse(JSON.stringify(json));
|
|
|
-
|
|
|
- // 1. 清理blocks结构
|
|
|
- if (cleanedJson.blocks && cleanedJson.blocks.blocks) {
|
|
|
- cleanedJson.blocks.blocks.forEach(block => {
|
|
|
- // 2. 移除inputs中的connection引用
|
|
|
- if (block.inputs) {
|
|
|
- Object.keys(block.inputs).forEach(inputKey => {
|
|
|
- const input = block.inputs[inputKey];
|
|
|
- if (input.connection) {
|
|
|
- delete input.connection;
|
|
|
- }
|
|
|
-
|
|
|
- // 3. 递归清理嵌套的block
|
|
|
- if (input.block) {
|
|
|
- cleanBlockConnections(input.block);
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 4. 清理next和previous连接
|
|
|
- if (block.next) {
|
|
|
- if (block.next.connection) {
|
|
|
- delete block.next.connection;
|
|
|
- }
|
|
|
- if (block.next.block) {
|
|
|
- cleanBlockConnections(block.next.block);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (block.previous) {
|
|
|
- if (block.previous.connection) {
|
|
|
- delete block.previous.connection;
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- return cleanedJson;
|
|
|
-}
|
|
|
-
|
|
|
-// 递归清理单个block的连接信息
|
|
|
-function cleanBlockConnections(block) {
|
|
|
- if (!block) return;
|
|
|
-
|
|
|
- // 清理inputs中的连接
|
|
|
- if (block.inputs) {
|
|
|
- Object.keys(block.inputs).forEach(inputKey => {
|
|
|
- const input = block.inputs[inputKey];
|
|
|
- if (input.connection) {
|
|
|
- delete input.connection;
|
|
|
- }
|
|
|
- if (input.block) {
|
|
|
- cleanBlockConnections(input.block);
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 清理next和previous连接
|
|
|
- if (block.next) {
|
|
|
- if (block.next.connection) {
|
|
|
- delete block.next.connection;
|
|
|
- }
|
|
|
- if (block.next.block) {
|
|
|
- cleanBlockConnections(block.next.block);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (block.previous) {
|
|
|
- if (block.previous.connection) {
|
|
|
- delete block.previous.connection;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 将JSON转换为XML(增强版)
|
|
|
-function jsonToXml(json) {
|
|
|
- // 先检查JSON是否有效
|
|
|
- if (!json || typeof json !== 'object') {
|
|
|
- console.error('无效的JSON数据');
|
|
|
- return '<xml xmlns="https://developers.google.com/blockly/xml"></xml>';
|
|
|
- }
|
|
|
-
|
|
|
- let xml = '<xml xmlns="https://developers.google.com/blockly/xml">';
|
|
|
-
|
|
|
- // 1. 处理变量
|
|
|
- if (json.variables && json.variables.variables) {
|
|
|
- xml += '<variables>';
|
|
|
- json.variables.variables.forEach(variable => {
|
|
|
- xml += `<variable id="${escapeXml(variable.id)}">${escapeXml(variable.name)}</variable>`;
|
|
|
- });
|
|
|
- xml += '</variables>';
|
|
|
- }
|
|
|
-
|
|
|
- // 2. 处理积木块 - 这是关键部分
|
|
|
- if (json.blocks && json.blocks.blocks) {
|
|
|
- // 找出所有根节点积木(没有previous连接的积木)
|
|
|
- const rootBlocks = json.blocks.blocks.filter(block => !block.previous);
|
|
|
- console.log(`找到${rootBlocks.length}个根积木块`);
|
|
|
-
|
|
|
- // 递归生成每个根积木块的XML
|
|
|
- rootBlocks.forEach(block => {
|
|
|
- xml += generateBlockXml(block);
|
|
|
- });
|
|
|
- } else {
|
|
|
- console.warn('JSON中没有找到blocks数据');
|
|
|
- }
|
|
|
-
|
|
|
- xml += '</xml>';
|
|
|
- return xml;
|
|
|
-}
|
|
|
-
|
|
|
-// 递归生成单个积木块的XML
|
|
|
-function generateBlockXml(block) {
|
|
|
- if (!block) return '';
|
|
|
-
|
|
|
- let blockXml = `<block type="${escapeXml(block.type)}" id="${escapeXml(block.id)}" x="${block.x || 0}" y="${block.y || 0}">`;
|
|
|
-
|
|
|
- // 处理fields
|
|
|
- if (block.fields) {
|
|
|
- Object.keys(block.fields).forEach(fieldName => {
|
|
|
- const fieldValue = block.fields[fieldName];
|
|
|
- blockXml += `<field name="${escapeXml(fieldName)}">${escapeXml(String(fieldValue))}</field>`;
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 处理inputs
|
|
|
- if (block.inputs) {
|
|
|
- Object.keys(block.inputs).forEach(inputName => {
|
|
|
- const input = block.inputs[inputName];
|
|
|
- if (input.block) {
|
|
|
- blockXml += `<value name="${escapeXml(inputName)}">`;
|
|
|
- blockXml += generateBlockXml(input.block);
|
|
|
- blockXml += '</value>';
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 处理嵌套的next积木
|
|
|
- if (block.next && block.next.block) {
|
|
|
- blockXml += '<next>';
|
|
|
- blockXml += generateBlockXml(block.next.block);
|
|
|
- blockXml += '</next>';
|
|
|
- }
|
|
|
-
|
|
|
- blockXml += '</block>';
|
|
|
- return blockXml;
|
|
|
-}
|
|
|
-
|
|
|
-// XML转义函数
|
|
|
-function escapeXml(text) {
|
|
|
- if (typeof text !== 'string') {
|
|
|
- text = String(text);
|
|
|
- }
|
|
|
- return text
|
|
|
- .replace(/&/g, '&')
|
|
|
- .replace(/</g, '<')
|
|
|
- .replace(/>/g, '>')
|
|
|
- .replace(/"/g, '"')
|
|
|
- .replace(/'/g, ''');
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-// 显示状态消息
|
|
|
-const showStatus = (message, type) => {
|
|
|
- statusMessage.value = message;
|
|
|
- statusType.value = type;
|
|
|
-
|
|
|
- // 3秒后自动清除状态消息
|
|
|
- setTimeout(() => {
|
|
|
- statusMessage.value = '';
|
|
|
- }, 3000);
|
|
|
-};
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-// 注册JavaScript代码生成器
|
|
|
-function registerJavaScriptGenerators() {
|
|
|
- // 语音输入
|
|
|
- javascriptGenerator.forBlock['ai_voice_input'] = function(block, generator) {
|
|
|
- const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
|
|
|
- const language = block.getFieldValue('LANGUAGE');
|
|
|
- const code = `await aiService.recognizeVoice(${prompt || "''"}, '${language}')`;
|
|
|
- return [code, javascriptGenerator.ORDER_ATOMIC];
|
|
|
- };
|
|
|
-
|
|
|
- // 文本生成图片
|
|
|
- javascriptGenerator.forBlock['ai_text_to_image'] = function(block, generator) {
|
|
|
- const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
|
|
|
- const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
|
|
|
- const code = `await aiService.textToImage(${prompt}, ${waitForCompletion});`;
|
|
|
- return code;
|
|
|
- };
|
|
|
-
|
|
|
- // 文本生成视频
|
|
|
- javascriptGenerator.forBlock['ai_text_to_video'] = function(block, generator) {
|
|
|
- const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
|
|
|
- const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
|
|
|
- const code = `await aiService.textToVideo(${prompt}, ${waitForCompletion});`;
|
|
|
- return code;
|
|
|
- };
|
|
|
-
|
|
|
- // 文本生成文本
|
|
|
- javascriptGenerator.forBlock['ai_text_to_text'] = function(block, generator) {
|
|
|
- const prompt = generator.valueToCode(block, 'PROMPT', javascriptGenerator.ORDER_ATOMIC);
|
|
|
- const model = block.getFieldValue('MODEL');
|
|
|
- const code = `await aiService.textToText(${prompt}, '${model}')`;
|
|
|
- return [code, javascriptGenerator.ORDER_ATOMIC];
|
|
|
- };
|
|
|
-
|
|
|
- // 智能台灯控制(单参数)
|
|
|
- javascriptGenerator.forBlock['ai_smart_lamp_single_param'] = function(block, generator) {
|
|
|
- const params = generator.valueToCode(block, 'PARAMS', javascriptGenerator.ORDER_ATOMIC);
|
|
|
- const code = `await aiService.controlLampWithSingleParam(${params || "'白,0,平静'"});`;
|
|
|
- return code;
|
|
|
- };
|
|
|
-
|
|
|
- // 智能台灯控制(多参数)
|
|
|
- javascriptGenerator.forBlock['ai_smart_lamp'] = function(block, generator) {
|
|
|
- const brightness = generator.valueToCode(block, 'BRIGHTNESS', javascriptGenerator.ORDER_ATOMIC);
|
|
|
- const color = generator.valueToCode(block, 'COLOR', javascriptGenerator.ORDER_ATOMIC);
|
|
|
- const code = `await aiService.controlLamp(${brightness || '0'}, ${color || "'白'"});`;
|
|
|
- return code;
|
|
|
- };
|
|
|
-
|
|
|
- // 设置台灯亮度
|
|
|
- javascriptGenerator.forBlock['ai_lamp_set_brightness'] = function(block, generator) {
|
|
|
- const brightness = generator.valueToCode(block, 'BRIGHTNESS', javascriptGenerator.ORDER_ATOMIC);
|
|
|
- const code = `await aiService.setLampBrightness(${brightness || '0'});`;
|
|
|
- return code;
|
|
|
- };
|
|
|
-
|
|
|
- // 设置台灯颜色
|
|
|
- javascriptGenerator.forBlock['ai_lamp_set_color'] = function(block, generator) {
|
|
|
- const color = generator.valueToCode(block, 'COLOR', javascriptGenerator.ORDER_ATOMIC);
|
|
|
- const code = `await aiService.setLampColor(${color || "'白'"});`;
|
|
|
- return code;
|
|
|
- };
|
|
|
-
|
|
|
- // 音乐播放
|
|
|
- javascriptGenerator.forBlock['ai_music_play'] = function(block, generator) {
|
|
|
- const musicType = block.getFieldValue('MUSIC_TYPE');
|
|
|
- const code = `await aiService.playMusic('${musicType}');`;
|
|
|
- return code;
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-// 注册Python代码生成器
|
|
|
-function registerPythonGenerators() {
|
|
|
- // 语音输入
|
|
|
- pythonGenerator.forBlock['ai_voice_input'] = function(block, generator) {
|
|
|
- const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
|
|
|
- const language = block.getFieldValue('LANGUAGE');
|
|
|
- const code = `ai_service.recognize_voice(${prompt || "''"}, '${language}')`;
|
|
|
- return [code, pythonGenerator.ORDER_ATOMIC];
|
|
|
- };
|
|
|
-
|
|
|
- // 文本生成图片
|
|
|
- pythonGenerator.forBlock['ai_text_to_image'] = function(block, generator) {
|
|
|
- const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
|
|
|
- const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
|
|
|
- const code = `ai_service.text_to_image(${prompt}, ${waitForCompletion})\n`;
|
|
|
- return code;
|
|
|
- };
|
|
|
-
|
|
|
- // 文本生成视频
|
|
|
- pythonGenerator.forBlock['ai_text_to_video'] = function(block, generator) {
|
|
|
- const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
|
|
|
- const waitForCompletion = block.getFieldValue('WAIT_FOR_COMPLETION') === 'TRUE';
|
|
|
- const code = `ai_service.text_to_video(${prompt}, ${waitForCompletion})\n`;
|
|
|
- return code;
|
|
|
- };
|
|
|
-
|
|
|
- // 文本生成文本
|
|
|
- pythonGenerator.forBlock['ai_text_to_text'] = function(block, generator) {
|
|
|
- const prompt = generator.valueToCode(block, 'PROMPT', pythonGenerator.ORDER_ATOMIC);
|
|
|
- const model = block.getFieldValue('MODEL');
|
|
|
- const code = `ai_service.text_to_text(${prompt}, '${model}')`;
|
|
|
- return [code, pythonGenerator.ORDER_ATOMIC];
|
|
|
- };
|
|
|
-
|
|
|
- // 智能台灯控制(单参数)
|
|
|
- pythonGenerator.forBlock['ai_smart_lamp_single_param'] = function(block, generator) {
|
|
|
- const params = generator.valueToCode(block, 'PARAMS', pythonGenerator.ORDER_ATOMIC);
|
|
|
- const code = `ai_service.control_lamp_with_single_param(${params || "'白,0,平静'"})\n`;
|
|
|
- return code;
|
|
|
- };
|
|
|
-
|
|
|
- // 智能台灯控制
|
|
|
- pythonGenerator.forBlock['ai_smart_lamp'] = function(block, generator) {
|
|
|
- const brightness = generator.valueToCode(block, 'BRIGHTNESS', pythonGenerator.ORDER_ATOMIC);
|
|
|
- const color = generator.valueToCode(block, 'COLOR', pythonGenerator.ORDER_ATOMIC);
|
|
|
- const code = `ai_service.control_lamp(${brightness || '0'}, ${color || "'白'"})\n`;
|
|
|
- return code;
|
|
|
- };
|
|
|
-
|
|
|
- // 设置台灯亮度
|
|
|
- pythonGenerator.forBlock['ai_lamp_set_brightness'] = function(block, generator) {
|
|
|
- const brightness = generator.valueToCode(block, 'BRIGHTNESS', pythonGenerator.ORDER_ATOMIC);
|
|
|
- const code = `ai_service.set_lamp_brightness(${brightness || '0'})\n`;
|
|
|
- return code;
|
|
|
- };
|
|
|
-
|
|
|
- // 设置台灯颜色
|
|
|
- pythonGenerator.forBlock['ai_lamp_set_color'] = function(block, generator) {
|
|
|
- const color = generator.valueToCode(block, 'COLOR', pythonGenerator.ORDER_ATOMIC);
|
|
|
- const code = `ai_service.set_lamp_color(${color || "'白'"})\n`;
|
|
|
- return code;
|
|
|
- };
|
|
|
-
|
|
|
- // 音乐播放
|
|
|
- pythonGenerator.forBlock['ai_music_play'] = function(block, generator) {
|
|
|
- const musicType = block.getFieldValue('MUSIC_TYPE');
|
|
|
- const code = `ai_service.play_music('${musicType}')\n`;
|
|
|
- return code;
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-// 生成代码
|
|
|
-const generateCode = (language) => {
|
|
|
- if (!state.workspace) {
|
|
|
- console.error("workspace 未正确初始化");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- let generator;
|
|
|
- if (language === "javascript") {
|
|
|
- generator = javascriptGenerator;
|
|
|
- } else if (language === "python") {
|
|
|
- generator = pythonGenerator;
|
|
|
- } else {
|
|
|
- console.error("不支持的语言类型");
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const code = generator.workspaceToCode(state.workspace);
|
|
|
- document.getElementById("textarea").value = code;
|
|
|
-};
|
|
|
-
|
|
|
-// 运行代码
|
|
|
-async function runCode() {
|
|
|
- try {
|
|
|
- const codeTextarea = document.getElementById("textarea");
|
|
|
- const code = codeTextarea.value;
|
|
|
- const resultElement = document.getElementById("run-result");
|
|
|
-
|
|
|
- // 清空之前的结果
|
|
|
- resultElement.innerHTML = '<div class="running-indicator">代码运行中...</div>';
|
|
|
-
|
|
|
- // 保存原始console方法
|
|
|
- const originalConsoleLog = console.log;
|
|
|
- const originalConsoleError = console.error;
|
|
|
- const originalConsoleWarn = console.warn;
|
|
|
-
|
|
|
- // 创建输出缓冲区
|
|
|
- let outputBuffer = "";
|
|
|
-
|
|
|
- // 重定义console方法 - 修复console.log不更新DOM的问题
|
|
|
- console.log = (...args) => {
|
|
|
- // 过滤掉包含Vue警告的日志
|
|
|
- const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' ');
|
|
|
-
|
|
|
- // 只显示不包含Vue warn的日志
|
|
|
- if (!message.includes('[Vue warn]')) {
|
|
|
- outputBuffer += '<div class="log-message">' + message + '</div>';
|
|
|
- resultElement.innerHTML = outputBuffer; // 关键修复:立即更新DOM
|
|
|
- }
|
|
|
- originalConsoleLog.apply(console, args);
|
|
|
- };
|
|
|
-
|
|
|
- console.error = (...args) => {
|
|
|
- // 过滤掉包含Vue警告的日志
|
|
|
- const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' ');
|
|
|
-
|
|
|
- // 只显示不包含Vue warn的日志
|
|
|
- if (!message.includes('[Vue warn]')) {
|
|
|
- outputBuffer += '<div class="error-message">' + message + '</div>';
|
|
|
- resultElement.innerHTML = outputBuffer;
|
|
|
- }
|
|
|
- originalConsoleError.apply(console, args);
|
|
|
- };
|
|
|
-
|
|
|
- console.warn = (...args) => {
|
|
|
- // 过滤掉包含Vue警告的日志
|
|
|
- const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(' ');
|
|
|
-
|
|
|
- // 只显示不包含Vue warn的日志
|
|
|
- if (!message.includes('[Vue warn]')) {
|
|
|
- outputBuffer += '<div class="warn-message">' + message + '</div>';
|
|
|
- resultElement.innerHTML = outputBuffer;
|
|
|
- }
|
|
|
- originalConsoleWarn.apply(console, args);
|
|
|
- };
|
|
|
-
|
|
|
- try {
|
|
|
- // 添加安全检查
|
|
|
- if (code.includes('eval(') || code.includes('Function(') ||
|
|
|
- code.includes('document.write') || code.includes('window.location')) {
|
|
|
- throw new Error('代码包含不安全的操作');
|
|
|
- }
|
|
|
-
|
|
|
- // 包装代码为异步函数执行,支持await
|
|
|
- const wrappedCode = `(async () => { ${code} })()`;
|
|
|
- await new Function(wrappedCode)();
|
|
|
-
|
|
|
- // outputBuffer += '<div class="success-message">代码执行成功</div>';
|
|
|
- resultElement.innerHTML = outputBuffer;
|
|
|
- } catch (error) {
|
|
|
- outputBuffer += `<div class="error-message">执行错误: ${error.message}</div>`;
|
|
|
- resultElement.innerHTML = outputBuffer;
|
|
|
- } finally {
|
|
|
- // 恢复原始console方法
|
|
|
- // console.log = originalConsoleLog;
|
|
|
- // console.error = originalConsoleError;
|
|
|
- // console.warn = originalConsoleWarn;
|
|
|
- }
|
|
|
- } catch (err) {
|
|
|
- console.error('运行代码时发生错误:', err);
|
|
|
- document.getElementById('run-result').innerHTML =
|
|
|
- `<div class="error-message">运行代码时发生错误: ${err.message}</div>`;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 保存JSON
|
|
|
-function saveJson() {
|
|
|
- try {
|
|
|
- const output = document.getElementById('textarea');
|
|
|
- const json = Blockly.serialization.workspaces.save(state.workspace);
|
|
|
- output.value = JSON.stringify(json, null, 2);
|
|
|
- output.focus();
|
|
|
- output.select();
|
|
|
- taChange();
|
|
|
- ElMessage.success('JSON保存成功');
|
|
|
- } catch (error) {
|
|
|
- console.error('保存JSON失败:', error);
|
|
|
- ElMessage.error('保存JSON失败: ' + error.message);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 保存XML
|
|
|
-function saveXml() {
|
|
|
- try {
|
|
|
- const output = document.getElementById('textarea');
|
|
|
- const xml = Blockly.Xml.workspaceToDom(state.workspace);
|
|
|
- output.value = Blockly.Xml.domToPrettyText(xml);
|
|
|
- output.focus();
|
|
|
- output.select();
|
|
|
- taChange();
|
|
|
- ElMessage.success('XML保存成功');
|
|
|
- } catch (error) {
|
|
|
- console.error('保存XML失败:', error);
|
|
|
- ElMessage.error('保存XML失败: ' + error.message);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 重新加载
|
|
|
-function load() {
|
|
|
- const input = document.getElementById('textarea');
|
|
|
- if (!input.value) return;
|
|
|
-
|
|
|
- try {
|
|
|
- //关闭预览
|
|
|
- handleClosePreview();
|
|
|
-
|
|
|
- const valid = saveIsValid(input.value);
|
|
|
- if (valid.json) {
|
|
|
- const parsedState = JSON.parse(input.value);
|
|
|
- Blockly.serialization.workspaces.load(parsedState, state.workspace);
|
|
|
- } else if (valid.xml) {
|
|
|
- const xml = Blockly.utils.xml.textToDom(input.value);
|
|
|
- Blockly.Xml.domToWorkspace(xml, state.workspace);
|
|
|
- }
|
|
|
- taChange();
|
|
|
- } catch (error) {
|
|
|
- console.error("加载失败:", error);
|
|
|
- ElMessage.error("加载失败: " + error.message);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 文本域变化时触发
|
|
|
-function taChange() {
|
|
|
- const textarea = document.getElementById("textarea");
|
|
|
- if (sessionStorage) {
|
|
|
- sessionStorage.setItem("textarea", textarea.value);
|
|
|
- }
|
|
|
- const valid = saveIsValid(textarea.value);
|
|
|
- document.getElementById("import").disabled = !valid.json && !valid.xml;
|
|
|
-}
|
|
|
-
|
|
|
-// 检查保存内容是否为有效JSON或XML
|
|
|
-function saveIsValid(save) {
|
|
|
- let validJson = true;
|
|
|
- try {
|
|
|
- JSON.parse(save);
|
|
|
- } catch (e) {
|
|
|
- validJson = false;
|
|
|
- }
|
|
|
-
|
|
|
- let validXml = true;
|
|
|
- try {
|
|
|
- Blockly.utils.xml.textToDom(save);
|
|
|
- } catch (e) {
|
|
|
- validXml = false;
|
|
|
- }
|
|
|
-
|
|
|
- return { json: validJson, xml: validXml };
|
|
|
-}
|
|
|
-
|
|
|
-// 关闭预览
|
|
|
-function handleClosePreview() {
|
|
|
- state.previewVisible = false;
|
|
|
- state.previewContent = "";
|
|
|
- state.previewType = "";
|
|
|
- state.generatedContent.text = null;
|
|
|
- state.generatedContent.imageUrl = null;
|
|
|
- state.generatedContent.videoUrl = null;
|
|
|
-}
|
|
|
-
|
|
|
-// 将aiService挂载到window,以便执行生成的代码时可以访问
|
|
|
-window.aiService = aiService;
|
|
|
-</script>
|
|
|
-
|
|
|
-<style scoped lang="scss">
|
|
|
-/* 原有样式保持不变 */
|
|
|
-@use "sass:math";
|
|
|
-
|
|
|
-@function rpx($px) {
|
|
|
- @return math.div($px, 750) * 100vw;
|
|
|
-}
|
|
|
-// 智能台灯
|
|
|
-.desk-lamp-container {
|
|
|
- position: fixed;
|
|
|
- top: 0;
|
|
|
- left: 0;
|
|
|
- right: 0;
|
|
|
- bottom: 0;
|
|
|
- overflow: hidden;
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
- align-items: center;
|
|
|
- z-index: 1000;
|
|
|
-}
|
|
|
-
|
|
|
-.full-screen-image {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- object-fit: cover;
|
|
|
-}
|
|
|
-
|
|
|
-/* 台灯灯光样式 - 使用动态颜色 */
|
|
|
-.lamp-light-mask {
|
|
|
- position: absolute;
|
|
|
- width: rpx(78);
|
|
|
- height: rpx(250);
|
|
|
- margin-top: rpx(49);
|
|
|
- margin-left: rpx(192);
|
|
|
- background: linear-gradient(to bottom, var(--lamp-color) 0%, var(--lamp-color) 10%, rgba(255, 255, 255, 0) 90%);
|
|
|
- filter: blur(60px);
|
|
|
- transform: rotate(9deg);
|
|
|
- transform-origin: top center;
|
|
|
- opacity: var(--lamp-opacity, 0.6);
|
|
|
- /* 创建扇形效果 */
|
|
|
- clip-path: polygon(0% 0%, 100% 0%, 250% 100%, -150% 100%);
|
|
|
-}
|
|
|
-
|
|
|
-/* 灯光信息显示 */
|
|
|
-.lamp-info {
|
|
|
- position: absolute;
|
|
|
- bottom: 100px;
|
|
|
- right: 30px;
|
|
|
- background-color: rgba(255, 255, 255, 0.2);
|
|
|
- padding: 10px 20px;
|
|
|
- border-radius: 8px;
|
|
|
- backdrop-filter: blur(10px);
|
|
|
- color: white;
|
|
|
- font-size: 14px;
|
|
|
- z-index: 1000;
|
|
|
-}
|
|
|
-
|
|
|
-/* 标题框样式 */
|
|
|
-.desk-lamp-title-box {
|
|
|
- position: absolute;
|
|
|
- top: 20px;
|
|
|
- left: 20px;
|
|
|
- z-index: 1000;
|
|
|
-}
|
|
|
-
|
|
|
-.desk-lamp-box-icon {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
- padding: 10px 20px;
|
|
|
- background-color: rgba(255, 255, 255, 0.2);
|
|
|
- border-radius: 30px;
|
|
|
- backdrop-filter: blur(10px);
|
|
|
- cursor: pointer;
|
|
|
- transition: all 0.3s ease;
|
|
|
- font-size: 16px;
|
|
|
- color: white;
|
|
|
- font-weight: 500;
|
|
|
-}
|
|
|
-
|
|
|
-.desk-lamp-box-icon:hover {
|
|
|
- background-color: rgba(255, 255, 255, 0.3);
|
|
|
- transform: translateX(-3px);
|
|
|
-}
|
|
|
-
|
|
|
-.left-icon {
|
|
|
- font-size: 18px;
|
|
|
-}
|
|
|
-
|
|
|
-/* 右下角按钮组样式 */
|
|
|
-.button-group {
|
|
|
- position: absolute;
|
|
|
- bottom: 30px;
|
|
|
- right: 30px;
|
|
|
- display: flex;
|
|
|
- gap: 15px;
|
|
|
- z-index: 1000;
|
|
|
-}
|
|
|
-
|
|
|
-.control-button {
|
|
|
- padding: 12px 24px;
|
|
|
- border-radius: 8px;
|
|
|
- font-size: 16px;
|
|
|
- font-weight: 500;
|
|
|
- transition: all 0.3s ease;
|
|
|
- backdrop-filter: blur(5px);
|
|
|
-}
|
|
|
-
|
|
|
-.run-button {
|
|
|
- background-color: rgba(64, 169, 255, 0.8);
|
|
|
- color: white;
|
|
|
- border: none;
|
|
|
-}
|
|
|
-
|
|
|
-.run-button:hover {
|
|
|
- background-color: rgba(64, 169, 255, 1);
|
|
|
- transform: translateY(-2px);
|
|
|
-}
|
|
|
-
|
|
|
-.code-button {
|
|
|
- background-color: rgba(132, 94, 255, 0.8);
|
|
|
- color: white;
|
|
|
- border: none;
|
|
|
-}
|
|
|
-
|
|
|
-.code-button:hover {
|
|
|
- background-color: rgba(132, 94, 255, 1);
|
|
|
- transform: translateY(-2px);
|
|
|
-}
|
|
|
-
|
|
|
-// 页面主容器样式
|
|
|
-.page-container {
|
|
|
- position: fixed;
|
|
|
- top: 0;
|
|
|
- left: 0;
|
|
|
- right: 0;
|
|
|
- bottom: 0;
|
|
|
- background: linear-gradient(to bottom, #001169, #b4a8e1);
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
-}
|
|
|
-
|
|
|
-// 标题样式
|
|
|
-.title-box {
|
|
|
- height: rpx(30);
|
|
|
-}
|
|
|
-.box-icon {
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- color: white;
|
|
|
- padding-left: rpx(15);
|
|
|
- font-size: rpx(10);
|
|
|
- cursor: pointer;
|
|
|
-}
|
|
|
-.box-icon .left-icon {
|
|
|
- margin-left: rpx(10);
|
|
|
- margin-right: rpx(5);
|
|
|
-}
|
|
|
-
|
|
|
-.home-container {
|
|
|
- flex: 1;
|
|
|
- display: flex;
|
|
|
- // padding-top: rpx(10);
|
|
|
-}
|
|
|
-
|
|
|
-.box-blockly {
|
|
|
- width: 60%;
|
|
|
- height: 90%;
|
|
|
- padding: rpx(10) rpx(10);
|
|
|
- float: left;
|
|
|
-}
|
|
|
-
|
|
|
-.box-code {
|
|
|
- color: white;
|
|
|
- width: 35%;
|
|
|
- height: 90%;
|
|
|
- padding: rpx(10) rpx(10);
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
-
|
|
|
- .button-container {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 8px;
|
|
|
- align-items: flex-start;
|
|
|
- }
|
|
|
-
|
|
|
- .button-container :deep(.el-button) {
|
|
|
- flex-shrink: 0;
|
|
|
- }
|
|
|
-
|
|
|
- .code-section {
|
|
|
- flex: 1;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- margin-bottom: 10px;
|
|
|
-
|
|
|
- .box-code-textarea {
|
|
|
- flex: 1;
|
|
|
- resize: none;
|
|
|
- background-color: #f8f9fa;
|
|
|
- border: 1px solid #ddd;
|
|
|
- border-radius: 4px;
|
|
|
- padding: 10px;
|
|
|
- overflow-y: auto;
|
|
|
- font-family: monospace;
|
|
|
- font-size: 14px;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- //运行结果区域样式
|
|
|
- .result-section {
|
|
|
- flex: 1;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
-
|
|
|
- .run-result-content {
|
|
|
- color: black;
|
|
|
- // 固定高度为300px,可根据需要调整
|
|
|
- height: 180px;
|
|
|
- background-color: #f8f9fa;
|
|
|
- border: 1px solid #ddd;
|
|
|
- border-radius: 4px;
|
|
|
- padding: 10px;
|
|
|
- overflow-y: auto;
|
|
|
- font-family: monospace;
|
|
|
- font-size: 14px;
|
|
|
-
|
|
|
- // 优化滚动条样式 - 适用于Webkit浏览器(Chrome, Safari)
|
|
|
- &::-webkit-scrollbar {
|
|
|
- width: 8px;
|
|
|
- height: 8px;
|
|
|
- }
|
|
|
-
|
|
|
- &::-webkit-scrollbar-track {
|
|
|
- background: #f1f1f1;
|
|
|
- border-radius: 4px;
|
|
|
- }
|
|
|
-
|
|
|
- &::-webkit-scrollbar-thumb {
|
|
|
- background: #c1c1c1;
|
|
|
- border-radius: 4px;
|
|
|
- }
|
|
|
-
|
|
|
- &::-webkit-scrollbar-thumb:hover {
|
|
|
- background: #a8a8a8;
|
|
|
- }
|
|
|
-
|
|
|
- // Firefox滚动条样式
|
|
|
- scrollbar-width: thin;
|
|
|
- scrollbar-color: #c1c1c1 #f1f1f1;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// AI模块样式
|
|
|
-[categorystyle="ai_category"] > .blocklyTreeRow {
|
|
|
- background-color: #9c27b0 !important;
|
|
|
-}
|
|
|
-
|
|
|
-// 运行结果样式
|
|
|
-.running-indicator {
|
|
|
- color: #666;
|
|
|
-}
|
|
|
-.log-message {
|
|
|
- color: #333;
|
|
|
-}
|
|
|
-.error-message {
|
|
|
- color: #dc3545;
|
|
|
-}
|
|
|
-.warn-message {
|
|
|
- color: #ffc107;
|
|
|
-}
|
|
|
-.success-message {
|
|
|
- color: #28a745;
|
|
|
-}
|
|
|
-
|
|
|
-// 预览样式
|
|
|
-.preview-image-container,
|
|
|
-.preview-video-container {
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
-}
|
|
|
-
|
|
|
-.preview-image,
|
|
|
-.preview-video {
|
|
|
- max-width: 100%;
|
|
|
- max-height: 60vh;
|
|
|
- border-radius: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.preview-text-container {
|
|
|
- max-height: 60vh;
|
|
|
- overflow-y: auto;
|
|
|
- padding: 10px;
|
|
|
- background-color: #f5f5f5;
|
|
|
- border-radius: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-//【文生图预览】
|
|
|
-.extra-image-preview {
|
|
|
- color: black;
|
|
|
- margin-top: 10px;
|
|
|
- padding: 10px;
|
|
|
- border: 1px solid #ddd;
|
|
|
- border-radius: 5px;
|
|
|
- background-color: #f9f9f9;
|
|
|
-}
|
|
|
-
|
|
|
-.extra-preview-image {
|
|
|
- max-width: 100%;
|
|
|
- max-height: 400px;
|
|
|
- border-radius: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-//台灯
|
|
|
-.lamp-preview-container {
|
|
|
- margin-top: 20px;
|
|
|
- padding: 10px;
|
|
|
- background-color: rgba(255, 255, 255, 0.1);
|
|
|
- border-radius: 8px;
|
|
|
- text-align: center;
|
|
|
-}
|
|
|
-
|
|
|
-.lamp-display {
|
|
|
- margin: 15px auto;
|
|
|
- position: relative;
|
|
|
- transition: all 0.3s ease;
|
|
|
-}
|
|
|
-
|
|
|
-.lamp-image {
|
|
|
- width: 100px;
|
|
|
- height: 150px;
|
|
|
- margin: 0 auto;
|
|
|
- background-color: #e0e0e0;
|
|
|
- border-radius: 10px 10px 0 0;
|
|
|
- position: relative;
|
|
|
- box-shadow: 0 0 40px 20px #ffffff80;
|
|
|
- transition: box-shadow 0.5s ease;
|
|
|
-
|
|
|
- &::before {
|
|
|
- content: "";
|
|
|
- position: absolute;
|
|
|
- top: 10px;
|
|
|
- left: 10px;
|
|
|
- right: 10px;
|
|
|
- bottom: 40px;
|
|
|
- background-color: #f5f5f5;
|
|
|
- border-radius: 5px;
|
|
|
- }
|
|
|
-
|
|
|
- &::after {
|
|
|
- content: "";
|
|
|
- position: absolute;
|
|
|
- bottom: 10px;
|
|
|
- left: 40px;
|
|
|
- width: 20px;
|
|
|
- height: 30px;
|
|
|
- background-color: #888888;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.lamp-info {
|
|
|
- margin-top: 10px;
|
|
|
- font-size: 14px;
|
|
|
- color: #ffffff;
|
|
|
-}
|
|
|
-
|
|
|
-//音乐播放器
|
|
|
-.music-player-container {
|
|
|
- margin-top: 20px;
|
|
|
- padding: 15px;
|
|
|
- background-color: #f9f9f9;
|
|
|
- border-radius: 8px;
|
|
|
- border: 1px solid #e0e0e0;
|
|
|
-}
|
|
|
-
|
|
|
-.music-player-container h5 {
|
|
|
- margin-top: 0;
|
|
|
- margin-bottom: 10px;
|
|
|
- color: #333;
|
|
|
- font-size: 16px;
|
|
|
-}
|
|
|
-
|
|
|
-.music-player-container audio {
|
|
|
- width: 100%;
|
|
|
- margin-bottom: 10px;
|
|
|
-}
|
|
|
-
|
|
|
-.music-status {
|
|
|
- font-size: 14px;
|
|
|
- color: #666;
|
|
|
- padding: 5px 0;
|
|
|
-}
|
|
|
-.json-section {
|
|
|
- margin-top: 15px;
|
|
|
- padding: 15px;
|
|
|
- background: #f1f8ff;
|
|
|
- border-radius: 8px;
|
|
|
- border: 1px solid #d1e7ff;
|
|
|
-}
|
|
|
-
|
|
|
-.json-section h3 {
|
|
|
- margin-bottom: 10px;
|
|
|
- color: #2c3e50;
|
|
|
-}
|
|
|
-
|
|
|
-textarea {
|
|
|
- width: 100%;
|
|
|
- min-height: 120px;
|
|
|
- padding: 10px;
|
|
|
- border: 1px solid #ddd;
|
|
|
- border-radius: 5px;
|
|
|
- font-family: 'Courier New', monospace;
|
|
|
- resize: vertical;
|
|
|
-}
|
|
|
-</style>
|