| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240 |
- <template>
- <div class="map-list-container">
- <!-- 顶部工具栏 -->
- <div class="toolbar-section">
- <!-- 搜索和筛选区 -->
- <div class="toolbar-filters">
- <el-input
- v-model="searchKeyword"
- placeholder="搜索地图名称..."
- :prefix-icon="Search"
- clearable
- class="search-input"
- @input="handleSearch"
- />
- <el-select
- v-model="statusFilter"
- placeholder="状态筛选"
- clearable
- class="status-filter"
- @change="handleFilterChange"
- >
- <el-option label="全部状态" value="all" />
- <el-option label="正常" value="available" />
- <el-option label="不可用" value="unavailable" />
- <el-option label="正在建图" value="building" />
- <el-option label="正在录制" value="recording" />
- <el-option label="实时建图" value="slaming" />
- </el-select>
- <el-select
- v-model="sortField"
- placeholder="排序方式"
- class="sort-select"
- @change="handleSortChange"
- >
- <el-option label="最近修改" value="updated" />
- <el-option label="名称" value="name" />
- <el-option label="状态" value="status" />
- </el-select>
- </div>
- <!-- 操作按钮区 -->
- <div class="toolbar-actions">
- <el-button
- type="default"
- :icon="Upload"
- @click="handleImport"
- >
- 导入地图
- </el-button>
- <el-button
- type="primary"
- :icon="Plus"
- @click="handleCreate"
- >
- 新建地图
- </el-button>
- <el-button
- type="success"
- :icon="MapLocation"
- @click="handleSlam"
- >
- 实时建图
- </el-button>
- <el-button-group class="view-toggle">
- <el-button
- :type="viewMode === 'card' ? 'primary' : 'default'"
- :icon="Grid"
- @click="setViewMode('card')"
- title="卡片视图"
- />
- <el-button
- :type="viewMode === 'table' ? 'primary' : 'default'"
- :icon="Menu"
- @click="setViewMode('table')"
- title="表格视图"
- />
- </el-button-group>
- </div>
- </div>
- <!-- 主要内容区域 -->
- <div class="main-content">
- <!-- 卡片视图 -->
- <template v-if="viewMode === 'card'">
- <div v-loading="loading" class="card-grid" :class="{ compact: isCompactMode }">
- <div v-if="mapList.length === 0 && !loading" class="empty-state">
- <el-icon class="empty-icon"><MapLocation/></el-icon>
- <h3>暂无地图</h3>
- <p>{{ hasSearchOrFilter ? '没有找到符合条件的地图' : '您还没有创建任何地图' }}</p>
- <el-button v-if="!hasSearchOrFilter" type="primary" @click="handleCreate">新建地图</el-button>
- </div>
- <el-card
- v-for="item in displayedList"
- :key="item.id"
- class="map-card"
- shadow="hover"
- @click.stop="handleCardClick(item)"
- >
- <div class="card-thumbnail">
- <img v-if="item.thumbUrl" :src="item.thumbUrl" alt="缩略图" />
- <div v-else class="thumbnail-placeholder">
- <el-icon :size="40"><MapLocation/></el-icon>
- </div>
- </div>
- <div class="card-info">
- <h4 class="map-name">{{ item.map || item.mapName || item.name }}</h4>
- <el-tag :type="getStatusType(item.state)" size="small">{{ getStatusText(item.state) }}</el-tag>
- </div>
- <div class="card-actions" @click.stop>
- <el-button link type="primary" @click.stop="handleNavigation(item)">导航</el-button>
- <el-button link type="primary" @click.stop="handleEdit(item)">编辑</el-button>
- <el-dropdown trigger="click" @command="cmd => handleCommand(item, cmd)">
- <el-button link type="primary">更多<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item command="rename">重命名</el-dropdown-item>
- <el-dropdown-item command="download">下载地图</el-dropdown-item>
- <el-dropdown-item command="build">构建地图</el-dropdown-item>
- <el-dropdown-item command="calibrate">坐标系标定</el-dropdown-item>
- <el-dropdown-item command="delete" divided>删除</el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- </div>
- </el-card>
- </div>
- </template>
- <!-- 表格视图 -->
- <template v-else>
- <el-table
- v-loading="loading"
- :data="displayedList"
- row-key="id"
- class="map-table"
- stripe
- >
- <el-table-column type="index" label="序号" width="80" align="center" />
- <el-table-column prop="map" label="地图名称" min-width="200" show-overflow-tooltip />
- <el-table-column label="状态" width="120" align="center">
- <template #default="{ row }">
- <el-tag :type="getStatusType(row.state)" size="small">{{ getStatusText(row.state) }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="200" align="center">
- <template #default="{ row }">
- <el-button link type="primary" @click="handleNavigation(row)">导航</el-button>
- <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
- <el-button link type="primary" @click="handleCommand(row, 'delete')">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- </template>
- </div>
- <!-- 分页 -->
- <div v-if="totalCount > pageSize" class="pagination-section">
- <el-pagination
- v-model:current-page="currentPage"
- v-model:page-size="pageSize"
- :total="totalCount"
- :page-sizes="[12, 24, 48, 96]"
- layout="total, sizes, prev, pager, next, jumper"
- @size-change="handleSizeChange"
- @current-change="handlePageChange"
- />
- </div>
- <!-- 新建地图对话框(事后建图模式:先录制再构建) -->
- <el-dialog v-model="createDialogVisible" title="新建地图" width="500px" append-to-body>
- <el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="100px">
- <el-form-item label="地图名称" prop="mapName">
- <el-input
- v-model="createForm.mapName"
- placeholder="请输入地图名称"
-
- />
- </el-form-item>
- <el-form-item label="建图模式" prop="mode">
- <el-radio-group v-model="createMode" size="small">
- <el-radio label="record">录制模式(事后建图)</el-radio>
- <el-radio label="slam">实时建图模式</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-form>
- <el-alert
- v-if="createMode === 'record'"
- type="info"
- :closable="false"
- show-icon
- style="margin-top: 8px;"
- >
- <template #title>
- 录制模式说明:
- <ul style="margin: 4px 0 0 16px; padding: 0;">
- <li>点击"开始录制"后,系统将启动传感器数据录制</li>
- <li>请控制机器人在场地内移动,采集环境数据</li>
- <li>录制完成后点击"停止录制"</li>
- <li>然后进行"构建地图"操作生成最终地图</li>
- </ul>
- </template>
- </el-alert>
- <template #footer>
- <el-button @click="createDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="submitCreate">
- {{ createMode === 'record' ? '开始录制' : '开始实时建图' }}
- </el-button>
- </template>
- </el-dialog>
- <!-- 录制进度对话框 -->
- <el-dialog
- v-model="recordingDialogVisible"
- title="正在录制地图"
- width="520px"
- append-to-body
- :close-on-click-modal="false"
- :show-close="false"
- >
- <div class="dialog-content">
- <div class="info-row">
- <span class="label">地图名称:</span>
- <span class="value"><strong>{{ recordingMapName }}</strong></span>
- </div>
- <div class="info-row">
- <span class="label">录制大小:</span>
- <span class="value"><strong>{{ formatFileSize(recordingProgress) }}</strong></span>
- </div>
- <div class="info-row">
- <span class="label">当前状态:</span>
- <el-tag type="warning" size="small">正在录制中</el-tag>
- </div>
- </div>
- <el-alert
- type="info"
- :closable="false"
- show-icon
- style="margin-top: 16px;"
- >
- <template #title>
- 录制提示:
- <ul style="margin: 4px 0 0 16px; padding: 0;">
- <li>请控制机器人移动,采集环境数据</li>
- <li>录制过程中请保持设备连接稳定</li>
- <li>完成后请点击"停止录制"按钮</li>
- </ul>
- </template>
- </el-alert>
- <template #footer>
- <el-button @click="recordingDialogVisible = false" :disabled="true" size="small">关闭</el-button>
- <el-button type="warning" @click="handleStopRecord" icon="VideoPause">停止录制</el-button>
- </template>
- </el-dialog>
- <!-- 构建地图进度对话框 -->
- <el-dialog
- v-model="buildingDialogVisible"
- title="正在构建地图"
- width="520px"
- append-to-body
- :close-on-click-modal="false"
- :show-close="false"
- >
- <div class="dialog-content">
- <div class="info-row">
- <span class="label">地图名称:</span>
- <span class="value"><strong>{{ buildingMapName }}</strong></span>
- </div>
- <div class="info-row">
- <span class="label">构建进度:</span>
- <span class="value"><strong>{{ buildingProgress }}%</strong></span>
- </div>
- <div class="progress-wrapper">
- <el-progress :percentage="buildingProgress" :stroke-width="16" status="warning" />
- </div>
- </div>
- <el-alert
- type="info"
- :closable="false"
- show-icon
- style="margin-top: 16px;"
- >
- <template #title>
- 构建提示:
- <ul style="margin: 4px 0 0 16px; padding: 0;">
- <li>正在处理传感器数据并生成地图文件</li>
- <li>构建时间取决于数据量大小</li>
- <li>请耐心等待,不要关闭此窗口</li>
- </ul>
- </template>
- </el-alert>
- <template #footer>
- <el-button @click="buildingDialogVisible = false" :disabled="true" size="small">关闭</el-button>
- <el-button type="danger" @click="handleStopBuild" icon="Close">取消构建</el-button>
- </template>
- </el-dialog>
- <!-- 实时建图对话框 -->
- <el-dialog
- v-model="slamDialogVisible"
- title="实时建图进行中"
- width="520px"
- append-to-body
- :close-on-click-modal="false"
- :show-close="false"
- >
- <div class="dialog-content">
- <div class="info-row">
- <span class="label">地图名称:</span>
- <span class="value"><strong>{{ slamMapName }}</strong></span>
- </div>
- <div class="info-row">
- <span class="label">当前状态:</span>
- <el-tag type="success" size="small">正在实时建图</el-tag>
- </div>
- </div>
- <el-alert
- type="success"
- :closable="false"
- show-icon
- style="margin-top: 16px;"
- >
- <template #title>
- 实时建图提示:
- <ul style="margin: 4px 0 0 16px; padding: 0;">
- <li>请控制机器人在场地内移动</li>
- <li>系统正在实时构建三维地图</li>
- <li>完成后点击"停止建图"自动生成地图文件</li>
- <li>停止后需要较长时间生成最终地图</li>
- </ul>
- </template>
- </el-alert>
- <template #footer>
- <el-button type="success" @click="handleStopSlam" icon="Check">停止建图</el-button>
- </template>
- </el-dialog>
- <!-- 下载对话框 -->
- <el-dialog v-model="downloadDialogVisible" title="下载地图" width="500px" append-to-body>
- <p style="margin-bottom: 16px;">请选择要下载的地图组件:</p>
- <el-checkbox-group v-model="downLoadTypes">
- <el-checkbox v-for="comp in availableComponents" :key="comp.label" :label="comp.label">{{ comp.name || comp.label }}</el-checkbox>
- </el-checkbox-group>
- <template #footer>
- <el-button @click="downloadDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="submitDownload">开始下载</el-button>
- </template>
- </el-dialog>
- <!-- 重命名对话框 -->
- <el-dialog v-model="renameDialogVisible" title="重命名地图" width="400px" append-to-body>
- <el-form label-width="100px">
- <el-form-item label="新名称">
- <el-input v-model="newMapName" placeholder="请输入新名称" />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="renameDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="submitRename">确定</el-button>
- </template>
- </el-dialog>
- <!-- 构建地图配置对话框 -->
- <el-dialog v-model="buildConfigDialogVisible" title="构建地图" width="500px" append-to-body>
- <el-form label-width="120px">
- <el-form-item label="地图名称">
- <span>{{ buildConfigMapName }}</span>
- </el-form-item>
- <el-form-item label="构建步骤">
- <el-checkbox-group v-model="selectedBuildSteps">
- <el-checkbox label="recon">重建(Recon)</el-checkbox>
- <el-checkbox label="kfmix">关键帧融合(KFMix)</el-checkbox>
- <el-checkbox label="octomap">八叉树地图(Octomap)</el-checkbox>
- <el-checkbox label="tilemap">瓦片地图(TileMap)</el-checkbox>
- <el-checkbox label="potree">点云地图(Potree)</el-checkbox>
- </el-checkbox-group>
- </el-form-item>
- <el-form-item>
- <el-alert type="info" :closable="false" show-icon>
- <template #title>
- 默认执行全部构建步骤,如只需构建部分步骤请取消选择对应选项
- </template>
- </el-alert>
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="buildConfigDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="handleStartBuild">开始构建</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup>
- import { ref, computed, reactive, onMounted, onUnmounted, watch } from 'vue'
- import { useRouter, useRoute } from 'vue-router'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { Search, Plus, Upload, MapLocation, Menu, Grid, ArrowDown, VideoPause, Close, Check } from '@element-plus/icons-vue'
- import { useMapList, useMapControl } from '@/composables/useMap'
- import { useWebSocket } from '@/composables/useWebSocket'
- import * as mapApi from '@/api/robot/map'
- const router = useRouter()
- const route = useRoute()
- // 地图列表相关
- const { loading, mapList, fetchMapList, fetchMapThumbnail, renameMap, removeMap, downloadMap } = useMapList()
- // 地图控制相关
- const deviceId = ref('ld000001') // TODO: 从配置或选择器获取
- // ASM 录制/构建/SLAM 状态管理
- const {
- isRecording, isBuilding, isSlaming,
- recordingMapName, buildingMapName, slamMapName, buildingProgress,
- startRecord, stopRecord,
- startBuild, stopBuild, completedBuild,
- startSlam, stopSlam,
- updateBuildingProgress
- } = useMapControl(deviceId.value)
- // WebSocket 连接和消息处理
- const {
- initWebSocket,
- disconnect: disconnectWs,
- subscribeToDevice,
- } = useWebSocket()
- // 视图状态
- const viewMode = ref('card')
- const isCompactMode = ref(false)
- const searchKeyword = ref('')
- const statusFilter = ref('all')
- const sortField = ref('updated')
- const currentPage = ref(1)
- const pageSize = ref(12)
- // 对话框状态
- const createDialogVisible = ref(false)
- const recordingDialogVisible = ref(false)
- const buildingDialogVisible = ref(false)
- const slamDialogVisible = ref(false)
- const downloadDialogVisible = ref(false)
- const renameDialogVisible = ref(false)
- const buildConfigDialogVisible = ref(false)
- // 录制进度
- const recordingProgress = ref(0)
- // 表单
- const createFormRef = ref()
- const createForm = reactive({ mapName: '' })
- const createMode = ref('record')
- const createRules = {
- mapName: [{ required: true, message: '请输入地图名称', trigger: 'blur' }]
- }
- // 下载相关
- const downLoadTypes = ref([])
- const availableComponents = ref([])
- const currentDownloadMap = ref('')
- // 重命名相关
- const currentRenameMap = ref('')
- const newMapName = ref('')
- // 构建配置相关
- const buildConfigMapName = ref('')
- const selectedBuildSteps = ref(['recon', 'kfmix', 'octomap', 'tilemap', 'potree'])
- // 等待响应状态
- const waitingForResponse = ref(false)
- // 计算属性
- const hasSearchOrFilter = computed(() => searchKeyword.value.trim() || statusFilter.value !== 'all')
- const filteredList = computed(() => {
- let list = [...mapList.value]
- // 搜索过滤
- if (searchKeyword.value.trim()) {
- const keyword = searchKeyword.value.toLowerCase()
- list = list.filter(item => {
- const name = (item.map || item.mapName || item.name || '').toLowerCase()
- return name.includes(keyword)
- })
- }
- // 状态筛选
- if (statusFilter.value !== 'all') {
- list = list.filter(item => (item.state || item.status) === statusFilter.value)
- }
- return list
- })
- const totalCount = computed(() => filteredList.value.length)
- const displayedList = computed(() => {
- const start = (currentPage.value - 1) * pageSize.value
- return filteredList.value.slice(start, start + pageSize.value)
- })
- // 状态配置
- const statusConfig = {
- available: { type: 'success', text: '正常' },
- unavailable: { type: 'danger', text: '不可用' },
- building: { type: 'warning', text: '正在建图' },
- recording: { type: 'warning', text: '正在录制' },
- slaming: { type: 'success', text: '实时建图' }
- }
- function getStatusType(state) {
- return statusConfig[state]?.type || 'info'
- }
- function getStatusText(state) {
- return statusConfig[state]?.text || state || '未知'
- }
- // 格式化文件大小
- function formatFileSize(bytes) {
- if (!bytes || bytes === 0) return '0 B'
- const units = ['B', 'KB', 'MB', 'GB', 'TB']
- const k = 1024
- const i = Math.floor(Math.log(bytes) / Math.log(k))
- return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + units[i]
- }
- // 加载数据
- async function loadData() {
- try {
- await fetchMapList(deviceId.value)
- // 加载缩略图(仅获取前6个地图的缩略图用于卡片显示)
- const limit = Math.min(6, mapList.value.length)
- for (let i = 0; i < limit; i++) {
- const mapItem = mapList.value[i]
- const name = mapItem.map || mapItem.mapName || mapItem.name
- if (name && !mapItem.thumbUrl) {
- try {
- const thumbUrl = await fetchMapThumbnail(name)
- if (thumbUrl) {
- mapItem.thumbUrl = thumbUrl
- }
- } catch (e) {
- console.warn('获取缩略图失败:', name)
- }
- }
- }
- } catch (error) {
- console.error('加载数据失败:', error)
- }
- }
- // WebSocket 消息处理
- function handleWsMessage(type, message) {
- switch (type) {
- case 'asm':
- handleAsmMessage(message)
- break
- case 'map':
- // 地图列表更新时刷新
- loadData()
- break
- }
- }
- // 处理 ASM 消息
- function handleAsmMessage(message) {
- const { type, function: funcName, progress, state, data } = message
- if (type === 'asm_progress') {
- // 进度反馈
- if (funcName === 'SensorRecorder.start') {
- // 录制进度
- recordingProgress.value = progress || 0
- } else if (funcName === 'MapBuilder.start') {
- // 构建进度
- updateBuildingProgress(progress || 0)
- }
- } else if (type === 'asm_state') {
- // 状态反馈
- waitingForResponse.value = false
- // 开始录制状态反馈
- if (funcName === 'ASM.sensor_record.start') {
- if (state === 2) {
- ElMessage.success(`地图"${recordingMapName.value}"开始录制`)
- } else if (state === 3) {
- ElMessage.error('启动录制失败')
- resetRecordingState()
- }
- }
- // 停止录制状态反馈
- else if (funcName === 'ASM.sensor_record.stop') {
- if (state === 2) {
- ElMessage.success('录制已停止')
- resetRecordingState()
- loadData()
- } else if (state === 3) {
- ElMessage.error('停止录制失败')
- }
- }
- // 开始构建状态反馈
- else if (funcName === 'ASM.map_build.start') {
- if (state === 2) {
- ElMessage.success(`地图"${buildingMapName.value}"开始构建`)
- buildingDialogVisible.value = true
- } else if (state === 3) {
- ElMessage.error('启动构建失败')
- resetBuildingState()
- }
- }
- // 停止构建状态反馈
- else if (funcName === 'ASM.map_build.stop') {
- if (state === 2) {
- ElMessage.info('构建已取消')
- resetBuildingState()
- } else if (state === 3) {
- ElMessage.error('取消构建失败')
- }
- }
- // MapBuilder 完成
- else if (funcName === 'MapBuilder.start') {
- if (state === 2) {
- ElMessage.success('地图构建完成')
- resetBuildingState()
- completedBuild(buildConfigMapName.value,"MapBuilder.start").then((res) => {
- loadData()
- }).catch(() => {
- ElMessage.error('更新地图状态失败')
- })
-
- } else if (state === 3) {
- ElMessage.error('地图构建失败')
- resetBuildingState()
- }
- }
- // 开始实时建图状态反馈
- else if (funcName === 'ASM.map_slam.start') {
- if (state === 2) {
- ElMessage.success(`实时建图"${slamMapName.value}"已开始`)
-
- console.log("slamMapName.value1:", slamMapName.value);
-
- completedBuild(slamMapName.value,"ASM.map_slam.start").then((res) => {
- loadData()
- }).catch(() => {
- ElMessage.error('更新地图状态失败')
- })
- } else if (state === 3) {
- ElMessage.error('启动实时建图失败')
- resetSlamState()
- }
- }
- // 停止实时建图状态反馈
- else if (funcName === 'ASM.map_slam.stop') {
- if (state === 2) {
-
- console.log("slamMapName.value",slamMapName.value);
-
- ElMessage.success('实时建图已停止,正在生成地图...')
- completedBuild(slamMapName.value,"ASM.map_slam.stop").then((res) => {
- loadData()
- resetSlamState()
- }).catch(() => {
- ElMessage.error('更新地图状态失败')
- })
- } else if (state === 3) {
- ElMessage.error('停止实时建图失败')
- }
- }
- }
- }
- // 重置录制状态
- function resetRecordingState() {
- recordingDialogVisible.value = false
- recordingProgress.value = 0
- recordingMapName.value = ''
- }
- // 重置构建状态
- function resetBuildingState() {
- buildingDialogVisible.value = false
- buildingProgress.value = 0
- buildingMapName.value = ''
- }
- // 重置实时建图状态
- function resetSlamState() {
- slamDialogVisible.value = false
- slamMapName.value = ''
- }
- // 搜索/筛选/排序
- function handleSearch() {
- currentPage.value = 1
- }
- function handleFilterChange() {
- currentPage.value = 1
- }
- function handleSortChange() {
- currentPage.value = 1
- }
- function handleSizeChange(size) {
- pageSize.value = size
- currentPage.value = 1
- }
- function handlePageChange(page) {
- currentPage.value = page
- }
- function setViewMode(mode) {
- viewMode.value = mode
- localStorage.setItem('map-view-mode', mode)
- }
- // 新建地图
- function handleCreate() {
- createForm.mapName = ''
- createMode.value = 'record'
- createDialogVisible.value = true
- }
- async function submitCreate() {
- if (!createForm.mapName.trim()) {
- ElMessage.warning('请输入地图名称')
- return
- }
- createDialogVisible.value = false
- if (createMode.value === 'record') {
- // 录制模式
- recordingMapName.value = createForm.mapName
- waitingForResponse.value = true
- try {
- await startRecord(createForm.mapName)
- recordingDialogVisible.value = true
- // 30秒超时
- setTimeout(() => {
- if (waitingForResponse.value && !recordingDialogVisible.value) {
- waitingForResponse.value = false
- ElMessage.error('启动录制超时,请检查设备连接')
- }
- }, 30000)
- await loadData()
- } catch (e) {
- waitingForResponse.value = false
- resetRecordingState()
- }
- } else {
- // 实时建图模式
- slamMapName.value = createForm.mapName
- waitingForResponse.value = true
- try {
- await startSlam(createForm.mapName)
- slamDialogVisible.value = true
- // 30秒超时
- setTimeout(() => {
- if (waitingForResponse.value && !slamDialogVisible.value) {
- waitingForResponse.value = false
- ElMessage.error('启动实时建图超时,请检查设备连接')
- }
- }, 30000)
- await loadData()
- } catch (e) {
- waitingForResponse.value = false
- resetSlamState()
- }
- }
- }
- // 实时建图(工具栏按钮)
- async function handleSlam() {
- try {
- const { value: mapName } = await ElMessageBox.prompt('请输入地图名称', '实时建图', {
- confirmButtonText: '开始实时建图',
- cancelButtonText: '取消',
- inputPattern: /\S+/
- })
- slamMapName.value = mapName
- waitingForResponse.value = true
- await startSlam(mapName)
- slamDialogVisible.value = true
- setTimeout(() => {
- if (waitingForResponse.value && !slamDialogVisible.value) {
- waitingForResponse.value = false
- ElMessage.error('启动实时建图超时,请检查设备连接')
- }
- }, 30000)
- await loadData()
- } catch (e) {
- // 用户取消
- }
- }
- // 停止录制
- async function handleStopRecord() {
- try {
- await ElMessageBox.confirm('确认停止录制?停止后将无法继续录制。', '停止录制', {
- type: 'warning',
- confirmButtonText: '确认停止',
- cancelButtonText: '继续录制'
- })
-
- waitingForResponse.value = true
-
- await stopRecord(recordingMapName.value)
- setTimeout(() => {
- if (waitingForResponse.value) {
- waitingForResponse.value = false
- ElMessage.error('停止录制超时,请检查设备连接')
- }
- }, 30000)
- } catch (e) {
- // 用户取消
- }
- }
- // 停止构建
- async function handleStopBuild() {
- try {
- await ElMessageBox.confirm('确认取消构建?', '取消构建', {
- type: 'warning',
- confirmButtonText: '确认取消',
- cancelButtonText: '继续构建'
- })
- waitingForResponse.value = true
- await stopBuild()
- setTimeout(() => {
- if (waitingForResponse.value) {
- waitingForResponse.value = false
- ElMessage.error('取消构建超时,请检查设备连接')
- }
- }, 30000)
- } catch (e) {
- // 用户取消
- }
- }
- // 停止实时建图
- async function handleStopSlam() {
- try {
- await ElMessageBox.confirm(
- '确认停止实时建图?停止后将自动生成地图文件,这可能需要较长时间。',
- '停止实时建图',
- {
- type: 'warning',
- confirmButtonText: '确认停止',
- cancelButtonText: '继续建图'
- }
- )
- waitingForResponse.value = true
- await stopSlam()
- setTimeout(() => {
- if (waitingForResponse.value) {
- waitingForResponse.value = false
- ElMessage.warning('停止实时建图超时,但地图可能仍在后台生成中,请稍后查看地图列表')
- }
- }, 60000)
- } catch (e) {
- // 用户取消
- }
- }
- // 卡片点击
- function handleCardClick(item) {
- // 默认导航到编辑页
- handleEdit(item)
- }
- // 导航
- function handleNavigation(item) {
- const name = item.map || item.mapName || item.name
- console.log("item",name);
- router.push(`/map/maplist/navigation/${encodeURIComponent(name)}`)
- }
- // 编辑
- function handleEdit(item) {
- const name = item.map || item.mapName || item.name
- router.push(`/map/maplist/edit/${encodeURIComponent(name)}`)
- }
- // 标定
- function handleCalibrate(item) {
- const name = item.map || item.mapName || item.name
- router.push(`/map/maplist/calibration/${encodeURIComponent(name)}`)
- }
- // 更多操作
- async function handleCommand(item, cmd) {
- const name = item.map || item.mapName || item.name
- switch (cmd) {
- case 'rename':
- currentRenameMap.value = name
- newMapName.value = name
- renameDialogVisible.value = true
- break
- case 'download':
- try {
- const res = await mapApi.getMapComponents(name)
- if (res.code === 200 && res.data) {
- availableComponents.value = res.data
- downLoadTypes.value = res.data.map(c => c.label)
- currentDownloadMap.value = name
- downloadDialogVisible.value = true
- }
- } catch (e) {
- ElMessage.error('获取组件列表失败')
- }
- break
- case 'build':
- // 检查地图状态
- if (item.state === 'recording') {
- ElMessage.warning('地图正在录制中,请先停止录制后再构建')
- return
- }
- if (item.state === 'building') {
- ElMessage.warning('地图正在构建中')
- return
- }
- if (item.state === 'slaming') {
- ElMessage.warning('地图正在实时建图中')
- return
- }
- buildConfigMapName.value = name
- selectedBuildSteps.value = ['recon', 'kfmix', 'octomap', 'tilemap', 'potree']
- buildConfigDialogVisible.value = true
- break
- case 'calibrate':
- handleCalibrate(item)
- break
- case 'delete':
- try {
- await ElMessageBox.confirm(`确定要删除地图"${name}"吗?此操作不可恢复`, '删除确认', {
- type: 'warning'
- })
- await removeMap(name)
- } catch (e) {
- // 用户取消
- }
- break
- }
- }
- // 开始构建地图
- async function handleStartBuild() {
- if (!buildConfigMapName.value) {
- ElMessage.warning('请选择要构建的地图')
- return
- }
- buildConfigDialogVisible.value = false
- buildingMapName.value = buildConfigMapName.value
- waitingForResponse.value = true
- try {
- await startBuild(buildConfigMapName.value)
- buildingDialogVisible.value = true
- setTimeout(() => {
- if (waitingForResponse.value && !buildingDialogVisible.value) {
- waitingForResponse.value = false
- ElMessage.error('启动构建超时,请检查设备连接')
- }
- }, 30000)
- await loadData()
- } catch (e) {
- waitingForResponse.value = false
- resetBuildingState()
- }
- }
- // 重命名
- async function submitRename() {
- if (!newMapName.value.trim()) {
- ElMessage.warning('请输入新名称')
- return
- }
- try {
- await renameMap(currentRenameMap.value, newMapName.value)
- renameDialogVisible.value = false
- await loadData()
- } catch (e) {
- // 错误已在composable中处理
- }
- }
- // 下载
- async function submitDownload() {
- try {
- await downloadMap(currentDownloadMap.value, downLoadTypes.value)
- downloadDialogVisible.value = false
- } catch (e) {
- // 错误已在composable中处理
- }
- }
- // 导入
- function handleImport() {
- ElMessage.info('导入功能开发中...')
- }
- // 初始化
- onMounted(() => {
- // 恢复视图模式
- const savedMode = localStorage.getItem('map-view-mode')
- if (savedMode) {
- viewMode.value = savedMode
- }
- // 初始化 WebSocket
- initWebSocket({
- deviceId: deviceId.value,
- onMessage: handleWsMessage,
- onConnect: () => {
- console.log('[MapList] WebSocket已连接')
- },
- onDisconnect: () => {
- console.log('[MapList] WebSocket已断开')
- }
- })
- loadData()
- })
- // 组件卸载时断开 WebSocket
- onUnmounted(() => {
- disconnectWs()
- })
- // 监听录制状态,自动显示/隐藏对话框
- watch(isRecording, (newVal) => {
- if (newVal && recordingMapName.value) {
- recordingDialogVisible.value = true
- }
- })
- watch(isBuilding, (newVal) => {
- if (!newVal && buildingMapName.value) {
- buildingDialogVisible.value = false
- }
- })
- watch(isSlaming, (newVal) => {
- if (!newVal && slamMapName.value) {
- slamDialogVisible.value = false
- }
- })
- </script>
- <style lang="scss" scoped>
- .map-list-container {
- padding: 16px;
- min-height: calc(100vh - 140px);
- }
- .toolbar-section {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 16px;
- padding: 16px;
- background: var(--el-bg-color);
- border-radius: 8px;
- margin-bottom: 16px;
- }
- .toolbar-filters {
- display: flex;
- gap: 12px;
- .search-input { width: 200px; }
- .status-filter, .sort-select { width: 140px; }
- }
- .toolbar-actions {
- display: flex;
- gap: 8px;
- }
- .main-content {
- min-height: 400px;
- }
- .card-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
- gap: 16px;
- &.compact {
- gap: 12px;
- grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
- }
- }
- .map-card {
- cursor: pointer;
- transition: all 0.2s;
- &:hover {
- transform: translateY(-2px);
- }
- }
- .card-thumbnail {
- height: 160px;
- background: var(--el-fill-color-light);
- border-radius: 4px;
- overflow: hidden;
- margin-bottom: 12px;
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- .thumbnail-placeholder {
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--el-text-color-placeholder);
- }
- }
- .card-info {
- margin-bottom: 12px;
- .map-name {
- margin: 0 0 8px 0;
- font-size: 16px;
- font-weight: 500;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- }
- .card-actions {
- display: flex;
- gap: 8px;
- align-items: center;
- }
- .empty-state {
- grid-column: 1 / -1;
- text-align: center;
- padding: 60px 20px;
- .empty-icon {
- font-size: 60px;
- color: var(--el-text-color-placeholder);
- margin-bottom: 16px;
- }
- h3 {
- margin: 0 0 8px 0;
- color: var(--el-text-color-primary);
- }
- p {
- margin: 0 0 16px 0;
- color: var(--el-text-color-secondary);
- }
- }
- .map-table {
- background: var(--el-bg-color);
- border-radius: 8px;
- }
- .pagination-section {
- display: flex;
- justify-content: flex-end;
- margin-top: 16px;
- }
- .dialog-content {
- padding: 8px 0;
- }
- .info-row {
- display: flex;
- align-items: center;
- margin-bottom: 12px;
- .label {
- width: 100px;
- color: var(--el-text-color-regular);
- }
- .value {
- flex: 1;
- }
- }
- .progress-wrapper {
- margin-top: 8px;
- padding: 0 4px;
- }
- </style>
|