add:地图编辑

This commit is contained in:
zhangzq
2026-01-21 19:57:46 +08:00
parent ce9740c396
commit d7e48dda08
3 changed files with 19 additions and 540 deletions

View File

@@ -89,8 +89,7 @@
@mousedown="handleDeviceMouseDown($event, device)"
>
<div class="device-marker">
<div class="device-marker-icon">
<img :src="'http://localhost:8082' + device.icon" alt="${device.name}" class="device-icon-img" style="width: 100%; height: 100%; object-fit: contain; transform: rotate(${device.angle || 0}deg);">
<div class="device-marker-icon" v-html="renderDeviceIcon(device)">
</div>
<div class="device-marker-label">{{ device.name }}</div>
</div>
@@ -294,14 +293,8 @@ export default {
renderDeviceIcon(device) {
const template = this.deviceTemplates.find(d => d.id === device.templateId)
if (!template) return '📍'
const iconFontSize = Math.round((device.size || 50) * 0.64)
if (this.isImageUrl(template.icon)) {
return `<img src=http:"${template.icon}" alt="${device.name}" class="device-icon-img" style="width: 100%; height: 100%; object-fit: contain; transform: rotate(${device.angle || 0}deg);">`
} else {
return `<span style="font-size: ${iconFontSize}px; transform: rotate(${device.angle || 0}deg); display: inline-block;">${template.icon || '📍'}</span>`
}
},
return `<img src="http://localhost:8082${device.icon}" alt="${device.name}" class="device-icon-img" style="width: 100%; height: 100%; object-fit: contain; transform: rotate(${device.angle || 0}deg);">`
},
async handleMapUpload(e) {
const file = e.target.files[0]
if (!file) return

View File

@@ -1,528 +0,0 @@
<template>
<div class="map-management">
<a-card title="地图管理" :bordered="false">
<template #extra>
<a-space>
<a-button type="primary" @click="showAddModal">
<template #icon><PlusOutlined /></template>
新增地图
</a-button>
<a-button @click="loadMapList">
<template #icon><ReloadOutlined /></template>
刷新
</a-button>
</a-space>
</template>
<!-- 搜索栏 -->
<div class="search-bar">
<a-form layout="inline">
<a-form-item label="地图名称">
<a-input
v-model:value="searchForm.name"
placeholder="请输入地图名称"
allow-clear
@pressEnter="handleSearch"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">查询</a-button>
<a-button @click="handleReset">重置</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
<!-- 地图列表 -->
<a-table
:columns="columns"
:data-source="mapList"
:loading="loading"
:pagination="pagination"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'preview'">
<a-image
:src="getImageUrl(record.url)"
:width="100"
:height="60"
:preview="true"
fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
/>
</template>
<template v-if="column.key === 'status'">
<a-tag :color="record.status === 1 ? 'green' : 'red'">
{{ record.status === 1 ? '启用' : '禁用' }}
</a-tag>
</template>
<template v-if="column.key === 'deviceCount'">
<a-badge :count="record.deviceCount || 0" :number-style="{ backgroundColor: '#52c41a' }" />
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="viewMap(record)">查看</a-button>
<a-button type="link" size="small" @click="editMap(record)">编辑</a-button>
<a-button
type="link"
size="small"
:danger="record.status === 1"
@click="toggleStatus(record)"
>
{{ record.status === 1 ? '禁用' : '启用' }}
</a-button>
<a-popconfirm
title="确定要删除这个地图吗?"
ok-text="确定"
cancel-text="取消"
@confirm="deleteMap(record)"
>
<a-button type="link" size="small" danger>删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 新增/编辑地图弹窗 -->
<a-modal
v-model:open="modalVisible"
:title="modalTitle"
:width="800"
@ok="handleSubmit"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
>
<a-form-item label="地图名称" name="name">
<a-input v-model:value="formData.name" placeholder="请输入地图名称" />
</a-form-item>
<a-form-item label="地图描述" name="description">
<a-textarea
v-model:value="formData.description"
placeholder="请输入地图描述"
:rows="3"
/>
</a-form-item>
<a-form-item label="地图文件" name="url">
<a-upload
:file-list="fileList"
:before-upload="beforeUpload"
:custom-request="handleUpload"
list-type="picture-card"
accept="image/*"
:max-count="1"
@remove="handleRemove"
>
<div v-if="fileList.length < 1">
<plus-outlined />
<div style="margin-top: 8px">上传地图</div>
</div>
</a-upload>
</a-form-item>
<a-form-item label="地图宽度" name="width">
<a-input-number
v-model:value="formData.width"
:min="0"
placeholder="自动获取"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="地图高度" name="height">
<a-input-number
v-model:value="formData.height"
:min="0"
placeholder="自动获取"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="原点X坐标" name="originX">
<a-input-number
v-model:value="formData.originX"
:min="0"
placeholder="请输入原点X坐标"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="原点Y坐标" name="originY">
<a-input-number
v-model:value="formData.originY"
:min="0"
placeholder="请输入原点Y坐标"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group v-model:value="formData.status">
<a-radio :value="1">启用</a-radio>
<a-radio :value="0">禁用</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup name="MapManagement">
import { ref, reactive, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue'
import { useRouter } from 'vue-router'
import mapApi from '@/api/agv/mapApi'
const router = useRouter()
// 表格列定义
const columns = [
{
title: '地图ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '预览',
key: 'preview',
width: 120
},
{
title: '地图名称',
dataIndex: 'name',
key: 'name'
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true
},
{
title: '尺寸',
key: 'size',
customRender: ({ record }) => `${record.width || 0} × ${record.height || 0}`
},
{
title: '设备数量',
key: 'deviceCount',
width: 100
},
{
title: '状态',
key: 'status',
width: 80
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 180
},
{
title: '操作',
key: 'action',
width: 250,
fixed: 'right'
}
]
// 状态
const loading = ref(false)
const mapList = ref([])
const searchForm = reactive({
name: ''
})
// 分页
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`
})
// 弹窗
const modalVisible = ref(false)
const modalTitle = computed(() => (formData.id ? '编辑地图' : '新增地图'))
const formRef = ref(null)
const formData = reactive({
id: null,
name: '',
description: '',
url: '',
width: null,
height: null,
originX: 0,
originY: 0,
status: 1
})
const formRules = {
name: [{ required: true, message: '请输入地图名称', trigger: 'blur' }],
url: [{ required: true, message: '请上传地图文件', trigger: 'change' }]
}
// 文件上传
const fileList = ref([])
// 获取图片URL
const getImageUrl = (url) => {
if (!url) return ''
if (url.startsWith('http')) return url
return `http://localhost:8082/${url}`
}
// 加载地图列表
const loadMapList = async () => {
loading.value = true
try {
const params = {
name: searchForm.name,
pageNum: pagination.current,
pageSize: pagination.pageSize
}
const response = await mapApi.list(params)
if (response && response.data) {
mapList.value = response.data.records || response.data.list || []
pagination.total = response.data.total || 0
}
} catch (error) {
console.error('加载地图列表失败:', error)
message.error('加载地图列表失败')
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
pagination.current = 1
loadMapList()
}
// 重置
const handleReset = () => {
searchForm.name = ''
pagination.current = 1
loadMapList()
}
// 表格变化
const handleTableChange = (pag) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadMapList()
}
// 显示新增弹窗
const showAddModal = () => {
resetForm()
modalVisible.value = true
}
// 查看地图
const viewMap = (record) => {
router.push({
path: '/nl_agv/screen',
query: { mapId: record.id }
})
}
// 编辑地图
const editMap = (record) => {
Object.assign(formData, {
id: record.id,
name: record.name,
description: record.description,
url: record.url,
width: record.width,
height: record.height,
originX: record.origin?.x || 0,
originY: record.origin?.y || 0,
status: record.status
})
if (record.url) {
fileList.value = [
{
uid: '-1',
name: 'map.png',
status: 'done',
url: getImageUrl(record.url)
}
]
}
modalVisible.value = true
}
// 切换状态
const toggleStatus = async (record) => {
try {
await mapApi.enable({
id: record.id,
status: record.status === 1 ? 0 : 1
})
message.success('操作成功')
loadMapList()
} catch (error) {
console.error('切换状态失败:', error)
message.error('操作失败')
}
}
// 删除地图
const deleteMap = async (record) => {
try {
await mapApi.delete({ id: record.id })
message.success('删除成功')
loadMapList()
} catch (error) {
console.error('删除地图失败:', error)
message.error('删除失败')
}
}
// 文件上传前
const beforeUpload = (file) => {
const isImage = file.type.startsWith('image/')
if (!isImage) {
message.error('只能上传图片文件!')
}
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
message.error('图片大小不能超过 10MB')
}
return isImage && isLt10M
}
// 自定义上传
const handleUpload = async ({ file, onSuccess, onError }) => {
try {
const formData = new FormData()
formData.append('file', file)
const response = await fetch('http://localhost:8081/api/localStorage/upload', {
method: 'POST',
body: formData
})
if (!response.ok) {
throw new Error('上传失败')
}
const result = await response.json()
if (result.code === 200 && result.data) {
formData.url = result.data.url || result.data.path
// 读取图片尺寸
const img = new Image()
img.onload = () => {
formData.width = img.naturalWidth
formData.height = img.naturalHeight
}
img.src = URL.createObjectURL(file)
onSuccess(result.data)
message.success('上传成功')
} else {
throw new Error(result.msg || '上传失败')
}
} catch (error) {
console.error('上传失败:', error)
onError(error)
message.error('上传失败')
}
}
// 移除文件
const handleRemove = () => {
formData.url = ''
fileList.value = []
}
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value.validate()
const data = {
...formData,
origin: {
x: formData.originX,
y: formData.originY
}
}
await mapApi.save(data)
message.success('保存成功')
modalVisible.value = false
loadMapList()
} catch (error) {
console.error('保存失败:', error)
if (error.errorFields) {
// 表单验证失败
return
}
message.error('保存失败')
}
}
// 取消
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
// 重置表单
const resetForm = () => {
Object.assign(formData, {
id: null,
name: '',
description: '',
url: '',
width: null,
height: null,
originX: 0,
originY: 0,
status: 1
})
fileList.value = []
formRef.value?.resetFields()
}
// 初始化
onMounted(() => {
loadMapList()
})
</script>
<style scoped lang="less">
.map-management {
padding: 20px;
.search-bar {
margin-bottom: 20px;
}
}
</style>

View File

@@ -108,6 +108,7 @@
:src="mapImageUrl"
alt="厂区背景"
class="map-background"
:style="mapBackgroundStyle"
@load="onMapImageLoad"
@error="onMapImageError"
/>
@@ -255,6 +256,17 @@ const progressStyle = computed(() => {
}
})
const mapBackgroundStyle = computed(() => {
const style = {}
if (mapState.width > 0) {
style.width = mapState.width + 'px'
}
if (mapState.height > 0) {
style.height = mapState.height + 'px'
}
return style
})
// ==================== 工具函数 ====================
const getDeviceIconUrl = (icon) => {
if (!icon) return ''
@@ -623,10 +635,12 @@ onMounted(async () => {
if (mapData) {
mapState.mapId = mapData.mapId
mapState.origin = mapData.origin || { x: 0, y: 0 }
mapState.width = mapData.width
mapState.height = mapData.height
mapState.width = mapData.width || 0
mapState.height = mapData.height || 0
staticDevices.value = mapData.devices || []
console.log('地图尺寸:', { width: mapState.width, height: mapState.height })
if (mapData.url) {
mapImageUrl.value = mapData.url.startsWith('http')
? mapData.url