403 lines
9.3 KiB
Vue
403 lines
9.3 KiB
Vue
<template>
|
||
<xn-form-container
|
||
:title="formData.id ? '编辑地图信息' : '新增地图信息'"
|
||
:width="900"
|
||
:visible="visible"
|
||
:destroy-on-close="true"
|
||
@close="onClose"
|
||
>
|
||
<div class="form-content">
|
||
<!-- 左侧:地图预览区域 -->
|
||
<div class="image-preview-section" v-if="formData.id && imagePreviewUrl">
|
||
<div class="preview-title">地图预览</div>
|
||
<div class="preview-container">
|
||
<a-spin :spinning="imageLoading" tip="加载中...">
|
||
<a-image v-if="imagePreviewUrl" :src="imagePreviewUrl" :preview="true" class="map-image" />
|
||
</a-spin>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:表单区域 -->
|
||
<div class="form-section" :class="{ 'full-width': !formData.id || !imagePreviewUrl }">
|
||
<a-form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||
<a-form-item label="地图ID:" name="mapId">
|
||
<a-input-number
|
||
v-model:value="formData.mapId"
|
||
placeholder="请输入地图ID"
|
||
:disabled="!!formData.id"
|
||
style="width: 100%"
|
||
:min="1"
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="地图名称:" name="mapName">
|
||
<a-input
|
||
v-model:value="formData.mapName"
|
||
placeholder="请输入地图名称"
|
||
allow-clear
|
||
/>
|
||
</a-form-item>
|
||
<a-form-item label="版本号:" name="version">
|
||
<a-input-number
|
||
v-model:value="formData.version"
|
||
placeholder="请输入版本号"
|
||
style="width: 100%"
|
||
:min="1"
|
||
/>
|
||
</a-form-item>
|
||
<a-row :gutter="16">
|
||
<a-col :span="12">
|
||
<a-form-item label="地图宽度:" name="width">
|
||
<a-input-number
|
||
v-model:value="formData.width"
|
||
placeholder="宽度"
|
||
style="width: 100%"
|
||
:min="1"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item label="地图高度:" name="height">
|
||
<a-input-number
|
||
v-model:value="formData.height"
|
||
placeholder="高度"
|
||
style="width: 100%"
|
||
:min="1"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-row :gutter="16">
|
||
<a-col :span="12">
|
||
<a-form-item label="原点X坐标:" name="x">
|
||
<a-input-number
|
||
v-model:value="formData.x"
|
||
placeholder="X坐标"
|
||
style="width: 100%"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item label="原点Y坐标:" name="y">
|
||
<a-input-number
|
||
v-model:value="formData.y"
|
||
placeholder="Y坐标"
|
||
style="width: 100%"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-form-item label="地图文件:" name="url">
|
||
<a-upload
|
||
v-model:file-list="fileList"
|
||
name="file"
|
||
list-type="picture-card"
|
||
class="map-uploader"
|
||
:show-upload-list="true"
|
||
:before-upload="beforeUpload"
|
||
:custom-request="handleUpload"
|
||
@remove="handleRemove"
|
||
@preview="handlePreview"
|
||
:max-count="1"
|
||
>
|
||
<div v-if="fileList.length < 1">
|
||
<plus-outlined />
|
||
<div style="margin-top: 8px">上传地图</div>
|
||
</div>
|
||
</a-upload>
|
||
<div class="upload-tip">支持jpg、png格式,建议上传清晰的地图图片</div>
|
||
</a-form-item>
|
||
<a-form-item label="启用状态:" name="enable">
|
||
<a-switch
|
||
v-model:checked="formData.enable"
|
||
checked-children="启用"
|
||
un-checked-children="禁用"
|
||
/>
|
||
</a-form-item>
|
||
</a-form>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<a-button class="xn-mr8" @click="onClose">关闭</a-button>
|
||
<a-button type="primary" @click="onSubmit" :loading="submitLoading">保存</a-button>
|
||
</template>
|
||
</xn-form-container>
|
||
</template>
|
||
|
||
<script setup name="nlAgvMapForm">
|
||
import { required } from '@/utils/formRules'
|
||
import mapApi from '@/api/agv/mapApi'
|
||
import fileApi from '@/api/dev/fileApi'
|
||
import { message } from 'ant-design-vue'
|
||
|
||
// 文件服务器地址
|
||
const FILE_SERVER = 'http://localhost:8082'
|
||
|
||
// 默认是关闭状态
|
||
const visible = ref(false)
|
||
const emit = defineEmits({ successful: null })
|
||
const formRef = ref()
|
||
|
||
// 表单数据
|
||
const formData = ref({
|
||
enable: false,
|
||
version: 1,
|
||
x: 0,
|
||
y: 0
|
||
})
|
||
const submitLoading = ref(false)
|
||
const fileList = ref([])
|
||
const imagePreviewUrl = ref('') // 图片预览URL
|
||
const imageLoading = ref(false) // 图片加载状态
|
||
|
||
// 加载地图图片
|
||
const loadMapImage = (url, updateFileList = false) => {
|
||
if (!url) return
|
||
|
||
imageLoading.value = true
|
||
|
||
// 构建完整的图片URL
|
||
let imageUrl = url
|
||
if (url.startsWith('/Users/') || url.startsWith('C:\\') || url.startsWith('/')) {
|
||
// 本地文件路径,通过文件服务器访问
|
||
imageUrl = FILE_SERVER + url
|
||
}
|
||
|
||
const img = new Image()
|
||
img.onload = () => {
|
||
imagePreviewUrl.value = imageUrl
|
||
|
||
// 如果需要更新文件列表(编辑时)
|
||
if (updateFileList) {
|
||
fileList.value = [
|
||
{
|
||
uid: '-1',
|
||
name: 'map-image.png',
|
||
status: 'done',
|
||
url: imageUrl,
|
||
thumbUrl: imageUrl
|
||
}
|
||
]
|
||
}
|
||
imageLoading.value = false
|
||
}
|
||
img.onerror = () => {
|
||
message.error('地图图片加载失败')
|
||
imageLoading.value = false
|
||
}
|
||
img.src = imageUrl
|
||
}
|
||
|
||
// 打开抽屉
|
||
const onOpen = (record) => {
|
||
visible.value = true
|
||
formData.value = {
|
||
enable: false,
|
||
version: 1,
|
||
x: 0,
|
||
y: 0
|
||
}
|
||
fileList.value = []
|
||
imagePreviewUrl.value = ''
|
||
|
||
if (record) {
|
||
submitLoading.value = true
|
||
const param = {
|
||
id: record.id
|
||
}
|
||
mapApi
|
||
.detail(param)
|
||
.then((data) => {
|
||
formData.value = Object.assign({}, data)
|
||
// 如果有地图URL,加载图片
|
||
if (data.url) {
|
||
loadMapImage(data.url, true)
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
message.error('获取详情失败: ' + (error.message || error))
|
||
})
|
||
.finally(() => {
|
||
submitLoading.value = false
|
||
})
|
||
}
|
||
}
|
||
|
||
// 关闭抽屉
|
||
const onClose = () => {
|
||
formRef.value.resetFields()
|
||
fileList.value = []
|
||
imagePreviewUrl.value = ''
|
||
visible.value = false
|
||
}
|
||
|
||
// 默认要校验的
|
||
const formRules = {
|
||
mapId: [required('请输入地图ID')],
|
||
mapName: [required('请输入地图名称')],
|
||
version: [required('请输入版本号')],
|
||
width: [required('请输入地图宽度')],
|
||
height: [required('请输入地图高度')],
|
||
url: [required('请上传地图文件')]
|
||
}
|
||
|
||
// 上传前校验
|
||
const beforeUpload = (file) => {
|
||
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
|
||
if (!isJpgOrPng) {
|
||
message.error('只能上传 JPG/PNG 格式的图片!')
|
||
return false
|
||
}
|
||
const isLt10M = file.size / 1024 / 1024 < 10
|
||
if (!isLt10M) {
|
||
message.error('图片大小不能超过 10MB!')
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
// 自定义上传
|
||
const handleUpload = ({ file, onSuccess, onError }) => {
|
||
const uploadFormData = new FormData()
|
||
uploadFormData.append('file', file)
|
||
|
||
fileApi
|
||
.fileUploadDynamicReturnId(uploadFormData)
|
||
.then((res) => {
|
||
if (res) {
|
||
// 保存文件路径到表单数据
|
||
formData.value.url = res.filePath || res.url || res
|
||
message.success('上传成功')
|
||
|
||
// 加载新上传的图片预览
|
||
if (formData.value.url) {
|
||
loadMapImage(formData.value.url)
|
||
}
|
||
|
||
onSuccess(res)
|
||
} else {
|
||
message.error('上传失败')
|
||
onError(new Error('上传失败'))
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
message.error('上传失败: ' + error.message)
|
||
onError(error)
|
||
})
|
||
}
|
||
|
||
// 删除图片
|
||
const handleRemove = () => {
|
||
formData.value.url = ''
|
||
imagePreviewUrl.value = ''
|
||
}
|
||
|
||
// 预览图片
|
||
const handlePreview = async (file) => {
|
||
// 如果已经有预览URL,直接使用
|
||
if (imagePreviewUrl.value) {
|
||
return
|
||
}
|
||
// 如果是新上传的文件,使用file对象创建预览
|
||
if (file.originFileObj) {
|
||
imagePreviewUrl.value = URL.createObjectURL(file.originFileObj)
|
||
}
|
||
}
|
||
|
||
// 验证并提交数据
|
||
const onSubmit = () => {
|
||
formRef.value
|
||
.validate()
|
||
.then(() => {
|
||
submitLoading.value = true
|
||
const apiMethod = formData.value.id ? mapApi.edit : mapApi.save
|
||
apiMethod(formData.value)
|
||
.then(() => {
|
||
message.success('保存成功')
|
||
onClose()
|
||
emit('successful')
|
||
})
|
||
.catch((error) => {
|
||
message.error('保存失败: ' + (error.message || error))
|
||
})
|
||
.finally(() => {
|
||
submitLoading.value = false
|
||
})
|
||
})
|
||
.catch(() => {})
|
||
}
|
||
|
||
// 调用这个函数将子组件的一些数据和方法暴露出去
|
||
defineExpose({
|
||
onOpen
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.form-content {
|
||
display: flex;
|
||
gap: 24px;
|
||
min-height: 400px;
|
||
}
|
||
|
||
.image-preview-section {
|
||
flex: 0 0 350px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.preview-title {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
margin-bottom: 16px;
|
||
color: #333;
|
||
}
|
||
|
||
.preview-container {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #f5f5f5;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
|
||
.map-image {
|
||
max-width: 100%;
|
||
max-height: 400px;
|
||
object-fit: contain;
|
||
}
|
||
}
|
||
}
|
||
|
||
.form-section {
|
||
flex: 1;
|
||
min-width: 0;
|
||
|
||
&.full-width {
|
||
flex: 1 1 100%;
|
||
}
|
||
}
|
||
|
||
.map-uploader {
|
||
:deep(.ant-upload-select-picture-card) {
|
||
width: 120px;
|
||
height: 120px;
|
||
}
|
||
|
||
:deep(.ant-upload-list-picture-card-container) {
|
||
width: 120px;
|
||
height: 120px;
|
||
}
|
||
}
|
||
|
||
.upload-tip {
|
||
color: #999;
|
||
font-size: 12px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.xn-mr8 {
|
||
margin-right: 8px;
|
||
}
|
||
</style>
|