|
|
@@ -0,0 +1,1137 @@
|
|
|
+<template>
|
|
|
+ <div class="map-game-container">
|
|
|
+ <!-- 标题栏 -->
|
|
|
+ <div class="title-box">
|
|
|
+ <div class="box-icon" @click="navigateBack">
|
|
|
+ <el-icon class="left-icon"><ArrowLeftBold /></el-icon>
|
|
|
+ 地图游戏编程
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="content">
|
|
|
+ <!-- 地图显示区域 -->
|
|
|
+ <div class="map-section">
|
|
|
+ <h2>游戏地图</h2>
|
|
|
+ <div class="map-container">
|
|
|
+ <!-- 地图背景 -->
|
|
|
+ <div class="map-background">
|
|
|
+ <img :src="mapBackground" alt="地图背景" class="map-image" />
|
|
|
+
|
|
|
+ <!-- 可行走区域标记 -->
|
|
|
+ <div
|
|
|
+ v-for="(point, index) in walkablePoints"
|
|
|
+ :key="index"
|
|
|
+ class="walkable-point"
|
|
|
+ :style="getPointStyle(point)"
|
|
|
+ ></div>
|
|
|
+
|
|
|
+ <!-- 起点标记 -->
|
|
|
+ <div
|
|
|
+ class="start-point"
|
|
|
+ :style="getPointStyle(startPoint)"
|
|
|
+ >
|
|
|
+ <span>起点</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 终点标记 -->
|
|
|
+ <div
|
|
|
+ class="end-point"
|
|
|
+ :style="getPointStyle(endPoint)"
|
|
|
+ >
|
|
|
+ <span>终点</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 玩家角色 -->
|
|
|
+ <div
|
|
|
+ class="player"
|
|
|
+ :style="playerStyle"
|
|
|
+ :class="{ 'collision': isColliding, 'success': hasReachedEnd }"
|
|
|
+ ></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 游戏状态提示 -->
|
|
|
+ <div v-if="gameMessage" :class="['game-message', messageType]">
|
|
|
+ {{ gameMessage }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Blockly工作区 -->
|
|
|
+ <div class="blockly-section">
|
|
|
+ <div class="toolbox-section">
|
|
|
+ <h2>工具箱</h2>
|
|
|
+ <div id="toolbox" style="display: none;">
|
|
|
+ <!-- 移动控制积木 -->
|
|
|
+ <category name="移动控制" colour="%{BKY_MOTION_HUE}">
|
|
|
+ <block type="move_forward"></block>
|
|
|
+ <block type="move_backward"></block>
|
|
|
+ <block type="turn_left"></block>
|
|
|
+ <block type="turn_right"></block>
|
|
|
+ <block type="turn_around"></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">5</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>
|
|
|
+ </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>
|
|
|
+ <button @click="resetPlayer">重置玩家</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="output-section">
|
|
|
+ <h2>输出</h2>
|
|
|
+ <div class="controls">
|
|
|
+ <button @click="clearOutput">清空输出</button>
|
|
|
+ </div>
|
|
|
+ <pre id="output">{{ output }}</pre>
|
|
|
+ </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";
|
|
|
+
|
|
|
+const router = useRouter();
|
|
|
+
|
|
|
+// 整合游戏状态
|
|
|
+const gameState = reactive({
|
|
|
+ // 地图配置
|
|
|
+ mapConfig: {
|
|
|
+ background: '@/assets/images/room.png',
|
|
|
+ tileSize: 50,
|
|
|
+ width: 10,
|
|
|
+ height: 8
|
|
|
+ },
|
|
|
+
|
|
|
+ // 玩家状态
|
|
|
+ player: {
|
|
|
+ position: { x: 1, y: 1 },
|
|
|
+ direction: 0, // 0=上, 1=右, 2=下, 3=左
|
|
|
+ isColliding: false,
|
|
|
+ hasReachedEnd: false,
|
|
|
+ collisionHappened: false // 标记是否已发生碰撞
|
|
|
+ },
|
|
|
+
|
|
|
+ // 游戏状态
|
|
|
+ status: {
|
|
|
+ message: '',
|
|
|
+ messageType: ''
|
|
|
+ },
|
|
|
+
|
|
|
+ // 地图数据
|
|
|
+ mapData: {
|
|
|
+ startPoint: { x: 1, y: 1 },
|
|
|
+ endPoint: { x: 8, y: 7 },
|
|
|
+ walkablePoints: [
|
|
|
+ { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 4, y: 1 }, { x: 5, y: 1 },
|
|
|
+ { x: 1, y: 2 }, { x: 5, y: 2 },
|
|
|
+ { x: 1, y: 3 }, { x: 2, y: 3 }, { x: 3, y: 3 }, { x: 4, y: 3 }, { x: 5, y: 3 },
|
|
|
+ { x: 1, y: 4 }, { x: 3, y: 4 }, { x: 5, y: 4 },
|
|
|
+ { x: 1, y: 5 }, { x: 2, y: 5 }, { x: 3, y: 5 }, { x: 4, y: 5 }, { x: 5, y: 5 },
|
|
|
+ { x: 1, y: 6 }, { x: 5, y: 6 },
|
|
|
+ { x: 1, y: 7 }, { x: 2, y: 7 }, { x: 3, y: 7 }, { x: 4, y: 7 }, { x: 5, y: 7 },
|
|
|
+ { x: 6, y: 7 }, { x: 7, y: 7 }, { x: 8, y: 7 }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// 计算属性 - 提高性能和可读性
|
|
|
+const mapBackground = computed(() => gameState.mapConfig.background);
|
|
|
+const tileSize = computed(() => gameState.mapConfig.tileSize);
|
|
|
+const walkablePoints = computed(() => gameState.mapData.walkablePoints);
|
|
|
+const startPoint = computed(() => gameState.mapData.startPoint);
|
|
|
+const endPoint = computed(() => gameState.mapData.endPoint);
|
|
|
+const playerPosition = computed(() => gameState.player.position);
|
|
|
+const playerDirection = computed(() => gameState.player.direction);
|
|
|
+const isColliding = computed(() => gameState.player.isColliding);
|
|
|
+const hasReachedEnd = computed(() => gameState.player.hasReachedEnd);
|
|
|
+const gameMessage = computed(() => gameState.status.message);
|
|
|
+const messageType = computed(() => gameState.status.messageType);
|
|
|
+
|
|
|
+// Blockly相关状态
|
|
|
+let workspace = null;
|
|
|
+const output = ref('// 输出将显示在这里\n');
|
|
|
+
|
|
|
+// 使用 Set 存储可行走点,提高查询效率
|
|
|
+let walkablePointsSet = new Set();
|
|
|
+
|
|
|
+// 初始化可行走点集合
|
|
|
+function initWalkablePointsSet() {
|
|
|
+ walkablePointsSet.clear();
|
|
|
+ gameState.mapData.walkablePoints.forEach(point => {
|
|
|
+ walkablePointsSet.add(`${point.x},${point.y}`);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// 优化后的可行走检查函数
|
|
|
+function isWalkable(x, y) {
|
|
|
+ return walkablePointsSet.has(`${x},${y}`);
|
|
|
+}
|
|
|
+
|
|
|
+// 计算点的样式
|
|
|
+function getPointStyle(point) {
|
|
|
+ return {
|
|
|
+ left: point.x * tileSize.value + 'px',
|
|
|
+ top: point.y * tileSize.value + 'px'
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// 计算玩家样式
|
|
|
+const playerStyle = computed(() => ({
|
|
|
+ left: playerPosition.value.x * tileSize.value + 'px',
|
|
|
+ top: playerPosition.value.y * tileSize.value + 'px',
|
|
|
+ transform: `rotate(${playerDirection.value * 90}deg)`
|
|
|
+}));
|
|
|
+
|
|
|
+// 显示游戏消息
|
|
|
+function showGameMessage(message, type = 'info') {
|
|
|
+ gameState.status.message = message;
|
|
|
+ gameState.status.messageType = type;
|
|
|
+
|
|
|
+ // 3秒后自动清除消息
|
|
|
+ setTimeout(() => {
|
|
|
+ gameState.status.message = '';
|
|
|
+ }, 3000);
|
|
|
+}
|
|
|
+
|
|
|
+// 导航返回
|
|
|
+function navigateBack() {
|
|
|
+ router.back();
|
|
|
+}
|
|
|
+
|
|
|
+// 注册自定义积木
|
|
|
+function registerCustomBlocks() {
|
|
|
+ // 向前移动积木
|
|
|
+ Blockly.Blocks['move_forward'] = {
|
|
|
+ init: function() {
|
|
|
+ this.jsonInit({
|
|
|
+ "type": "move_forward",
|
|
|
+ "message0": "向前移动",
|
|
|
+ "previousStatement": null,
|
|
|
+ "nextStatement": null,
|
|
|
+ "colour": 230,
|
|
|
+ "tooltip": "控制角色向前移动一格",
|
|
|
+ "helpUrl": ""
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 向后移动积木
|
|
|
+ Blockly.Blocks['move_backward'] = {
|
|
|
+ init: function() {
|
|
|
+ this.jsonInit({
|
|
|
+ "type": "move_backward",
|
|
|
+ "message0": "向后移动",
|
|
|
+ "previousStatement": null,
|
|
|
+ "nextStatement": null,
|
|
|
+ "colour": 230,
|
|
|
+ "tooltip": "控制角色向后移动一格",
|
|
|
+ "helpUrl": ""
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 向左转积木
|
|
|
+ Blockly.Blocks['turn_left'] = {
|
|
|
+ init: function() {
|
|
|
+ this.jsonInit({
|
|
|
+ "type": "turn_left",
|
|
|
+ "message0": "向左转",
|
|
|
+ "previousStatement": null,
|
|
|
+ "nextStatement": null,
|
|
|
+ "colour": 230,
|
|
|
+ "tooltip": "控制角色向左转",
|
|
|
+ "helpUrl": ""
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 向右转积木
|
|
|
+ Blockly.Blocks['turn_right'] = {
|
|
|
+ init: function() {
|
|
|
+ this.jsonInit({
|
|
|
+ "type": "turn_right",
|
|
|
+ "message0": "向右转",
|
|
|
+ "previousStatement": null,
|
|
|
+ "nextStatement": null,
|
|
|
+ "colour": 230,
|
|
|
+ "tooltip": "控制角色向右转",
|
|
|
+ "helpUrl": ""
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 向后转积木
|
|
|
+ Blockly.Blocks['turn_around'] = {
|
|
|
+ init: function() {
|
|
|
+ this.jsonInit({
|
|
|
+ "type": "turn_around",
|
|
|
+ "message0": "向后转",
|
|
|
+ "previousStatement": null,
|
|
|
+ "nextStatement": null,
|
|
|
+ "colour": 230,
|
|
|
+ "tooltip": "控制角色向后转",
|
|
|
+ "helpUrl": ""
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// 注册JavaScript生成器
|
|
|
+function registerJavaScriptGenerators() {
|
|
|
+ // 向前移动生成器
|
|
|
+ javascriptGenerator.forBlock['move_forward'] = function(block) {
|
|
|
+ return 'await moveForward();\n';
|
|
|
+ };
|
|
|
+
|
|
|
+ // 向后移动生成器
|
|
|
+ javascriptGenerator.forBlock['move_backward'] = function(block) {
|
|
|
+ return 'await moveBackward();\n';
|
|
|
+ };
|
|
|
+
|
|
|
+ // 向左转生成器
|
|
|
+ javascriptGenerator.forBlock['turn_left'] = function(block) {
|
|
|
+ return 'await turnLeft();\n';
|
|
|
+ };
|
|
|
+
|
|
|
+ // 向右转生成器
|
|
|
+ javascriptGenerator.forBlock['turn_right'] = function(block) {
|
|
|
+ return 'await turnRight();\n';
|
|
|
+ };
|
|
|
+
|
|
|
+ // 向后转生成器
|
|
|
+ javascriptGenerator.forBlock['turn_around'] = function(block) {
|
|
|
+ return 'await turnAround();\n';
|
|
|
+ };
|
|
|
+
|
|
|
+ // 为重复循环块注册自定义生成器,确保支持异步操作
|
|
|
+ javascriptGenerator.forBlock['controls_repeat_ext'] = function(block) {
|
|
|
+ // 获取循环次数 - 修改为使用 valueToCode 方法
|
|
|
+ const repeats = javascriptGenerator.valueToCode(block, 'TIMES', javascriptGenerator.ORDER_ATOMIC) || '0';
|
|
|
+
|
|
|
+ // 确保获取到的是数字类型,如果是字符串需要转换
|
|
|
+ const safeRepeats = `(function() {
|
|
|
+ const num = Number(${repeats});
|
|
|
+ return isNaN(num) ? 0 : Math.max(0, Math.floor(num));
|
|
|
+ })()`;
|
|
|
+
|
|
|
+ // 获取循环体代码
|
|
|
+ const branch = javascriptGenerator.statementToCode(block, 'DO');
|
|
|
+
|
|
|
+ // 生成支持异步的循环代码
|
|
|
+ let code = `for (let i = 0; i < ${safeRepeats}; i++) {\n`;
|
|
|
+ code += javascriptGenerator.prefixLines(branch, javascriptGenerator.INDENT);
|
|
|
+ code += '}\n';
|
|
|
+
|
|
|
+ return code;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 为while/until循环块注册自定义生成器
|
|
|
+ javascriptGenerator.forBlock['controls_whileUntil'] = function(block) {
|
|
|
+ const until = block.getFieldValue('MODE') === 'UNTIL';
|
|
|
+ const condition = javascriptGenerator.valueToCode(block, 'CONDITION',
|
|
|
+ javascriptGenerator.ORDER_NONE) || 'false';
|
|
|
+ const branch = javascriptGenerator.statementToCode(block, 'DO');
|
|
|
+
|
|
|
+ // 修复变量作用域问题,使用IIFE包装循环
|
|
|
+ let code = '(async function() {\n';
|
|
|
+ code += ' let loopCount = 0;\n';
|
|
|
+ code += until ? ' while (!((' + condition + ')) && loopCount < 100) {\n' :
|
|
|
+ ' while (((' + condition + ')) && loopCount < 100) {\n';
|
|
|
+ code += javascriptGenerator.prefixLines(branch, javascriptGenerator.INDENT + ' ');
|
|
|
+ code += ' loopCount++';
|
|
|
+ code += ' await new Promise(resolve => setTimeout(resolve, 10));\n'; // 防止UI阻塞
|
|
|
+ code += ' }\n';
|
|
|
+ code += '})();\n';
|
|
|
+
|
|
|
+ return code;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 为text_print块添加生成器,用于调试
|
|
|
+ javascriptGenerator.forBlock['text_print'] = function(block) {
|
|
|
+ const msg = javascriptGenerator.valueToCode(block, 'TEXT', javascriptGenerator.ORDER_NONE) || '';
|
|
|
+ return `output.value += ${msg} + '\n';\n`;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化Blockly工作区
|
|
|
+function initBlockly() {
|
|
|
+ const toolbox = document.getElementById('toolbox');
|
|
|
+ workspace = Blockly.inject('blocklyDiv', {
|
|
|
+ toolbox: toolbox,
|
|
|
+ theme: Blockly.Themes.Classic,
|
|
|
+ grid: {
|
|
|
+ spacing: 20,
|
|
|
+ length: 3,
|
|
|
+ colour: '#ccc',
|
|
|
+ snap: true
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// 平滑移动函数
|
|
|
+async function smoothMoveTo(targetX, targetY) {
|
|
|
+ const startX = playerPosition.value.x;
|
|
|
+ const startY = playerPosition.value.y;
|
|
|
+ const duration = 500; // 移动动画持续时间(毫秒)
|
|
|
+ const startTime = performance.now();
|
|
|
+
|
|
|
+ // 使用 requestAnimationFrame 实现平滑动画
|
|
|
+ return new Promise(resolve => {
|
|
|
+ function animate(currentTime) {
|
|
|
+ const elapsedTime = currentTime - startTime;
|
|
|
+
|
|
|
+ // 计算进度,确保不会超过1
|
|
|
+ const progress = Math.min(elapsedTime / duration, 1);
|
|
|
+
|
|
|
+ // 使用缓动函数使动画更自然
|
|
|
+ const easedProgress = progress * (2 - progress); // easeOutQuad 缓动
|
|
|
+
|
|
|
+ // 计算当前位置
|
|
|
+ const currentX = startX + (targetX - startX) * easedProgress;
|
|
|
+ const currentY = startY + (targetY - startY) * easedProgress;
|
|
|
+
|
|
|
+ // 更新玩家位置
|
|
|
+ gameState.player.position = { x: currentX, y: currentY };
|
|
|
+
|
|
|
+ // 如果动画未完成,继续下一帧
|
|
|
+ if (progress < 1) {
|
|
|
+ requestAnimationFrame(animate);
|
|
|
+ } else {
|
|
|
+ // 动画完成后解析Promise
|
|
|
+ resolve();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 开始动画
|
|
|
+ requestAnimationFrame(animate);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// 获取方向文本描述
|
|
|
+function getDirectionText() {
|
|
|
+ const directions = ['上', '右', '下', '左'];
|
|
|
+ return directions[playerDirection.value];
|
|
|
+}
|
|
|
+
|
|
|
+// 移动函数(供生成的代码调用)
|
|
|
+window.moveForward = async function() {
|
|
|
+ // 如果已经发生过碰撞,不再执行任何移动
|
|
|
+ if (isColliding.value || gameState.player.collisionHappened) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let newX = playerPosition.value.x;
|
|
|
+ let newY = playerPosition.value.y;
|
|
|
+
|
|
|
+ // 根据当前方向计算新位置
|
|
|
+ switch(playerDirection.value) {
|
|
|
+ case 0: // 上
|
|
|
+ newY--;
|
|
|
+ break;
|
|
|
+ case 1: // 右
|
|
|
+ newX++;
|
|
|
+ break;
|
|
|
+ case 2: // 下
|
|
|
+ newY++;
|
|
|
+ break;
|
|
|
+ case 3: // 左
|
|
|
+ newX--;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否可以移动
|
|
|
+ if (isWalkable(newX, newY)) {
|
|
|
+ // 使用平滑移动动画
|
|
|
+ await smoothMoveTo(newX, newY);
|
|
|
+ output.value += `移动到: (${newX}, ${newY})\n`;
|
|
|
+
|
|
|
+ // 检查是否到达终点
|
|
|
+ if (newX === endPoint.value.x && newY === endPoint.value.y) {
|
|
|
+ gameState.player.hasReachedEnd = true;
|
|
|
+ showGameMessage('恭喜你到达终点!', 'success');
|
|
|
+ output.value += '恭喜你到达终点!\n';
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 发生碰撞
|
|
|
+ gameState.player.isColliding = true;
|
|
|
+ gameState.player.collisionHappened = true; // 标记已发生碰撞
|
|
|
+ showGameMessage('哎呀,撞到墙了!', 'error');
|
|
|
+ output.value += '碰撞!不能移动到这个位置\n';
|
|
|
+
|
|
|
+ // 立即中止整个代码执行
|
|
|
+ if (executionAbortController) {
|
|
|
+ executionAbortController.abort();
|
|
|
+ }
|
|
|
+ // 添加状态日志
|
|
|
+ console.log('碰撞发生,设置碰撞状态:', {
|
|
|
+ collisionHappened: gameState.player.collisionHappened,
|
|
|
+ isColliding: gameState.player.isColliding
|
|
|
+ });
|
|
|
+
|
|
|
+ // 1秒后取消碰撞状态
|
|
|
+ setTimeout(() => {
|
|
|
+ gameState.player.isColliding = false;
|
|
|
+ }, 1000);
|
|
|
+
|
|
|
+ // 添加碰撞延迟
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+window.moveBackward = async function() {
|
|
|
+ // 如果已经发生过碰撞,不再执行任何移动
|
|
|
+ if (isColliding.value || gameState.player.collisionHappened) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let newX = playerPosition.value.x;
|
|
|
+ let newY = playerPosition.value.y;
|
|
|
+
|
|
|
+ // 根据当前方向计算新位置(向后移动)
|
|
|
+ switch(playerDirection.value) {
|
|
|
+ case 0: // 上 -> 向下移动
|
|
|
+ newY++;
|
|
|
+ break;
|
|
|
+ case 1: // 右 -> 向左移动
|
|
|
+ newX--;
|
|
|
+ break;
|
|
|
+ case 2: // 下 -> 向上移动
|
|
|
+ newY--;
|
|
|
+ break;
|
|
|
+ case 3: // 左 -> 向右移动
|
|
|
+ newX++;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否可以移动
|
|
|
+ if (isWalkable(newX, newY)) {
|
|
|
+ // 使用平滑移动动画
|
|
|
+ await smoothMoveTo(newX, newY);
|
|
|
+ output.value += `向后移动到: (${newX}, ${newY})\n`;
|
|
|
+
|
|
|
+ // 检查是否到达终点
|
|
|
+ if (newX === endPoint.value.x && newY === endPoint.value.y) {
|
|
|
+ gameState.player.hasReachedEnd = true;
|
|
|
+ showGameMessage('恭喜你到达终点!', 'success');
|
|
|
+ output.value += '恭喜你到达终点!\n';
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 发生碰撞
|
|
|
+ gameState.player.isColliding = true;
|
|
|
+ gameState.player.collisionHappened = true; // 标记已发生碰撞
|
|
|
+ showGameMessage('哎呀,撞到墙了!', 'error');
|
|
|
+ output.value += '碰撞!不能移动到这个位置\n';
|
|
|
+
|
|
|
+ // 立即中止整个代码执行
|
|
|
+ if (executionAbortController) {
|
|
|
+ executionAbortController.abort();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1秒后取消碰撞状态
|
|
|
+ setTimeout(() => {
|
|
|
+ gameState.player.isColliding = false;
|
|
|
+ }, 1000);
|
|
|
+
|
|
|
+ // 添加碰撞延迟
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+window.turnLeft = async function() {
|
|
|
+ // 如果已经发生过碰撞,不再执行任何旋转
|
|
|
+ console.log('turnLeft',gameState.player.collisionHappened);
|
|
|
+ if (isColliding.value || gameState.player.collisionHappened) {
|
|
|
+ console.log('turnLeft: 检测到碰撞,停止旋转');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 向左转(逆时针旋转90度)
|
|
|
+ gameState.player.direction = (playerDirection.value - 1 + 4) % 4;
|
|
|
+ output.value += `向左转,当前方向: ${getDirectionText()}\n`;
|
|
|
+
|
|
|
+ // 添加旋转延迟
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
+ console.log('turnLeft222222222222',gameState.player.collisionHappened);
|
|
|
+};
|
|
|
+
|
|
|
+window.turnRight = async function() {
|
|
|
+ // 如果已经发生过碰撞,不再执行任何旋转
|
|
|
+ console.log('turnRight',gameState.player.collisionHappened);
|
|
|
+ if (isColliding.value || gameState.player.collisionHappened) {
|
|
|
+ console.log('turnRight: 检测到碰撞,停止旋转');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 向右转(顺时针旋转90度)
|
|
|
+ gameState.player.direction = (playerDirection.value + 1) % 4;
|
|
|
+ output.value += `向右转,当前方向: ${getDirectionText()}\n`;
|
|
|
+
|
|
|
+ // 添加旋转延迟
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
+ console.log('turnRight222222222222',gameState.player.collisionHappened);
|
|
|
+};
|
|
|
+
|
|
|
+window.turnAround = async function() {
|
|
|
+ // 如果已经发生过碰撞,不再执行任何旋转
|
|
|
+ if (isColliding.value || gameState.player.collisionHappened) {
|
|
|
+ console.log('turnRight: 检测到碰撞,停止旋转');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 向后转(旋转180度)
|
|
|
+ gameState.player.direction = (playerDirection.value + 2) % 4;
|
|
|
+ output.value += `向后转,当前方向: ${getDirectionText()}\n`;
|
|
|
+
|
|
|
+ // 添加旋转延迟
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+// 添加一个变量来跟踪当前执行的代码
|
|
|
+let currentExecutionPromise = null;
|
|
|
+let executionAbortController = null;
|
|
|
+// 运行代码
|
|
|
+const runCode = async () => {
|
|
|
+ try {
|
|
|
+ // 取消任何正在执行的代码
|
|
|
+ if (executionAbortController) {
|
|
|
+ executionAbortController.abort();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建新的AbortController用于取消执行
|
|
|
+ executionAbortController = new AbortController();
|
|
|
+ const signal = executionAbortController.signal;
|
|
|
+
|
|
|
+ // 重置玩家状态
|
|
|
+ gameState.player.isColliding = false;
|
|
|
+ gameState.player.hasReachedEnd = false;
|
|
|
+ gameState.player.collisionHappened = false; // 重置碰撞标志
|
|
|
+
|
|
|
+ // 添加状态日志
|
|
|
+ console.log('执行开始,初始状态:', {
|
|
|
+ collisionHappened: gameState.player.collisionHappened,
|
|
|
+ isColliding: gameState.player.isColliding
|
|
|
+ });
|
|
|
+
|
|
|
+
|
|
|
+ // 初始化输出
|
|
|
+ output.value = '// 执行结果:\n';
|
|
|
+
|
|
|
+ // 确保生成器和工作区都存在
|
|
|
+ if (!javascriptGenerator || !workspace) {
|
|
|
+ throw new Error('生成器或工作区未正确初始化');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成JavaScript代码
|
|
|
+ const code = javascriptGenerator.workspaceToCode(workspace);
|
|
|
+ output.value += '// 生成的代码:\n' + code + '\n\n// 执行过程:\n';
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 增强的安全检查
|
|
|
+ const unsafePatterns = [
|
|
|
+ 'eval(', 'Function(', 'document.write', 'window.location',
|
|
|
+ 'document.createElement', 'XMLHttpRequest', 'fetch',
|
|
|
+ 'setInterval', 'setTimeout', 'window.'
|
|
|
+ ];
|
|
|
+
|
|
|
+ const hasUnsafeCode = unsafePatterns.some(pattern => code.includes(pattern));
|
|
|
+
|
|
|
+ if (hasUnsafeCode) {
|
|
|
+ throw new Error('代码包含不安全的操作');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 包装代码为异步函数执行,并设置超时保护
|
|
|
+ currentExecutionPromise = new Promise(async (resolve, reject) => {
|
|
|
+ try {
|
|
|
+ // 检查信号是否已中止
|
|
|
+ if (signal.aborted) {
|
|
|
+ throw new Error('执行已取消');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加信号监听
|
|
|
+ signal.addEventListener('abort', () => {
|
|
|
+ reject(new Error('执行已取消'));
|
|
|
+ });
|
|
|
+
|
|
|
+ const wrappedCode = `(async () => { ${code} })()`;
|
|
|
+ await new Function(wrappedCode)();
|
|
|
+ resolve();
|
|
|
+ } catch (error) {
|
|
|
+ reject(error);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 设置执行超时(例如10秒)
|
|
|
+ await Promise.race([
|
|
|
+ currentExecutionPromise,
|
|
|
+ new Promise((_, reject) => setTimeout(() => reject(new Error('代码执行超时')), 10000))
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 检查是否到达终点
|
|
|
+ if (!gameState.player.hasReachedEnd) {
|
|
|
+ showGameMessage('程序执行完毕,但未到达终点', 'info');
|
|
|
+ output.value += '程序执行完毕,但未到达终点\n';
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ // 捕获并显示执行错误
|
|
|
+ if (error.message !== '执行已取消') {
|
|
|
+ output.value += '\n// 执行错误: ' + error.message + '\n';
|
|
|
+ showGameMessage('代码执行错误: ' + error.message, 'error');
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ // 清除当前执行的Promise引用
|
|
|
+ currentExecutionPromise = null;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ output.value += '\n// 错误: ' + error.message + '\n';
|
|
|
+ showGameMessage('运行时错误: ' + error.message, 'error');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 清空工作区
|
|
|
+const clearWorkspace = () => {
|
|
|
+ workspace.clear();
|
|
|
+ showGameMessage('工作区已清空', 'info');
|
|
|
+};
|
|
|
+
|
|
|
+// 清空输出
|
|
|
+const clearOutput = () => {
|
|
|
+ output.value = '// 输出将显示在这里\n';
|
|
|
+};
|
|
|
+
|
|
|
+// 重置玩家位置和状态
|
|
|
+const resetPlayer = () => {
|
|
|
+ // 取消任何正在执行的代码
|
|
|
+ if (executionAbortController) {
|
|
|
+ executionAbortController.abort();
|
|
|
+ executionAbortController = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (currentExecutionPromise) {
|
|
|
+ currentExecutionPromise = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ gameState.player.position = { ...startPoint.value };
|
|
|
+ gameState.player.direction = 0; // 重置为向上方向
|
|
|
+ gameState.player.isColliding = false;
|
|
|
+ gameState.player.hasReachedEnd = false;
|
|
|
+ gameState.player.collisionHappened = false; // 重置碰撞标志
|
|
|
+ showGameMessage('玩家已重置到起点', 'info');
|
|
|
+ output.value += '玩家已重置到起点\n';
|
|
|
+};
|
|
|
+
|
|
|
+// 组件挂载时初始化
|
|
|
+onMounted(() => {
|
|
|
+ // 初始化可行走点集合
|
|
|
+ initWalkablePointsSet();
|
|
|
+
|
|
|
+ registerCustomBlocks();
|
|
|
+ registerJavaScriptGenerators();
|
|
|
+ initBlockly();
|
|
|
+});
|
|
|
+
|
|
|
+// 组件卸载时清理
|
|
|
+onUnmounted(() => {
|
|
|
+ if (workspace) {
|
|
|
+ workspace.dispose();
|
|
|
+ }
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+@use "sass:math";
|
|
|
+
|
|
|
+@function rpx($px) {
|
|
|
+ @return math.div($px, 750) * 100vw;
|
|
|
+}
|
|
|
+
|
|
|
+.map-game-container {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: transparent;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+/* 自定义滚动条样式 */
|
|
|
+.map-game-container::-webkit-scrollbar {
|
|
|
+ width: rpx(2); /* 滚动条宽度 */
|
|
|
+}
|
|
|
+
|
|
|
+.map-game-container::-webkit-scrollbar-track {
|
|
|
+ background: #f1effd; /* 滚动条轨道背景色 */
|
|
|
+ border-radius: rpx(4);
|
|
|
+}
|
|
|
+
|
|
|
+.map-game-container::-webkit-scrollbar-thumb {
|
|
|
+ background: #e2ddfc; /* 滚动条滑块颜色 */
|
|
|
+ border-radius: rpx(4);
|
|
|
+}
|
|
|
+
|
|
|
+.map-game-container::-webkit-scrollbar-thumb:hover {
|
|
|
+ background: #e2ddfc; /* 滚动条滑块 hover 状态颜色 */
|
|
|
+}
|
|
|
+
|
|
|
+.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, 0.8);
|
|
|
+ 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, 0.9);
|
|
|
+ transform: translate(-3px);
|
|
|
+}
|
|
|
+
|
|
|
+.left-icon {
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.content {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 20px;
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 地图区域样式 */
|
|
|
+.map-section {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 500px;
|
|
|
+ background: rgba(248, 249, 250, 0.82);
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.map-section h2 {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ color: #2c3e50;
|
|
|
+ border-bottom: 2px solid #3498db;
|
|
|
+ padding-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.map-container {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ min-height: 450px;
|
|
|
+}
|
|
|
+
|
|
|
+.map-background {
|
|
|
+ position: relative;
|
|
|
+ width: 500px;
|
|
|
+ height: 400px;
|
|
|
+ margin: 0 auto;
|
|
|
+ border: 2px solid #ddd;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ background-color: #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.map-image {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ opacity: 0.7;
|
|
|
+}
|
|
|
+
|
|
|
+/* 可行走区域样式 */
|
|
|
+.walkable-point {
|
|
|
+ position: absolute;
|
|
|
+ width: 50px;
|
|
|
+ height: 50px;
|
|
|
+ background-color: rgba(52, 152, 219, 0.2);
|
|
|
+ border: 1px solid rgba(52, 152, 219, 0.5);
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+/* 起点和终点样式 */
|
|
|
+.start-point,
|
|
|
+.end-point {
|
|
|
+ position: absolute;
|
|
|
+ width: 50px;
|
|
|
+ height: 50px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ font-weight: bold;
|
|
|
+ color: white;
|
|
|
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7);
|
|
|
+ border-radius: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.start-point {
|
|
|
+ background-color: rgba(46, 204, 113, 0.8);
|
|
|
+}
|
|
|
+
|
|
|
+.end-point {
|
|
|
+ background-color: rgba(231, 76, 60, 0.8);
|
|
|
+}
|
|
|
+
|
|
|
+/* 玩家样式 */
|
|
|
+.player {
|
|
|
+ position: absolute;
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ margin: 5px;
|
|
|
+ background-color: rgba(155, 89, 182, 0.9);
|
|
|
+ border-radius: 5px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ z-index: 10;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ color: white;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 14px;
|
|
|
+ /* 箭头指示器 */
|
|
|
+ &::after {
|
|
|
+ content: '▶';
|
|
|
+ font-size: 20px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 碰撞动画 */
|
|
|
+.player.collision {
|
|
|
+ animation: collision 0.5s ease-in-out;
|
|
|
+ background-color: rgba(231, 76, 60, 0.9);
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes collision {
|
|
|
+ 0%, 100% { transform: translateX(0) rotate(var(--player-direction)); }
|
|
|
+ 25% { transform: translateX(-5px) rotate(var(--player-direction)); }
|
|
|
+ 75% { transform: translateX(5px) rotate(var(--player-direction)); }
|
|
|
+}
|
|
|
+
|
|
|
+/* 成功到达终点动画 */
|
|
|
+.player.success {
|
|
|
+ animation: success 1s ease-in-out;
|
|
|
+ background-color: rgba(46, 204, 113, 0.9);
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes success {
|
|
|
+ 0% { transform: scale(1) rotate(var(--player-direction)); }
|
|
|
+ 50% { transform: scale(1.2) rotate(var(--player-direction)); }
|
|
|
+ 100% { transform: scale(1) rotate(var(--player-direction)); }
|
|
|
+}
|
|
|
+
|
|
|
+/* 游戏消息样式 */
|
|
|
+.game-message {
|
|
|
+ position: absolute;
|
|
|
+ top: 20px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ padding: 10px 20px;
|
|
|
+ border-radius: 5px;
|
|
|
+ font-weight: bold;
|
|
|
+ z-index: 20;
|
|
|
+ min-width: 200px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.game-message.success {
|
|
|
+ background-color: #d4edda;
|
|
|
+ color: #155724;
|
|
|
+ border: 1px solid #c3e6cb;
|
|
|
+}
|
|
|
+
|
|
|
+.game-message.error {
|
|
|
+ background-color: #f8d7da;
|
|
|
+ color: #721c24;
|
|
|
+ border: 1px solid #f5c6cb;
|
|
|
+}
|
|
|
+
|
|
|
+.game-message.info {
|
|
|
+ background-color: #d1ecf1;
|
|
|
+ color: #0c5460;
|
|
|
+ border: 1px solid #bee5eb;
|
|
|
+}
|
|
|
+
|
|
|
+/* Blockly区域样式 */
|
|
|
+.blockly-section {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 600px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbox-section {
|
|
|
+ background: rgba(248, 249, 250, 0.82);
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbox-section h2 {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ color: #2c3e50;
|
|
|
+ border-bottom: 2px solid #3498db;
|
|
|
+ padding-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.workspace-section {
|
|
|
+ background: rgba(248, 249, 250, 0.82);
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 15px;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.workspace-section h2 {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ color: #2c3e50;
|
|
|
+ border-bottom: 2px solid #3498db;
|
|
|
+ padding-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+#blocklyDiv {
|
|
|
+ height: 300px;
|
|
|
+ width: 100%;
|
|
|
+ background: #fff;
|
|
|
+ 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: #fff;
|
|
|
+ font-weight: 700;
|
|
|
+ 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);
|
|
|
+}
|
|
|
+
|
|
|
+#runCode {
|
|
|
+ background: #e74c3c;
|
|
|
+}
|
|
|
+
|
|
|
+#runCode:hover {
|
|
|
+ background: #c0392b;
|
|
|
+}
|
|
|
+
|
|
|
+.output-section {
|
|
|
+ background: rgba(241, 248, 255, 0.84);
|
|
|
+ color: #000;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid #d1e7ff;
|
|
|
+}
|
|
|
+
|
|
|
+.output-section h2 {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ color: #2c3e50;
|
|
|
+ border-bottom: 2px solid #3498db;
|
|
|
+ padding-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+#output {
|
|
|
+ background: rgba(255, 255, 255, 0.5);
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 5px;
|
|
|
+ min-height: 100px;
|
|
|
+ max-height: 200px;
|
|
|
+ margin-top: 10px;
|
|
|
+ font-family: 'Courier New', monospace;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ overflow-y: auto;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式布局 */
|
|
|
+@media (max-width: 1200px) {
|
|
|
+ .content {
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .map-section,
|
|
|
+ .blockly-section {
|
|
|
+ min-width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .map-background {
|
|
|
+ width: 100%;
|
|
|
+ height: 400px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|