add:地图编辑
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user