修改设备管理界面相关功能

This commit is contained in:
lxd 2025-06-11 09:50:14 +08:00
parent 3e58485d2f
commit fde6cd9bf9
6 changed files with 414 additions and 282 deletions

View File

@ -47,6 +47,10 @@ export const DeviceApi = {
deleteDevice: async (id: number) => {
return await request.delete({ url: `/system/device/delete?id=` + id })
},
// 删除设备
deleteDeviceCode: async (devicecode: number) => {
return await request.delete({ url: `/system/device/deletecode?devicecode=` + devicecode })
},
// 导出设备 Excel
exportDevice: async (params) => {

View File

@ -23,6 +23,10 @@ export const DeviceuserApi = {
getDeviceuser: async (id: number) => {
return await request.get({ url: `/system/deviceuser/get?id=` + id })
},
// 查询设备人员关联数量
getDevCount: async (devicecode: number) => {
return await request.get({ url: `/system/deviceuser/getDevCount?devicecode=` + devicecode })
},
// 新增设备人员关联
createDeviceuser: async (data: DeviceuserVO) => {
@ -42,5 +46,5 @@ export const DeviceuserApi = {
// 导出设备人员关联 Excel
exportDeviceuser: async (params) => {
return await request.download({ url: `/system/deviceuser/export-excel`, params })
},
}
}

View File

@ -5,13 +5,14 @@
v-loading="formLoading"
:model="formData"
label-width="100px"
:disabled="true"
:disabled="!isEditing"
>
<el-form-item label="设备ID">
<span>{{ formData.devicecode }}</span>
</el-form-item>
<el-form-item label="设备名称">
<span>{{ formData.devicename }}</span>
<el-input v-if="isEditing" v-model="formData.devicename" />
<span v-else>{{ formData.devicename }}</span>
</el-form-item>
<el-form-item label="所属机构">
<span>{{ formData.orgname }}</span>
@ -19,18 +20,37 @@
<el-form-item label="设备类型">
<span>{{ getDeviceTypeName(formData.devicetype) }}</span>
</el-form-item>
<el-form-item label="设备位置">
<span>{{ formData.location }}</span>
<el-form-item label="设备位置" class="location-form-item">
<template v-if="isEditing">
<el-cascader
v-model="selectedOptions"
:options="options"
:props="{ expandTrigger: 'hover' }"
placeholder="请选择省/市/区"
@change="handleAddressChange"
class="location-cascader"
clearable
/>
<el-input
v-model="locationDetail"
placeholder="请输入详细地址"
class="detail-address-input"
/>
</template>
<span v-else>{{ fullLocation }}</span>
</el-form-item>
<el-form-item label="设备状态">
<span>{{ getDeviceStatusName(formData.devicestatus) }}</span>
</el-form-item>
<el-form-item label="设备描述">
<span>{{ formData.description }}</span>
<el-input v-if="isEditing" v-model="formData.description" type="textarea" :rows="3" />
<span v-else>{{ formData.description }}</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
<el-button v-if="!isEditing" type="primary" @click="handleEdit"> </el-button>
<el-button v-else type="primary" @click="handleSave"> </el-button>
<el-button @click="handleClose"> </el-button>
</template>
</Dialog>
</template>
@ -38,6 +58,8 @@
<script lang="ts" setup>
import { DeviceApi } from '@/api/device'
import { ElMessage } from 'element-plus'
import type { DeviceVO } from '@/api/device'
import { regionData, codeToText } from 'element-china-area-data'
defineOptions({ name: 'DeviceDetailsForm' })
@ -66,44 +88,202 @@ const getDeviceStatusName = (status: string | number) => {
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) //
const formData = ref({
devicecode: undefined,
const formData = ref<DeviceVO>({
id: 0,
devicecode: 0,
devicename: '',
orgname: '',
devicetype: '',
location: '',
devicestatus: '',
description: ''
devicestatus: 0,
orgid: 0,
orgname: '',
description: '',
createtime: new Date(),
updatetime: new Date(),
createby: '',
updateby: ''
})
const isEditing = ref(false) //
const locationDetail = ref('') //
const options = ref(regionData) //
const selectedOptions = ref<string[]>([]) //
/** 打开弹窗 */
const open = async (id: number) => {
dialogVisible.value = true
dialogTitle.value = '设备详情'
resetForm()
isEditing.value = false
//
if (id) {
formLoading.value = true
try {
const res = await DeviceApi.getDeviceId(id)
formData.value = res
//
if (res.location) {
const addressParts = res.location.split('/')
if (addressParts.length > 3) {
formData.value.location = addressParts.slice(0, 3).join('/')
locationDetail.value = addressParts.slice(3).join('/')
} else {
formData.value.location = res.location
locationDetail.value = ''
}
}
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 处理编辑按钮点击 */
const handleEdit = () => {
isEditing.value = true
//
if (formData.value.location) {
const addressParts = formData.value.location.split('/')
if (addressParts.length === 3) {
//
const findCode = (name: string, data: any[]): string | undefined => {
for (const item of data) {
if (item.label === name) {
return item.value
}
if (item.children) {
for (const child of item.children) {
if (child.label === name) {
return child.value
}
if (child.children) {
for (const grandChild of child.children) {
if (grandChild.label === name) {
return grandChild.value
}
}
}
}
}
}
return undefined
}
//
const isMunicipality = ['北京市', '天津市', '上海市', '重庆市'].includes(addressParts[0])
let provinceCode, cityCode, areaCode
if (isMunicipality) {
//
provinceCode = findCode(addressParts[0], options.value)
// ""
const province = options.value.find(item => item.value === provinceCode)
if (province && province.children) {
//
const district = province.children.find(item => item.label === '市辖区')
if (district) {
cityCode = district.value
//
if (district.children) {
const area = district.children.find(item => item.label === addressParts[2])
if (area) {
areaCode = area.value
}
}
}
}
} else {
//
provinceCode = findCode(addressParts[0], options.value)
cityCode = findCode(addressParts[1], options.value)
areaCode = findCode(addressParts[2], options.value)
}
if (provinceCode && cityCode && areaCode) {
selectedOptions.value = [provinceCode, cityCode, areaCode]
}
}
}
}
/** 处理地址变化 */
const handleAddressChange = (value: string[]) => {
if (value && value.length === 3) {
const address = codeToText[value[0]] + '/' + codeToText[value[1]] + '/' + codeToText[value[2]]
formData.value.location = address
}
}
/** 处理保存按钮点击 */
const handleSave = async () => {
try {
formLoading.value = true
//
const fullAddress = formData.value.location + (locationDetail.value ? '/' + locationDetail.value : '')
const updateData = {
...formData.value,
location: fullAddress
}
await DeviceApi.updateDevice(updateData)
ElMessage.success('保存成功')
isEditing.value = false
} catch (error) {
ElMessage.error('保存失败')
} finally {
formLoading.value = false
}
}
/** 处理关闭按钮点击 */
const handleClose = () => {
dialogVisible.value = false
isEditing.value = false
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
devicecode: undefined,
id: 0,
devicecode: 0,
devicename: '',
orgname: '',
devicetype: '',
location: '',
devicestatus: '',
description: ''
devicestatus: 0,
orgid: 0,
orgname: '',
description: '',
createtime: new Date(),
updatetime: new Date(),
createby: '',
updateby: ''
}
locationDetail.value = ''
selectedOptions.value = []
}
//
const fullLocation = computed(() => {
if (!formData.value.location) return ''
return formData.value.location + (locationDetail.value ? '/' + locationDetail.value : '')
})
defineExpose({ open }) // open
</script>
<style lang="scss">
.location-form-item {
:deep(.el-form-item__content) {
display: inline-flex;
align-items: center;
gap: 10px;
}
.location-cascader {
width: 240px;
}
.detail-address-input {
width: 300px;
}
}
</style>

View File

@ -1,5 +1,12 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<Dialog
v-model="dialogVisible"
:title="dialogTitle"
:modal="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
class="device-dialog"
>
<el-form
ref="formRef"
v-loading="formLoading"
@ -7,59 +14,42 @@
:rules="formRules"
label-width="100px"
:disabled="isDetail"
class="device-form"
>
<el-form-item label="设备ID" prop="deviceId">
<el-input v-model="formData.deviceId" placeholder="请输入设备ID" />
<el-form-item label="设备ID" prop="devicecode">
<el-input v-model="formData.devicecode" placeholder="请输入设备ID" />
</el-form-item>
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="formData.deviceName" placeholder="请输入设备名称" />
<el-form-item label="设备名称" prop="devicename">
<el-input v-model="formData.devicename" placeholder="请输入设备名称" />
</el-form-item>
<el-form-item label="所属机构" prop="orgId">
<el-select v-model="formData.orgId" placeholder="请选择所属机构" @change="handleProductChange">
<el-form-item label="设备类型" prop="devicetype">
<el-select v-model="formData.devicetype" placeholder="请选择设备类型">
<el-option
v-for="item in productList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="设备类型" prop="deviceType">
<el-select v-model="formData.deviceType" placeholder="请选择设备类型">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_TYPE)"
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_DEVICE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="设备位置" prop="address">
<div class="flex items-center gap-2">
<el-form-item label="设备位置" prop="location" class="location-form-item" inline>
<el-cascader
v-model="selectedOptions"
:options="options"
@change="handleAddressChange"
:props="{ expandTrigger: 'hover' }"
placeholder="请选择省/市/区"
@change="handleAddressChange"
class="location-cascader"
clearable
class="w-[450px]"
/>
<el-input
v-model="formData.detailAddress"
v-model="locationDetail"
placeholder="请输入详细地址"
class="w-[450px]"
class="detail-address-input"
/>
</div>
</el-form-item>
<el-form-item label="设备状态" prop="state">
<el-select v-model="formData.state" placeholder="请选择设备状态">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
<el-form-item label="设备状态" prop="devicestatus">
<span>待激活</span>
</el-form-item>
<el-form-item label="设备描述" prop="description">
<el-input
@ -78,19 +68,11 @@
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import * as DeviceApi from '@/api/iot/device/device'
import * as ProductApi from '@/api/iot/product/product'
import * as DeviceGroupApi from '@/api/iot/device/group'
import { DeviceStateEnum } from '@/api/iot/device/device'
import { DICT_TYPE, getIntDictOptions,getStrDictOptions } from '@/utils/dict'
import { FormRules } from 'element-plus'
import { regionData, codeToText } from 'element-china-area-data'
interface CascaderOption {
value: string
label: string
children?: CascaderOption[]
}
import { DeviceApi, DeviceVO } from '@/api/device'
import { getUserProfile } from '@/api/system/user/profile'
defineOptions({ name: 'DeviceForm' })
@ -102,50 +84,35 @@ const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const isDetail = ref(false) //
const formData = ref<DeviceApi.DeviceVO>({
id: undefined,
deviceId: undefined,
deviceName: undefined,
productId: undefined,
productKey: undefined,
deviceType: undefined,
nickname: undefined,
gatewayId: undefined,
state: DeviceStateEnum.INACTIVE,
onlineTime: undefined,
offlineTime: undefined,
activeTime: undefined,
createTime: undefined,
ip: undefined,
firmwareVersion: undefined,
deviceSecret: undefined,
mqttClientId: undefined,
mqttUsername: undefined,
mqttPassword: undefined,
authType: undefined,
latitude: undefined,
longitude: undefined,
areaId: undefined,
address: undefined,
detailAddress: undefined,
serialNumber: undefined,
config: undefined,
description: undefined,
groupIds: []
const locationDetail = ref('') //
const formData = ref<Partial<DeviceVO>>({
devicename: '',
devicecode: undefined,
devicetype: '',
location: '',
devicestatus: 0,
orgid: undefined,
orgname: '',
description: '',
createby: '',
updateby: ''
})
//
const options = ref(regionData)
const selectedOptions = ref<string[]>([])
const formRules = reactive<FormRules>({
deviceId: [{ required: true, message: '设备ID不能为空', trigger: 'blur' }],
deviceName: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }],
deviceType: [{ required: true, message: '设备类型不能为空', trigger: 'blur' }],
address: [{ required: true, message: '设备位置不能为空', trigger: 'blur' }],
devicecode: [{ required: true, message: '设备ID不能为空', trigger: 'blur' }],
devicename: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }],
devicetype: [{ required: true, message: '设备类型不能为空', trigger: 'blur' }],
location: [{ required: true, message: '设备位置不能为空', trigger: 'blur' }],
})
//
const userProfile = ref()
const formRef = ref() // Ref
const productList = ref<ProductApi.ProductVO[]>([]) //
const groupList = ref<DeviceGroupApi.DeviceGroupVO[]>([]) //
const selectedOptions = ref<string[]>([]) //
const options = ref<CascaderOption[]>(regionData as unknown as CascaderOption[])
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
@ -154,70 +121,11 @@ const open = async (type: string, id?: number) => {
formType.value = type
isDetail.value = type === 'details' //
resetForm()
//
if (id) {
formLoading.value = true
try {
// API
const mockDeviceData = {
id: id,
deviceId: 'Device' + id,
deviceName: '测试设备' + id,
deviceType: 1,
state: DeviceStateEnum.INACTIVE,
address: '',
detailAddress: '测试地址123号',
description: '这是一个测试设备的描述信息',
productId: 1,
productKey: 'product_key_' + id,
nickname: '设备昵称' + id,
gatewayId: 1,
onlineTime: new Date(),
offlineTime: new Date(),
activeTime: new Date(),
createTime: new Date(),
ip: '192.168.1.1',
firmwareVersion: '1.0.0',
deviceSecret: 'secret_' + id,
mqttClientId: 'client_' + id,
mqttUsername: 'username_' + id,
mqttPassword: 'password_' + id,
authType: 'password',
latitude: 39.123456,
longitude: 117.123456,
areaId: 1,
serialNumber: 'SN' + id,
config: '{}',
groupIds: []
}
formData.value = mockDeviceData
//
if (formData.value.address) {
const addressParts = formData.value.address.split('/')
if (addressParts.length >= 3) {
//
const provinceCode = Object.keys(codeToText).find(key => codeToText[key] === addressParts[0])
let cityCode = Object.keys(codeToText).find(key => codeToText[key] === addressParts[1])
if(provinceCode == '12'){
cityCode = '1201'
}
const areaCode = Object.keys(codeToText).find(key => codeToText[key] === addressParts[2])
if (provinceCode && cityCode && areaCode) {
selectedOptions.value = [provinceCode, cityCode, areaCode]
//
userProfile.value = await getUserProfile()
console.log(userProfile.value.dept.name)
}
//
if (addressParts.length > 3) {
formData.value.detailAddress = addressParts.slice(3).join('/')
}
}
}
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
@ -230,8 +138,19 @@ const submitForm = async () => {
//
formLoading.value = true
try {
const data = formData.value
//
const fullAddress = formData.value.location + (locationDetail.value ? '/' + locationDetail.value : '')
const data = {
...formData.value,
devicecode: formData.value.devicecode,
location: fullAddress,
orgid: userProfile.value.dept.id,
orgname: userProfile.value.dept.name
} as DeviceVO
if (formType.value === 'create') {
console.log(data)
await DeviceApi.createDevice(data)
message.success(t('common.createSuccess'))
} else {
@ -249,75 +168,81 @@ const submitForm = async () => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
deviceId: undefined,
deviceName: undefined,
productId: undefined,
productKey: undefined,
deviceType: undefined,
nickname: undefined,
gatewayId: undefined,
state: DeviceStateEnum.INACTIVE,
onlineTime: undefined,
offlineTime: undefined,
activeTime: undefined,
createTime: undefined,
ip: undefined,
firmwareVersion: undefined,
deviceSecret: undefined,
mqttClientId: undefined,
mqttUsername: undefined,
mqttPassword: undefined,
authType: undefined,
latitude: undefined,
longitude: undefined,
areaId: undefined,
address: undefined,
detailAddress: undefined,
serialNumber: undefined,
config: undefined,
description: undefined,
groupIds: []
devicename: '',
devicecode: undefined,
devicetype: '',
location: '',
devicestatus: 0,
orgid: undefined,
orgname: '',
description: '',
createby: '',
updateby: ''
}
locationDetail.value = ''
selectedOptions.value = []
formRef.value?.resetFields()
}
/** 检查设备标识 */
const checkDeviceKey = async () => {
// TODO:
message.warning('设备标识检查功能待实现')
}
/** 处理产品变更 */
const handleProductChange = (productId: number) => {
const product = productList.value.find(item => item.id === productId)
if (product) {
formData.value.productKey = product.productKey
}
// productList
// const product = productList.value.find(item => item.id === productId)
// if (product) {
// formData.value.productKey = product.productKey
// }
}
/** 处理地址变化 */
const handleAddressChange = (value: any) => {
const handleAddressChange = (value: string[]) => {
if (value && value.length === 3) {
const address = codeToText[value[0]] + '/' + codeToText[value[1]] + '/' + codeToText[value[2]]
if (formData.value.detailAddress) {
formData.value.address = address + '/' + formData.value.detailAddress
} else {
formData.value.address = address
formData.value.location = address
}
}
</script>
<style lang="scss">
.device-dialog {
position: relative;
:deep(.el-dialog) {
position: relative;
z-index: 2000;
}
}
//
watch(() => formData.value.detailAddress, (newValue) => {
if (selectedOptions.value && selectedOptions.value.length === 3) {
const address = codeToText[selectedOptions.value[0]] + '/' + codeToText[selectedOptions.value[1]] + '/' + codeToText[selectedOptions.value[2]]
if (newValue) {
formData.value.address = address + '/' + newValue
} else {
formData.value.address = address
.device-form {
position: relative;
}
.status-select-item {
position: relative;
}
.device-status-select {
position: absolute !important;
z-index: 2001 !important;
}
:deep(.el-select__popper) {
position: absolute !important;
z-index: 2001 !important;
}
.location-form-item {
:deep(.el-form-item__content) {
display: inline-flex;
align-items: center;
gap: 10px;
}
.location-cascader {
width: 240px;
}
.detail-address-input {
width: 300px;
}
}
})
</script>
</style>

View File

@ -57,7 +57,7 @@
<el-button
type="primary"
size="small"
:disabled="deviceInfo.devicestatus === 1"
:disabled="deviceInfo.devicestatus === 1 || deviceInfo.devicestatus === 0"
@click="handleAction('enable')"
>
启用
@ -66,7 +66,7 @@
<el-button
type="warning"
size="small"
:disabled="deviceInfo.devicestatus === 2"
:disabled="deviceInfo.devicestatus === 2 || deviceInfo.devicestatus === 0"
@click="handleAction('disable')"
>
停用

View File

@ -97,9 +97,10 @@ import DeviceCard from '../devices/devices_cards.vue'
import DeviceForm from './DevFrom.vue'
import DetailsForm from './DetailsForm.vue'
import { DeviceApi } from '@/api/device'
import { DeviceuserApi } from '@/api/deviceuser'
import { getUserProfile } from '@/api/system/user/profile'
import ECG_datas from './Device_Data_Components/ECG_datas.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
//
interface QueryParams {
devicename: string
@ -118,7 +119,7 @@ const queryParams = reactive<QueryParams>({
pageSize: 10,
orgid: 0
})
const message = useMessage() //
//
const total = ref(0)
//
@ -164,7 +165,7 @@ const openForm = (type: string) => {
}
//
const handleDeviceAction = (action: any) => {
const handleDeviceAction = async (action: any) => {
console.log('设备操作:', action)
if (action.action === 'details') {
//
@ -172,8 +173,26 @@ const handleDeviceAction = (action: any) => {
} else if (action.action === 'openECGData') {
//
ecgDataRef.value?.open(action.deviceId, action.deviceName)
} else if (action.action === 'delete') {
//
const res = await DeviceuserApi.getDevCount(action.deviceId)
if (res> 0) {
message.error('设备下有人员,不能删除,请解绑后再进行操作')
return
}
ElMessageBox.confirm('是否删除该设备?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res2 = await DeviceApi.deleteDeviceCode(action.deviceId)
if (res2) {
message.success('删除成功')
handleQuery()
}
})
} else {
console.log('设备操作:', action)
//
}
}