add:设备基础信息

This commit is contained in:
zhangzq
2026-02-24 15:27:56 +08:00
parent 898d67422c
commit 69cd7bf476
15 changed files with 778 additions and 129 deletions

View File

@@ -0,0 +1,315 @@
<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="device-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="设备编码:" name="code">
<a-input v-model:value="formData.code" placeholder="请输入设备编码" allow-clear />
</a-form-item>
<a-form-item label="设备名称:" name="name">
<a-input v-model:value="formData.name" placeholder="请输入设备名称" allow-clear />
</a-form-item>
<a-form-item label="所属区域:" name="region">
<a-input v-model:value="formData.region" placeholder="请输入所属区域" allow-clear />
</a-form-item>
<a-form-item label="设备图片:" name="icon">
<a-upload
v-model:file-list="fileList"
name="file"
list-type="picture-card"
class="device-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">支持jpgpng格式建议尺寸200x200像素</div>
</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="nlBaseDataDeviceForm">
import { required } from '@/utils/formRules'
import deviceApi from '@/api/baseData/deviceApi'
import fileApi from '@/api/dev/fileApi'
import { message } from 'ant-design-vue'
// 默认是关闭状态
const visible = ref(false)
const emit = defineEmits({ successful: null })
const formRef = ref()
// 表单数据
const formData = ref({})
const submitLoading = ref(false)
const fileList = ref([])
const imagePreviewUrl = ref('') // 图片预览URLblob格式
const imageLoading = ref(false) // 图片加载状态
// 根据文件ID获取图片blob并转换为URL
const loadImageById = (fileId, updateFileList = false) => {
if (!fileId) return
imageLoading.value = true
const param = {
storageId: fileId
}
fileApi
.fileDownload(param)
.then((response) => {
const blob = response.data
if (blob && blob instanceof Blob) {
// 将blob转换为URL用于预览
const blobUrl = URL.createObjectURL(blob)
imagePreviewUrl.value = blobUrl
// 如果需要更新文件列表(编辑时)
if (updateFileList) {
fileList.value = [
{
uid: fileId,
name: 'device-image.png',
status: 'done',
url: blobUrl,
thumbUrl: blobUrl
}
]
}
} else {
message.error('图片格式错误')
}
})
.catch((error) => {
message.error('图片加载失败: ' + (error.message || error))
})
.finally(() => {
imageLoading.value = false
})
}
// 打开抽屉
const onOpen = (record) => {
visible.value = true
formData.value = {}
fileList.value = []
imagePreviewUrl.value = ''
if (record) {
formData.value = Object.assign({}, record)
// 如果有图片ID加载图片并设置文件列表
if (record.icon) {
loadImageById(record.icon, true)
}
}
}
// 关闭抽屉
const onClose = () => {
formRef.value.resetFields()
fileList.value = []
// 释放blob URL
if (imagePreviewUrl.value) {
URL.revokeObjectURL(imagePreviewUrl.value)
imagePreviewUrl.value = ''
}
visible.value = false
}
// 默认要校验的
const formRules = {
name: [required('请输入设备名称')],
region: [required('请输入所属区域')],
x: [required('请输入X坐标')],
y: [required('请输入Y坐标')],
angle: [required('请输入角度')],
size: [required('请输入放大比例')],
icon: [required('请上传设备图片')]
}
// 上传前校验
const beforeUpload = (file) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
if (!isJpgOrPng) {
message.error('只能上传 JPG/PNG 格式的图片!')
return false
}
const isLt5M = file.size / 1024 / 1024 < 5
if (!isLt5M) {
message.error('图片大小不能超过 5MB!')
return false
}
return true
}
// 自定义上传
const handleUpload = ({ file, onSuccess, onError }) => {
const uploadFormData = new FormData()
uploadFormData.append('file', file)
fileApi
.fileUploadDynamicReturnId(uploadFormData)
.then((res) => {
if (res) {
// 保存文件ID到表单数据
const fileId = res.id || res
formData.value.icon = fileId
message.success('上传成功')
// 加载新上传的图片预览
loadImageById(fileId)
onSuccess(res)
} else {
message.error('上传失败')
onError(new Error('上传失败'))
}
})
.catch((error) => {
message.error('上传失败: ' + error.message)
onError(error)
})
}
// 删除图片
const handleRemove = () => {
formData.value.icon = ''
// 释放blob URL
if (imagePreviewUrl.value) {
URL.revokeObjectURL(imagePreviewUrl.value)
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 ? deviceApi.edit : deviceApi.add
apiMethod(formData.value)
.then(() => {
message.success('保存成功')
onClose()
emit('successful')
})
.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;
.device-image {
max-width: 100%;
max-height: 400px;
object-fit: contain;
}
}
}
.form-section {
flex: 1;
min-width: 0;
&.full-width {
flex: 1 1 100%;
}
}
.device-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;
}
</style>

View File

