|
|
@@ -0,0 +1,1387 @@
|
|
|
+<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}`,
|
|
|
+ disabled: (node) => node.ctTypeNode === undefined || node.ctTypeNode === '0'
|
|
|
+ }"
|
|
|
+ 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="请选择课程标签"
|
|
|
+ @change="()=>{
|
|
|
+ if(!formData.bcName){
|
|
|
+ const selectedDict = getStrDictOptions(DICT_TYPE.BLOCKLY_COURSE_LABEL).find(
|
|
|
+ dict => dict.value === formData.bcLabel
|
|
|
+ );
|
|
|
+ if(selectedDict){
|
|
|
+ formData.bcName = selectedDict.label;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }"
|
|
|
+ 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.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="24">
|
|
|
+ <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="24">
|
|
|
+ <el-form-item label="路线配置" required>
|
|
|
+ <el-button type="primary" @click="addRoute" style="margin-bottom: 12px;">新增路线</el-button>
|
|
|
+ <div class="route-config-container">
|
|
|
+ <div v-for="(route, index) in blocklyRoutes" :key="route.id" class="route-config-item">
|
|
|
+ <div class="route-row">
|
|
|
+ <div class="route-title">
|
|
|
+ <span>路线 {{ index + 1 }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="route-fields">
|
|
|
+
|
|
|
+ <!-- 人物朝向 -->
|
|
|
+ <div class="route-field">
|
|
|
+ <span class="field-label">人物朝向:</span>
|
|
|
+ <el-radio-group v-model="route.direction" class="direction-radio">
|
|
|
+ <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>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 开始坐标 -->
|
|
|
+ <div class="route-field">
|
|
|
+ <span class="field-label">开始坐标:</span>
|
|
|
+ <div class="coordinate-group">
|
|
|
+ <el-input-number
|
|
|
+ v-model="route.startPoint.x"
|
|
|
+ placeholder="X"
|
|
|
+ :min="1"
|
|
|
+ :max="blocklyTileX"
|
|
|
+ :step="1"
|
|
|
+ class="coordinate-input"
|
|
|
+ style="width: 100px"
|
|
|
+ />
|
|
|
+ <el-input-number
|
|
|
+ v-model="route.startPoint.y"
|
|
|
+ placeholder="Y"
|
|
|
+ :min="1"
|
|
|
+ :max="blocklyTileY"
|
|
|
+ :step="1"
|
|
|
+ class="coordinate-input"
|
|
|
+ style="width: 100px"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 结束坐标 -->
|
|
|
+ <div class="route-field">
|
|
|
+ <span class="field-label">结束坐标:</span>
|
|
|
+ <div class="coordinate-group">
|
|
|
+ <el-input-number
|
|
|
+ v-model="route.endPoint.x"
|
|
|
+ placeholder="X"
|
|
|
+ :min="1"
|
|
|
+ :max="blocklyTileX"
|
|
|
+ :step="1"
|
|
|
+ class="coordinate-input"
|
|
|
+ style="width: 100px"
|
|
|
+ />
|
|
|
+ <el-input-number
|
|
|
+ v-model="route.endPoint.y"
|
|
|
+ placeholder="Y"
|
|
|
+ :min="1"
|
|
|
+ :max="blocklyTileY"
|
|
|
+ :step="1"
|
|
|
+ class="coordinate-input"
|
|
|
+ style="width: 100px"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 删除按钮 -->
|
|
|
+ <div class="route-delete-btn">
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ size="small"
|
|
|
+ @click="deleteRoute(route.id)"
|
|
|
+ :disabled="blocklyRoutes.length <= 1"
|
|
|
+ plain
|
|
|
+ >
|
|
|
+ 删除
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </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 || 6}, 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-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>
|
|
|
+ <el-form
|
|
|
+ label-width="100px"
|
|
|
+ :model="selectedBlocklyPoint"
|
|
|
+ class="config-form"
|
|
|
+ size="large"
|
|
|
+ >
|
|
|
+
|
|
|
+ <!-- 第一行:是否可行走 -->
|
|
|
+ <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" style="display: flex; flex-wrap: wrap; gap: 20px;">
|
|
|
+ <el-form-item label="地图类型" class="config-item"
|
|
|
+ :style="(selectedBlocklyPoint.type === 'task' || selectedBlocklyPoint.type === 'item' || selectedBlocklyPoint.type === 'mark') ? 'width: calc(50% - 20px);' : 'width: 100%;'">
|
|
|
+ <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 === 'item'"
|
|
|
+ style="width: calc(50% - 20px);"
|
|
|
+ >
|
|
|
+ <el-switch
|
|
|
+ v-model="selectedBlocklyPoint.must"
|
|
|
+ active-text="是"
|
|
|
+ inactive-text="否"
|
|
|
+ size="large"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item
|
|
|
+ label="限制通行"
|
|
|
+ class="config-item"
|
|
|
+ v-if="selectedBlocklyPoint.type === 'task'"
|
|
|
+ style="width: calc(50% - 20px);"
|
|
|
+ >
|
|
|
+ <el-switch
|
|
|
+ v-model="selectedBlocklyPoint.noPassing"
|
|
|
+ active-text="是"
|
|
|
+ inactive-text="否"
|
|
|
+ size="large"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item
|
|
|
+ label="标记标签"
|
|
|
+ class="config-item"
|
|
|
+ v-if="selectedBlocklyPoint.type === 'mark'"
|
|
|
+ style="width: calc(50% - 20px);"
|
|
|
+ >
|
|
|
+ <el-select
|
|
|
+ v-model="selectedBlocklyPoint.mark"
|
|
|
+ placeholder="标记标签"
|
|
|
+ clearable
|
|
|
+ size="large"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="dict in getStrDictOptions(DICT_TYPE.BLOCKLY_MAP_MARK)"
|
|
|
+ :key="dict.value"
|
|
|
+ :label="dict.label"
|
|
|
+ :value="dict.value"
|
|
|
+ @click="()=>{selectedBlocklyPoint.img = dict.cssClass}"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 第三行:图标上传 -->
|
|
|
+ <div class="config-row" style="display: flex; flex-wrap: wrap; gap: 20px;">
|
|
|
+ <el-form-item label="初始图标" class="config-item"
|
|
|
+ :style="selectedBlocklyPoint.type === 'task' ? 'width: calc(50% - 20px);' : 'width: 100%;'">
|
|
|
+ <div class="icon-upload-wrapper">
|
|
|
+ <UploadImg
|
|
|
+ :disabled="selectedBlocklyPoint.type === 'mark'"
|
|
|
+ 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'"
|
|
|
+ style="width: calc(50% - 20px);"
|
|
|
+ >
|
|
|
+ <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>
|
|
|
+
|
|
|
+ <!-- 完成动画字段 -->
|
|
|
+ <el-form-item
|
|
|
+ label="完成动画"
|
|
|
+ class="config-item"
|
|
|
+ v-if="selectedBlocklyPoint.type && selectedBlocklyPoint.type === 'task'"
|
|
|
+ >
|
|
|
+ <div class="icon-upload-wrapper">
|
|
|
+ <UploadVideo
|
|
|
+ v-model="selectedBlocklyPoint.finishAnimation"
|
|
|
+ title="点击上传完成动画"
|
|
|
+ class="custom-upload"
|
|
|
+ @upload-progress="handleBlocklyUploadProgress"
|
|
|
+ @upload-start="handleBlocklyUploadStart"
|
|
|
+ @upload-complete="handleBlocklyUploadComplete"
|
|
|
+ />
|
|
|
+ <div v-if="selectedBlocklyPoint.finishAnimation" class="upload-tip">已上传</div>
|
|
|
+ <!-- Blockly配置弹框中的视频上传进度条 -->
|
|
|
+ <div v-if="blocklyIsUploading" class="uploadProgress">
|
|
|
+ <el-progress :percentage="blocklyUploadProgress" />
|
|
|
+ <div class="text-xs text-gray-500 text-right mt-1">{{ blocklyUploadProgress }}% 已上传</div>
|
|
|
+ </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-form>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else class="no-selection">
|
|
|
+ <el-empty description="请点击左侧方格进行配置" />
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="特殊方块" prop="blocklySpecialBlocks">
|
|
|
+ <el-select
|
|
|
+ v-model="formData.blocklySpecialBlocks"
|
|
|
+ placeholder="请选择特殊方块"
|
|
|
+ multiple
|
|
|
+ clearable
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="dict in getStrDictOptions(DICT_TYPE.BLOCKLY_MAP_SPECIAL)"
|
|
|
+ :key="dict.value"
|
|
|
+ :label="dict.label"
|
|
|
+ :value="dict.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="saveBlocklyConfig" type="primary" :disabled="blocklyIsUploading">确 定</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 blocklyTileX = ref<number>(6) // 横向数量,默认5
|
|
|
+const blocklyTileY = ref<number>(5) // 纵向数量,默认5
|
|
|
+
|
|
|
+// 选中的点
|
|
|
+const selectedBlocklyPoint = ref<BlocklyWalkablePoint | null>(null)
|
|
|
+
|
|
|
+// 路线ID计数器
|
|
|
+const routeIdCounter = ref(1)
|
|
|
+
|
|
|
+// 可行走点接口定义
|
|
|
+interface BlocklyWalkablePoint {
|
|
|
+ x: number
|
|
|
+ y: number
|
|
|
+ walkable: boolean
|
|
|
+ type: string
|
|
|
+ must: boolean
|
|
|
+ mark: String
|
|
|
+ noPassing: boolean
|
|
|
+ img: string
|
|
|
+ endImg: string
|
|
|
+ finishAnimation: string
|
|
|
+ tip: string
|
|
|
+ unfinishedTip: string
|
|
|
+ finishedTip: string
|
|
|
+}
|
|
|
+
|
|
|
+// 路线接口定义
|
|
|
+interface BlocklyRoute {
|
|
|
+ id: number
|
|
|
+ direction: number
|
|
|
+ startPoint: { x: number, y: number }
|
|
|
+ endPoint: { x: number, y: number }
|
|
|
+}
|
|
|
+
|
|
|
+// 可行走点数组
|
|
|
+const blocklyWalkablePoints = ref<BlocklyWalkablePoint[]>([])
|
|
|
+
|
|
|
+// 路线数组
|
|
|
+const blocklyRoutes = ref<BlocklyRoute[]>([])
|
|
|
+
|
|
|
+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',
|
|
|
+ blocklyTileSize: undefined,
|
|
|
+ blocklyBackground: undefined,
|
|
|
+ blocklyWalkablePoints: undefined,
|
|
|
+ blocklySpecialBlocks: undefined,
|
|
|
+ blocklyRouteList: undefined,
|
|
|
+
|
|
|
+ bcIsInspect: "false",
|
|
|
+ bcType: undefined,
|
|
|
+ bcLabel: undefined,
|
|
|
+ bcOrder: undefined,
|
|
|
+ bcStatus: "0",
|
|
|
+ tenantId: undefined,
|
|
|
+})
|
|
|
+
|
|
|
+// 添加上传进度相关的状态
|
|
|
+const uploadProgress = ref(0)
|
|
|
+const isUploading = ref(false)
|
|
|
+
|
|
|
+// 为Blockly配置弹框中的完成动画视频上传添加独立的进度状态
|
|
|
+const blocklyUploadProgress = ref(0)
|
|
|
+const blocklyIsUploading = ref(false)
|
|
|
+
|
|
|
+// 所有格子数据
|
|
|
+const blocklyGridCells = computed(() => {
|
|
|
+ const cells = []
|
|
|
+ for (let y = 1; y <= (blocklyTileY.value || 6); 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 &&
|
|
|
+ blocklyRoutes.value.length > 0 &&
|
|
|
+ blocklyRoutes.value.every(route =>
|
|
|
+ route.startPoint.x !== undefined &&
|
|
|
+ route.startPoint.y !== undefined &&
|
|
|
+ route.endPoint.x !== undefined &&
|
|
|
+ route.endPoint.y !== 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' }]
|
|
|
+ // 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,
|
|
|
+ mark: '',
|
|
|
+ noPassing: false,
|
|
|
+ img: '',
|
|
|
+ endImg: '',
|
|
|
+ finishAnimation: '',
|
|
|
+ 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 (type === 'create' && id) {
|
|
|
+ // 确保 bcType 为正确的 id 类型
|
|
|
+ formData.value.bcType = id ? Number(id) : undefined
|
|
|
+ }
|
|
|
+ // 修改时,设置数据
|
|
|
+ if (type === 'update' && 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,
|
|
|
+ bcInfo: blocklyData.bcInfo || ''
|
|
|
+ }
|
|
|
+ // 确保 bcType 为正确的 id 类型
|
|
|
+ formData.value.bcType = blocklyData.bcType ? Number(blocklyData.bcType) : undefined
|
|
|
+
|
|
|
+ // 处理回显数据 - 解析路线JSON
|
|
|
+ if (blocklyData.blocklyRouteList) {
|
|
|
+ try {
|
|
|
+ blocklyRoutes.value = JSON.parse(blocklyData.blocklyRouteList)
|
|
|
+ routeIdCounter.value = Math.max(...blocklyRoutes.value.map(route => route.id)) + 1
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析路线数据失败:', e)
|
|
|
+ // 如果没有路线数据,使用旧的单路线数据
|
|
|
+ blocklyRoutes.value = []
|
|
|
+ if (blocklyData.blocklyStartPoint && blocklyData.blocklyEndPoint) {
|
|
|
+ try {
|
|
|
+ const startPoint = JSON.parse(blocklyData.blocklyStartPoint)
|
|
|
+ const endPoint = JSON.parse(blocklyData.blocklyEndPoint)
|
|
|
+ blocklyRoutes.value.push({
|
|
|
+ id: routeIdCounter.value++,
|
|
|
+ direction: blocklyData.blocklyUserDirection || 0,
|
|
|
+ startPoint: startPoint,
|
|
|
+ endPoint: endPoint
|
|
|
+ })
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析旧路线数据失败:', e)
|
|
|
+ blocklyRoutes.value = [{
|
|
|
+ id: routeIdCounter.value++,
|
|
|
+ direction: 0,
|
|
|
+ startPoint: { x: undefined, y: undefined },
|
|
|
+ endPoint: { x: undefined, y: undefined }
|
|
|
+ }]
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ blocklyRoutes.value = [{
|
|
|
+ id: routeIdCounter.value++,
|
|
|
+ direction: 0,
|
|
|
+ startPoint: { x: undefined, y: undefined },
|
|
|
+ endPoint: { x: undefined, y: undefined }
|
|
|
+ }]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 兼容旧数据结构
|
|
|
+ blocklyRoutes.value = []
|
|
|
+ if (blocklyData.blocklyStartPoint && blocklyData.blocklyEndPoint) {
|
|
|
+ try {
|
|
|
+ const startPoint = JSON.parse(blocklyData.blocklyStartPoint)
|
|
|
+ const endPoint = JSON.parse(blocklyData.blocklyEndPoint)
|
|
|
+ blocklyRoutes.value.push({
|
|
|
+ id: routeIdCounter.value++,
|
|
|
+ direction: blocklyData.blocklyUserDirection || 0,
|
|
|
+ startPoint: startPoint,
|
|
|
+ endPoint: endPoint
|
|
|
+ })
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析旧路线数据失败:', e)
|
|
|
+ blocklyRoutes.value = [{
|
|
|
+ id: routeIdCounter.value++,
|
|
|
+ direction: 0,
|
|
|
+ startPoint: { x: undefined, y: undefined },
|
|
|
+ endPoint: { x: undefined, y: undefined }
|
|
|
+ }]
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ blocklyRoutes.value = [{
|
|
|
+ id: routeIdCounter.value++,
|
|
|
+ direction: 0,
|
|
|
+ startPoint: { x: undefined, y: undefined },
|
|
|
+ endPoint: { x: undefined, y: undefined }
|
|
|
+ }]
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析地图尺寸
|
|
|
+ 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,
|
|
|
+ mark: p.mark || '',
|
|
|
+ noPassing: p.noPassing || false,
|
|
|
+ img: p.img || '',
|
|
|
+ endImg: p.endImg || '',
|
|
|
+ finishAnimation: p.finishAnimation || '',
|
|
|
+ tip: p.tip || '',
|
|
|
+ unfinishedTip: p.unfinishedTip || '',
|
|
|
+ finishedTip: p.finishedTip || ''
|
|
|
+ }))
|
|
|
+ } catch (e) {
|
|
|
+ console.error('解析地图可行走坐标失败:', e)
|
|
|
+ blocklyWalkablePoints.value = []
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理 blocklySpecialBlocks 字段,将字符串转换为数组
|
|
|
+ if (blocklyData.blocklySpecialBlocks) {
|
|
|
+ formData.value.blocklySpecialBlocks = blocklyData.blocklySpecialBlocks.split(',')
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ formLoading.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ await getbcTypeTree()
|
|
|
+}
|
|
|
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
|
|
+
|
|
|
+/** 打开Blockly配置弹框 */
|
|
|
+const openBlocklyConfigDialog = () => {
|
|
|
+ blocklyConfigVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 添加新路线
|
|
|
+const addRoute = () => {
|
|
|
+ blocklyRoutes.value.push({
|
|
|
+ id: routeIdCounter.value++,
|
|
|
+ direction: 0,
|
|
|
+ startPoint: { x: undefined, y: undefined },
|
|
|
+ endPoint: { x: undefined, y: undefined }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 删除路线
|
|
|
+const deleteRoute = (routeId: number) => {
|
|
|
+ if (blocklyRoutes.value.length <= 1) {
|
|
|
+ message.warning('不能删除最后一条路线')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const index = blocklyRoutes.value.findIndex(route => route.id === routeId)
|
|
|
+ if (index > -1) {
|
|
|
+ blocklyRoutes.value.splice(index, 1)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 保存Blockly配置 */
|
|
|
+const saveBlocklyConfig = async () => {
|
|
|
+ // 坐标数据验证
|
|
|
+ if (blocklyRoutes.value.length === 0) {
|
|
|
+ message.error('至少需要配置一条路线')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ for (let i = 0; i < blocklyRoutes.value.length; i++) {
|
|
|
+ const route = blocklyRoutes.value[i]
|
|
|
+ if (route.startPoint.x === undefined || route.startPoint.y === undefined) {
|
|
|
+ message.error(`路线 ${i + 1} 的开始坐标不能为空`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (route.endPoint.x === undefined || route.endPoint.y === undefined) {
|
|
|
+ message.error(`路线 ${i + 1} 的结束坐标不能为空`)
|
|
|
+ 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.blocklyRouteList = JSON.stringify(blocklyRoutes.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(',')
|
|
|
+ }
|
|
|
+ // 将 blocklySpecialBlocks 数组转换为字符串
|
|
|
+ if (data.blocklySpecialBlocks && Array.isArray(data.blocklySpecialBlocks)) {
|
|
|
+ data.blocklySpecialBlocks = data.blocklySpecialBlocks.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
|
|
|
+}
|
|
|
+
|
|
|
+// Blockly配置弹框中完成动画视频上传的进度处理方法
|
|
|
+const handleBlocklyUploadProgress = (progress: number) => {
|
|
|
+ blocklyUploadProgress.value = progress
|
|
|
+}
|
|
|
+
|
|
|
+const handleBlocklyUploadStart = () => {
|
|
|
+ blocklyIsUploading.value = true
|
|
|
+ blocklyUploadProgress.value = 0
|
|
|
+}
|
|
|
+
|
|
|
+const handleBlocklyUploadComplete = () => {
|
|
|
+ blocklyIsUploading.value = false
|
|
|
+ blocklyUploadProgress.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',
|
|
|
+ blocklyTileSize: undefined,
|
|
|
+ blocklyBackground: undefined,
|
|
|
+ blocklyWalkablePoints: undefined,
|
|
|
+ blocklySpecialBlocks: undefined,
|
|
|
+ blocklyRouteList: undefined,
|
|
|
+
|
|
|
+ bcIsInspect: "false",
|
|
|
+ bcType: undefined,
|
|
|
+ bcLabel: undefined,
|
|
|
+ bcOrder: undefined,
|
|
|
+ bcStatus: "0",
|
|
|
+ tenantId: undefined,
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重置Blockly配置相关数据
|
|
|
+ blocklyTileX.value = 6
|
|
|
+ blocklyTileY.value = 5
|
|
|
+ blocklyWalkablePoints.value = []
|
|
|
+ selectedBlocklyPoint.value = null
|
|
|
+
|
|
|
+ // 重置路线数据
|
|
|
+ blocklyRoutes.value = [{
|
|
|
+ id: routeIdCounter.value++,
|
|
|
+ direction: 0,
|
|
|
+ startPoint: { x: undefined, y: undefined },
|
|
|
+ endPoint: { x: undefined, y: undefined }
|
|
|
+ }]
|
|
|
+
|
|
|
+ // 重置上传进度状态
|
|
|
+ uploadProgress.value = 0
|
|
|
+ isUploading.value = false
|
|
|
+ blocklyUploadProgress.value = 0
|
|
|
+ blocklyIsUploading.value = false
|
|
|
+
|
|
|
+ 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 scoped>
|
|
|
+/* 大纲课程树选择器样式 */
|
|
|
+:deep(.el-tree-select__dropdown .el-tree-node__content) {
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 上传进度条样式 */
|
|
|
+.uploadProgress {
|
|
|
+ width: 85%;
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 地图坐标输入组样式 */
|
|
|
+.coordinate-group {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 地图网格容器样式 */
|
|
|
+.map-grid-container {
|
|
|
+ padding: 10px;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ height: 400px;
|
|
|
+ overflow: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.map-grid {
|
|
|
+ display: grid;
|
|
|
+ gap: 1px;
|
|
|
+ background-color: #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 1px;
|
|
|
+}
|
|
|
+
|
|
|
+.grid-cell {
|
|
|
+ aspect-ratio: 1;
|
|
|
+ background-color: rgba(255, 255, 255, 0.5);
|
|
|
+ position: relative;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.grid-cell:hover {
|
|
|
+ background-color: rgba(0, 150, 136, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+.grid-cell.walkable {
|
|
|
+ background-color: rgba(240, 249, 235, 0.8);
|
|
|
+ border: 3px solid rgb(103, 194, 58);
|
|
|
+}
|
|
|
+
|
|
|
+.grid-cell.selected {
|
|
|
+ background-color: rgba(64, 158, 255, 0.5);
|
|
|
+ box-shadow: 0 0 0 2px #409eff;
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.cell-coordinate {
|
|
|
+ position: absolute;
|
|
|
+ top: 2px;
|
|
|
+ left: 2px;
|
|
|
+ background-color: rgba(255, 255, 255, 0.7);
|
|
|
+ padding: 1px 3px;
|
|
|
+ border-radius: 3px;
|
|
|
+ height: 15px;
|
|
|
+ line-height: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.cell-icon {
|
|
|
+ max-width: 80%;
|
|
|
+ max-height: 80%;
|
|
|
+}
|
|
|
+
|
|
|
+.cell-icon img {
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
+}
|
|
|
+
|
|
|
+/* 右侧配置区域样式 - 修复高度显示问题 */
|
|
|
+.point-config-container {
|
|
|
+ padding: 10px;
|
|
|
+ //height: 400px;
|
|
|
+}
|
|
|
+
|
|
|
+.config-card {
|
|
|
+ height: calc(100% - 20px);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.config-card .el-card__body {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.config-form {
|
|
|
+ width: 100%;
|
|
|
+ max-height: 400px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.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 {
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 配置项样式 */
|
|
|
+.config-item {
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表单项全宽样式 */
|
|
|
+.form-item-full {
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 路线配置容器 */
|
|
|
+.route-config-container {
|
|
|
+ margin-top: 10px;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+/* 路线配置项样式 */
|
|
|
+.route-config-item {
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ border-radius: 6px;
|
|
|
+ padding: 12px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ background-color: #ffffff;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
|
+ transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
|
|
+}
|
|
|
+
|
|
|
+.route-config-item:hover {
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
+ border-color: #409eff;
|
|
|
+ transform: translateY(-1px);
|
|
|
+}
|
|
|
+
|
|
|
+/* 路线行样式 */
|
|
|
+.route-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.route-row:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 路线标题样式 */
|
|
|
+.route-title {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ height: 28px;
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #409eff;
|
|
|
+ background-color: #ecf5ff;
|
|
|
+ padding: 0 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+ border-left: 2px solid #409eff;
|
|
|
+ margin-right: 15px;
|
|
|
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+/* 路线字段容器 */
|
|
|
+.route-fields {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ flex: 1;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 路线字段样式 */
|
|
|
+.route-field {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+/* 字段标签样式 */
|
|
|
+.field-label {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+ margin-right: 8px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+/* 坐标组样式 */
|
|
|
+.coordinate-group {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 坐标输入框样式 */
|
|
|
+.coordinate-input {
|
|
|
+ width: 60px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 方向单选按钮样式 */
|
|
|
+.direction-radio {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+}
|
|
|
+
|
|
|
+.direction-radio .el-radio-button__inner {
|
|
|
+ padding: 4px 10px;
|
|
|
+ font-size: 12px;
|
|
|
+ border-radius: 0;
|
|
|
+ border: none;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.direction-radio .el-radio-button__orig-radio:checked + .el-radio-button__inner {
|
|
|
+ background-color: #409eff;
|
|
|
+ border-color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.direction-radio .el-radio-button:first-child .el-radio-button__inner {
|
|
|
+ border-radius: 3px 0 0 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.direction-radio .el-radio-button:last-child .el-radio-button__inner {
|
|
|
+ border-radius: 0 3px 3px 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 删除按钮样式 */
|
|
|
+.route-delete-btn {
|
|
|
+ margin-left: auto;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式调整 */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .route-row {
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .route-fields {
|
|
|
+ width: 100%;
|
|
|
+ margin-top: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .route-delete-btn {
|
|
|
+ margin-left: 0;
|
|
|
+ margin-top: 8px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 路线头部样式 - 保留用于兼容性 */
|
|
|
+.route-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图标上传包装器 */
|
|
|
+.icon-upload-wrapper {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+/* 上传提示 */
|
|
|
+.upload-tip {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #67c23a;
|
|
|
+ margin-top: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 无选择状态样式 */
|
|
|
+.no-selection {
|
|
|
+ height: 400px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 自定义上传组件样式 */
|
|
|
+.custom-upload {
|
|
|
+ margin-bottom: 5px;
|
|
|
+}
|
|
|
+</style>
|