|
|
@@ -0,0 +1,2325 @@
|
|
|
+<template>
|
|
|
+ <!-- 智能台灯 -->
|
|
|
+ <div v-if="showLampPreview" class="desk-lamp-container">
|
|
|
+ <!-- 标题框 -->
|
|
|
+ <div class="desk-lamp-title-box">
|
|
|
+ <div class="desk-lamp-box-icon" @click="goBack">
|
|
|
+ <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
|
|
|
+ 返回虚拟实验室
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 收音状态显示区域 - 添加这段代码 -->
|
|
|
+ <div v-if="isRecording" class="recording-status-container">
|
|
|
+ <div class="recording-text">正在收音...</div>
|
|
|
+ <div class="equalizer">
|
|
|
+ <div class="bar bar-1"></div>
|
|
|
+ <div class="bar bar-2"></div>
|
|
|
+ <div class="bar bar-3"></div>
|
|
|
+ <div class="bar bar-4"></div>
|
|
|
+ <div class="bar bar-5"></div>
|
|
|
+ <div class="bar bar-6"></div>
|
|
|
+ <div class="bar bar-7"></div>
|
|
|
+ </div>
|
|
|
+ <div v-if="recordingCountdown <= 5" class="recording-countdown">{{ recordingCountdown }}秒</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <img :src="device.image" 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 v-if="state.isMusicPlaying" class="music-info">
|
|
|
+ <p>正在播放: {{ state.currentMusicName }}</p>
|
|
|
+ <button class="stop-music-btn" @click="handleStopMusic">
|
|
|
+ 停止播放
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+
|
|
|
+ <!-- Blockly编程界面 -->
|
|
|
+ <div v-show="!showLampPreview" class="container">
|
|
|
+ <!-- 返回智能台灯 -->
|
|
|
+ <div class="title-box">
|
|
|
+ <div class="box-icon" @click="goLabShow">
|
|
|
+ <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
|
|
|
+ 返回智能台灯
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 工具箱-->
|
|
|
+ <div class="content">
|
|
|
+ <div class="toolbox-section">
|
|
|
+ <h2>工具箱</h2>
|
|
|
+ <div 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>
|
|
|
+
|
|
|
+ <block type="ai_turn_on_tv"></block>
|
|
|
+ <block type="ai_turn_off_tv"></block>
|
|
|
+
|
|
|
+ <!-- 添加窗帘控制积木 -->
|
|
|
+ <block type="ai_curtain_open"></block>
|
|
|
+ <block type="ai_curtain_close"></block>
|
|
|
+ </category>
|
|
|
+
|
|
|
+ <category name="逻辑" colour="%{BKY_LOGIC_HUE}">
|
|
|
+ <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>
|
|
|
+ </category>
|
|
|
+ <category name="循环" colour="%{BKY_LOOPS_HUE}">
|
|
|
+ <block type="controls_repeat_ext">
|
|
|
+ <value name="TIMES">
|
|
|
+ <shadow type="math_number">
|
|
|
+ <field name="NUM">10</field>
|
|
|
+ </shadow>
|
|
|
+ </value>
|
|
|
+ </block>
|
|
|
+ <block type="controls_whileUntil"></block>
|
|
|
+ </category>
|
|
|
+ <category name="数学" colour="%{BKY_MATH_HUE}">
|
|
|
+ <block type="math_number"></block>
|
|
|
+ <block type="math_arithmetic"></block>
|
|
|
+ <block type="math_single"></block>
|
|
|
+ </category>
|
|
|
+ <category name="文本" colour="%{BKY_TEXTS_HUE}">
|
|
|
+ <block type="text"></block>
|
|
|
+ <block type="text_length"></block>
|
|
|
+ <block type="text_print"></block>
|
|
|
+ </category>
|
|
|
+ <category name="变量" colour="%{BKY_VARIABLES_HUE}" custom="VARIABLE"></category>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="json-section">
|
|
|
+ <h3> 数据</h3>
|
|
|
+ <textarea v-model="jsonDataString" rows="10" placeholder="在此输入JSON格式的积木块数据..."></textarea>
|
|
|
+ <div class="controls">
|
|
|
+ <button @click="loadWorkspaceFromJson">加载JSON到工作区</button>
|
|
|
+ <button @click="exportWorkspaceToJson">导出工作区为JSON</button>
|
|
|
+ <button id="generateCode" @click="generateCode('javascript')">生成JavaScript代码</button>
|
|
|
+ <button id="generateCode" @click="generateCode('python')">生成Python代码</button>
|
|
|
+ </div>
|
|
|
+ <div v-if="statusMessage" :class="['status', statusType]">
|
|
|
+ {{ statusMessage }}
|
|
|
+ </div>
|
|
|
+ </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 class="workspace-section">
|
|
|
+ <h2>工作区</h2>
|
|
|
+ <div id="blocklyDiv"></div>
|
|
|
+ <div class="controls">
|
|
|
+ <button id="runCode" @click="runCode">运行代码</button>
|
|
|
+ <button @click="clearWorkspace">清空工作区</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 输出-->
|
|
|
+ <div class="output-section">
|
|
|
+ <h2>输出</h2>
|
|
|
+ <div class="controls">
|
|
|
+ <button @click="clearOutput">清空输出</button>
|
|
|
+ </div>
|
|
|
+ <pre id="output">{{ output }}</pre>
|
|
|
+ </div>
|
|
|
+
|
|
|
+
|
|
|
+ <!-- AI结果预览模态框 -->
|
|
|
+ <div v-if="state.previewVisible" class="preview-modal" @click="handleClosePreview">
|
|
|
+ <div class="preview-content" @click.stop>
|
|
|
+ <button class="close-button" @click="handleClosePreview">×</button>
|
|
|
+ <div v-if="state.previewType === 'image'" class="preview-image-container">
|
|
|
+ <img :src="state.previewContent" alt="AI生成图片" class="preview-image">
|
|
|
+ </div>
|
|
|
+ <div v-else-if="state.previewType === 'video'" class="preview-video-container">
|
|
|
+ <video :src="state.previewContent" controls class="preview-video"></video>
|
|
|
+ </div>
|
|
|
+ <div v-else-if="state.previewType === 'text'" class="preview-text-container">
|
|
|
+ {{ state.previewContent }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+// 仅保留必要的导入和主组件逻辑
|
|
|
+import {ref, onMounted, onUnmounted, reactive, computed} from 'vue';
|
|
|
+import { useRouter } from 'vue-router';
|
|
|
+import { ArrowLeftBold } from '@element-plus/icons-vue';
|
|
|
+import * as Blockly from "blockly";
|
|
|
+import 'blockly/msg/zh-hans';
|
|
|
+import { javascriptGenerator } from "blockly/javascript";
|
|
|
+import { pythonGenerator } from "blockly/python";
|
|
|
+
|
|
|
+// 【文生图】文生图
|
|
|
+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";
|
|
|
+import {ElButton} from "element-plus";
|
|
|
+
|
|
|
+const router = useRouter();
|
|
|
+
|
|
|
+// 设备信息
|
|
|
+const device = ref({
|
|
|
+ name: "",
|
|
|
+ image: "",
|
|
|
+ jsonData: {}
|
|
|
+});
|
|
|
+
|
|
|
+// 台灯预览显示状态
|
|
|
+const showLampPreview = ref(true);
|
|
|
+// 语音识别
|
|
|
+const isRecording = ref(false);
|
|
|
+const recordingCountdown = ref(10);
|
|
|
+let countdownInterval = null;
|
|
|
+
|
|
|
+// 返回虚拟实验室
|
|
|
+const goLabShow = () => {
|
|
|
+ showLampPreview.value = true;
|
|
|
+};
|
|
|
+const goBack = () => {
|
|
|
+ router.push("/virtual-laboratory");
|
|
|
+};
|
|
|
+// 切换灯光状态
|
|
|
+const toggleLight = () => {
|
|
|
+ state.lamp.isLightOn = true;
|
|
|
+ generateCode('javascript');
|
|
|
+ // 在运行前设置为正在录音状态
|
|
|
+ startRecordingStatus();
|
|
|
+ runCode();
|
|
|
+};
|
|
|
+
|
|
|
+// 添加开始录音状态函数
|
|
|
+const handleMusicEnded = () => {
|
|
|
+ onMusicEnded(state);
|
|
|
+};
|
|
|
+
|
|
|
+// 添加开始录音状态函数
|
|
|
+function startRecordingStatus() {
|
|
|
+ isRecording.value = true;
|
|
|
+ recordingCountdown.value = 10;
|
|
|
+
|
|
|
+ // 清除之前的定时器
|
|
|
+ if (countdownInterval) {
|
|
|
+ clearInterval(countdownInterval);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置新的倒计时
|
|
|
+ countdownInterval = setInterval(() => {
|
|
|
+ recordingCountdown.value--;
|
|
|
+ if (recordingCountdown.value <= 0) {
|
|
|
+clearInterval(countdownInterval);
|
|
|
+ endRecordingStatus();
|
|
|
+ }
|
|
|
+ }, 1000);
|
|
|
+}
|
|
|
+
|
|
|
+// 添加结束录音状态函数
|
|
|
+function endRecordingStatus() {
|
|
|
+ isRecording.value = false;
|
|
|
+ if (countdownInterval) {
|
|
|
+ clearInterval(countdownInterval);
|
|
|
+ countdownInterval = null;
|
|
|
+ }
|
|
|
+}
|
|
|
+// 查看代码编程界面显示状态
|
|
|
+const handleViewCode = () => {
|
|
|
+ showLampPreview.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+// 创建计算属性处理 JSON 字符串的序列化和反序列化
|
|
|
+const jsonDataString = computed({
|
|
|
+ get() {
|
|
|
+ // 获取时序列化对象为字符串
|
|
|
+ return JSON.stringify(device.value.jsonData, null, 2);
|
|
|
+ },
|
|
|
+ set(value) {
|
|
|
+ // 设置时解析字符串为对象
|
|
|
+ try {
|
|
|
+ device.value.jsonData = JSON.parse(value);
|
|
|
+ } catch (e) {
|
|
|
+ console.error("无效的JSON格式", e);
|
|
|
+ // 可以添加错误提示给用户
|
|
|
+ }
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// 响应式变量
|
|
|
+// const jsonData = ref({
|
|
|
+// "blocks": {
|
|
|
+// "languageVersion": 0,
|
|
|
+// "blocks": [
|
|
|
+// {
|
|
|
+// "type": "variables_set",
|
|
|
+// "id": "kM:Fgf:wd4U3Z$j0x8oK",
|
|
|
+// "x": 90,
|
|
|
+// "y": 130,
|
|
|
+// "fields": {
|
|
|
+// "VAR": {
|
|
|
+// "id": "MHW(ZbOKhL!/An`5N@6`"
|
|
|
+// }
|
|
|
+// },
|
|
|
+// "inputs": {
|
|
|
+// "VALUE": {
|
|
|
+// "block": {
|
|
|
+// "type": "ai_voice_input",
|
|
|
+// "id": "l5E=g|1L+4hThQ8v})lQ",
|
|
|
+// "fields": {
|
|
|
+// "LANGUAGE": "zh-CN"
|
|
|
+// },
|
|
|
+// "inputs": {
|
|
|
+// "PROMPT": {
|
|
|
+// "block": {
|
|
|
+// "type": "text",
|
|
|
+// "id": "Q*n.c_)@7j^E2=s5/X!n",
|
|
|
+// "fields": {
|
|
|
+// "TEXT": "请发言:"
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// },
|
|
|
+// "next": {
|
|
|
+// "block": {
|
|
|
+// "type": "variables_set",
|
|
|
+// "id": "]g.xbBe.i=a9B*Kfw@|`",
|
|
|
+// "fields": {
|
|
|
+// "VAR": {
|
|
|
+// "id": "zn.7{ZqbUaH1?P,R05hF"
|
|
|
+// }
|
|
|
+// },
|
|
|
+// "inputs": {
|
|
|
+// "VALUE": {
|
|
|
+// "block": {
|
|
|
+// "type": "ai_text_to_text",
|
|
|
+// "id": "R$h+R!6#@+4=+WX1*nvh",
|
|
|
+// "inputs": {
|
|
|
+// "PROMPT": {
|
|
|
+// "block": {
|
|
|
+// "type": "variables_get",
|
|
|
+// "id": "h$S$nt)3VU.=nX*W-mo~",
|
|
|
+// "fields": {
|
|
|
+// "VAR": {
|
|
|
+// "id": "MHW(ZbOKhL!/An`5N@6`"
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// },
|
|
|
+// "提示词": {
|
|
|
+// "block": {
|
|
|
+// "type": "text",
|
|
|
+// "id": "7k%sgLP?i]e[,m^49P++",
|
|
|
+// "fields": {
|
|
|
+// "TEXT": "请只回复我指定格式:白,100,热闹"
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// },
|
|
|
+// "next": {
|
|
|
+// "block": {
|
|
|
+// "type": "ai_smart_lamp_single_param",
|
|
|
+// "id": "!.0;Ktwm+Z?o8_9FRa}G",
|
|
|
+// "inputs": {
|
|
|
+// "PARAMS": {
|
|
|
+// "block": {
|
|
|
+// "type": "variables_get",
|
|
|
+// "id": "d{cIJ-kEFFQcn~%A,g@g",
|
|
|
+// "fields": {
|
|
|
+// "VAR": {
|
|
|
+// "id": "zn.7{ZqbUaH1?P,R05hF"
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+// ]
|
|
|
+// },
|
|
|
+// "variables": [
|
|
|
+// {
|
|
|
+// "name": "inputText",
|
|
|
+// "id": "MHW(ZbOKhL!/An`5N@6`"
|
|
|
+// },
|
|
|
+// {
|
|
|
+// "name": "lampConfig",
|
|
|
+// "id": "zn.7{ZqbUaH1?P,R05hF"
|
|
|
+// }
|
|
|
+// ]
|
|
|
+// });
|
|
|
+
|
|
|
+//输出结果
|
|
|
+const output = ref('');
|
|
|
+const statusMessage = ref('');
|
|
|
+const statusType = ref('');
|
|
|
+let workspace = null;
|
|
|
+
|
|
|
+// 创建音乐播放器引用
|
|
|
+const musicPlayer = ref(null);
|
|
|
+
|
|
|
+// 状态管理
|
|
|
+const state = reactive({
|
|
|
+ workspace: null,
|
|
|
+ generatedContent: {
|
|
|
+ imageUrl: "",
|
|
|
+ videoUrl: "",
|
|
|
+ text: "",
|
|
|
+ },
|
|
|
+ previewVisible: false,
|
|
|
+ previewType: "",
|
|
|
+ previewContent: "",
|
|
|
+ isProcessing: false,
|
|
|
+
|
|
|
+ //年级
|
|
|
+ gradeId: "",
|
|
|
+
|
|
|
+ //【文生图】文生图
|
|
|
+ inProgressImageMap: {},
|
|
|
+
|
|
|
+ //【文生视频】文生视频
|
|
|
+ inProgressVideoMap: {},
|
|
|
+
|
|
|
+ // 台灯状态
|
|
|
+ lamp: {
|
|
|
+ isLightOn: false,// 台灯是否亮着
|
|
|
+ brightness: 50, // 默认亮度50%
|
|
|
+ color: "#ffffff", // 默认颜色白色
|
|
|
+ colorLog: "白", // 默认颜色白色
|
|
|
+ },
|
|
|
+
|
|
|
+ // 【文本文】对话相关状态
|
|
|
+ activeConversationId: null,
|
|
|
+ conversationInAbortController: null,
|
|
|
+
|
|
|
+ // 独立的音乐播放状态
|
|
|
+ currentMusicUrl: '',
|
|
|
+ currentMusicName: '',
|
|
|
+ isMusicPlaying: false,
|
|
|
+});
|
|
|
+
|
|
|
+// 统一轮询管理器
|
|
|
+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);
|
|
|
+ showStatus(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
|
|
|
+ ) {
|
|
|
+ showStatus("您的浏览器不支持语音识别功能");
|
|
|
+ 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}秒)`;
|
|
|
+ showStatus(messageText);
|
|
|
+
|
|
|
+ // 倒计时
|
|
|
+ const timer = setInterval(() => {
|
|
|
+ countdown--;
|
|
|
+ if (countdown > 0) {
|
|
|
+ const newText = promptText ? `${promptText}\n请开始说话...(${countdown}秒)` : `请开始说话...(${countdown}秒)`;
|
|
|
+ showStatus(newText);
|
|
|
+ } else {
|
|
|
+ clearInterval(timer);
|
|
|
+ // 倒计时结束时也要调用endRecordingStatus
|
|
|
+ endRecordingStatus();
|
|
|
+ }
|
|
|
+ }, 1000);
|
|
|
+
|
|
|
+ recognition.onresult = (event) => {
|
|
|
+ const speechResult = event.results[0][0].transcript;
|
|
|
+ console.log("语音识别结果:", speechResult);
|
|
|
+ showStatus("语音识别完成");
|
|
|
+ endRecordingStatus(); // 添加这行,在识别成功时结束语音状态
|
|
|
+ resolve(speechResult);
|
|
|
+ };
|
|
|
+
|
|
|
+ recognition.onerror = (event) => {
|
|
|
+ console.error("语音识别错误:", event.error);
|
|
|
+ showStatus("语音识别发生错误: " + event.error, 'error');
|
|
|
+ endRecordingStatus(); // 添加这行,在识别错误时结束语音状态
|
|
|
+ resolve("");
|
|
|
+ };
|
|
|
+
|
|
|
+ // 添加onend事件处理程序,确保语音识别无论如何结束都会清除状态
|
|
|
+ recognition.onend = () => {
|
|
|
+ console.log("语音识别结束");
|
|
|
+ endRecordingStatus(); // 确保在识别结束时调用
|
|
|
+ clearInterval(timer); // 确保清除倒计时定时器
|
|
|
+ };
|
|
|
+
|
|
|
+ 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) {
|
|
|
+ showStatus("获取模型ID失败", 'error');
|
|
|
+ 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) {
|
|
|
+ showStatus("获取模型ID失败", 'error');
|
|
|
+ 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;
|
|
|
+ } 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+ }, '设置台灯颜色失败'),
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 播放音乐功能
|
|
|
+ * @param {string} musicType - 音乐类型
|
|
|
+ * @returns {Promise<void>} - 返回Promise
|
|
|
+ */
|
|
|
+ playMusic: withErrorHandling('播放音乐', async function(musicType) {
|
|
|
+ return playMusic(musicType, state, musicPlayer);
|
|
|
+ }, '播放音乐失败'),
|
|
|
+ /**
|
|
|
+ * 停止音乐播放
|
|
|
+ * @returns {Promise<void>} - 返回Promise
|
|
|
+ */
|
|
|
+ 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 };
|
|
|
+ }, '智能台灯控制失败'),
|
|
|
+
|
|
|
+ // 打开电视
|
|
|
+ turnOnTv: withErrorHandling('打开电视', async function() {
|
|
|
+ // 模拟API调用(实际项目中可替换为真实API)
|
|
|
+ console.log('电视已打开');
|
|
|
+ showStatus('电视已打开');
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }, '打开电视失败'),
|
|
|
+
|
|
|
+ // 关闭电视
|
|
|
+ turnOffTv: withErrorHandling('关闭电视', async function() {
|
|
|
+ // 模拟API调用(实际项目中可替换为真实API)
|
|
|
+ console.log('电视已关闭');
|
|
|
+ showStatus('电视已关闭');
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }, '关闭电视失败'),
|
|
|
+
|
|
|
+ // 打开窗帘
|
|
|
+ openCurtain: withErrorHandling('打开窗帘', async function() {
|
|
|
+ console.log("打开窗帘");
|
|
|
+ // 这里可以添加实际的窗帘控制逻辑
|
|
|
+ showStatus("窗帘已打开");
|
|
|
+ return true;
|
|
|
+ }, '打开窗帘失败'),
|
|
|
+
|
|
|
+ // 关闭窗帘
|
|
|
+ closeCurtain: withErrorHandling('关闭窗帘', async function() {
|
|
|
+ console.log("关闭窗帘");
|
|
|
+ // 这里可以添加实际的窗帘控制逻辑
|
|
|
+ showStatus("窗帘已关闭");
|
|
|
+ return true;
|
|
|
+ }, '关闭窗帘失败'),
|
|
|
+
|
|
|
+ // 启动任务轮询
|
|
|
+ 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) => {
|
|
|
+ showStatus("图片生成失败: " + (image.error || "未知错误"), '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) => {
|
|
|
+ showStatus("视频生成失败: " + (video.error || "未知错误"), 'error');
|
|
|
+ console.error("视频生成失败:", video.id, video.error);
|
|
|
+ }
|
|
|
+ );
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 专门的停止音乐处理函数
|
|
|
+const handleStopMusic = () => {
|
|
|
+ // 直接调用导入的stopMusic函数并传递正确的参数
|
|
|
+ stopMusic(state, musicPlayer);
|
|
|
+ // 提示信息
|
|
|
+ showStatus('音乐已停止播放');
|
|
|
+};
|
|
|
+
|
|
|
+// 关闭预览
|
|
|
+const handleClosePreview = () => {
|
|
|
+ state.previewVisible = false;
|
|
|
+ state.previewContent = "";
|
|
|
+ state.previewType = "";
|
|
|
+ state.generatedContent.text = null;
|
|
|
+ state.generatedContent.imageUrl = null;
|
|
|
+ state.generatedContent.videoUrl = null;
|
|
|
+};
|
|
|
+
|
|
|
+// 组件挂载后初始化Blockly
|
|
|
+onMounted(() => {
|
|
|
+ // 从全局状态初始化年级ID
|
|
|
+ state.gradeId = globalState.initGradeId();
|
|
|
+
|
|
|
+ device.value.name = router.currentRoute.value.query.name || "";
|
|
|
+ device.value.image = router.currentRoute.value.query.image || "";
|
|
|
+ device.value.jsonData = JSON.parse(router.currentRoute.value.query.jsonData || {});
|
|
|
+
|
|
|
+ // 注册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('');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 注册打开电视积木
|
|
|
+ Blockly.Blocks['ai_turn_on_tv'] = {
|
|
|
+ init: function() {
|
|
|
+ this.appendDummyInput()
|
|
|
+ .appendField('打开电视');
|
|
|
+ this.setInputsInline(false);
|
|
|
+ this.setPreviousStatement(true, null);
|
|
|
+ this.setNextStatement(true, null);
|
|
|
+ this.setColour(260);
|
|
|
+ this.setTooltip('打开电视');
|
|
|
+ this.setHelpUrl('');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 注册关闭电视积木
|
|
|
+ Blockly.Blocks['ai_turn_off_tv'] = {
|
|
|
+ init: function() {
|
|
|
+ this.appendDummyInput()
|
|
|
+ .appendField('关闭电视');
|
|
|
+ this.setInputsInline(false);
|
|
|
+ this.setPreviousStatement(true, null);
|
|
|
+ this.setNextStatement(true, null);
|
|
|
+ this.setColour(260);
|
|
|
+ this.setTooltip('关闭电视');
|
|
|
+ this.setHelpUrl('');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 注册打开窗帘积木
|
|
|
+ Blockly.Blocks['ai_curtain_open'] = {
|
|
|
+ init: function() {
|
|
|
+ this.appendDummyInput()
|
|
|
+ .appendField('打开窗帘');
|
|
|
+ this.setInputsInline(false);
|
|
|
+ this.setPreviousStatement(true, null);
|
|
|
+ this.setNextStatement(true, null);
|
|
|
+ this.setColour(200);
|
|
|
+ this.setTooltip('打开窗帘');
|
|
|
+ this.setHelpUrl('');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 注册关闭窗帘积木
|
|
|
+ Blockly.Blocks['ai_curtain_close'] = {
|
|
|
+ init: function() {
|
|
|
+ this.appendDummyInput()
|
|
|
+ .appendField('关闭窗帘');
|
|
|
+ this.setInputsInline(false);
|
|
|
+ this.setPreviousStatement(true, null);
|
|
|
+ this.setNextStatement(true, null);
|
|
|
+ this.setColour(200);
|
|
|
+ this.setTooltip('关闭窗帘');
|
|
|
+ this.setHelpUrl('');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 注册JavaScript代码生成器
|
|
|
+ registerJavaScriptGenerators();
|
|
|
+
|
|
|
+ // 注册Python代码生成器
|
|
|
+ registerPythonGenerators();
|
|
|
+
|
|
|
+ // 初始化Blockly工作区
|
|
|
+ const blocklyDiv = document.getElementById('blocklyDiv');
|
|
|
+ const toolbox = document.getElementById('toolbox');
|
|
|
+
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 使用state.workspace替代workspace
|
|
|
+ state.workspace = workspace;
|
|
|
+
|
|
|
+ // 加载初始JSON数据
|
|
|
+ loadWorkspaceFromJson();
|
|
|
+
|
|
|
+ // 修改工作区变化监听器,使其包含拖拽修复逻辑
|
|
|
+ workspace.addChangeListener((event) => {
|
|
|
+ // 生成代码
|
|
|
+ generateCode("javascript");
|
|
|
+
|
|
|
+ // 拖拽修复逻辑
|
|
|
+ if (event.type === Blockly.Events.BLOCK_CREATE) {
|
|
|
+ const block = workspace.getBlockById(event.blockId);
|
|
|
+ if (block) {
|
|
|
+ block.setEditable(true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 将aiService挂载到window,以便执行生成的代码时可以访问
|
|
|
+ window.aiService = aiService;
|
|
|
+});
|
|
|
+
|
|
|
+// 组件卸载时清除所有资源
|
|
|
+onUnmounted(() => {
|
|
|
+ // 清除所有定时器
|
|
|
+ if (countdownInterval) {
|
|
|
+ clearInterval(countdownInterval);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 停止所有轮询
|
|
|
+ pollingManager.stopAll();
|
|
|
+
|
|
|
+ // 关闭语音识别(如果正在进行)
|
|
|
+ if (recognition) {
|
|
|
+ recognition.stop();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清理工作区
|
|
|
+ if (workspace) {
|
|
|
+ workspace.dispose();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清理音频资源
|
|
|
+ if (musicPlayer.value) {
|
|
|
+ musicPlayer.value.pause();
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// 注册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;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 打开电视
|
|
|
+ javascriptGenerator.forBlock['ai_turn_on_tv'] = function(block, generator) {
|
|
|
+ const code = `await aiService.turnOnTv();`;
|
|
|
+ return code;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 关闭电视
|
|
|
+ javascriptGenerator.forBlock['ai_turn_off_tv'] = function(block, generator) {
|
|
|
+ const code = `await aiService.turnOffTv();`;
|
|
|
+ return code;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 打开窗帘
|
|
|
+ javascriptGenerator.forBlock['ai_curtain_open'] = function(block, generator) {
|
|
|
+ const code = `await aiService.openCurtain();`;
|
|
|
+ return code;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 关闭窗帘
|
|
|
+ javascriptGenerator.forBlock['ai_curtain_close'] = function(block, generator) {
|
|
|
+ const code = `await aiService.closeCurtain();`;
|
|
|
+ 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;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 打开电视
|
|
|
+ pythonGenerator.forBlock['ai_turn_on_tv'] = function(block, generator) {
|
|
|
+ const code = `ai_service.turn_on_tv()\n`;
|
|
|
+ return code;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 关闭电视
|
|
|
+ pythonGenerator.forBlock['ai_turn_off_tv'] = function(block, generator) {
|
|
|
+ const code = `ai_service.turn_off_tv()\n`;
|
|
|
+ return code;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 打开窗帘
|
|
|
+ pythonGenerator.forBlock['ai_curtain_open'] = function(block, generator) {
|
|
|
+ const code = `ai_service.open_curtain()\n`;
|
|
|
+ return code;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 关闭窗帘
|
|
|
+ pythonGenerator.forBlock['ai_curtain_close'] = function(block, generator) {
|
|
|
+ const code = `ai_service.close_curtain()\n`;
|
|
|
+ return code;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// 从JSON加载工作区
|
|
|
+const loadWorkspaceFromJson = () => {
|
|
|
+ try {
|
|
|
+ // const json = JSON.parse(jsonData.value);
|
|
|
+ const json = device.value.jsonData;
|
|
|
+ console.log(typeof json);
|
|
|
+ Blockly.serialization.workspaces.load(json, workspace);
|
|
|
+ showStatus('工作区已成功从JSON加载!');
|
|
|
+ } catch (error) {
|
|
|
+ showStatus('JSON解析错误: ' + error.message, 'error');
|
|
|
+ console.error('JSON解析错误:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 导出工作区为JSON
|
|
|
+const exportWorkspaceToJson = () => {
|
|
|
+ try {
|
|
|
+ const state = Blockly.serialization.workspaces.save(workspace);
|
|
|
+ // jsonData.value = JSON.stringify(state, null, 2);
|
|
|
+ device.value.jsonData = state;
|
|
|
+ showStatus('工作区已成功导出为JSON!');
|
|
|
+ } catch (error) {
|
|
|
+ showStatus('导出错误: ' + error.message, 'error');
|
|
|
+ console.error('导出错误:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 生成代码
|
|
|
+const generateCode = (language = 'javascript') => {
|
|
|
+ try {
|
|
|
+ let generator;
|
|
|
+ if (language == "javascript") {
|
|
|
+ generator = javascriptGenerator;
|
|
|
+ } else if (language == "python") {
|
|
|
+ generator = pythonGenerator;
|
|
|
+ } else {
|
|
|
+ console.error("不支持的语言类型");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const code = generator.workspaceToCode(workspace);
|
|
|
+ output.value = code;
|
|
|
+
|
|
|
+ // 将生成的代码也输出到JSON框中
|
|
|
+ // 创建一个包含代码的JSON对象
|
|
|
+ const codeJson = {
|
|
|
+ "generated_code": {
|
|
|
+ "language": language,
|
|
|
+ "code": code
|
|
|
+ }
|
|
|
+ };
|
|
|
+ // 将JSON对象格式化为字符串并设置到JSON框
|
|
|
+ // jsonData.value = JSON.stringify(codeJson, null, 2);
|
|
|
+ device.value.jsonData = codeJson;
|
|
|
+
|
|
|
+ showStatus('代码生成成功!已同时输出到JSON框');
|
|
|
+ } catch (error) {
|
|
|
+ output.value = '// 代码生成错误: ' + error.message;
|
|
|
+ showStatus('代码生成错误: ' + error.message, 'error');
|
|
|
+ console.error('代码生成错误:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 运行代码
|
|
|
+const runCode = async () => {
|
|
|
+ try {
|
|
|
+ const code = javascriptGenerator.workspaceToCode(workspace);
|
|
|
+ // 初始化输出区域,显示生成的代码和执行结果标题
|
|
|
+ output.value = code + '\n\n// 执行结果:\n';
|
|
|
+
|
|
|
+ // 保存原始console方法
|
|
|
+ const originalConsoleLog = console.log;
|
|
|
+ const originalConsoleError = console.error;
|
|
|
+ const originalConsoleWarn = console.warn;
|
|
|
+
|
|
|
+ // 创建输出缓冲区
|
|
|
+ let outputBuffer = "";
|
|
|
+
|
|
|
+ // 重定义console.log方法,确保所有日志都输出到output变量
|
|
|
+ console.log = (...args) => {
|
|
|
+ // 将参数转换为字符串
|
|
|
+ const message = args.map(arg => {
|
|
|
+ if (typeof arg === 'object') {
|
|
|
+ try {
|
|
|
+ return JSON.stringify(arg, null, 2);
|
|
|
+ } catch (e) {
|
|
|
+ return String(arg);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return arg;
|
|
|
+ }).join(' ');
|
|
|
+
|
|
|
+ // 过滤掉Vue警告信息
|
|
|
+ if (!message.includes('[Vue warn]')) {
|
|
|
+ outputBuffer += message + '\n';
|
|
|
+ // 实时更新输出区域内容
|
|
|
+ output.value = code + '\n\n// 执行结果:\n' + outputBuffer;
|
|
|
+ }
|
|
|
+ // 保留原始console.log功能
|
|
|
+ originalConsoleLog.apply(console, args);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 重定义console.error方法
|
|
|
+ console.error = (...args) => {
|
|
|
+ const message = args.map(arg => {
|
|
|
+ if (typeof arg === 'object') {
|
|
|
+ try {
|
|
|
+ return JSON.stringify(arg, null, 2);
|
|
|
+ } catch (e) {
|
|
|
+ return String(arg);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return arg;
|
|
|
+ }).join(' ');
|
|
|
+
|
|
|
+ if (!message.includes('[Vue warn]')) {
|
|
|
+ outputBuffer += '// 错误: ' + message + '\n';
|
|
|
+ output.value = code + '\n\n// 执行结果:\n' + outputBuffer;
|
|
|
+ }
|
|
|
+ originalConsoleError.apply(console, args);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 重定义console.warn方法
|
|
|
+ console.warn = (...args) => {
|
|
|
+ const message = args.map(arg => {
|
|
|
+ if (typeof arg === 'object') {
|
|
|
+ try {
|
|
|
+ return JSON.stringify(arg, null, 2);
|
|
|
+ } catch (e) {
|
|
|
+ return String(arg);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return arg;
|
|
|
+ }).join(' ');
|
|
|
+
|
|
|
+ if (!message.includes('[Vue warn]')) {
|
|
|
+ outputBuffer += '// 警告: ' + message + '\n';
|
|
|
+ output.value = code + '\n\n// 执行结果:\n' + 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)();
|
|
|
+
|
|
|
+ // 确保最终结果被正确显示
|
|
|
+ output.value = code + '\n\n// 执行结果:\n' + outputBuffer;
|
|
|
+ } catch (error) {
|
|
|
+ // 捕获并显示执行错误
|
|
|
+ outputBuffer += '\n// 执行错误: ' + error.message;
|
|
|
+ output.value = code + '\n\n// 执行结果:\n' + outputBuffer;
|
|
|
+ } finally {
|
|
|
+ // 恢复原始console方法
|
|
|
+ // console.log = originalConsoleLog;
|
|
|
+ // console.error = originalConsoleError;
|
|
|
+ // console.warn = originalConsoleWarn;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示状态消息
|
|
|
+ showStatus('代码执行成功!');
|
|
|
+ } catch (error) {
|
|
|
+ // 处理runCode函数本身的错误
|
|
|
+ output.value += '\n// 执行错误: ' + error.message;
|
|
|
+ showStatus('代码执行错误: ' + error.message, 'error');
|
|
|
+ console.error('代码执行错误:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 清空工作区
|
|
|
+const clearWorkspace = () => {
|
|
|
+ workspace.clear();
|
|
|
+ showStatus('工作区已清空!');
|
|
|
+};
|
|
|
+
|
|
|
+// 清空输出
|
|
|
+const clearOutput = () => {
|
|
|
+ output.value = '// 输出已清空\n';
|
|
|
+};
|
|
|
+
|
|
|
+// 显示状态消息
|
|
|
+const showStatus = (message, type = 'success') => {
|
|
|
+ statusMessage.value = message;
|
|
|
+ statusType.value = type;
|
|
|
+
|
|
|
+ // 3秒后自动清除状态消息
|
|
|
+ // setTimeout(() => {
|
|
|
+ // statusMessage.value = '';
|
|
|
+ // }, 3000);
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+@use "sass:math";
|
|
|
+
|
|
|
+@function rpx($px) {
|
|
|
+ @return math.div($px, 750) * 100vw;
|
|
|
+}
|
|
|
+
|
|
|
+* {
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ box-sizing: border-box;
|
|
|
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
|
+}
|
|
|
+
|
|
|
+.container {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background-image: url('@/assets/images/desklamp.png');
|
|
|
+ background-size: cover;
|
|
|
+ background-position: center;
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ /* 添加黑色透明遮挡层 */
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
+ background-blend-mode: overlay;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+/* 自定义滚动条样式 */
|
|
|
+.container::-webkit-scrollbar {
|
|
|
+ width: rpx(2); /* 滚动条宽度 */
|
|
|
+}
|
|
|
+.container::-webkit-scrollbar-track {
|
|
|
+ background: #f1effd; /* 滚动条轨道背景色 */
|
|
|
+ border-radius: rpx(4);
|
|
|
+}
|
|
|
+.container::-webkit-scrollbar-thumb {
|
|
|
+ background: #e2ddfc; /* 滚动条滑块颜色 */
|
|
|
+ border-radius: rpx(4);
|
|
|
+}
|
|
|
+.container::-webkit-scrollbar-thumb:hover {
|
|
|
+ background: #e2ddfc; /* 滚动条滑块 hover 状态颜色 */
|
|
|
+}
|
|
|
+
|
|
|
+.content {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ min-height: 600px;
|
|
|
+}
|
|
|
+
|
|
|
+//工具箱
|
|
|
+.toolbox-section {
|
|
|
+ flex: 1;
|
|
|
+ //min-width: 250px;
|
|
|
+ background: rgba(248, 249, 250, 0.82);
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 15px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ width: 30%;
|
|
|
+ margin-left: 10px;
|
|
|
+ margin-right: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+//工作区
|
|
|
+.workspace-section {
|
|
|
+ flex: 3;
|
|
|
+ //min-width: 400px;
|
|
|
+ padding: 15px;
|
|
|
+ position: relative;
|
|
|
+ width: 65%;
|
|
|
+ height: 70%;
|
|
|
+ background: rgba(248, 249, 250, 0.82);
|
|
|
+ border-radius: 10px;
|
|
|
+ margin-left: 10px;
|
|
|
+ margin-right: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.output-section {
|
|
|
+ background: rgba(241, 248, 255, 84%);
|
|
|
+ color: black;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid #d1e7ff;
|
|
|
+ width: 100%;
|
|
|
+ margin: 15px 10px;
|
|
|
+}
|
|
|
+
|
|
|
+/* AI模块样式 */
|
|
|
+[categorystyle="ai_category"] > .blocklyTreeRow {
|
|
|
+ background-color: #9c27b0 !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 预览样式 */
|
|
|
+.preview-modal {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background-color: rgba(0, 0, 0, 0.7);
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ z-index: 1000;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-content {
|
|
|
+ background-color: white;
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ max-width: 80%;
|
|
|
+ max-height: 80%;
|
|
|
+ overflow: auto;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.close-button {
|
|
|
+ position: absolute;
|
|
|
+ top: 10px;
|
|
|
+ right: 10px;
|
|
|
+ font-size: 24px;
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ cursor: pointer;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.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;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+/* 文生图预览 */
|
|
|
+.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;
|
|
|
+}
|
|
|
+
|
|
|
+/* 音乐播放器 */
|
|
|
+.music-player-container {
|
|
|
+ margin-top: 20px;
|
|
|
+ padding: 15px;
|
|
|
+ background-color: #f9f9f9;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid #e0e0e0;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.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;
|
|
|
+}
|
|
|
+
|
|
|
+h2 {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ color: #2c3e50;
|
|
|
+ border-bottom: 2px solid #3498db;
|
|
|
+ padding-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.output-section h2 {
|
|
|
+ color: black;
|
|
|
+ border-bottom: 2px solid #3498db;
|
|
|
+}
|
|
|
+
|
|
|
+#blocklyDiv {
|
|
|
+ height: 500px;
|
|
|
+ width: 100%;
|
|
|
+ background: white;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.controls {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ margin-top: 15px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+button {
|
|
|
+ padding: 10px 20px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 5px;
|
|
|
+ background: #3498db;
|
|
|
+ color: white;
|
|
|
+ font-weight: bold;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+button:hover {
|
|
|
+ background: #2980b9;
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+#generateCode {
|
|
|
+ background: #2ecc71;
|
|
|
+}
|
|
|
+
|
|
|
+#generateCode:hover {
|
|
|
+ background: #27ae60;
|
|
|
+}
|
|
|
+
|
|
|
+#runCode {
|
|
|
+ background: #e74c3c;
|
|
|
+}
|
|
|
+
|
|
|
+#runCode:hover {
|
|
|
+ background: #c0392b;
|
|
|
+}
|
|
|
+
|
|
|
+#output {
|
|
|
+ // background: #1a2530;
|
|
|
+ background: rgba($color: #ffffff, $alpha: 0.5);
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 5px;
|
|
|
+ min-height: 100px;
|
|
|
+ max-height: 300px;
|
|
|
+ margin-top: 10px;
|
|
|
+ font-family: 'Courier New', monospace;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ overflow-y: auto;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.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;
|
|
|
+}
|
|
|
+
|
|
|
+.status {
|
|
|
+ margin-top: 10px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.success {
|
|
|
+ background: #d4edda;
|
|
|
+ color: #155724;
|
|
|
+ border: 1px solid #c3e6cb;
|
|
|
+}
|
|
|
+
|
|
|
+.error {
|
|
|
+ background: #f8d7da;
|
|
|
+ color: #721c24;
|
|
|
+ border: 1px solid #f5c6cb;
|
|
|
+}
|
|
|
+
|
|
|
+/* 智能台灯 */
|
|
|
+.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;
|
|
|
+ /* 使用更精确的定位,并针对不同屏幕尺寸进行优化 */
|
|
|
+ top: 56%; /* 调整这个值以匹配台灯的位置 */
|
|
|
+ left: 64%; /* 调整这个值以匹配台灯的位置 */
|
|
|
+ width: 18vmin; /* 使用视口单位使灯光大小随屏幕变化 */
|
|
|
+ height: 60vmin;
|
|
|
+ /* 使用transform进行精确定位 */
|
|
|
+ transform: translate(-50%, -50%) rotate(9deg);
|
|
|
+ transform-origin: top center;
|
|
|
+ background: radial-gradient(circle at center top, var(--lamp-color) 0%, var(--lamp-color) 20%, rgba(255, 255, 255, 0) 70%);
|
|
|
+ filter: blur(40px);
|
|
|
+ opacity: var(--lamp-opacity, 0.6);
|
|
|
+ /* 创建扇形效果 */
|
|
|
+ clip-path: polygon(0% 0%, 100% 0%, 200% 100%, -100% 100%);
|
|
|
+ z-index: 1; /* 确保在图片上方但在UI控件下方 */
|
|
|
+ /* 固定定位确保在全屏时也能正确显示 */
|
|
|
+ will-change: transform;
|
|
|
+}
|
|
|
+/* 响应式调整:在不同屏幕尺寸下微调灯光位置 */
|
|
|
+@media (max-aspect-ratio: 4/3) {
|
|
|
+ .lamp-light-mask {
|
|
|
+ top: 56%;
|
|
|
+ left: 64%;
|
|
|
+ }
|
|
|
+}
|
|
|
+@media (min-aspect-ratio: 16/9) {
|
|
|
+ .lamp-light-mask {
|
|
|
+ top: 55%;
|
|
|
+ left: 63%;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 灯光信息显示 */
|
|
|
+.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);
|
|
|
+}
|
|
|
+/* 返回按钮样式 */
|
|
|
+.title-box {
|
|
|
+ position: relative;
|
|
|
+ top: 10px;
|
|
|
+ padding-left: 15px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.box-icon {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 10px 20px;
|
|
|
+ background-color: rgba(255, 255, 255, 80%);
|
|
|
+ border-radius: 30px;
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ font-size: 16px;
|
|
|
+ color: #333;
|
|
|
+ font-weight: 500;
|
|
|
+ width: fit-content;
|
|
|
+}
|
|
|
+
|
|
|
+.box-icon:hover {
|
|
|
+ background-color: rgba(255, 255, 255, 90%);
|
|
|
+ transform: translateX(-3px);
|
|
|
+}
|
|
|
+
|
|
|
+.left-icon {
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+/* 语音识别-录音状态容器 */
|
|
|
+.recording-status-container {
|
|
|
+ position: absolute;
|
|
|
+ top: 20%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
+ color: white;
|
|
|
+ padding: 20px 40px;
|
|
|
+ border-radius: 10px;
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
|
+ text-align: center;
|
|
|
+ z-index: 1000;
|
|
|
+ backdrop-filter: blur(5px);
|
|
|
+}
|
|
|
+
|
|
|
+.recording-text {
|
|
|
+ font-size: 18px;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.recording-countdown {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #4CAF50;
|
|
|
+ margin-top: 15px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.equalizer {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: flex-end;
|
|
|
+ height: 60px;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.bar {
|
|
|
+ width: 8px;
|
|
|
+ background: linear-gradient(180deg, #4CAF50 0%, #8BC34A 100%);
|
|
|
+ border-radius: 4px;
|
|
|
+ animation: equalize 1s infinite ease-in-out;
|
|
|
+}
|
|
|
+
|
|
|
+.bar-1 { animation-delay: 0s; }
|
|
|
+.bar-2 { animation-delay: 0.1s; }
|
|
|
+.bar-3 { animation-delay: 0.2s; }
|
|
|
+.bar-4 { animation-delay: 0.3s; }
|
|
|
+.bar-5 { animation-delay: 0.4s; }
|
|
|
+.bar-6 { animation-delay: 0.5s; }
|
|
|
+.bar-7 { animation-delay: 0.6s; }
|
|
|
+
|
|
|
+@keyframes equalize {
|
|
|
+ 0%, 100% { height: 10px; }
|
|
|
+ 25% { height: 40px; }
|
|
|
+ 50% { height: 60px; }
|
|
|
+ 75% { height: 25px; }
|
|
|
+}
|
|
|
+
|
|
|
+//台灯播放音乐
|
|
|
+.music-info {
|
|
|
+ margin-top: 10px;
|
|
|
+ padding: 8px;
|
|
|
+ background-color: rgba(255, 255, 255, 0.1);
|
|
|
+ border-radius: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.music-info p {
|
|
|
+ margin: 0 0 8px 0;
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.stop-music-btn {
|
|
|
+ background-color: #ff4d4f;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ padding: 6px 12px;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 12px;
|
|
|
+ transition: background-color 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.stop-music-btn:hover {
|
|
|
+ background-color: #ff7875;
|
|
|
+}
|
|
|
+</style>
|