|
|
@@ -0,0 +1,1007 @@
|
|
|
+<template>
|
|
|
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1000px">
|
|
|
+ <el-form
|
|
|
+ ref="formRef"
|
|
|
+ :model="formData"
|
|
|
+ :rules="formRules"
|
|
|
+ label-width="120px"
|
|
|
+ v-loading="formLoading"
|
|
|
+ >
|
|
|
+ <el-form-item label="大纲课程" prop="bcType">
|
|
|
+ <el-tree-select
|
|
|
+ v-model="formData.bcType"
|
|
|
+ :data="bcTypeTree"
|
|
|
+ :props="{...defaultProps,
|
|
|
+ label: (node) => `${node.ctTypeNode === undefined ? node.ctType : node.ctTypeSort + '、' + node.ctType}`,
|
|
|
+ }"
|
|
|
+ placeholder="请选择课程类型"
|
|
|
+ :default-expand-all="true"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="课程名称" prop="bcName">
|
|
|
+ <el-input v-model="formData.bcName" placeholder="请输入课程名称" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="课程标签" prop="bcLabel">
|
|
|
+ <el-select
|
|
|
+ v-model="formData.bcLabel"
|
|
|
+ placeholder="请选择课程标签"
|
|
|
+ clearable
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="dict in getStrDictOptions(DICT_TYPE.BLOCKLY_COURSE_LABEL)"
|
|
|
+ :key="dict.value"
|
|
|
+ :label="dict.label"
|
|
|
+ :value="dict.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="内容类型" prop="bcContentType">
|
|
|
+ <el-segmented v-model="formData.bcContentType" :options="getStrDictOptions(DICT_TYPE.BLOCKLY_COURSE_COUTNET_TYPE)" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item v-if="formData.bcContentType === 'all'" label="课程内容" prop="bcContent">
|
|
|
+ <Editor v-model="formData.bcContent" height="150px" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item v-if="formData.bcContentType === 'text'" label="纯文本" prop="bcContent">
|
|
|
+ <Editor v-model="formData.bcContent" height="150px" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item v-if="formData.bcContentType === 'image'" label="课程图片集" prop="bcContent">
|
|
|
+ <UploadImgs v-model="formData.bcContent"
|
|
|
+ @upload-progress="handleUploadProgress"
|
|
|
+ @upload-start="handleUploadStart"
|
|
|
+ @upload-complete="handleUploadComplete"/>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item v-if="formData.bcContentType === 'music'" label="课程音频" prop="bcContent">
|
|
|
+ <UploadMusic v-model="formData.bcContent"
|
|
|
+ @upload-progress="handleUploadProgress"
|
|
|
+ @upload-start="handleUploadStart"
|
|
|
+ @upload-complete="handleUploadComplete"/>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item v-if="formData.bcContentType === 'video'" label="课程视频" prop="bcContent">
|
|
|
+ <UploadVideo
|
|
|
+ v-model="formData.bcContent"
|
|
|
+ @upload-progress="handleUploadProgress"
|
|
|
+ @upload-start="handleUploadStart"
|
|
|
+ @upload-complete="handleUploadComplete"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item v-if="formData.bcContentType === 'ppt'" label="课程PPT" prop="bcContent">
|
|
|
+ <UploadFile v-model="formData.bcContent" :fileType="['ppt','pptx']" :fileSize="50"
|
|
|
+ @upload-progress="handleUploadProgress"
|
|
|
+ @upload-start="handleUploadStart"
|
|
|
+ @upload-complete="handleUploadComplete"/>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 上传进度条 -->
|
|
|
+ <el-form-item>
|
|
|
+ <div v-if="isUploading" class="uploadProgress">
|
|
|
+ <el-progress :percentage="uploadProgress" />
|
|
|
+ <div class="text-xs text-gray-500 text-right mt-1">{{ uploadProgress }}% 已上传</div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <template v-if="formData.bcContentType === 'blockly'">
|
|
|
+ <el-form-item label="Blockly配置" prop="blocklyConfig">
|
|
|
+ <el-button type="primary" @click="openBlocklyConfigDialog">配置Blockly数据</el-button>
|
|
|
+ <div v-if="hasBlocklyConfig" class="mt-2 text-sm text-green-600">
|
|
|
+ ✓ Blockly数据已配置
|
|
|
+ </div>
|
|
|
+ <div v-else class="mt-2 text-sm text-red-600">
|
|
|
+ ⚠ Blockly数据未配置
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else>
|
|
|
+ <el-form-item label="课程是否有检查" prop="bcIsInspect">
|
|
|
+ <el-radio-group v-model="formData.bcIsInspect">
|
|
|
+ <el-radio
|
|
|
+ v-for="dict in getStrDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
|
|
|
+ :key="dict.value"
|
|
|
+ :label="dict.value"
|
|
|
+ >
|
|
|
+ {{ dict.label }}
|
|
|
+ </el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-form-item label="课程排序" prop="bcOrder">
|
|
|
+ <el-input-number v-model="formData.bcOrder" placeholder="请输入课程排序" class="!w-1/1" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="课程状态" prop="bcStatus">
|
|
|
+ <el-radio-group v-model="formData.bcStatus">
|
|
|
+ <el-radio
|
|
|
+ v-for="dict in getStrDictOptions(DICT_TYPE.COMMON_STATUS)"
|
|
|
+ :key="dict.value"
|
|
|
+ :label="dict.value"
|
|
|
+ >
|
|
|
+ {{ dict.label }}
|
|
|
+ </el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="submitForm" type="primary" :disabled="formLoading || isUploading">确 定</el-button>
|
|
|
+ <el-button @click="dialogVisible = false">取 消</el-button>
|
|
|
+ </template>
|
|
|
+ </Dialog>
|
|
|
+
|
|
|
+ <!-- Blockly配置弹框 -->
|
|
|
+ <Dialog :title="blocklyConfigTitle" v-model="blocklyConfigVisible" width="1250px" >
|
|
|
+ <el-form
|
|
|
+ ref="blocklyConfigFormRef"
|
|
|
+ :model="formData"
|
|
|
+ :rules="blocklyConfigRules"
|
|
|
+ label-width="130px"
|
|
|
+ >
|
|
|
+
|
|
|
+ <el-form-item label="Blockly信息" prop="blocklyInfo">
|
|
|
+ <Editor v-model="formData.blocklyInfo" height="150px" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-row>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="人物图标" prop="blocklyUserImage">
|
|
|
+ <UploadImg v-model="formData.blocklyUserImage" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="地图背景图" prop="blocklyBackground">
|
|
|
+ <UploadImg v-model="formData.blocklyBackground" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="人物朝向" prop="blocklyUserDirection">
|
|
|
+ <el-radio-group v-model="formData.blocklyUserDirection" size="large">
|
|
|
+ <el-radio-button label="上" :value="0" />
|
|
|
+ <el-radio-button label="右" :value="1" />
|
|
|
+ <el-radio-button label="下" :value="2" />
|
|
|
+ <el-radio-button label="左" :value="3" />
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <!-- 地图开始坐标 - 拆分为X轴和Y轴 -->
|
|
|
+ <el-form-item label="地图开始坐标" required>
|
|
|
+ <div class="coordinate-group">
|
|
|
+ X:
|
|
|
+ <el-input-number
|
|
|
+ v-model="blocklyStartPointX"
|
|
|
+ placeholder="X轴"
|
|
|
+ :min="1"
|
|
|
+ :max="blocklyTileX"
|
|
|
+ :step="1"
|
|
|
+ style="width: 120px"
|
|
|
+ />
|
|
|
+ Y:
|
|
|
+ <el-input-number
|
|
|
+ v-model="blocklyStartPointY"
|
|
|
+ placeholder="Y轴"
|
|
|
+ :min="1"
|
|
|
+ :max="blocklyTileY"
|
|
|
+ :step="1"
|
|
|
+ style="width: 120px"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="地图方格尺寸" required>
|
|
|
+ <div class="coordinate-group">
|
|
|
+ X:
|
|
|
+ <el-input-number
|
|
|
+ v-model="blocklyTileX"
|
|
|
+ placeholder="横向数量"
|
|
|
+ :min="1"
|
|
|
+ :step="1"
|
|
|
+ style="width: 120px"
|
|
|
+ />
|
|
|
+ Y:
|
|
|
+ <el-input-number
|
|
|
+ v-model="blocklyTileY"
|
|
|
+ placeholder="纵向数量"
|
|
|
+ :min="1"
|
|
|
+ :step="1"
|
|
|
+ style="width: 120px"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <!-- 地图结束坐标 - 拆分为X轴和Y轴 -->
|
|
|
+ <el-form-item label="地图结束坐标" required>
|
|
|
+ <div class="coordinate-group">
|
|
|
+ X:
|
|
|
+ <el-input-number
|
|
|
+ v-model="blocklyEndPointX"
|
|
|
+ placeholder="X轴"
|
|
|
+ :min="1"
|
|
|
+ :max="blocklyTileX"
|
|
|
+ :step="1"
|
|
|
+ style="width: 120px"
|
|
|
+ />
|
|
|
+ Y:
|
|
|
+ <el-input-number
|
|
|
+ v-model="blocklyEndPointY"
|
|
|
+ placeholder="Y轴"
|
|
|
+ :min="1"
|
|
|
+ :max="blocklyTileY"
|
|
|
+ :step="1"
|
|
|
+ style="width: 120px"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 地图可行走坐标 - 动态矩阵显示 -->
|
|
|
+ <el-form-item label="地图方格配置" required>
|
|
|
+ <el-row type="flex" style="width: 100%;">
|
|
|
+ <!-- 左侧方格矩阵 -->
|
|
|
+ <el-col :span="10">
|
|
|
+ <div class="map-grid-container">
|
|
|
+ <div
|
|
|
+ class="map-grid"
|
|
|
+ :style="{ gridTemplateColumns: `repeat(${blocklyTileX || 5}, 1fr)`,
|
|
|
+ gridTemplateRows: `repeat(${blocklyTileY || 5}, 1fr)` ,
|
|
|
+ backgroundImage: formData.blocklyBackground ? `url(${formData.blocklyBackground})` : 'none', backgroundSize: 'cover',
|
|
|
+ backgroundPosition: 'center' }"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ v-for="(cell, index) in blocklyGridCells"
|
|
|
+ :key="index"
|
|
|
+ class="grid-cell"
|
|
|
+ :class="{
|
|
|
+ 'walkable': isBlocklyWalkable(cell.x, cell.y),
|
|
|
+ 'selected': selectedBlocklyPoint && selectedBlocklyPoint.x === cell.x && selectedBlocklyPoint.y === cell.y,
|
|
|
+ 'has-icon': getBlocklyPointByXY(cell.x, cell.y)?.img
|
|
|
+ }"
|
|
|
+ @click="selectBlocklyCell(cell)"
|
|
|
+ >
|
|
|
+ <div class="cell-coordinate">{{ cell.x }},{{ cell.y }}</div>
|
|
|
+ <div v-if="getBlocklyPointByXY(cell.x, cell.y)?.img" class="cell-icon">
|
|
|
+ <img :src="getBlocklyPointByXY(cell.x, cell.y).img" alt="图标" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <!-- 右侧配置项 -->
|
|
|
+ <el-col :span="14">
|
|
|
+ <div class="point-config-container" v-if="selectedBlocklyPoint">
|
|
|
+ <el-form
|
|
|
+ label-width="100px"
|
|
|
+ :model="selectedBlocklyPoint"
|
|
|
+ class="config-form"
|
|
|
+ size="large"
|
|
|
+ >
|
|
|
+ <el-card class="config-card" shadow="hover">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>坐标配置</span>
|
|
|
+ <el-tag size="small" type="info">{{ selectedBlocklyPoint.x }},{{ selectedBlocklyPoint.y }}</el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 第一行:是否可行走 -->
|
|
|
+ <div class="config-row">
|
|
|
+ <el-form-item label="是否可行走" class="config-item">
|
|
|
+ <el-switch
|
|
|
+ v-model="selectedBlocklyPoint.walkable"
|
|
|
+ active-text="是"
|
|
|
+ inactive-text="否"
|
|
|
+ size="large"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <template v-if="selectedBlocklyPoint.walkable">
|
|
|
+ <!-- 第二行:地图类型 -->
|
|
|
+ <div class="config-row">
|
|
|
+ <el-form-item label="地图类型" class="config-item">
|
|
|
+ <el-select
|
|
|
+ v-model="selectedBlocklyPoint.type"
|
|
|
+ placeholder="选择地图类型"
|
|
|
+ style="width: 100%"
|
|
|
+ clearable
|
|
|
+ size="large"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="dict in getStrDictOptions(DICT_TYPE.AI_BLOCKLY_MAP_TYPE)"
|
|
|
+ :key="dict.value"
|
|
|
+ :label="dict.label"
|
|
|
+ :value="dict.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item
|
|
|
+ label="是否必须完成"
|
|
|
+ class="config-item"
|
|
|
+ v-if="selectedBlocklyPoint.type === 'task'"
|
|
|
+ >
|
|
|
+ <el-switch
|
|
|
+ v-model="selectedBlocklyPoint.must"
|
|
|
+ active-text="是"
|
|
|
+ inactive-text="否"
|
|
|
+ size="large"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 第三行:图标上传 -->
|
|
|
+ <div class="config-row">
|
|
|
+ <el-form-item label="初始图标" class="config-item" v-if="selectedBlocklyPoint.type !== ''">
|
|
|
+ <div class="icon-upload-wrapper">
|
|
|
+ <UploadImg
|
|
|
+ v-model="selectedBlocklyPoint.img"
|
|
|
+ title="点击上传初始图标"
|
|
|
+ class="custom-upload"
|
|
|
+ />
|
|
|
+ <div v-if="selectedBlocklyPoint.img" class="upload-tip">已上传</div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item
|
|
|
+ label="完成图标"
|
|
|
+ class="config-item"
|
|
|
+ v-if="selectedBlocklyPoint.type !== '' && selectedBlocklyPoint.type === 'task'"
|
|
|
+ >
|
|
|
+ <div class="icon-upload-wrapper">
|
|
|
+ <UploadImg
|
|
|
+ v-model="selectedBlocklyPoint.endImg"
|
|
|
+ title="点击上传完成图标"
|
|
|
+ class="custom-upload"
|
|
|
+ />
|
|
|
+ <div v-if="selectedBlocklyPoint.endImg" class="upload-tip">已上传</div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-form-item label="提示语" class="form-item-full" v-if="selectedBlocklyPoint.type !== '' && selectedBlocklyPoint.type !== 'task'">
|
|
|
+ <el-input
|
|
|
+ v-model="selectedBlocklyPoint.tip"
|
|
|
+ placeholder="输入初始提示语"
|
|
|
+ style="width: 100%"
|
|
|
+ show-word-limit
|
|
|
+ maxlength="100"
|
|
|
+ type="textarea"
|
|
|
+ :rows="2"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="未完成提示语" class="form-item-full" v-if="selectedBlocklyPoint.type !== '' && selectedBlocklyPoint.type === 'task'">
|
|
|
+ <el-input
|
|
|
+ v-model="selectedBlocklyPoint.unfinishedTip"
|
|
|
+ placeholder="输入未完成提示语"
|
|
|
+ style="width: 100%"
|
|
|
+ show-word-limit
|
|
|
+ maxlength="100"
|
|
|
+ type="textarea"
|
|
|
+ :rows="2"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="完成提示语" class="form-item-full" v-if="selectedBlocklyPoint.type !== '' && selectedBlocklyPoint.type === 'task'">
|
|
|
+ <el-input
|
|
|
+ v-model="selectedBlocklyPoint.finishedTip"
|
|
|
+ placeholder="输入完成提示语"
|
|
|
+ style="width: 100%"
|
|
|
+ show-word-limit
|
|
|
+ maxlength="100"
|
|
|
+ type="textarea"
|
|
|
+ :rows="2"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
+ </el-card>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else class="no-selection">
|
|
|
+ <el-empty description="请点击左侧方格进行配置" />
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="saveBlocklyConfig" type="primary">确 定</el-button>
|
|
|
+ <el-button @click="blocklyConfigVisible = false">取 消</el-button>
|
|
|
+ </template>
|
|
|
+ </Dialog>
|
|
|
+</template>
|
|
|
+<script setup lang="ts">
|
|
|
+import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
|
|
|
+import { BlocklyApi, BlocklyVO } from '@/api/blockly/blockly'
|
|
|
+import { defaultProps, handleTree } from '@/utils/tree'
|
|
|
+import { BlocklyTypeApi } from '@/api/blockly/blocklyType'
|
|
|
+
|
|
|
+/** 课程 表单 */
|
|
|
+defineOptions({ name: 'BlocklyForm' })
|
|
|
+
|
|
|
+const { t } = useI18n() // 国际化
|
|
|
+const message = useMessage() // 消息弹窗
|
|
|
+
|
|
|
+const dialogVisible = ref(false) // 弹窗的是否展示
|
|
|
+const dialogTitle = ref('') // 弹窗的标题
|
|
|
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
|
|
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
|
|
+
|
|
|
+// Blockly配置弹框相关状态
|
|
|
+const blocklyConfigVisible = ref(false)
|
|
|
+const blocklyConfigTitle = ref('配置Blockly数据')
|
|
|
+const blocklyConfigFormRef = ref() // Blockly配置表单Ref
|
|
|
+
|
|
|
+// Blockly配置相关响应式数据
|
|
|
+const blocklyStartPointX = ref<number>()
|
|
|
+const blocklyStartPointY = ref<number>()
|
|
|
+const blocklyEndPointX = ref<number>()
|
|
|
+const blocklyEndPointY = ref<number>()
|
|
|
+const blocklyTileX = ref<number>(5) // 横向数量,默认5
|
|
|
+const blocklyTileY = ref<number>(5) // 纵向数量,默认5
|
|
|
+
|
|
|
+// 选中的点
|
|
|
+const selectedBlocklyPoint = ref<BlocklyWalkablePoint | null>(null)
|
|
|
+
|
|
|
+// 可行走点接口定义
|
|
|
+interface BlocklyWalkablePoint {
|
|
|
+ x: number
|
|
|
+ y: number
|
|
|
+ walkable: boolean
|
|
|
+ type: string
|
|
|
+ must: boolean
|
|
|
+ img: string
|
|
|
+ endImg: string
|
|
|
+ tip: string
|
|
|
+ unfinishedTip: string
|
|
|
+ finishedTip: string
|
|
|
+}
|
|
|
+
|
|
|
+// 可行走点数组
|
|
|
+const blocklyWalkablePoints = ref<BlocklyWalkablePoint[]>([])
|
|
|
+
|
|
|
+const formData = ref({
|
|
|
+ id: undefined,
|
|
|
+ bcName: undefined,
|
|
|
+ bcContentType: undefined,
|
|
|
+ bcContent: undefined,
|
|
|
+
|
|
|
+ blocklyInfo: undefined,
|
|
|
+ blocklyUserImage: 'https://learn-ai.com.cn/admin-api/infra/file/29/get/20251107/user_1762504554550.png',
|
|
|
+ blocklyUserDirection: 0,
|
|
|
+ blocklyTileSize: undefined, // 保持兼容性
|
|
|
+ blocklyStartPoint: undefined,
|
|
|
+ blocklyBackground: undefined,
|
|
|
+ blocklyEndPoint: undefined,
|
|
|
+ blocklyWalkablePoints: undefined,
|
|
|
+
|
|
|
+ bcIsInspect: "false",
|
|
|
+ bcType: undefined,
|
|
|
+ bcLabel: undefined,
|
|
|
+ bcOrder: undefined,
|
|
|
+ bcStatus: "0",
|
|
|
+ tenantId: undefined,
|
|
|
+})
|
|
|
+
|
|
|
+// 添加上传进度相关的状态
|
|
|
+const uploadProgress = ref(0)
|
|
|
+const isUploading = ref(false)
|
|
|
+
|
|
|
+// 所有格子数据
|
|
|
+const blocklyGridCells = computed(() => {
|
|
|
+ const cells = []
|
|
|
+ for (let y = 1; y <= (blocklyTileY.value || 5); y++) {
|
|
|
+ for (let x = 1; x <= (blocklyTileX.value || 5); x++) {
|
|
|
+ cells.push({ x, y })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return cells
|
|
|
+})
|
|
|
+
|
|
|
+// 计算属性:检查是否已配置Blockly数据
|
|
|
+const hasBlocklyConfig = computed(() => {
|
|
|
+ return formData.value.blocklyUserImage &&
|
|
|
+ formData.value.blocklyBackground &&
|
|
|
+ blocklyStartPointX.value !== undefined &&
|
|
|
+ blocklyStartPointY.value !== undefined &&
|
|
|
+ blocklyEndPointX.value !== undefined &&
|
|
|
+ blocklyEndPointY.value !== undefined &&
|
|
|
+ blocklyTileX.value !== undefined &&
|
|
|
+ blocklyTileY.value !== undefined
|
|
|
+})
|
|
|
+
|
|
|
+const formRules = reactive({
|
|
|
+ bcType: [{ required: true, message: '课程类型不能为空', trigger: 'blur' }],
|
|
|
+ bcName: [{ required: true, message: '课程名称不能为空', trigger: 'blur' }],
|
|
|
+ bcLabel: [{ required: true, message: '课程标签不能为空', trigger: 'blur' }],
|
|
|
+ bcOrder: [{ required: true, message: '课程排序不能为空', trigger: 'blur' }],
|
|
|
+ // 当内容类型为blockly时,需要验证blockly配置
|
|
|
+ blocklyConfig: [{
|
|
|
+ validator: (rule: any, value: any, callback: any) => {
|
|
|
+ if (formData.value.bcContentType === 'blockly' && !hasBlocklyConfig.value) {
|
|
|
+ callback(new Error('请配置Blockly数据'))
|
|
|
+ } else {
|
|
|
+ callback()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: 'blur'
|
|
|
+ }]
|
|
|
+})
|
|
|
+
|
|
|
+// Blockly配置表单验证规则
|
|
|
+const blocklyConfigRules = reactive({
|
|
|
+ blocklyUserImage: [{ required: true, message: '人物图标不能为空', trigger: 'blur' }],
|
|
|
+ blocklyBackground: [{ required: true, message: '地图背景图不能为空', trigger: 'blur' }],
|
|
|
+ blocklyUserDirection: [{ required: true, message: '人物朝向不能为空', trigger: 'blur' }],
|
|
|
+ // blocklyInfo: [{ required: true, message: 'Blockly信息不能为空', trigger: 'blur' }]
|
|
|
+})
|
|
|
+
|
|
|
+const formRef = ref() // 表单 Ref
|
|
|
+const bcTypeTree = ref() // 树形结构
|
|
|
+
|
|
|
+/** 获取指定坐标的点配置 */
|
|
|
+const getBlocklyPointByXY = (x: number, y: number): BlocklyWalkablePoint | undefined => {
|
|
|
+ return blocklyWalkablePoints.value.find(point => point.x === x && point.y === y)
|
|
|
+}
|
|
|
+
|
|
|
+/** 判断指定坐标是否可行走 */
|
|
|
+const isBlocklyWalkable = (x: number, y: number): boolean => {
|
|
|
+ const point = getBlocklyPointByXY(x, y)
|
|
|
+ return point ? point.walkable : false
|
|
|
+}
|
|
|
+
|
|
|
+/** 选择格子 */
|
|
|
+const selectBlocklyCell = (cell: { x: number, y: number }) => {
|
|
|
+ // 查找是否已有该点的配置
|
|
|
+ let point = getBlocklyPointByXY(cell.x, cell.y)
|
|
|
+
|
|
|
+ // 如果没有,创建新的配置
|
|
|
+ if (!point) {
|
|
|
+ point = {
|
|
|
+ x: cell.x,
|
|
|
+ y: cell.y,
|
|
|
+ walkable: false,
|
|
|
+ type: '',
|
|
|
+ must: false,
|
|
|
+ img: '',
|
|
|
+ endImg: '',
|
|
|
+ tip: '',
|
|
|
+ unfinishedTip: '',
|
|
|
+ finishedTip: ''
|
|
|
+ }
|
|
|
+ blocklyWalkablePoints.value.push(point)
|
|
|
+ }
|
|
|
+
|
|
|
+ selectedBlocklyPoint.value = point
|
|
|
+}
|
|
|
+
|
|
|
+/** 打开弹窗 */
|
|
|
+const open = async (type: string, id?: number) => {
|
|
|
+ dialogVisible.value = true
|
|
|
+ dialogTitle.value = t('action.' + type)
|
|
|
+ formType.value = type
|
|
|
+ resetForm()
|
|
|
+ // 修改时,设置数据
|
|
|
+ if (id) {
|
|
|
+ formLoading.value = true
|
|
|
+ try {
|
|
|
+ const blocklyData = await BlocklyApi.getBlockly(id)
|
|
|
+ formData.value = {
|
|
|
+ ...formData.value,
|
|
|
+ ...blocklyData,
|
|
|
+ bcContent: blocklyData.bcType === "image" ? blocklyData.bcContent?.split(',') : blocklyData.bcContent
|
|
|
+ }
|
|
|
+ // 确保 bcType 为正确的 id 类型
|
|
|
+ formData.value.bcType = blocklyData.bcType ? Number(blocklyData.bcType) : undefined
|
|
|
+
|
|
|
+ // 处理回显数据 - 解析坐标JSON
|
|
|
+ if (blocklyData.blocklyStartPoint) {
|
|
|
+ try {
|
|
|
+ const point = JSON.parse(blocklyData.blocklyStartPoint)
|
|
|
+ blocklyStartPointX.value = point.x
|
|
|
+ blocklyStartPointY.value = point.y
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析地图开始坐标失败:', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (blocklyData.blocklyEndPoint) {
|
|
|
+ try {
|
|
|
+ const point = JSON.parse(blocklyData.blocklyEndPoint)
|
|
|
+ blocklyEndPointX.value = point.x
|
|
|
+ blocklyEndPointY.value = point.y
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析地图结束坐标失败:', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析地图尺寸
|
|
|
+ if (blocklyData.blocklyTileSize) {
|
|
|
+ try {
|
|
|
+ const point = JSON.parse(blocklyData.blocklyTileSize)
|
|
|
+ blocklyTileX.value = point.x
|
|
|
+ blocklyTileY.value = point.y
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析地图尺寸失败:', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析地图可行走点
|
|
|
+ if (blocklyData.blocklyWalkablePoints) {
|
|
|
+ try {
|
|
|
+ const points = JSON.parse(blocklyData.blocklyWalkablePoints)
|
|
|
+ blocklyWalkablePoints.value = points.map((p: any) => ({
|
|
|
+ x: p.x,
|
|
|
+ y: p.y,
|
|
|
+ walkable: true,
|
|
|
+ type: p.type || '',
|
|
|
+ must: p.must || false,
|
|
|
+ img: p.img || '',
|
|
|
+ endImg: p.endImg || '',
|
|
|
+ tip: p.tip || '',
|
|
|
+ unfinishedTip: p.unfinishedTip || '',
|
|
|
+ finishedTip: p.finishedTip || ''
|
|
|
+ }))
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析地图可行走坐标失败:', e)
|
|
|
+ blocklyWalkablePoints.value = []
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ formLoading.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ await getbcTypeTree()
|
|
|
+}
|
|
|
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
|
|
+
|
|
|
+/** 打开Blockly配置弹框 */
|
|
|
+const openBlocklyConfigDialog = () => {
|
|
|
+ blocklyConfigVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+/** 保存Blockly配置 */
|
|
|
+const saveBlocklyConfig = async () => {
|
|
|
+ // 坐标数据验证
|
|
|
+ if (blocklyStartPointX.value === undefined || blocklyStartPointY.value === undefined) {
|
|
|
+ message.error('地图开始坐标不能为空')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (blocklyEndPointX.value === undefined || blocklyEndPointY.value === undefined) {
|
|
|
+ message.error('地图结束坐标不能为空')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (blocklyTileX.value === undefined || blocklyTileY.value === undefined) {
|
|
|
+ message.error('地图方格尺寸不能为空')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 校验Blockly配置表单
|
|
|
+ await blocklyConfigFormRef.value.validate()
|
|
|
+
|
|
|
+ // 设置坐标JSON
|
|
|
+ formData.value.blocklyTileSize = JSON.stringify({ x: blocklyTileX.value, y: blocklyTileY.value })
|
|
|
+ formData.value.blocklyStartPoint = JSON.stringify({ x: blocklyStartPointX.value, y: blocklyStartPointY.value })
|
|
|
+ formData.value.blocklyEndPoint = JSON.stringify({ x: blocklyEndPointX.value, y: blocklyEndPointY.value })
|
|
|
+
|
|
|
+ // 只保存可行走的点
|
|
|
+ const walkableOnlyPoints = blocklyWalkablePoints.value.filter(point => point.walkable)
|
|
|
+ formData.value.blocklyWalkablePoints = JSON.stringify(walkableOnlyPoints)
|
|
|
+
|
|
|
+ blocklyConfigVisible.value = false
|
|
|
+ message.success('Blockly配置已保存')
|
|
|
+}
|
|
|
+
|
|
|
+/** 提交表单 */
|
|
|
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
|
|
+const submitForm = async () => {
|
|
|
+ // 校验表单
|
|
|
+ await formRef.value.validate()
|
|
|
+
|
|
|
+ // 特殊校验:当内容类型为blockly时,需要验证blockly配置
|
|
|
+ if (formData.value.bcContentType === 'blockly' && !hasBlocklyConfig.value) {
|
|
|
+ message.error('请先配置Blockly数据')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提交请求
|
|
|
+ formLoading.value = true
|
|
|
+ try {
|
|
|
+ const data = { ...formData.value } as unknown as BlocklyVO
|
|
|
+ if (data.bcContentType === 'image') {
|
|
|
+ data.bcContent = data.bcContent?.join(',')
|
|
|
+ }
|
|
|
+ if (formType.value === 'create') {
|
|
|
+ await BlocklyApi.createBlockly(data)
|
|
|
+ message.success(t('common.createSuccess'))
|
|
|
+ } else {
|
|
|
+ await BlocklyApi.updateBlockly(data)
|
|
|
+ message.success(t('common.updateSuccess'))
|
|
|
+ }
|
|
|
+ dialogVisible.value = false
|
|
|
+
|
|
|
+ // 发送操作成功的事件
|
|
|
+ emit('success')
|
|
|
+ } finally {
|
|
|
+ formLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 添加处理上传进度的方法
|
|
|
+const handleUploadProgress = (progress: number) => {
|
|
|
+ uploadProgress.value = progress
|
|
|
+}
|
|
|
+
|
|
|
+const handleUploadStart = () => {
|
|
|
+ isUploading.value = true
|
|
|
+ uploadProgress.value = 0
|
|
|
+}
|
|
|
+
|
|
|
+const handleUploadComplete = () => {
|
|
|
+ isUploading.value = false
|
|
|
+ uploadProgress.value = 100
|
|
|
+}
|
|
|
+
|
|
|
+/** 重置表单 */
|
|
|
+const resetForm = () => {
|
|
|
+ formData.value = {
|
|
|
+ id: undefined,
|
|
|
+ bcName: undefined,
|
|
|
+ bcContentType: undefined,
|
|
|
+ bcContent: undefined,
|
|
|
+
|
|
|
+ blocklyInfo: undefined,
|
|
|
+ blocklyUserImage: 'https://learn-ai.com.cn/admin-api/infra/file/29/get/20251107/user_1762504554550.png',
|
|
|
+ blocklyUserDirection: 0,
|
|
|
+ blocklyTileSize: undefined, // 保持兼容性
|
|
|
+ blocklyStartPoint: undefined,
|
|
|
+ blocklyBackground: undefined,
|
|
|
+ blocklyEndPoint: undefined,
|
|
|
+ blocklyWalkablePoints: undefined,
|
|
|
+
|
|
|
+ bcIsInspect: "false",
|
|
|
+ bcType: undefined,
|
|
|
+ bcLabel: undefined,
|
|
|
+ bcOrder: undefined,
|
|
|
+ bcStatus: "0",
|
|
|
+ tenantId: undefined,
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重置Blockly配置相关数据
|
|
|
+ blocklyStartPointX.value = undefined
|
|
|
+ blocklyStartPointY.value = undefined
|
|
|
+ blocklyEndPointX.value = undefined
|
|
|
+ blocklyEndPointY.value = undefined
|
|
|
+ blocklyTileX.value = 5
|
|
|
+ blocklyTileY.value = 5
|
|
|
+ blocklyWalkablePoints.value = []
|
|
|
+ selectedBlocklyPoint.value = null
|
|
|
+
|
|
|
+ formRef.value?.resetFields()
|
|
|
+}
|
|
|
+/** 获得课程-类型树 */
|
|
|
+const getbcTypeTree = async () => {
|
|
|
+ bcTypeTree.value = []
|
|
|
+ const data = await BlocklyTypeApi.getBlocklyTypeSimpleList()
|
|
|
+ const root: Tree = { id: 0, ctType: '课程类型', children: [] }
|
|
|
+ root.children = handleTree(data, 'id', 'ctParentId')
|
|
|
+ bcTypeTree.value.push(root)
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style>
|
|
|
+.demo-tabs > .el-tabs__content {
|
|
|
+ padding: 32px;
|
|
|
+ color: #6b778c;
|
|
|
+ font-size: 32px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.demo-tabs .custom-tabs-label .el-icon {
|
|
|
+ vertical-align: middle;
|
|
|
+}
|
|
|
+.demo-tabs .custom-tabs-label span {
|
|
|
+ vertical-align: middle;
|
|
|
+ margin-left: 4px;
|
|
|
+}
|
|
|
+.uploadProgress{
|
|
|
+ width: 50%;
|
|
|
+}
|
|
|
+
|
|
|
+/* 与MapGameForm.vue保持一致的样式 */
|
|
|
+.coordinate-group {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.map-grid-container {
|
|
|
+ overflow: auto;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 10px;
|
|
|
+ background-color: #f9f9f9;
|
|
|
+}
|
|
|
+
|
|
|
+.map-grid {
|
|
|
+ display: grid;
|
|
|
+ gap: 2px;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.grid-cell {
|
|
|
+ aspect-ratio: 1;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ background-color: rgba(255, 255, 255, 0.9);
|
|
|
+ cursor: pointer;
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition: all 0.3s;
|
|
|
+ min-height: 40px;
|
|
|
+}
|
|
|
+
|
|
|
+.grid-cell:hover {
|
|
|
+ border-color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.grid-cell.selected {
|
|
|
+ border: 3px solid #409eff;
|
|
|
+ background-color: rgba(236, 245, 255, 0.8);
|
|
|
+ box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.3);
|
|
|
+ transform: scale(1.05);
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.grid-cell.selected.walkable {
|
|
|
+ border: 3px solid #409eff;
|
|
|
+ background-color: rgba(230, 247, 233, 0.8);
|
|
|
+ box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.grid-cell.selected.has-icon {
|
|
|
+ border: 3px solid #409eff;
|
|
|
+ background-color: rgba(255, 243, 230, 0.8);
|
|
|
+ box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.grid-cell.walkable {
|
|
|
+ background-color: rgba(240, 249, 235, 0.8);
|
|
|
+ border: 3px solid rgb(103, 194, 58);
|
|
|
+}
|
|
|
+
|
|
|
+.grid-cell.has-icon {
|
|
|
+ background-color: rgba(253, 246, 236, 0.8);
|
|
|
+}
|
|
|
+
|
|
|
+.cell-coordinate {
|
|
|
+ position: absolute;
|
|
|
+ top: 2px;
|
|
|
+ left: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+.cell-icon {
|
|
|
+ max-width: 80%;
|
|
|
+ max-height: 80%;
|
|
|
+}
|
|
|
+
|
|
|
+.cell-icon img {
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
+}
|
|
|
+
|
|
|
+/* 右侧配置区域样式 - 修复高度显示问题 */
|
|
|
+.point-config-container {
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.config-form {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.config-card {
|
|
|
+ margin-bottom: 0;
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+/* 配置项行布局 */
|
|
|
+.config-row {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 20px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ align-items: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+.config-item {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 200px;
|
|
|
+ margin-bottom: 0 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.form-item-full {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图标上传包装器 */
|
|
|
+.icon-upload-wrapper {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-tip {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #67c23a;
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.no-selection {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 300px;
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px dashed #dcdfe6;
|
|
|
+}
|
|
|
+
|
|
|
+/* 优化滚动条样式 */
|
|
|
+.point-config-container::-webkit-scrollbar {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.point-config-container::-webkit-scrollbar-thumb {
|
|
|
+ border-radius: 4px;
|
|
|
+ background: #c0c4cc;
|
|
|
+}
|
|
|
+
|
|
|
+.point-config-container::-webkit-scrollbar-track {
|
|
|
+ background: #f0f0f0;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 调整标签宽度以适应行布局 */
|
|
|
+:deep(.el-form-item__label) {
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式调整 */
|
|
|
+@media screen and (max-width: 768px) {
|
|
|
+ .config-row {
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .config-item {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|