@@ -0,0 +1,223 @@
<template>
<a-card :bordered="false" :body-style="{ 'padding-bottom': '0px' }" class="mb-2">
<a-form ref="searchFormRef" name="advanced_search" :model="searchFormState" class="ant-advanced-search-form">
<a-row :gutter="24">
<a-col :span="6">
<a-form-item label="关键词" name="searchKey">
<a-input v-model:value="searchFormState.searchKey" placeholder="请输入设备名称或编码" />
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="所属区域" name="region">
<a-input v-model:value="searchFormState.region" placeholder="请输入所属区域" />
</a-form-item>
</a-col>
<a-col :span="6">
<a-button type="primary" @click="tableRef.refresh(true)">查询</a-button>
<a-button class="xn-mg08" @click="reset">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false">
<s-table
ref="tableRef"
:columns="columns"
:data="loadData"
:alert="options.alert.show"
:row-key="(record) => record.id"
:tool-config="toolConfig"
:row-selection="options.rowSelection"
>
<template #operator class="table-operator">
<a-space>
<a-button type="primary" @click="formRef.onOpen()">
<template #icon><plus-outlined /></template>
新增设备
</a-button>
<xn-batch-button
buttonName="批量删除"
icon="DeleteOutlined"
buttonDanger
:selectedRowKeys="selectedRowKeys"
@batchCallBack="deleteBatchDevice"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'code'">
<span>{{ record.code }}</span>
</template>
<template v-if="column.dataIndex === 'icon'">
<a-image v-if="record.icon && getImageUrl(record.icon)" :width="50" :src="getImageUrl(record.icon)" />
<a-spin v-else-if="record.icon" size="small" />
<span v-else>-</span>
</template>
<template v-if="column.dataIndex === 'action'">
<a-space>
<a @click="formRef.onOpen(record)">编辑</a>
<a-divider type="vertical" />
<a-popconfirm title="确定要删除此设备吗" @confirm="deleteDevice(record)">
<a-button type="link" danger size="small">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</s-table>
</a-card>
<Form ref="formRef" @successful="tableRef.refresh(true)" />
</template>
<script setup name="nlBaseDataDevice">
import Form from './form.vue'
import deviceApi from '@/api/baseData/deviceApi'
import fileApi from '@/api/dev/fileApi'
const searchFormState = ref({})
const searchFormRef = ref()
const tableRef = ref()
const formRef = ref()
const toolConfig = { refresh: true, height: true, columnSetting: false, striped: false }
// 存储图片blob URL的映射
const imageUrlMap = ref(new Map())
const columns = [
{
title: '设备编码',
dataIndex: 'code'
},
{
title: '设备名称',
dataIndex: 'name'
},
{
title: '所属区域',
dataIndex: 'region'
},
{
title: '设备图片',
dataIndex: 'icon'
},
{
title: 'X坐标',
dataIndex: 'x'
},
{
title: 'Y坐标',
dataIndex: 'y'
},
{
title: '角度',
dataIndex: 'angle'
},
{
title: '放大比例',
dataIndex: 'size'
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: '150px'
}
]
let selectedRowKeys = ref([])
// 列表选择配置
const options = {
alert: {
show: false,
clear: () => {
selectedRowKeys = ref([])
}
},
rowSelection: {
onChange: (selectedRowKey, selectedRows) => {
selectedRowKeys.value = selectedRowKey
}
}
}
// 根据文件ID加载图片
const loadImageById = (fileId) => {
if (!fileId || imageUrlMap.value.has(fileId)) {
return
}
const param = {
storageId: fileId
}
fileApi
.fileDownload(param)
.then((response) => {
const blob = response.data
if (blob && blob instanceof Blob) {
const url = URL.createObjectURL(blob)
imageUrlMap.value.set(fileId, url)
}
})
.catch((error) => {
console.error('图片加载失败:', error)
})
}
// 获取图片URL
const getImageUrl = (fileId) => {
return imageUrlMap.value.get(fileId) || ''
}
const loadData = (parameter) => {
return deviceApi.list(Object.assign(parameter, searchFormState.value)).then((res) => {
console.log('API返回的原始数据:', res)
// 加载所有图片
if (res.records && res.records.length > 0) {
res.records.forEach((item) => {
if (item.icon) {
loadImageById(item.icon)
}
})
}
// 转换为表格需要的格式s-table 需要 rows 字段)
const result = {
rows: res.records || [],
total: res.total || 0,
size: res.size || 10,
current: res.current || 1
}
console.log('转换后的数据:', result)
return result
})
}
// 重置
const reset = () => {
searchFormRef.value.resetFields()
tableRef.value.refresh(true)
}
// 删除
const deleteDevice = (record) => {
deviceApi.delete({ id: record.id }).then(() => {
tableRef.value.refresh(true)
})
}
// 批量删除
const deleteBatchDevice = (params) => {
const ids = params.map((item) => item.id)
Promise.all(ids.map((id) => deviceApi.delete({ id }))).then(() => {
tableRef.value.clearRefreshSelected()
})
}
// 组件卸载时清理blob URL
onUnmounted(() => {
imageUrlMap.value.forEach((url) => {
URL.revokeObjectURL(url)
})
imageUrlMap.value.clear()
})
</script>