feat:必硕1.0页面迁移

master
黄伟杰 2 months ago
parent d4b8bc4c94
commit f84277c967

@ -4,6 +4,9 @@ import request from '@/config/axios'
export interface ProductUnitVO {
id: number // 单位编号
name: string // 单位名字
primaryFlag: string // 是否主单位
primaryId: number // 关联主单位id
changeRate: number // 换算比例
status: number // 单位状态
}
@ -42,5 +45,15 @@ export const ProductUnitApi = {
// 导出产品单位 Excel
exportProductUnit: async (params) => {
return await request.download({ url: `/erp/product-unit/export-excel`, params })
}
},
// 获得产品单位列表
getProductUnitList: async () => {
return await request.get({ url: `/erp/product-unit/list`})
},
// 获得产品主单位列表
getProductUnitListByFlag: async () => {
return await request.get({ url: `/erp/product-unit/flag-list`})
},
}

@ -1,184 +0,0 @@
import request from '@/config/axios'
// IoT 设备 VO
export interface DeviceVO {
id: number // 设备 ID主键自增
deviceName: string // 设备名称
productId: number // 产品编号
productName?: string // 产品名称(只有部分接口返回,例如 getDeviceLocationList
productKey: string // 产品标识
deviceType: number // 设备类型
nickname: string // 设备备注名称
gatewayId: number // 网关设备 ID
state: number // 设备状态
onlineTime: Date // 最后上线时间
offlineTime: Date // 最后离线时间
activeTime: Date // 设备激活时间
createTime: Date // 创建时间
ip: string // 设备的 IP 地址
firmwareVersion: string // 设备的固件版本
deviceSecret: string // 设备密钥,用于设备认证,需安全存储
mqttClientId: string // MQTT 客户端 ID
mqttUsername: string // MQTT 用户名
mqttPassword: string // MQTT 密码
latitude?: number // 设备位置的纬度
longitude?: number // 设备位置的经度
areaId: number // 地区编码
address: string // 设备详细地址
serialNumber: string // 设备序列号
config: string // 设备配置
groupIds?: number[] // 添加分组 ID
}
// IoT 设备属性详细 VO
export interface IotDevicePropertyDetailRespVO {
identifier: string // 属性标识符
value: string // 最新值
updateTime: Date // 更新时间
name: string // 属性名称
dataType: string // 数据类型
dataSpecs: any // 数据定义
dataSpecsList: any[] // 数据定义列表
}
// IoT 设备属性 VO
export interface IotDevicePropertyRespVO {
identifier: string // 属性标识符
value: string // 最新值
updateTime: Date // 更新时间
}
// 设备认证参数 VO
export interface IotDeviceAuthInfoVO {
clientId: string // 客户端 ID
username: string // 用户名
password: string // 密码
}
// IoT 设备发送消息 Request VO
export interface IotDeviceMessageSendReqVO {
deviceId: number // 设备编号
method: string // 请求方法
params?: any // 请求参数
}
// 设备 API
export const DeviceApi = {
// 查询设备分页
getDevicePage: async (params: any) => {
return await request.get({ url: `/iot/device/page`, params })
},
// 查询设备详情
getDevice: async (id: number) => {
return await request.get({ url: `/iot/device/get?id=` + id })
},
// 新增设备
createDevice: async (data: DeviceVO) => {
return await request.post({ url: `/iot/device/create`, data })
},
// 修改设备
updateDevice: async (data: DeviceVO) => {
return await request.put({ url: `/iot/device/update`, data })
},
// 修改设备分组
updateDeviceGroup: async (data: { ids: number[]; groupIds: number[] }) => {
return await request.put({ url: `/iot/device/update-group`, data })
},
// 删除单个设备
deleteDevice: async (id: number) => {
return await request.delete({ url: `/iot/device/delete?id=` + id })
},
// 删除多个设备
deleteDeviceList: async (ids: number[]) => {
return await request.delete({ url: `/iot/device/delete-list`, params: { ids: ids.join(',') } })
},
// 导出设备
exportDeviceExcel: async (params: any) => {
return await request.download({ url: `/iot/device/export-excel`, params })
},
// 获取设备数量
getDeviceCount: async (productId: number) => {
return await request.get({ url: `/iot/device/count?productId=` + productId })
},
// 获取设备的精简信息列表
getSimpleDeviceList: async (deviceType?: number, productId?: number) => {
return await request.get({ url: `/iot/device/simple-list?`, params: { deviceType, productId } })
},
// 获取设备位置列表(用于地图展示)
getDeviceLocationList: async () => {
return await request.get<DeviceVO[]>({ url: `/iot/device/location-list` })
},
// 根据产品编号,获取设备的精简信息列表
getDeviceListByProductId: async (productId: number) => {
return await request.get({ url: `/iot/device/simple-list?`, params: { productId } })
},
// 获取导入模板
importDeviceTemplate: async () => {
return await request.download({ url: `/iot/device/get-import-template` })
},
// 获取设备属性最新数据
getLatestDeviceProperties: async (params: any) => {
return await request.get({ url: `/iot/device/property/get-latest`, params })
},
// 获取设备属性历史数据
getHistoryDevicePropertyList: async (params: any) => {
return await request.get({ url: `/iot/device/property/history-list`, params })
},
// 获取设备认证信息
getDeviceAuthInfo: async (id: number) => {
return await request.get({ url: `/iot/device/get-auth-info`, params: { id } })
},
// 查询设备消息分页
getDeviceMessagePage: async (params: any) => {
return await request.get({ url: `/iot/device/message/page`, params })
},
// 查询设备消息配对分页
getDeviceMessagePairPage: async (params: any) => {
return await request.get({ url: `/iot/device/message/pair-page`, params })
},
// 发送设备消息
sendDeviceMessage: async (params: IotDeviceMessageSendReqVO) => {
return await request.post({ url: `/iot/device/message/send`, data: params })
},
// 绑定子设备到网关
bindDeviceGateway: async (data: { subIds: number[]; gatewayId: number }) => {
return await request.put({ url: `/iot/device/bind-gateway`, data })
},
// 解绑子设备与网关
unbindDeviceGateway: async (data: { subIds: number[]; gatewayId: number }) => {
return await request.put({ url: `/iot/device/unbind-gateway`, data })
},
// 获取网关的子设备列表
getSubDeviceList: async (gatewayId: number) => {
return await request.get<DeviceVO[]>({
url: `/iot/device/sub-device-list`,
params: { gatewayId }
})
},
// 获取未绑定网关的子设备分页
getUnboundSubDevicePage: async (params: any) => {
return await request.get({ url: `/iot/device/unbound-sub-device-page`, params })
}
}

@ -1,43 +0,0 @@
import request from '@/config/axios'
// IoT 设备分组 VO
export interface DeviceGroupVO {
id: number // 分组 ID
name: string // 分组名字
status: number // 分组状态
description: string // 分组描述
deviceCount?: number // 设备数量
}
// IoT 设备分组 API
export const DeviceGroupApi = {
// 查询设备分组分页
getDeviceGroupPage: async (params: any) => {
return await request.get({ url: `/iot/device-group/page`, params })
},
// 查询设备分组详情
getDeviceGroup: async (id: number) => {
return await request.get({ url: `/iot/device-group/get?id=` + id })
},
// 新增设备分组
createDeviceGroup: async (data: DeviceGroupVO) => {
return await request.post({ url: `/iot/device-group/create`, data })
},
// 修改设备分组
updateDeviceGroup: async (data: DeviceGroupVO) => {
return await request.put({ url: `/iot/device-group/update`, data })
},
// 删除设备分组
deleteDeviceGroup: async (id: number) => {
return await request.delete({ url: `/iot/device-group/delete?id=` + id })
},
// 获取设备分组的精简信息列表
getSimpleDeviceGroupList: async () => {
return await request.get({ url: `/iot/device-group/simple-list` })
}
}

@ -0,0 +1,202 @@
import request from '@/config/axios'
import qs from 'qs'
// 物联设备 VO
export interface DeviceVO {
id: number // ID
deviceCode: string // 设备编号
deviceName: string // 设备名称
deviceType: string // 设备类型
status: string // 状态
isConnect?: string | number
operatingStatus?: string | number
readTopic: string // 读主题
writeTopic: string // 写主题
gatewayId: number // 网关id
deviceBrandId: number // 设备品牌id
offLineDuration: number // 离线间隔
lastOnlineTime: Date // 最后上线时间
remark: string // 备注
isEnable: boolean // 是否启用
deviceModelId: number // 关联设备模型
protocol: string // 通讯协议
sampleCycle: number // 采集周期
url: string // 端点url
username: string // 用户名
password: string // 密码
certificate?: string // 证书
secretKey?: string // 秘钥
collectionTime?: string | number
}
export interface DeviceConnectParams {
id: string | number
isConnect: string | number
}
export interface LineDeviceVO {
id?: string | number
lineNode?: string
lineName?: string
deviceCode?: string
deviceName?: string
status?: string | number
collectionTime?: string | number
}
export interface LineDevicePageParams {
pageNo: number
pageSize: number
id?: string | number
lineNode?: string
lineName?: string
deviceCode?: string
deviceName?: string
status?: string | number
collectionTime?: string | number
collectionTimeStart?: string
collectionTimeEnd?: string
}
export interface SingleDeviceParams {
deviceId: string | number
}
export interface HistoryRecordParams {
deviceId: string | number
collectionStartTime?: string
collectionEndTime?: string
attributeCodes?: string[]
}
export interface DeviceContactModelVO {
id: number
attributeCode?: string
attributeName?: string
attributeType?: string
dataType?: string
dataUnit?: string
}
// 物联设备 API
export const DeviceApi = {
// 查询物联设备分页
getDevicePage: async (params: any) => {
return await request.get({ url: `/iot/device/page`, params })
},
// 查询物联设备
getDeviceList: async () => {
return await request.get({ url: `/iot/device/deviceList` })
},
// 查询物联设备详情
getDevice: async (id: number) => {
return await request.get({ url: `/iot/device/get?id=` + id })
},
getDeviceListByNoUsed: async () => {
return await request.get({ url: `/iot/device/noUsedlist` })
},
getDeviceList2ByNoUsed: async (id: number) => {
return await request.get({ url: `/iot/device/noUsedlist2?id=` + id })
},
// 新增物联设备
createDevice: async (data: DeviceVO) => {
return await request.post({ url: `/iot/device/create`, data })
},
// 批量获取设备属性列表
getDeviceAttributeBatchList: async (params: { goviewId: number | string; orgId?: number | string }) => {
return await request.get({ url: `/iot/device/device-attribute/batchList`, params })
},
// 修改物联设备
updateDevice: async (data: DeviceVO) => {
return await request.put({ url: `/iot/device/update`, data })
},
// 删除物联设备
deleteDevice: async (ids: string) => {
return await request.delete({ url: `/iot/device/delete?ids=` + ids })
},
// 复制物联设备
copyDevice: async (id: number) => {
return await request.post({ url: `/iot/device/copy`, params: { id } })
},
connectDevice: async (params: DeviceConnectParams) => {
return await request.post({ url: `/iot/device/connect`, data: params })
},
// 导出物联设备 Excel
exportDevice: async (params) => {
return await request.download({ url: `/iot/device/export-excel`, params })
},
exportLineDevice: async (params) => {
return await request.download({ url: `/iot/device/export-line-device`, params })
},
getLineDevicePage: async (params: LineDevicePageParams) => {
return await request.get({ url: `/iot/device/lineDevicePage`, params })
},
getSingleDevice: async (params: SingleDeviceParams) => {
return await request.get({ url: `/iot/device/singleDevice`, params })
},
getHistoryRecord: async (params: HistoryRecordParams) => {
return await request.get({
url: `/iot/device/historyRecord`,
params,
paramsSerializer: (p) => qs.stringify(p, { allowDots: true, arrayFormat: 'repeat' })
})
},
devicePointList: async () => {
return await request.get({ url: `/iot/device/devicePointList` })
},
updateDeviceEnabled: async (id: number | string, enabled: string) => {
const data = { id, enabled }
return await request.put({ url: `/iot/device/update-enabled`, data })
},
// ==================== 子表(设备属性) ====================
// 获得设备属性分页
getDeviceAttributePage: async (params) => {
return await request.get({ url: `/iot/device/device-attribute/page`, params })
},
// 获得设备属性列表
getDeviceAttributeList: async (deviceId: number | string) => {
return await request.get({ url: `/iot/device/device-attribute/list?deviceId=` + deviceId })
},
getDeviceContactModelPage: async () => {
return await request.get({ url: `/iot/device-contact-model/page` })
},
// 新增设备属性
createDeviceAttribute: async (data) => {
return await request.post({ url: `/iot/device-contact-model/create`, data })
},
// 修改设备属性
updateDeviceAttribute: async (data) => {
return await request.put({ url: `/iot/device-contact-model/update`, data })
},
// 删除设备属性
deleteDeviceAttribute: async (ids: string) => {
return await request.delete({ url: `/iot/device-contact-model/delete?ids=` + ids })
},
// 导出设备属性 Excel
exportDeviceContactModel: async (params) => {
return await request.download({ url: `/iot/device-contact-model/export-excel`, params })
},
// 获得设备属性
getDeviceAttribute: async (id: number) => {
return await request.get({ url: `/iot/device-contact-model/get?id=` + id })
}
}

@ -1,31 +0,0 @@
import request from '@/config/axios'
/** Modbus 连接配置 VO */
export interface DeviceModbusConfigVO {
id?: number // 主键
deviceId: number // 设备编号
ip: string // Modbus 服务器 IP 地址
port: number // Modbus 服务器端口
slaveId: number // 从站地址
timeout: number // 连接超时时间,单位:毫秒
retryInterval: number // 重试间隔,单位:毫秒
mode: number // 模式
frameFormat: number // 帧格式
status: number // 状态
}
/** Modbus 连接配置 API */
export const DeviceModbusConfigApi = {
/** 获取设备的 Modbus 连接配置 */
getModbusConfig: async (deviceId: number) => {
return await request.get<DeviceModbusConfigVO>({
url: `/iot/device-modbus-config/get`,
params: { deviceId }
})
},
/** 保存 Modbus 连接配置 */
saveModbusConfig: async (data: DeviceModbusConfigVO) => {
return await request.post({ url: `/iot/device-modbus-config/save`, data })
}
}

@ -1,48 +0,0 @@
import request from '@/config/axios'
/** Modbus 点位配置 VO */
export interface DeviceModbusPointVO {
id?: number // 主键
deviceId: number // 设备编号
thingModelId?: number // 物模型属性编号
identifier: string // 属性标识符
name: string // 属性名称
functionCode?: number // Modbus 功能码
registerAddress?: number // 寄存器起始地址
registerCount?: number // 寄存器数量
byteOrder?: string // 字节序
rawDataType?: string // 原始数据类型
scale: number // 缩放因子
pollInterval: number // 轮询间隔,单位:毫秒
status: number // 状态
}
/** Modbus 点位配置 API */
export const DeviceModbusPointApi = {
/** 获取设备的 Modbus 点位分页 */
getModbusPointPage: async (params: any) => {
return await request.get({ url: `/iot/device-modbus-point/page`, params })
},
/** 获取 Modbus 点位详情 */
getModbusPoint: async (id: number) => {
return await request.get<DeviceModbusPointVO>({
url: `/iot/device-modbus-point/get?id=${id}`
})
},
/** 创建 Modbus 点位配置 */
createModbusPoint: async (data: DeviceModbusPointVO) => {
return await request.post({ url: `/iot/device-modbus-point/create`, data })
},
/** 更新 Modbus 点位配置 */
updateModbusPoint: async (data: DeviceModbusPointVO) => {
return await request.put({ url: `/iot/device-modbus-point/update`, data })
},
/** 删除 Modbus 点位配置 */
deleteModbusPoint: async (id: number) => {
return await request.delete({ url: `/iot/device-modbus-point/delete?id=${id}` })
}
}

@ -0,0 +1,48 @@
import request from '@/config/axios'
// 采集点分类 VO
export interface DeviceAttributeTypeVO {
id: number // ID
code: string // 分类编码
name: string // 分类名称
sort: number // 分类顺序
remark: string // 备注
}
// 采集点分类 API
export const DeviceAttributeTypeApi = {
// 查询采集点分类分页
getDeviceAttributeTypePage: async (params: any) => {
return await request.get({ url: `/iot/device-attribute-type/page`, params })
},
// 查询采集点分类详情
getDeviceAttributeType: async (id: number) => {
return await request.get({ url: `/iot/device-attribute-type/get?id=` + id })
},
// 新增采集点分类
createDeviceAttributeType: async (data: DeviceAttributeTypeVO) => {
return await request.post({ url: `/iot/device-attribute-type/create`, data })
},
// 修改采集点分类
updateDeviceAttributeType: async (data: DeviceAttributeTypeVO) => {
return await request.put({ url: `/iot/device-attribute-type/update`, data })
},
// 删除采集点分类
deleteDeviceAttributeType: async (ids: string) => {
return await request.delete({ url: `/iot/device-attribute-type/delete?ids=` + ids })
},
// 导出采集点分类 Excel
exportDeviceAttributeType: async (params) => {
return await request.download({ url: `/iot/device-attribute-type/export-excel`, params })
},
// 获得采集点分类列表
getDeviceAttributeTypeList: async () => {
return await request.get({ url: `/iot/device-attribute-type/list` })
},
}

@ -0,0 +1,53 @@
import request from '@/config/axios'
// 采集设备模型 VO
export interface DeviceModelVO {
id: number // ID
code: string // 分类编码
name: string // 分类名称
protocol: string // 通讯协议
remark: string // 备注
}
// 采集设备模型 API
export const DeviceModelApi = {
// 查询采集设备模型分页
getDeviceModelPage: async (params: any) => {
return await request.get({ url: `/iot/device-model/page`, params })
},
// 查询采集设备模型详情
getDeviceModel: async (id: number) => {
return await request.get({ url: `/iot/device-model/get?id=` + id })
},
// 新增采集设备模型
createDeviceModel: async (data: DeviceModelVO) => {
return await request.post({ url: `/iot/device-model/create`, data })
},
// 修改采集设备模型
updateDeviceModel: async (data: DeviceModelVO) => {
return await request.put({ url: `/iot/device-model/update`, data })
},
// 删除采集设备模型
deleteDeviceModel: async (ids: string) => {
return await request.delete({ url: `/iot/device-model/delete?ids=` + ids })
},
// 复制采集设备模型
copyDeviceModel: async (id: number) => {
return await request.post({ url: `/iot/device-model/copy`, params: { id } })
},
// 导出采集设备模型 Excel
exportDeviceModel: async (params) => {
return await request.download({ url: `/iot/device-model/export-excel`, params })
},
// 获得采集设备模型列表
getDeviceModelList: async () => {
return await request.get({ url: `/iot/device-model/list` })
},
}

@ -0,0 +1,67 @@
import request from '@/config/axios'
// 采集设备模型-点位管理 VO
export interface DeviceModelAttributeVO {
id: number // ID
attributeCode: string // 点位编码
attributeName: string // 点位名称
attributeType: string // 点位类型
dataType: string // 数据类型
address: string // 寄存器地址
dataUnit: string // 单位
ratio: number // 倍率
remark: string // 备注
deviceModelId: number // 采集设备模型id
}
// 采集设备模型-点位管理 API
export const DeviceModelAttributeApi = {
// 查询采集设备模型-点位管理分页
getDeviceModelAttributePage: async (params: any) => {
return await request.get({ url: `/iot/device-model-attribute/page`, params })
},
// 查询采集设备模型-点位管理列表(当前设备模型下的点位)
getDeviceModelAttributeList: async (id: number) => {
return await request.get({ url: `/iot/device-model-attribute/list`, params: { id } })
},
// 查询采集设备模型-点位管理详情
getDeviceModelAttribute: async (id: number) => {
return await request.get({ url: `/iot/device-model-attribute/get?id=` + id })
},
// 新增采集设备模型-点位管理
createDeviceModelAttribute: async (data) => {
return await request.post({ url: `/iot/device-model-attribute/create`, data })
},
// 修改采集设备模型-点位管理
updateDeviceModelAttribute: async (data) => {
return await request.put({ url: `/iot/device-model-attribute/update`, data })
},
// 删除采集设备模型-点位管理
deleteDeviceModelAttribute: async (ids: string) => {
return await request.delete({ url: `/iot/device-model-attribute/delete?ids=` + ids })
},
// 导出采集设备模型-点位管理 Excel
exportDeviceModelAttribute: async (params) => {
return await request.download({ url: `/iot/device-model-attribute/export-excel`, params })
},
// 下载采集设备模型-点位管理导入模板
importDeviceModelAttributeTemplate: async () => {
return await request.download({ url: `/iot/device-model-attribute/get-import-template` })
},
operationAnalysisDetails: async (params: {
deviceId: number
modelId?: number
collectionStartTime?: string
collectionEndTime?: string
}) => {
return await request.get({ url: `/iot/device-model-attribute/operationAnalysisDetails`, params })
}
}

@ -485,5 +485,835 @@ export default {
btn_zoom_in: 'Zoom in',
btn_zoom_out: 'Zoom out',
preview: 'Preivew'
},
DataCollection: {
DeviceAttributeType: {
moduleName: 'Acquisition Point Type',
index: 'Index',
code: 'Type Code',
name: 'Type Name',
sort: 'Display Order',
remark: 'Remark',
createTime: 'Create Time',
operate: 'Operate',
search: 'Search',
reset: 'Reset',
create: 'Create',
batchDelete: 'Batch Delete',
export: 'Export',
placeholderCode: 'Please enter type code',
placeholderName: 'Please enter type name',
placeholderSort: 'Please enter display order',
placeholderRemark: 'Please enter remark',
dialogOk: 'Confirm',
dialogCancel: 'Cancel',
validatorCodeRequired: 'Type code can not be empty',
validatorNameRequired: 'Type name can not be empty',
validatorSortRequired: 'Display order can not be empty',
exportFilename: 'AcquisitionPointType.xls'
},
DeviceModel: {
moduleName: 'Acquisition Device Model',
index: 'Index',
code: 'Model Code',
name: 'Model Name',
protocol: 'Protocol',
remark: 'Remark',
createTime: 'Create Time',
operate: 'Operate',
search: 'Search',
reset: 'Reset',
create: 'Create',
batchDelete: 'Batch Delete',
export: 'Export',
placeholderCode: 'Please enter model code',
placeholderName: 'Please enter model name',
placeholderProtocol: 'Please select protocol',
placeholderRemark: 'Please enter remark',
dialogOk: 'Confirm',
dialogCancel: 'Cancel',
validatorCodeRequired: 'Model code can not be empty',
validatorNameRequired: 'Model name can not be empty',
validatorProtocolRequired: 'Protocol can not be empty',
validatorDataTypeRequired: 'Data type can not be empty',
exportFilename: 'AcquisitionDeviceModel.xls',
attributeModuleName: 'Point Management',
attributeCode: 'Point Code',
attributeName: 'Point Name',
attributeType: 'Point Type',
dataType: 'Data Type',
address: 'Register Address',
dataUnit: 'Unit',
ratio: 'Ratio',
placeholderAttributeCode: 'Please enter point code',
placeholderAttributeName: 'Please enter point name',
placeholderAttributeType: 'Please select point type',
placeholderDataType: 'Please select data type',
placeholderAddress: 'Please enter register address',
placeholderDataUnit: 'Please select unit',
placeholderRatio: 'Please enter ratio',
placeholderAttributeRemark: 'Please enter remark',
validatorAttributeCodeRequired: 'Point code can not be empty',
validatorAttributeNameRequired: 'Point name can not be empty',
attributeExportFilename: 'AcquisitionDeviceModelPoint.xls',
ruleTabLabel: 'Point Rule',
ruleIdentifier: 'Identifier',
ruleFieldName: 'Name',
ruleFieldRule: 'Rule',
ruleDefaultValue: 'Default Value',
ruleCreateTime: 'Create Time',
ruleOperate: 'Operate',
ruleSearchIdentifierPlaceholder: 'Please enter identifier',
ruleSearchFieldNamePlaceholder: 'Please enter name',
ruleSearchDefaultValuePlaceholder: 'Please enter default value',
ruleSearch: 'Search',
ruleReset: 'Reset',
ruleCreateButton: 'Add Alarm Rule',
ruleEditRuleButton: 'Edit',
ruleDeleteRuleButton: 'Delete',
ruleDialogTitle: 'Edit Point Rule',
ruleDialogIdentifier: 'Identifier',
ruleDialogFieldName: 'Name',
ruleDialogDefaultValue: 'Default Value',
ruleDialogAlarmLevel: 'Alarm Level',
ruleDialogFieldRule: 'Point Rule',
ruleDialogRule: 'Rule',
ruleDialogRuleAttribute: 'Point',
ruleDialogRuleOperator: 'Condition',
ruleDialogRuleValue: 'Value',
ruleDialogAlarmLevelPlaceholder: 'Please select alarm level',
ruleDialogFieldRulePlaceholder: 'Please select point rule',
ruleDialogRuleAttributePlaceholder: 'Please select point',
ruleDialogRuleOperatorPlaceholder: 'Please select condition',
ruleDialogRuleValuePlaceholder: 'Please enter value'
},
Device: {
moduleName: 'Acquisition Device',
index: 'Index',
deviceCode: 'Device Code',
deviceName: 'Device Name',
operatingStatus: 'Operating Status',
protocol: 'Protocol',
status: 'Connection Status',
sampleCycle: 'Sample Cycle(s)',
isEnable: 'Enabled',
collectionTime: 'Collection Time',
operate: 'Operation',
search: 'Search',
reset: 'Reset',
create: 'Create',
batchDelete: 'Batch Delete',
export: 'Export',
placeholderDeviceCode: 'Please enter device code',
placeholderDeviceName: 'Please enter device name',
placeholderModel: 'Please select device model',
placeholderSampleCycle: 'Please enter sample cycle',
placeholderUrl: 'Please enter endpoint URL',
placeholderUsername: 'Please enter username',
placeholderPassword: 'Please enter password',
placeholderTopic: 'Please enter subscription topic',
model: 'Device Model',
url: 'Endpoint URL',
username: 'Username',
password: 'Password',
topic: 'Subscription Topic',
settingDialogTitle: 'Device Settings',
connect: 'Connect',
disconnect: 'Disconnect',
attributeModuleName: 'Point',
attributeCode: 'Point Code',
attributeName: 'Point Name',
attributeType: 'Point Type',
dataType: 'Data Type',
address: 'Register Address',
dataUnit: 'Unit',
ratio: 'Ratio',
remark: 'Remark',
deviceAttributeTabLabel: 'Device Attributes',
deviceRuleTabLabel: 'Point Rules',
currentDeviceLabel: 'Current Device: ',
alarmHistoryTitle: 'Device Alarm History Data',
alarmRuleName: 'Rule Name',
alarmPointName: 'Point Name',
alarmPointValue: 'Point Value',
alarmLevel: 'Alarm Level',
alarmTime: 'Alarm Time',
emptyDescription: 'Click "Point" in the device list to view points and rules',
exportFilename: 'IoTDevice.xls',
attributeExportFilename: 'AcquisitionDevice-Point.xls',
attributeLatestValue: 'Latest Value',
attributeLatestCollectionTime: 'Latest Collection Time',
attributeSort: 'Order',
attributePlaceholderSort: 'Please enter order',
placeholderAttributeCode: 'Please enter point code',
placeholderAttributeName: 'Please enter point name',
placeholderAttributeType: 'Please select point type',
placeholderDataType: 'Please select data type',
placeholderAddress: 'Please enter register address',
placeholderDataUnit: 'Please enter unit',
placeholderRatio: 'Please enter ratio',
placeholderRemark: 'Please enter remark',
validatorDeviceCodeRequired: 'Device code can not be empty',
validatorDeviceNameRequired: 'Device name can not be empty',
validatorSampleCycleRequired: 'Sample cycle can not be empty',
validatorIsEnableRequired: 'Enable status can not be empty',
validatorUrlRequired: 'Endpoint URL can not be empty',
attributeValidatorCodeRequired: 'Point code can not be empty',
attributeValidatorNameRequired: 'Point name can not be empty',
attributeValidatorCodeNoChinese: 'Point code can not contain Chinese characters',
attributeValidatorSortNumber: 'Order must contain only numbers',
attributeValidatorRemarkTooLong: 'Remark can not exceed 100 characters',
messageSelectDeviceRequired: 'Please select a device',
messageDeviceInfoMissingForRules: 'Device information is missing, unable to load point rules',
gridView: 'gridView',
sudoku: 'sudoku'
},
RunReport: {
moduleName: 'Device Operation Report',
searchDeviceCodeLabel: 'Device Code',
searchDeviceCodePlaceholder: 'Please enter device code',
searchDeviceNameLabel: 'Device Name',
searchDeviceNamePlaceholder: 'Please enter device name',
searchTimeRangeLabel: 'Device Running Time',
searchTimeRangeStartPlaceholder: 'Start Time',
searchTimeRangeEndPlaceholder: 'End Time',
searchButtonText: 'Search',
resetButtonText: 'Reset',
exportButtonText: 'Export',
tableDeviceCodeColumn: 'Device Code',
tableDeviceNameColumn: 'Device Name',
tableRunningTimeColumn: 'Running Time (Hours)',
tableStandbyTimeColumn: 'Standby Time (Hours)',
tableFaultTimeColumn: 'Fault Time (Hours)',
tableWarningTimeColumn: 'Warning Time (Hours)',
tableUtilizationRateColumn: 'Utilization Rate',
tableUtilizationRateTooltip:
'Power-on Rate = Online Time / Total Time\nOnline Time = Running Time + Standby Time + Fault Time\nUtilization Rate = Running Time / Online Time\nPower-on Rate = (Running Time + Standby Time + Fault Time) / Total Time',
tablePowerOnRateColumn: 'Power-on Rate',
tableStartTimeColumn: 'Device Run Start Time',
tableEndTimeColumn: 'Device Run End Time',
exportFilename: 'DeviceOperationReport.xls'
},
RealTimeMonitoring: {
moduleName: 'Real-time Data Monitoring',
searchLineCodeLabel: 'Line Code',
searchLineCodePlaceholder: 'Please enter line code',
searchLineNameLabel: 'Line Name',
searchLineNamePlaceholder: 'Please enter line name',
searchDeviceCodeLabel: 'Device Code',
searchDeviceCodePlaceholder: 'Please enter device code',
searchDeviceNameLabel: 'Device Name',
searchDeviceNamePlaceholder: 'Please enter device name',
searchButtonText: 'Search',
resetButtonText: 'Reset',
exportButtonText: 'Export',
tableLineCodeColumn: 'Line Code',
tableLineNameColumn: 'Line Name',
tableDeviceCodeColumn: 'Device Code',
tableDeviceNameColumn: 'Device Name',
tableStatusColumn: 'Connection Status',
tableCollectionTimeColumn: 'Latest Collection Time',
tableOperateColumn: 'Operation',
tableActionSingleMonitorLabel: 'Single Device Monitoring',
dialogTitle: 'Single Device Monitoring',
dialogDeviceNameLabel: 'Device Name: ',
dialogCollectionTimeLabel: 'Collection Time: ',
emptyDescription: 'No data',
defaultGroupName: 'Default',
defaultFieldLabelPrefix: 'Field ',
messageDeviceInfoIncomplete: 'Device information is incomplete',
exportFilename: 'RealTimeMonitoringDevice.xls'
},
HistoryData: {
moduleName: 'History Record Query',
searchLineCodeLabel: 'Line Code',
searchLineCodePlaceholder: 'Please enter line code',
searchLineNameLabel: 'Line Name',
searchLineNamePlaceholder: 'Please enter line name',
searchDeviceCodeLabel: 'Device Code',
searchDeviceCodePlaceholder: 'Please enter device code',
searchDeviceNameLabel: 'Device Name',
searchDeviceNamePlaceholder: 'Please enter device name',
searchButtonText: 'Search',
resetButtonText: 'Reset',
exportButtonText: 'Export',
tableLineCodeColumn: 'Line Code',
tableLineNameColumn: 'Line Name',
tableDeviceCodeColumn: 'Device Code',
tableDeviceNameColumn: 'Device Name',
tableCollectionTimeColumn: 'Collection Time',
tableOperateColumn: 'Operation',
tableActionHistoryLabel: 'History',
dialogTitlePrefix: 'History: ',
dialogCollectionTimeLabel: 'Collection Time',
dialogCollectionTimeStartPlaceholder: 'Start Time',
dialogCollectionTimeEndPlaceholder: 'End Time',
dialogSearchButtonText: 'Search',
dialogResetButtonText: 'Reset',
dialogRecordCollectionTimePrefix: 'Collection Time: ',
emptyDescription: 'No data',
defaultFieldLabelPrefix: 'Field ',
exportFilename: 'HistoryDataDevice.xls'
},
DeviceParamAnalysis: {
moduleName: 'Device Operation Parameter Analysis',
treeSearchPlaceholder: 'Search device or parameter',
formTimeLabel: 'Time',
formTimeStartPlaceholder: 'Start Date',
formTimeEndPlaceholder: 'End Date',
shortcutLast7Days: 'Last 7 Days',
shortcutLastWeek: 'Last Week',
shortcutLastMonth: 'Last Month',
shortcutLast3Months: 'Last 3 Months',
searchButtonText: 'Search',
resetButtonText: 'Reset',
emptyDescription: 'No data',
emptySelectNodeDescription: 'Please select a node on the left',
selectedParamTitle: 'Node: {label}{unit}',
defaultSeriesName: 'Parameter',
messageLoadTreeFailed: 'Failed to load tree data',
messageNodeNoParams: 'No parameters under this node',
messageDeviceNoParams: 'No parameters under this device',
messageFetchChartFailed: 'Failed to load chart data'
}
},
FactoryModeling: {
FactoryStructure: {
moduleName: 'Factory Structure',
searchCodeLabel: 'Organization Code',
searchCodePlaceholder: 'Please enter organization code',
searchParentLabel: 'Parent Organization',
searchParentPlaceholder: 'Please select parent organization',
searchNameLabel: 'Organization Name',
searchNamePlaceholder: 'Please enter organization name',
searchOrgClassLabel: 'Organization Level',
searchOrgClassPlaceholder: 'Please select organization level',
searchButtonText: 'Search',
resetButtonText: 'Reset',
addButtonText: 'Add',
exportButtonText: 'Export',
expandButtonText: 'Expand/Collapse',
tableCodeColumn: 'Organization Code',
tableNameColumn: 'Organization Name',
tableSortColumn: 'Display Order',
tableMachineColumn: 'Related Device',
tableOrgClassColumn: 'Organization Level',
tableOrgTypeColumn: 'Process',
tableCreateTimeColumn: 'Create Time',
tableOperateColumn: 'Operation',
tableEditAction: 'Edit',
tableDeleteAction: 'Delete',
dialogCodeLabel: 'Code',
dialogCodeTooltip: 'Factory structure code',
dialogCodePlaceholder: 'Code is auto generated after saving',
dialogParentLabel: 'Parent Organization',
dialogParentPlaceholder: 'Please select parent organization',
dialogNameLabel: 'Organization Name',
dialogNamePlaceholder: 'Please enter organization name',
dialogOrgClassLabel: 'Organization Level',
dialogSortLabel: 'Display Order',
dialogSortTooltip: 'Same parent nodes are sorted in ascending order',
dialogSortPlaceholder: 'Please enter display order',
dialogOrgTypeLabel: 'Process',
dialogOrgTypeTooltip: 'Source: Data dictionary - Process',
dialogOrgTypePlaceholder: 'Please select type',
dialogMachineLabel: 'Related Device',
dialogMachinePlaceholder: 'Please select device',
dialogCancelButton: 'Cancel',
dialogSubmitButton: 'Confirm',
orgTreeRootName: 'Top Organization',
validatorNameRequired: 'Organization name can not be empty',
validatorParentRequired: 'Parent organization can not be empty',
validatorOrgClassRequired: 'Organization level can not be empty'
},
FactoryWorker: {
moduleName: 'Workstation Worker',
searchNameLabel: 'Organization Name',
searchNamePlaceholder: 'Please enter organization name',
searchStatusLabel: 'Organization Status',
searchStatusPlaceholder: 'Please select organization status',
searchButtonText: 'Search',
resetButtonText: 'Reset',
exportButtonText: 'Export',
expandButtonText: 'Expand/Collapse',
tabsZhijiang: 'Pulping',
tabsChengxing: 'Forming',
tabsHonggan: 'Drying',
tabsZhuanyi: 'Transfer',
tabsJiashi: 'Humidifying',
tabsReya: 'Hot Press',
tabsQiebian: 'Trimming',
tabsPanjian: 'Inspection',
tabsDabao: 'Packing',
tabsTiebiao: 'Labeling',
tabsPinyin: 'Printing',
tabsSufeng: 'Sealing',
tabsAll: 'All',
tableNameColumn: 'Organization Name',
tableOrgClassColumn: 'Organization Level',
tableOrgTypeColumn: 'Type',
tableWorkerColumn: 'Today Worker',
tableOperateColumn: 'Operation',
tableDispatchAction: 'Dispatch',
exportFileName: 'Workstation.xls'
},
ProductCategory: {
moduleName: 'Product Material Category',
searchNameLabel: 'Category Name',
searchNamePlaceholder: 'Please enter category name',
searchStatusLabel: 'Enable Status',
searchStatusPlaceholder: 'Please select enable status',
searchButtonText: 'Search',
resetButtonText: 'Reset',
addButtonText: 'Add',
exportButtonText: 'Export',
expandButtonText: 'Expand/Collapse',
tableCodeColumn: 'Category Code',
tableNameColumn: 'Category Name',
tableSortColumn: 'Sort',
tableStatusColumn: 'Status',
tableCreateTimeColumn: 'Create Time',
tableOperateColumn: 'Operation',
tableEditAction: 'Edit',
tableDeleteAction: 'Delete',
dialogParentLabel: 'Parent Category',
dialogParentPlaceholder: 'Please select parent category',
dialogCodeLabel: 'Code',
dialogCodePlaceholder: 'Please enter code',
dialogNameLabel: 'Name',
dialogNamePlaceholder: 'Please enter name',
dialogSortLabel: 'Sort',
dialogSortPlaceholder: 'Please enter sort',
dialogStatusLabel: 'Status',
dialogCancelButton: 'Cancel',
dialogSubmitButton: 'Confirm',
treeRootName: 'Top Product Category',
validatorParentRequired: 'Parent category can not be empty',
validatorNameRequired: 'Name can not be empty',
validatorCodeRequired: 'Code can not be empty',
validatorSortRequired: 'Sort can not be empty',
validatorStatusRequired: 'Status can not be empty'
},
ProductInformation: {
moduleName: 'Product Material Information',
searchNameLabel: 'Name',
searchNamePlaceholder: 'Please enter name',
searchButtonText: 'Search',
resetButtonText: 'Reset',
addButtonText: 'Add',
exportButtonText: 'Export',
tableBarCodeColumn: 'Code',
tableNameColumn: 'Name',
tableStandardColumn: 'Specification',
tableCategoryColumn: 'Category',
tableUnitColumn: 'Unit',
tableStatusColumn: 'Status',
tableCreateTimeColumn: 'Create Time',
tableOperateColumn: 'Operation',
tableEditAction: 'Edit',
tableDeleteAction: 'Delete',
qrcode: 'QR Code/Barcode',
dialogBarCodeLabel: 'Code',
dialogBarCodePlaceholder: 'Please enter code',
dialogNameLabel: 'Name',
dialogNamePlaceholder: 'Please enter name',
dialogCategoryLabel: 'Category',
dialogCategoryPlaceholder: 'Please select category',
dialogUnitLabel: 'Unit',
dialogUnitPlaceholder: 'Please select unit',
dialogStandardLabel: 'Specification',
dialogStandardPlaceholder: 'Please enter specification',
dialogExpiryDayLabel: 'Shelf Life (Days)',
dialogExpiryDayPlaceholder: 'Please enter shelf life (days)',
dialogWeightLabel: 'Weight (g)',
dialogWeightPlaceholder: 'Please enter weight (g)',
dialogPurchasePriceLabel: 'Purchase Price',
dialogPurchasePricePlaceholder: 'Please enter purchase price (CNY)',
dialogSalePriceLabel: 'Sale Price',
dialogSalePricePlaceholder: 'Please enter sale price (CNY)',
dialogMinPriceLabel: 'Minimum Price',
dialogMinPricePlaceholder: 'Please enter minimum price (CNY)',
dialogStatusLabel: 'Status',
dialogRemarkLabel: 'Remark',
dialogRemarkPlaceholder: 'Please enter remark',
dialogCancelButton: 'Cancel',
dialogSubmitButton: 'Confirm',
qrcodeLoadError: 'Failed to load QR code',
qrcodeEmpty: 'No QR code',
validatorNameRequired: 'Product name can not be empty',
validatorBarCodeRequired: 'Product barcode can not be empty',
validatorCategoryRequired: 'Product category id can not be empty',
validatorUnitRequired: 'Unit id can not be empty',
validatorStatusRequired: 'Product status can not be empty'
},
AutocodeRule: {
moduleName: 'Code Rules',
searchRuleCodeLabel: 'Rule Code',
searchRuleCodePlaceholder: 'Please enter rule code',
searchRuleNameLabel: 'Rule Name',
searchRuleNamePlaceholder: 'Please enter rule name',
searchRuleDescLabel: 'Description',
searchRuleDescPlaceholder: 'Please enter description',
searchRemarkLabel: 'Remark',
searchRemarkPlaceholder: 'Please enter remark',
searchIsEnableLabel: 'Enabled',
searchIsEnablePlaceholder: 'Please select enabled',
searchButtonText: 'Search',
resetButtonText: 'Reset',
addButtonText: 'Add',
exportButtonText: 'Export',
tableRuleCodeColumn: 'Rule Code',
tableRuleNameColumn: 'Rule Name',
tableBarcodeTypeColumn: 'Code Type',
tableRuleDescColumn: 'Description',
tableMaxLengthColumn: 'Max Length',
tableIsPaddedColumn: 'Pad',
tablePaddedCharColumn: 'Pad Char',
tablePaddedMethodColumn: 'Pad Method',
tableIsEnableColumn: 'Enabled',
tableOperateColumn: 'Operation',
tableEditAction: 'Edit',
tableDeleteAction: 'Delete',
tableTestCodeAction: 'Test Code',
listPartTabLabel: 'Code Parts',
dialogPartTabLabel: 'Code Rule Parts',
exportFilename: 'CodeRules.xls',
dialogRuleCodeLabel: 'Rule Code',
dialogRuleCodePlaceholder: 'Please enter rule code',
dialogRuleNameLabel: 'Rule Name',
dialogRuleNamePlaceholder: 'Please enter rule name',
dialogBarcodeTypeLabel: 'Code Type',
barcodeTypeBarcodeLabel: 'Barcode',
barcodeTypeQrcodeLabel: 'QR Code',
dialogRuleDescLabel: 'Description',
dialogRuleDescPlaceholder: 'Please enter description',
dialogMaxLengthLabel: 'Max Length',
dialogMaxLengthPlaceholder: 'Please enter max length',
dialogIsPaddedLabel: 'Pad',
dialogPaddedCharLabel: 'Pad Char',
dialogPaddedCharPlaceholder: 'Please enter pad char',
dialogPaddedMethodLabel: 'Pad Method',
dialogRemarkLabel: 'Remark',
dialogRemarkPlaceholder: 'Please enter remark',
dialogIsEnableLabel: 'Enabled',
validatorIsEnableRequired: 'Enabled can not be empty',
validatorRuleIdRequired: 'Rule id can not be empty',
validatorPartIndexRequired: 'Part index can not be empty',
validatorPartLengthRequired: 'Part length can not be empty',
partTableIndexColumn: 'Index',
partTableIdColumn: 'ID',
partTablePartIndexColumn: 'Part Index',
partTablePartTypeColumn: 'Part Type',
partTablePartCodeColumn: 'Part Code',
partTablePartNameColumn: 'Part Name',
partTablePartLengthColumn: 'Part Length',
partTableDatetimeFormatColumn: 'Datetime Format',
partTableInputCharacterColumn: 'Input Char',
partTableFixCharacterColumn: 'Fixed Char',
partTableSeriaStartNoColumn: 'Serial Start',
partTableSeriaStepColumn: 'Serial Step',
partTableSeriaNowNoColumn: 'Serial Current',
partTableCycleFlagColumn: 'Serial Cycle',
partTableCycleMethodColumn: 'Cycle Method',
partTableRemarkColumn: 'Remark',
partTableIsEnableColumn: 'Enabled',
partTableCreateTimeColumn: 'Create Time',
partTableOperateColumn: 'Operation',
partPlaceholderPartIndex: 'Please enter part index',
partPlaceholderPartCode: 'Please enter part code',
partPlaceholderPartName: 'Please enter part name',
partPlaceholderPartLength: 'Please enter part length',
partPlaceholderDatetimeFormat: 'Please enter datetime format',
partPlaceholderInputCharacter: 'Please enter input char',
partPlaceholderFixCharacter: 'Please enter fixed char',
partPlaceholderSeriaStartNo: 'Please enter serial start',
partPlaceholderSeriaStep: 'Please enter serial step',
partPlaceholderSeriaNowNo: 'Please enter current serial',
partPlaceholderRemark: 'Please enter remark',
addPartButtonText: 'Add Code Rule Part'
},
ProductBOM: {
moduleName: 'Product BOM',
searchCodeLabel: 'BOM Code',
searchCodePlaceholder: 'Please enter BOM code',
searchProductLabel: 'Product',
searchProductPlaceholder: 'Please select product',
searchRemarkLabel: 'Remark',
searchRemarkPlaceholder: 'Please enter remark',
searchEnableLabel: 'Enable Status',
searchEnablePlaceholder: 'Please select enable status',
searchButtonText: 'Search',
resetButtonText: 'Reset',
addButtonText: 'Add',
exportButtonText: 'Export',
tableCodeColumn: 'BOM Code',
tableVersionColumn: 'BOM Version',
tableProductColumn: 'Product',
tableUnitColumn: 'Unit',
tableYieldRateColumn: 'Yield Rate %',
tableRemarkColumn: 'Remark',
tableEnableColumn: 'Enabled',
tableCreateTimeColumn: 'Create Time',
tableOperateColumn: 'Operation',
tableEditAction: 'Edit',
tableDeleteAction: 'Delete',
detailTabLabel: 'Product BOM Detail',
dialogCodeLabel: 'BOM Code',
dialogCodePlaceholder: 'Please enter BOM code',
dialogVersionLabel: 'BOM Version',
dialogVersionPlaceholder: 'Please enter BOM version',
dialogProductLabel: 'Product',
dialogProductPlaceholder: 'Please select product',
dialogUnitLabel: 'Unit',
dialogUnitPlaceholder: 'Please select unit',
dialogYieldRateLabel: 'Yield Rate %',
dialogYieldRatePlaceholder: 'Please enter yield rate',
dialogRemarkLabel: 'Remark',
dialogRemarkPlaceholder: 'Please enter remark',
dialogEnableLabel: 'Enabled',
dialogCancelButton: 'Cancel',
dialogSubmitButton: 'Confirm',
detailTableIndexColumn: 'No.',
detailTableMaterialColumn: 'Material',
detailTableUsageNumberColumn: 'Usage Quantity',
detailTableUnitColumn: 'Unit',
detailTableLossRateColumn: 'Loss Rate %',
detailTableRemarkColumn: 'Remark',
detailTableOperateColumn: 'Operation',
detailMaterialPlaceholder: 'Please select material',
detailUnitPlaceholder: 'Please select unit',
detailUsageNumberPlaceholder: 'Please enter usage quantity',
detailLossRatePlaceholder: 'Please enter loss rate',
detailRemarkPlaceholder: 'Please enter remark',
detailAddButtonText: 'Add Product BOM Detail',
validatorCodeRequired: 'BOM code can not be empty',
validatorVersionRequired: 'BOM version can not be empty',
validatorProductRequired: 'Product can not be empty',
validatorUnitRequired: 'Unit can not be empty',
validatorEnableRequired: 'Enable status can not be empty',
validatorDetailMaterialRequired: 'Material can not be empty',
validatorDetailBomRequired: 'BOM id can not be empty',
validatorDetailUsageNumberRequired: 'Usage quantity can not be empty'
},
WorkTeam: {
moduleName: 'Work Team Management',
searchTeamNameLabel: 'Team Name',
searchTeamNamePlaceholder: 'Please enter team name',
searchProcessLabel: 'Process',
searchProcessPlaceholder: 'Please select process',
searchRemarkLabel: 'Remark',
searchRemarkPlaceholder: 'Please enter remark',
searchButtonText: 'Search',
resetButtonText: 'Reset',
addButtonText: 'Add',
exportButtonText: 'Export',
tabsAll: 'All',
tabsDay: 'Day Shift',
tabsNight: 'Night Shift',
tabsLongDay: 'Long Day Shift',
tableTeamNameColumn: 'Team Name',
tableLeaderColumn: 'Leader',
tableGroupTypeColumn: 'Team Type',
tableWorkTimeColumn: 'Working Time',
tableProcessColumn: 'Process',
tableRemarkColumn: 'Remark',
tableEnableColumn: 'Enabled',
tableCreateTimeColumn: 'Create Time',
tableOperateColumn: 'Operation',
tableEditAction: 'Edit',
tableDeleteAction: 'Delete',
detailTabLabel: 'Team Members',
dialogTeamNameLabel: 'Team Name',
dialogTeamNamePlaceholder: 'Please enter team name',
dialogProcessLabel: 'Process',
dialogProcessTooltip: 'Source: Data dictionary - Process',
dialogGroupTypeLabel: 'Team Type',
dialogGroupTypeTooltip: 'Source: Data dictionary - Team Type',
dialogWorkTimeLabel: 'Working Time',
dialogWorkTimeRangeSeparator: 'To',
dialogWorkTimeStartPlaceholder: 'Start Time',
dialogWorkTimeEndPlaceholder: 'End Time',
dialogRemarkLabel: 'Remark',
dialogRemarkPlaceholder: 'Please enter remark',
dialogEnableLabel: 'Enabled',
dialogCancelButton: 'Cancel',
dialogSubmitButton: 'Confirm',
detailAddButtonText: 'Add',
detailTableJobNoColumn: 'Job No.',
detailTableRoleColumn: 'Role',
detailTableRemarkColumn: 'Remark',
detailTableCreateTimeColumn: 'Create Time',
detailTableOperateColumn: 'Operation',
detailDialogJobNoLabel: 'Job No.',
detailDialogJobNoPlaceholder: 'Please select job no.',
detailDialogRoleLabel: 'Role',
detailDialogRoleTooltip: 'Source: Data dictionary - Team Member Role',
detailDialogRemarkLabel: 'Remark',
detailDialogRemarkPlaceholder: 'Please enter remark',
detailDialogMemberSuffix: 'Member',
validatorTeamNameRequired: 'Team name can not be empty',
validatorProcessRequired: 'Process type can not be empty',
validatorGroupTypeRequired: 'Team type can not be empty',
validatorEnableRequired: 'Enable status can not be empty',
validatorDetailUserRequired: 'User id can not be empty',
validatorDetailRoleRequired: 'Role can not be empty',
validatorSelectWorkTeamFirst: 'Please select a work team first'
},
ProductUnit: {
moduleName: 'Unit Management',
searchNameLabel: 'Unit Name',
searchNamePlaceholder: 'Please enter unit name',
searchStatusLabel: 'Unit Status',
searchStatusPlaceholder: 'Please select unit status',
searchButtonText: 'Search',
resetButtonText: 'Reset',
addButtonText: 'Add',
exportButtonText: 'Export',
tableNameColumn: 'Unit Name',
tablePrimaryFlagColumn: 'Is Primary Unit',
tableChangeRateColumn: 'Conversion Rate to Primary Unit',
tableStatusColumn: 'Status',
tableCreateTimeColumn: 'Create Time',
tableOperateColumn: 'Operation',
tableEditAction: 'Edit',
tableDeleteAction: 'Delete',
dialogNameLabel: 'Unit Name',
dialogNamePlaceholder: 'Please enter unit name',
dialogPrimaryFlagLabel: 'Is Primary Unit',
dialogPrimaryUnitLabel: 'Related Primary Unit',
dialogPrimaryUnitPlaceholder: 'Please select primary unit',
dialogChangeRateLabel: 'Conversion Rate',
dialogChangeRatePlaceholder: 'Please enter conversion rate',
dialogStatusLabel: 'Unit Status',
dialogCancelButton: 'Cancel',
dialogSubmitButton: 'Confirm',
validatorNameRequired: 'Unit name can not be empty',
validatorStatusRequired: 'Unit status can not be empty',
validatorPrimaryFlagRequired: 'Primary unit flag can not be empty'
},
CalHoliday: {
setWorkingDays: 'set working days',
setHoliday: 'set holiday',
lastMonth: 'last month',
today: 'today',
nextMonth: 'next month',
work: 'work',
rest: 'rest',
searchDateStartPlaceholder: '开始日期',
searchDateEndPlaceholder: '结束日期',
searchTypeLabel: '类型',
searchTypePlaceholder: '请选择类型',
searchRemarkLabel: '备注',
searchRemarkPlaceholder: '请输入备注',
searchButtonText: '搜索',
resetButtonText: '重置',
addButtonText: '新增',
exportButtonText: '导出'
},
EsopFile: {
moduleName: 'File Management',
searchCodeLabel: 'File Code',
searchCodePlaceholder: 'Please enter file code',
searchFileNameLabel: 'File_Name',
searchFileNamePlaceholder: 'Please enter file name',
searchClassificationLabel: 'File Classification',
searchClassificationPlaceholder: 'Please select file Classification',
searchStatusLabel: 'File Status',
searchStatusPlaceholder: 'Please select file status',
searchButtonText: 'Search',
resetButtonText: 'Reset',
uploadButtonText: 'Upload',
exportButtonText: 'Export',
tableCodeColumn: 'File Code',
tableNameColumn: 'File Name',
tableClassificationColumn: 'File Classification',
tableTypeColumn: 'File Type',
tableStatusColumn: 'File Status',
tableCreatorNameColumn: 'Uploader',
tableCreateTimeColumn: 'Upload Time',
tableOperateColumn: 'Operation',
tableEditAction: 'Edit',
tableDownloadAction: 'Download',
tableDeleteAction: 'Delete',
dialogFileNameLabel: 'File Name',
dialogFileNamePlaceholder: 'Please enter file name',
dialogClassificationLabel: 'File Classification',
dialogClassificationPlaceholder: 'Please select file classification',
dialogStatusLabel: 'File Status',
dialogCancelButton: 'Cancel',
dialogSubmitButton: 'Confirm',
messageOne: 'Tip: Only ',
messageTwo: ' format files are allowed to be imported!',
messageThree: ' Drag the file to this area, or ',
messageFour: ' click to upload',
validatorCodeRequired: 'File code can not be empty',
validatorNameRequired: 'File name can not be empty',
validatorClassificationRequired: 'File classification can not be empty',
validatorTypeRequired: 'File type can not be empty',
validatorStatusRequired: 'File status can not be empty'
}
}
}

@ -301,6 +301,7 @@ export default {
delete: '删除',
edit: '编辑',
update: '编辑',
copy: '复制',
preview: '预览',
more: '更多',
sync: '同步',
@ -308,13 +309,17 @@ export default {
detail: '详情',
export: '导出',
import: '导入',
approve: '审批',
unapprove: '反审批',
generate: '生成',
logout: '强制退出',
test: '测试',
typeCreate: '字典类型新增',
typeUpdate: '字典类型编辑',
dataCreate: '字典数据新增',
dataUpdate: '字典数据编辑'
dataUpdate: '字典数据编辑',
select: '选择',
batchDelete: '批量删除'
},
cus: {
management: {
@ -481,5 +486,831 @@ export default {
btn_zoom_out: '缩小',
preview: '预览'
},
'OAuth 2.0': 'OAuth 2.0' // 避免菜单名是 OAuth 2.0 时,一直 warn 报错
'OAuth 2.0': 'OAuth 2.0', // 避免菜单名是 OAuth 2.0 时,一直 warn 报错
DataCollection: {
DeviceAttributeType: {
moduleName: '采集点类型',
index: '序号',
code: '类型编码',
name: '类型名称',
sort: '显示顺序',
remark: '备注',
createTime: '创建时间',
operate: '操作',
search: '搜索',
reset: '重置',
create: '新增',
batchDelete: '批量删除',
export: '导出',
placeholderCode: '请输入类型编码',
placeholderName: '请输入类型名称',
placeholderSort: '请输入显示顺序',
placeholderRemark: '请输入备注',
dialogOk: '确 定',
dialogCancel: '取 消',
validatorCodeRequired: '类型编码不能为空',
validatorNameRequired: '类型名称不能为空',
validatorSortRequired: '显示顺序不能为空',
exportFilename: '采集点类型.xls'
},
DeviceModel: {
moduleName: '采集设备模型',
index: '序号',
code: '模型编码',
name: '模型名称',
protocol: '通讯协议',
remark: '备注',
createTime: '创建时间',
operate: '操作',
search: '搜索',
reset: '重置',
create: '新增',
batchDelete: '批量删除',
export: '导出',
placeholderCode: '请输入模型编码',
placeholderName: '请输入模型名称',
placeholderProtocol: '请选择通讯协议',
placeholderRemark: '请输入备注',
dialogOk: '确 定',
dialogCancel: '取 消',
validatorCodeRequired: '模型编码不能为空',
validatorNameRequired: '模型名称不能为空',
validatorProtocolRequired: '通讯协议不能为空',
validatorDataTypeRequired: '数据类型不能为空',
exportFilename: '采集设备模型.xls',
attributeModuleName: '采集点管理',
attributeCode: '点位编码',
attributeName: '点位名称',
attributeType: '点位类型',
dataType: '数据类型',
address: '寄存器地址',
dataUnit: '单位',
ratio: '倍率',
placeholderAttributeCode: '请输入点位编码',
placeholderAttributeName: '请输入点位名称',
placeholderAttributeType: '请选择点位类型',
placeholderDataType: '请选择数据类型',
placeholderAddress: '请输入寄存器地址',
placeholderDataUnit: '请选择单位',
placeholderRatio: '请输入倍率',
placeholderAttributeRemark: '请输入备注',
validatorAttributeCodeRequired: '点位编码不能为空',
validatorAttributeNameRequired: '点位名称不能为空',
attributeExportFilename: '采集设备模型-点位管理.xls',
ruleTabLabel: '点位规则',
ruleIdentifier: '标识符',
ruleFieldName: '名称',
ruleFieldRule: '规则',
ruleDefaultValue: '默认值',
ruleCreateTime: '创建时间',
ruleOperate: '操作',
ruleSearchIdentifierPlaceholder: '请输入标识符',
ruleSearchFieldNamePlaceholder: '请输入名称',
ruleSearchDefaultValuePlaceholder: '请输入默认值',
ruleSearch: '搜索',
ruleReset: '重置',
ruleCreateButton: '新增告警规则',
ruleEditRuleButton: '编辑',
ruleDeleteRuleButton: '删除',
ruleDialogTitle: '编辑点位规则',
ruleDialogIdentifier: '标识符',
ruleDialogFieldName: '名称',
ruleDialogDefaultValue: '默认值',
ruleDialogAlarmLevel: '告警登记',
ruleDialogFieldRule: '点位规则',
ruleDialogRule: '规则',
ruleDialogRuleAttribute: '点位',
ruleDialogRuleOperator: '条件',
ruleDialogRuleValue: '数值',
ruleDialogAlarmLevelPlaceholder: '请选择告警登记',
ruleDialogFieldRulePlaceholder: '请选择点位规则',
ruleDialogRuleAttributePlaceholder: '请选择点位',
ruleDialogRuleOperatorPlaceholder: '请选择条件',
ruleDialogRuleValuePlaceholder: '请输入数值'
},
Device: {
moduleName: '采集设备',
index: '序号',
deviceCode: '设备编号',
deviceName: '设备名称',
operatingStatus: '运行状态',
protocol: '采集协议',
status: '连接状态',
sampleCycle: '采集周期(s)',
isEnable: '启用',
collectionTime: '采集时间',
operate: '操作',
search: '搜索',
reset: '重置',
create: '新增',
batchDelete: '批量删除',
export: '导出',
placeholderDeviceCode: '请输入设备编号',
placeholderDeviceName: '请输入设备名称',
placeholderModel: '请选择设备模型',
placeholderSampleCycle: '请输入采集周期',
placeholderUrl: '请输入端点URL',
placeholderUsername: '请输入用户名',
placeholderPassword: '请输入密码',
placeholderTopic: '请输入订阅主题',
model: '设备模型',
url: '端点URL',
username: '用户名',
password: '密码',
topic: '订阅主题',
settingDialogTitle: '设备设置',
connect: '连接',
disconnect: '断开连接',
attributeModuleName: '点位',
attributeCode: '点位编码',
attributeName: '点位名称',
attributeType: '点位类型',
dataType: '数据类型',
address: '寄存器地址',
dataUnit: '单位',
ratio: '倍率',
remark: '备注',
deviceAttributeTabLabel: '设备属性',
deviceRuleTabLabel: '点位规则',
currentDeviceLabel: '当前设备:',
alarmHistoryTitle: '设备告警历史数据',
alarmRuleName: '规则名称',
alarmPointName: '点位名称',
alarmPointValue: '点位值',
alarmLevel: '告警等级',
alarmTime: '告警时间',
emptyDescription: '请点击设备列表的“点位”查看采集点和点位规则',
exportFilename: '物联设备.xls',
attributeExportFilename: '采集设备-点位管理.xls',
attributeLatestValue: '最新值',
attributeLatestCollectionTime: '最新采集时间',
attributeSort: '顺序',
attributePlaceholderSort: '请输入顺序',
placeholderAttributeCode: '请输入点位编码',
placeholderAttributeName: '请输入点位名称',
placeholderAttributeType: '请选择点位类型',
placeholderDataType: '请选择数据类型',
placeholderAddress: '请输入寄存器地址',
placeholderDataUnit: '请输入单位',
placeholderRatio: '请输入倍率',
placeholderRemark: '请输入备注',
validatorDeviceCodeRequired: '设备编号不能为空',
validatorDeviceNameRequired: '设备名称不能为空',
validatorSampleCycleRequired: '采集周期不能为空',
validatorIsEnableRequired: '是否启用不能为空',
validatorUrlRequired: '端点URL不能为空',
attributeValidatorCodeRequired: '点位编码不能为空',
attributeValidatorNameRequired: '点位名称不能为空',
attributeValidatorCodeNoChinese: '点位编码不允许输入中文',
attributeValidatorSortNumber: '顺序只能输入数字',
attributeValidatorRemarkTooLong: '备注不能超过100字',
messageSelectDeviceRequired: '请选择一个物联设备',
messageDeviceInfoMissingForRules: '设备信息缺失,无法加载点位规则',
gridView: '表格视图',
sudoku: '九宫格'
},
RunReport: {
moduleName: '设备运行报表',
searchDeviceCodeLabel: '设备编码',
searchDeviceCodePlaceholder: '请输入设备编码',
searchDeviceNameLabel: '设备名称',
searchDeviceNamePlaceholder: '请输入设备名称',
searchTimeRangeLabel: '设备运行时间',
searchTimeRangeStartPlaceholder: '开始时间',
searchTimeRangeEndPlaceholder: '结束时间',
searchButtonText: '搜索',
resetButtonText: '重置',
exportButtonText: '导出',
tableDeviceCodeColumn: '设备编码',
tableDeviceNameColumn: '设备名称',
tableRunningTimeColumn: '运行时间(小时)',
tableStandbyTimeColumn: '待机时间(小时)',
tableFaultTimeColumn: '故障时间(小时)',
tableWarningTimeColumn: '警告时间(小时)',
tableUtilizationRateColumn: '稼动率',
tableUtilizationRateTooltip:
'开机率 = 在线时间 / 总的时间\n在线时间 = 运行时间 + 空闲时间 + 故障时间\n稼动率 = 运行时间 / 在线时间\n开机率 =(运行时间 + 空闲时间 + 故障时间)/ 总的时间',
tablePowerOnRateColumn: '开机率',
tableStartTimeColumn: '设备运行开始时间',
tableEndTimeColumn: '设备运行结束时间',
exportFilename: '设备运行报表.xls'
},
RealTimeMonitoring: {
moduleName: '数据实时监控',
searchLineCodeLabel: '产线编码',
searchLineCodePlaceholder: '请输入产线编码',
searchLineNameLabel: '产线名称',
searchLineNamePlaceholder: '请输入产线名称',
searchDeviceCodeLabel: '设备编码',
searchDeviceCodePlaceholder: '请输入设备编码',
searchDeviceNameLabel: '设备名称',
searchDeviceNamePlaceholder: '请输入设备名称',
searchButtonText: '搜索',
resetButtonText: '重置',
exportButtonText: '导出',
tableLineCodeColumn: '产线编码',
tableLineNameColumn: '产线名称',
tableDeviceCodeColumn: '设备编码',
tableDeviceNameColumn: '设备名称',
tableStatusColumn: '连接状态',
tableCollectionTimeColumn: '最新采集时间',
tableOperateColumn: '操作',
tableActionSingleMonitorLabel: '单设备监控',
dialogTitle: '单设备监控',
dialogDeviceNameLabel: '设备名称:',
dialogCollectionTimeLabel: '采集时间:',
emptyDescription: '暂无数据',
defaultGroupName: '默认',
defaultFieldLabelPrefix: '字段',
messageDeviceInfoIncomplete: '设备信息不完整',
exportFilename: '实时监控设备.xls'
},
HistoryData: {
moduleName: '历史记录查询',
searchLineCodeLabel: '产线编码',
searchLineCodePlaceholder: '请输入产线编码',
searchLineNameLabel: '产线名称',
searchLineNamePlaceholder: '请输入产线名称',
searchDeviceCodeLabel: '设备编码',
searchDeviceCodePlaceholder: '请输入设备编码',
searchDeviceNameLabel: '设备名称',
searchDeviceNamePlaceholder: '请输入设备名称',
searchButtonText: '搜索',
resetButtonText: '重置',
exportButtonText: '导出',
tableLineCodeColumn: '产线编码',
tableLineNameColumn: '产线名称',
tableDeviceCodeColumn: '设备编码',
tableDeviceNameColumn: '设备名称',
tableCollectionTimeColumn: '采集时间',
tableOperateColumn: '操作',
tableActionHistoryLabel: '历史记录',
dialogTitlePrefix: '历史记录:',
dialogCollectionTimeLabel: '采集时间',
dialogCollectionTimeStartPlaceholder: '开始时间',
dialogCollectionTimeEndPlaceholder: '结束时间',
dialogSearchButtonText: '查询',
dialogResetButtonText: '重置',
dialogRecordCollectionTimePrefix: '采集时间:',
emptyDescription: '暂无数据',
defaultFieldLabelPrefix: '字段',
exportFilename: '历史数据设备.xls'
},
DeviceParamAnalysis: {
moduleName: '设备运行参数分析',
treeSearchPlaceholder: '搜索设备或参数',
formTimeLabel: '时间',
formTimeStartPlaceholder: '开始日期',
formTimeEndPlaceholder: '结束日期',
shortcutLast7Days: '最近 7 天',
shortcutLastWeek: '上周',
shortcutLastMonth: '上个月',
shortcutLast3Months: '三个月内',
searchButtonText: '搜索',
resetButtonText: '重置',
emptyDescription: '暂无数据',
emptySelectNodeDescription: '请选择左侧节点',
selectedParamTitle: '节点:{label}{unit}',
defaultSeriesName: '参数',
messageLoadTreeFailed: '获取树数据失败',
messageNodeNoParams: '该节点下暂无参数',
messageDeviceNoParams: '该设备下没有参数',
messageFetchChartFailed: '获取图表数据失败'
}
},
FactoryModeling: {
FactoryStructure: {
moduleName: '厂区结构',
searchCodeLabel: '组织编码',
searchCodePlaceholder: '请输入组织编码',
searchParentLabel: '父组织',
searchParentPlaceholder: '请选择父组织',
searchNameLabel: '组织名称',
searchNamePlaceholder: '请输入组织名称',
searchOrgClassLabel: '组织等级',
searchOrgClassPlaceholder: '请选择组织等级',
searchButtonText: '搜索',
resetButtonText: '重置',
addButtonText: '新增',
exportButtonText: '导出',
expandButtonText: '展开/折叠',
tableCodeColumn: '组织编码',
tableNameColumn: '组织名称',
tableSortColumn: '显示顺序',
tableMachineColumn: '关联设备',
tableOrgClassColumn: '组织等级',
tableOrgTypeColumn: '工序',
tableCreateTimeColumn: '创建时间',
tableOperateColumn: '操作',
tableEditAction: '编辑',
tableDeleteAction: '删除',
dialogCodeLabel: '编码',
dialogCodeTooltip: '厂区结构编码',
dialogCodePlaceholder: '编码保存后自动生成',
dialogParentLabel: '父组织',
dialogParentPlaceholder: '请选择父组织',
dialogNameLabel: '组织名称',
dialogNamePlaceholder: '请输入组织名称',
dialogOrgClassLabel: '组织等级',
dialogSortLabel: '显示顺序',
dialogSortTooltip: '同父级按照显示顺序从小到大排序',
dialogSortPlaceholder: '请输入显示顺序',
dialogOrgTypeLabel: '工序',
dialogOrgTypeTooltip: '来源:数据字典-工序',
dialogOrgTypePlaceholder: '请选择类型',
dialogMachineLabel: '关联设备',
dialogMachinePlaceholder: '请选择设备',
dialogCancelButton: '取 消',
dialogSubmitButton: '确 定',
orgTreeRootName: '顶级组织',
validatorNameRequired: '组织名称不能为空',
validatorParentRequired: '父组织不能为空',
validatorOrgClassRequired: '组织等级不能为空'
},
FactoryWorker: {
moduleName: '工位人员',
searchNameLabel: '组织名称',
searchNamePlaceholder: '请输入组织名称',
searchStatusLabel: '组织状态',
searchStatusPlaceholder: '请选择组织状态',
searchButtonText: '搜索',
resetButtonText: '重置',
exportButtonText: '导出',
expandButtonText: '展开/折叠',
tabsZhijiang: '制浆',
tabsChengxing: '成型',
tabsHonggan: '烘干',
tabsZhuanyi: '转移',
tabsJiashi: '加湿',
tabsReya: '热压',
tabsQiebian: '切边',
tabsPanjian: '品检',
tabsDabao: '打包',
tabsTiebiao: '贴标',
tabsPinyin: '品印',
tabsSufeng: '塑封',
tabsAll: '所有',
tableNameColumn: '组织名称',
tableOrgClassColumn: '组织等级',
tableOrgTypeColumn: '类型',
tableWorkerColumn: '今日工人',
tableOperateColumn: '操作',
tableDispatchAction: '派工',
exportFileName: '产线工位.xls'
},
ProductCategory: {
moduleName: '产品物料分类',
searchNameLabel: '分类名称',
searchNamePlaceholder: '请输入分类名称',
searchStatusLabel: '开启状态',
searchStatusPlaceholder: '请选择开启状态',
searchButtonText: '搜索',
resetButtonText: '重置',
addButtonText: '新增',
exportButtonText: '导出',
expandButtonText: '展开/折叠',
tableCodeColumn: '分类编码',
tableNameColumn: '分类名称',
tableSortColumn: '排序',
tableStatusColumn: '状态',
tableCreateTimeColumn: '创建时间',
tableOperateColumn: '操作',
tableEditAction: '编辑',
tableDeleteAction: '删除',
dialogParentLabel: '上级编号',
dialogParentPlaceholder: '请选择上级编号',
dialogCodeLabel: '编码',
dialogCodePlaceholder: '请输入编码',
dialogNameLabel: '名称',
dialogNamePlaceholder: '请输入名称',
dialogSortLabel: '排序',
dialogSortPlaceholder: '请输入排序',
dialogStatusLabel: '状态',
dialogCancelButton: '取 消',
dialogSubmitButton: '确 定',
treeRootName: '顶级产品分类',
validatorParentRequired: '上级编号不能为空',
validatorNameRequired: '名称不能为空',
validatorCodeRequired: '编码不能为空',
validatorSortRequired: '排序不能为空',
validatorStatusRequired: '状态不能为空'
},
ProductInformation: {
moduleName: '产品物料信息',
searchNameLabel: '名称',
searchNamePlaceholder: '请输入名称',
searchButtonText: '搜索',
resetButtonText: '重置',
addButtonText: '新增',
exportButtonText: '导出',
tableBarCodeColumn: '编码',
tableNameColumn: '名称',
tableStandardColumn: '规格',
tableCategoryColumn: '分类',
tableUnitColumn: '单位',
tableStatusColumn: '状态',
tableCreateTimeColumn: '创建时间',
tableOperateColumn: '操作',
tableEditAction: '编辑',
tableDeleteAction: '删除',
qrcode: '二维码/条形码',
dialogBarCodeLabel: '编码',
dialogBarCodePlaceholder: '请输入编码',
dialogNameLabel: '名称',
dialogNamePlaceholder: '请输入名称',
dialogCategoryLabel: '分类',
dialogCategoryPlaceholder: '请选择分类',
dialogUnitLabel: '单位',
dialogUnitPlaceholder: '请选择单位',
dialogStandardLabel: '规格',
dialogStandardPlaceholder: '请输入规格',
dialogExpiryDayLabel: '保质期天数',
dialogExpiryDayPlaceholder: '请输入保质期天数',
dialogWeightLabel: '重量g',
dialogWeightPlaceholder: '请输入重量g',
dialogPurchasePriceLabel: '采购价格',
dialogPurchasePricePlaceholder: '请输入采购价格,单位:元',
dialogSalePriceLabel: '销售价格',
dialogSalePricePlaceholder: '请输入销售价格,单位:元',
dialogMinPriceLabel: '最低价格',
dialogMinPricePlaceholder: '请输入最低价格,单位:元',
dialogStatusLabel: '状态',
dialogRemarkLabel: '备注',
dialogRemarkPlaceholder: '请输入备注',
dialogCancelButton: '取 消',
dialogSubmitButton: '确 定',
qrcodeLoadError: '二维码加载失败',
qrcodeEmpty: '暂无二维码',
validatorNameRequired: '产品名称不能为空',
validatorBarCodeRequired: '产品条码不能为空',
validatorCategoryRequired: '产品分类编号不能为空',
validatorUnitRequired: '单位编号不能为空',
validatorStatusRequired: '产品状态不能为空'
},
AutocodeRule: {
moduleName: '编码规则',
searchRuleCodeLabel: '规则编码',
searchRuleCodePlaceholder: '请输入规则编码',
searchRuleNameLabel: '规则名称',
searchRuleNamePlaceholder: '请输入规则名称',
searchRuleDescLabel: '描述',
searchRuleDescPlaceholder: '请输入描述',
searchRemarkLabel: '备注',
searchRemarkPlaceholder: '请输入备注',
searchIsEnableLabel: '是否启用',
searchIsEnablePlaceholder: '请选择是否启用',
searchButtonText: '搜索',
resetButtonText: '重置',
addButtonText: '新增',
exportButtonText: '导出',
tableRuleCodeColumn: '规则编码',
tableRuleNameColumn: '规则名称',
tableBarcodeTypeColumn: '码类型',
tableRuleDescColumn: '描述',
tableMaxLengthColumn: '最大长度',
tableIsPaddedColumn: '是否补齐',
tablePaddedCharColumn: '补齐字符',
tablePaddedMethodColumn: '补齐方式',
tableIsEnableColumn: '是否启用',
tableOperateColumn: '操作',
tableEditAction: '编辑',
tableDeleteAction: '删除',
tableTestCodeAction: '测试编码',
listPartTabLabel: '编码组成',
dialogPartTabLabel: '编码规则组成',
exportFilename: '编码规则.xls',
dialogRuleCodeLabel: '规则编码',
dialogRuleCodePlaceholder: '请输入规则编码',
dialogRuleNameLabel: '规则名称',
dialogRuleNamePlaceholder: '请输入规则名称',
dialogBarcodeTypeLabel: '码类型',
barcodeTypeBarcodeLabel: '条形码',
barcodeTypeQrcodeLabel: '二维码',
dialogRuleDescLabel: '描述',
dialogRuleDescPlaceholder: '请输入描述',
dialogMaxLengthLabel: '最大长度',
dialogMaxLengthPlaceholder: '请输入最大长度',
dialogIsPaddedLabel: '是否补齐',
dialogPaddedCharLabel: '补齐字符',
dialogPaddedCharPlaceholder: '请输入补齐字符',
dialogPaddedMethodLabel: '补齐方式',
dialogRemarkLabel: '备注',
dialogRemarkPlaceholder: '请输入备注',
dialogIsEnableLabel: '是否启用',
validatorIsEnableRequired: '是否启用不能为空',
validatorRuleIdRequired: '规则ID不能为空',
validatorPartIndexRequired: '分段序号不能为空',
validatorPartLengthRequired: '分段长度不能为空',
partTableIndexColumn: '序号',
partTableIdColumn: 'ID',
partTablePartIndexColumn: '分段序号',
partTablePartTypeColumn: '分段类型',
partTablePartCodeColumn: '分段编号',
partTablePartNameColumn: '分段名称',
partTablePartLengthColumn: '分段长度',
partTableDatetimeFormatColumn: '日期时间格式',
partTableInputCharacterColumn: '输入字符',
partTableFixCharacterColumn: '固定字符',
partTableSeriaStartNoColumn: '流水号起始值',
partTableSeriaStepColumn: '流水号步长',
partTableSeriaNowNoColumn: '流水号当前值',
partTableCycleFlagColumn: '流水号是否循环',
partTableCycleMethodColumn: '循环方式',
partTableRemarkColumn: '备注',
partTableIsEnableColumn: '是否启用',
partTableCreateTimeColumn: '创建时间',
partTableOperateColumn: '操作',
partPlaceholderPartIndex: '请输入分段序号',
partPlaceholderPartCode: '请输入分段编号',
partPlaceholderPartName: '请输入分段名称',
partPlaceholderPartLength: '请输入分段长度',
partPlaceholderDatetimeFormat: '请输入日期时间格式',
partPlaceholderInputCharacter: '请输入输入字符',
partPlaceholderFixCharacter: '请输入固定字符',
partPlaceholderSeriaStartNo: '请输入流水号起始值',
partPlaceholderSeriaStep: '请输入流水号步长',
partPlaceholderSeriaNowNo: '请输入流水号当前值',
partPlaceholderRemark: '请输入备注',
addPartButtonText: '添加编码规则组成'
},
ProductBOM: {
moduleName: '产品BOM',
searchCodeLabel: 'BOM编码',
searchCodePlaceholder: '请输入BOM编码',
searchProductLabel: '产品',
searchProductPlaceholder: '请选择产品',
searchRemarkLabel: '备注',
searchRemarkPlaceholder: '请输入备注',
searchEnableLabel: '开启状态',
searchEnablePlaceholder: '请选择是否启用',
searchButtonText: '搜索',
resetButtonText: '重置',
addButtonText: '新增',
exportButtonText: '导出',
tableCodeColumn: 'BOM编码',
tableVersionColumn: 'BOM版本',
tableProductColumn: '产品',
tableUnitColumn: '单位',
tableYieldRateColumn: '成品率%',
tableRemarkColumn: '备注',
tableEnableColumn: '是否启用',
tableCreateTimeColumn: '创建时间',
tableOperateColumn: '操作',
tableEditAction: '编辑',
tableDeleteAction: '删除',
detailTabLabel: '产品BOM明细',
dialogCodeLabel: 'BOM编码',
dialogCodePlaceholder: '请输入BOM编码',
dialogVersionLabel: 'BOM版本',
dialogVersionPlaceholder: '请输入BOM版本',
dialogProductLabel: '产品',
dialogProductPlaceholder: '请选择产品',
dialogUnitLabel: '单位',
dialogUnitPlaceholder: '请选择单位',
dialogYieldRateLabel: '成品率%',
dialogYieldRatePlaceholder: '请输入成品率',
dialogRemarkLabel: '备注',
dialogRemarkPlaceholder: '请输入备注',
dialogEnableLabel: '是否启用',
dialogCancelButton: '取 消',
dialogSubmitButton: '确 定',
detailTableIndexColumn: '序号',
detailTableMaterialColumn: '原料',
detailTableUsageNumberColumn: '用量',
detailTableUnitColumn: '单位',
detailTableLossRateColumn: '损耗率%',
detailTableRemarkColumn: '备注',
detailTableOperateColumn: '操作',
detailMaterialPlaceholder: '请选择原料',
detailUnitPlaceholder: '请选择单位',
detailUsageNumberPlaceholder: '请输入用量',
detailLossRatePlaceholder: '请输入损耗率',
detailRemarkPlaceholder: '请输入备注',
detailAddButtonText: '添加产品BOM明细',
validatorCodeRequired: 'BOM编码不能为空',
validatorVersionRequired: 'BOM版本不能为空',
validatorProductRequired: '产品不能为空',
validatorUnitRequired: '单位不能为空',
validatorEnableRequired: '是否启用不能为空',
validatorDetailMaterialRequired: '原料不能为空',
validatorDetailBomRequired: 'BOM 不能为空',
validatorDetailUsageNumberRequired: '用量不能为空'
},
WorkTeam: {
moduleName: '班组管理',
searchTeamNameLabel: '班组名称',
searchTeamNamePlaceholder: '请输入班组名称',
searchProcessLabel: '工序',
searchProcessPlaceholder: '请选择工序',
searchRemarkLabel: '备注',
searchRemarkPlaceholder: '请输入备注',
searchButtonText: '搜索',
resetButtonText: '重置',
addButtonText: '新增',
exportButtonText: '导出',
tabsAll: '所有',
tabsDay: '白班',
tabsNight: '夜班',
tabsLongDay: '长白班',
tableTeamNameColumn: '班组名称',
tableLeaderColumn: '组长',
tableGroupTypeColumn: '班组类型',
tableWorkTimeColumn: '工作时间',
tableProcessColumn: '工序',
tableRemarkColumn: '备注',
tableEnableColumn: '是否启用',
tableCreateTimeColumn: '创建时间',
tableOperateColumn: '操作',
tableEditAction: '编辑',
tableDeleteAction: '删除',
detailTabLabel: '班组成员',
dialogTeamNameLabel: '班组名称',
dialogTeamNamePlaceholder: '请输入班组名称',
dialogProcessLabel: '工序',
dialogProcessTooltip: '来源:数据字典-工序',
dialogGroupTypeLabel: '班组类型',
dialogGroupTypeTooltip: '来源:数据字典-班组类型',
dialogWorkTimeLabel: '工作时间',
dialogWorkTimeRangeSeparator: '至',
dialogWorkTimeStartPlaceholder: '开始时间',
dialogWorkTimeEndPlaceholder: '结束时间',
dialogRemarkLabel: '备注',
dialogRemarkPlaceholder: '请输入备注',
dialogEnableLabel: '是否启用',
dialogCancelButton: '取 消',
dialogSubmitButton: '确 定',
detailAddButtonText: '新增',
detailTableJobNoColumn: '工号',
detailTableRoleColumn: '角色',
detailTableRemarkColumn: '备注',
detailTableCreateTimeColumn: '创建时间',
detailTableOperateColumn: '操作',
detailDialogJobNoLabel: '工号',
detailDialogJobNoPlaceholder: '请选择工号',
detailDialogRoleLabel: '角色',
detailDialogRoleTooltip: '来源:数据字典-班组成员角色',
detailDialogRemarkLabel: '备注',
detailDialogRemarkPlaceholder: '请输入备注',
detailDialogMemberSuffix: '成员',
validatorTeamNameRequired: '班组名称不能为空',
validatorProcessRequired: '工序类型不能为空',
validatorGroupTypeRequired: '班组类型不能为空',
validatorEnableRequired: '是否启用不能为空',
validatorDetailUserRequired: '用户ID不能为空',
validatorDetailRoleRequired: '角色不能为空',
validatorSelectWorkTeamFirst: '请选择一个生产班组'
},
ProductUnit: {
moduleName: '单位管理',
searchNameLabel: '单位名称',
searchNamePlaceholder: '请输入单位名称',
searchStatusLabel: '单位状态',
searchStatusPlaceholder: '请选择单位状态',
searchButtonText: '搜索',
resetButtonText: '重置',
addButtonText: '新增',
exportButtonText: '导出',
tableNameColumn: '单位名称',
tablePrimaryFlagColumn: '是否主单位',
tableChangeRateColumn: '与主单位换算比例',
tableStatusColumn: '状态',
tableCreateTimeColumn: '创建时间',
tableOperateColumn: '操作',
tableEditAction: '编辑',
tableDeleteAction: '删除',
dialogNameLabel: '单位名称',
dialogNamePlaceholder: '请输入单位名称',
dialogPrimaryFlagLabel: '是否主单位',
dialogPrimaryUnitLabel: '关联主单位',
dialogPrimaryUnitPlaceholder: '请选择主单位',
dialogChangeRateLabel: '换算比例',
dialogChangeRatePlaceholder: '请输入换算比例',
dialogStatusLabel: '单位状态',
dialogCancelButton: '取 消',
dialogSubmitButton: '确 定',
validatorNameRequired: '单位名称不能为空',
validatorStatusRequired: '单位状态不能为空',
validatorPrimaryFlagRequired: '是否主单位不能为空'
},
//假日管理
CalHoliday: {
setWorkingDays: '设置工作日',
setHoliday: '设置休息日',
lastMonth: '上个月',
today: '今天',
nextMonth: '下个月',
work: '班',
rest: '休',
searchDateStartPlaceholder: '开始日期',
searchDateEndPlaceholder: '结束日期',
searchTypeLabel: '类型',
searchTypePlaceholder: '请选择类型',
searchRemarkLabel: '备注',
searchRemarkPlaceholder: '请输入备注',
searchButtonText: '搜索',
resetButtonText: '重置',
addButtonText: '新增',
exportButtonText: '导出'
},
//esop文件管理
EsopFile: {
searchFileNameLabel: '文件名称',
searchFileNamePlaceholder: '请输入文件名',
searchClassificationLabel: '文件分类',
searchClassificationPlaceholder: '请选择文件分类',
searchStatusLabel: '文件状态',
searchStatusPlaceholder: '请选择文件状态',
searchButtonText: '搜索',
resetButtonText: '重置',
uploadButtonText: '文件上传',
addButtonText: '新增',
exportButtonText: '导出',
tableCodeColumn: '文件编码',
tableNameColumn: '文件名称',
tableClassificationColumn: '文件分类',
tableTypeColumn: '文件类型',
tableStatusColumn: '文件状态',
tableCreatorNameColumn: '上传人',
tableCreateTimeColumn: '上传时间',
tableOperateColumn: '操作',
tableEditAction: '编辑',
tableDownloadAction: '下载',
tableDeleteAction: '删除',
dialogFileNameLabel: '文件名',
dialogFileNamePlaceholder: '请输入文件名',
dialogClassificationLabel: '文件分类',
dialogClassificationPlaceholder: '请选择文件分类',
dialogStatusLabel: '文件状态',
dialogCancelButton: '取 消',
dialogSubmitButton: '确 定',
messageOne: '提示:仅允许导入 ',
messageTwo: ' 格式文件!',
messageThree: ' 将文件拖到此处,或',
messageFour: '点击上传',
validatorNameRequired: '文件名不能为空',
validatorStatusRequired: '文件状态不能为空'
}
}
}

@ -732,7 +732,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true,
activeMenu: '/iot/device/device'
},
component: () => import('@/views/iot/device/device/detail/index.vue')
component: () => import('@/views/iot/device/index.vue')
},
{
path: 'ota/operation/firmware/detail/:id',

@ -250,5 +250,8 @@ export enum DICT_TYPE {
IOT_OTA_TASK_STATUS = 'iot_ota_task_status', // IoT OTA 任务状态
IOT_OTA_TASK_RECORD_STATUS = 'iot_ota_task_record_status', // IoT OTA 记录状态
IOT_MODBUS_MODE = 'iot_modbus_mode', // IoT Modbus 工作模式
IOT_MODBUS_FRAME_FORMAT = 'iot_modbus_frame_format' // IoT Modbus 帧格式
IOT_MODBUS_FRAME_FORMAT = 'iot_modbus_frame_format', // IoT Modbus 帧格式
IOT_PROTOCOL = 'iot_protocol',
IOT_DEVICE_DATA_TYPE = 'iot_device_data_type',
PRIMARY_FLAG = 'primary_flag'
}

@ -4,7 +4,7 @@
ref="formRef"
:model="formData"
:rules="formRules"
min-label-width="100px"
label-width="100px"
v-loading="formLoading || geoInitLoading"
:element-loading-text="t('cus.management.loading.countries')"
>

@ -4,18 +4,65 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
label-width="120px"
v-loading="formLoading"
>
<el-form-item label="单位名字" prop="name">
<el-input v-model="formData.name" placeholder="请输入单位名字" />
<el-form-item :label="t('FactoryModeling.ProductUnit.dialogNameLabel')" prop="name">
<el-input
v-model="formData.name"
:placeholder="t('FactoryModeling.ProductUnit.dialogNamePlaceholder')"
:disabled="formType === 'update'"
/>
</el-form-item>
<el-form-item label="单位状态" prop="status">
<el-form-item :label="t('FactoryModeling.ProductUnit.dialogPrimaryFlagLabel')" prop="primaryFlag">
<el-radio-group v-model="formData.primaryFlag">
<el-radio
v-for="dict in getStrDictOptions(DICT_TYPE.PRIMARY_FLAG)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
:label="t('FactoryModeling.ProductUnit.dialogPrimaryUnitLabel')"
v-if="formData.primaryFlag === 'N'"
prop="primaryId"
>
<el-select
v-model="formData.primaryId"
clearable
filterable
:placeholder="t('FactoryModeling.ProductUnit.dialogPrimaryUnitPlaceholder')"
>
<el-option
v-for="item in unitList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item
:label="t('FactoryModeling.ProductUnit.dialogChangeRateLabel')"
v-if="formData.primaryFlag === 'N'"
prop="changeRate"
>
<el-input-number
v-model="formData.changeRate"
:step="0.1"
:min="0.00"
:placeholder="t('FactoryModeling.ProductUnit.dialogChangeRatePlaceholder')"
class="!w-full"
/>
</el-form-item>
<el-form-item :label="t('FactoryModeling.ProductUnit.dialogStatusLabel')" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:value="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
@ -23,14 +70,18 @@
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading">
{{ t('FactoryModeling.ProductUnit.dialogSubmitButton') }}
</el-button>
<el-button @click="dialogVisible = false">
{{ t('FactoryModeling.ProductUnit.dialogCancelButton') }}
</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ProductUnitApi } from '@/api/erp/product/unit'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { ProductUnitApi,ProductUnitVO } from '@/api/erp/product/unit'
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
/** ERP 产品单位表单 */
@ -39,6 +90,7 @@ defineOptions({ name: 'ProductUnitForm' })
const { t } = useI18n() //
const message = useMessage() //
const unitList = ref<ProductUnitVO[]>([]) //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
@ -46,11 +98,33 @@ const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
name: undefined,
status: undefined
status: undefined,
primaryFlag: undefined,
primaryId: undefined,
changeRate: undefined,
})
const formRules = reactive({
name: [{ required: true, message: '单位名字不能为空', trigger: 'blur' }],
status: [{ required: true, message: '单位状态不能为空', trigger: 'blur' }]
name: [
{
required: true,
message: t('FactoryModeling.ProductUnit.validatorNameRequired'),
trigger: 'blur'
}
],
status: [
{
required: true,
message: t('FactoryModeling.ProductUnit.validatorStatusRequired'),
trigger: 'blur'
}
],
primaryFlag: [
{
required: true,
message: t('FactoryModeling.ProductUnit.validatorPrimaryFlagRequired'),
trigger: 'blur'
}
]
})
const formRef = ref() // Ref
@ -80,7 +154,7 @@ const submitForm = async () => {
//
formLoading.value = true
try {
const data = formData.value as unknown as ProductUnitApi.ProductUnitVO
const data = formData.value as unknown as ProductUnitVO
if (formType.value === 'create') {
await ProductUnitApi.createProductUnit(data)
message.success(t('common.createSuccess'))
@ -93,6 +167,7 @@ const submitForm = async () => {
emit('success')
} finally {
formLoading.value = false
unitList.value = await ProductUnitApi.getProductUnitListByFlag()
}
}
@ -101,8 +176,16 @@ const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
primaryFlag: 'N',
primaryId: undefined,
changeRate: undefined,
status: CommonStatusEnum.ENABLE
}
formRef.value?.resetFields()
}
/** 初始化 **/
onMounted(async () => {
unitList.value = await ProductUnitApi.getProductUnitListByFlag()
})
</script>

@ -1,6 +1,4 @@
<template>
<doc-alert title="【产品】产品信息、分类、单位" url="https://doc.iocoder.cn/erp/product/" />
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
@ -8,21 +6,21 @@
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
min-label-width="68px"
>
<el-form-item label="单位名字" prop="name">
<el-form-item :label="t('FactoryModeling.ProductUnit.searchNameLabel')" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入单位名字"
:placeholder="t('FactoryModeling.ProductUnit.searchNamePlaceholder')"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="单位状态" prop="status">
<el-form-item :label="t('FactoryModeling.ProductUnit.searchStatusLabel')" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择单位状态"
:placeholder="t('FactoryModeling.ProductUnit.searchStatusPlaceholder')"
clearable
class="!w-240px"
>
@ -35,15 +33,22 @@
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" />
{{ t('FactoryModeling.ProductUnit.searchButtonText') }}
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
{{ t('FactoryModeling.ProductUnit.resetButtonText') }}
</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['erp:product-unit:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
<Icon icon="ep:plus" class="mr-5px" />
{{ t('FactoryModeling.ProductUnit.addButtonText') }}
</el-button>
<el-button
type="success"
@ -52,7 +57,8 @@
:loading="exportLoading"
v-hasPermi="['erp:product-unit:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
<Icon icon="ep:download" class="mr-5px" />
{{ t('FactoryModeling.ProductUnit.exportButtonText') }}
</el-button>
</el-form-item>
</el-form>
@ -61,20 +67,37 @@
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="名字" align="center" prop="name" />
<el-table-column label="状态" align="center" prop="status">
<el-table-column
:label="t('FactoryModeling.ProductUnit.tableNameColumn')"
align="center"
prop="name"
sortable
/>
<el-table-column
:label="t('FactoryModeling.ProductUnit.tablePrimaryFlagColumn')"
align="center"
prop="primaryFlag"
/>
<!-- <el-table-column label="关联主单位" align="center" prop="primaryId" /> -->
<el-table-column
:label="t('FactoryModeling.ProductUnit.tableChangeRateColumn')"
align="center"
prop="changeRate"
/>
<el-table-column :label="t('FactoryModeling.ProductUnit.tableStatusColumn')" align="center" prop="status" sortable>
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
:label="t('FactoryModeling.ProductUnit.tableCreateTimeColumn')"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
sortable
/>
<el-table-column label="操作" align="center">
<el-table-column :label="t('FactoryModeling.ProductUnit.tableOperateColumn')" align="center">
<template #default="scope">
<el-button
link
@ -82,7 +105,7 @@
@click="openForm('update', scope.row.id)"
v-hasPermi="['erp:product-unit:update']"
>
编辑
{{ t('FactoryModeling.ProductUnit.tableEditAction') }}
</el-button>
<el-button
link
@ -90,7 +113,7 @@
@click="handleDelete(scope.row.id)"
v-hasPermi="['erp:product-unit:delete']"
>
删除
{{ t('FactoryModeling.ProductUnit.tableDeleteAction') }}
</el-button>
</template>
</el-table-column>

@ -0,0 +1,248 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<template #title>
<div class="flex flex-col">
<div>{{ dialogTitle }}</div>
<div
v-if="formType === 'setting' && formData.deviceName"
class="text-12px leading-16px text-[var(--el-text-color-secondary)]"
>
<!-- {{ t('DataCollection.Device.deviceName') + formData.deviceName }} -->
</div>
</div>
</template>
<el-form
ref="formRef"
:model="formData"
:rules="activeRules"
label-width="120px"
v-loading="formLoading"
>
<template v-if="formType === 'create' || formType === 'update'">
<el-form-item :label="t('DataCollection.Device.deviceCode')" prop="deviceCode">
<el-input
v-model="formData.deviceCode"
:disabled="formType === 'update'"
:placeholder="t('DataCollection.Device.placeholderDeviceCode')"
/>
</el-form-item>
<el-form-item :label="t('DataCollection.Device.deviceName')" prop="deviceName">
<el-input
v-model="formData.deviceName"
:placeholder="t('DataCollection.Device.placeholderDeviceName')"
/>
</el-form-item>
<el-form-item :label="t('DataCollection.Device.model')" prop="deviceModelId">
<el-select
v-model="formData.deviceModelId"
clearable
filterable
:disabled="formType === 'update'"
:placeholder="t('DataCollection.Device.placeholderModel')"
>
<el-option v-for="item in modelList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</template>
<!-- <el-form-item label="设备类型" prop="deviceType">
<el-select v-model="formData.deviceType" placeholder="请选择设备类型">
<el-option
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="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_GATEWAY_STATUS)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item> -->
<!-- <el-form-item label="读主题" prop="readTopic">
<el-input v-model="formData.readTopic" placeholder="请输入读主题" />
</el-form-item>
<el-form-item label="写主题" prop="writeTopic">
<el-input v-model="formData.writeTopic" placeholder="请输入写主题" />
</el-form-item>
<el-form-item label="网关" prop="gatewayId">
<el-input v-model="formData.gatewayId" placeholder="请输入网关" />
</el-form-item> -->
<!-- <el-form-item label="设备品牌id" prop="deviceBrandId">-->
<!-- <el-input v-model="formData.deviceBrandId" placeholder="请输入设备品牌id" />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="离线间隔" prop="offLineDuration">
<el-input v-model="formData.offLineDuration" placeholder="请输入离线间隔" />
</el-form-item> -->
<template v-else>
<el-form-item :label="t('DataCollection.Device.topic')" prop="topic">
<el-input
v-model="formData.topic"
:placeholder="t('DataCollection.Device.placeholderTopic')"
/>
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">{{ t('common.cancel') }}</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading">
{{ t('common.ok') }}
</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
import { DeviceApi, DeviceVO } from '@/api/iot/device'
import {DeviceModelApi, DeviceModelVO} from "@/api/iot/devicemodel";
/** 物联设备 表单 */
defineOptions({ name: 'DeviceForm' })
const { t } = useI18n() //
const message = useMessage() //
const modelList = ref<DeviceModelVO[]>([]) //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
deviceCode: undefined,
deviceName: undefined,
deviceType: undefined,
status: undefined,
readTopic: undefined,
writeTopic: undefined,
gatewayId: undefined,
deviceBrandId: undefined,
offLineDuration: undefined,
lastOnlineTime: undefined,
remark: undefined,
isEnable: false,
deviceModelId: undefined,
protocol: undefined,
sampleCycle: undefined,
url: undefined,
username: undefined,
password: undefined,
topic: undefined,
})
const formRules = reactive({
create: {
deviceCode: [{ required: true, message: '设备编码不能为空', trigger: 'blur' }],
deviceName: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }]
},
update: {
deviceCode: [{ required: true, message: '设备编码不能为空', trigger: 'blur' }],
deviceName: [{ required: true, message: '设备名称不能为空', trigger: 'blur' }]
},
setting: {
topic: [{ required: true, message: '订阅主题不能为空', trigger: 'blur' }]
}
})
const activeRules = computed(() => {
return (formRules as any)[formType.value] ?? {}
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value =
type === 'setting' ? t('DataCollection.Device.settingDialogTitle') : t('action.' + type)
formType.value = type
resetForm()
try {
const models = await DeviceModelApi.getDeviceModelList()
modelList.value = Array.isArray(models) ? models : []
} catch {
modelList.value = []
}
if (id) {
formLoading.value = true
try {
formData.value = await DeviceApi.getDevice(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const { id, deviceCode, deviceName, deviceModelId, sampleCycle, remark, isEnable, topic } = formData.value as any
if (formType.value === 'create') {
const data: Partial<DeviceVO> = {
deviceCode,
deviceName,
deviceModelId,
sampleCycle,
remark,
isEnable: false
}
await DeviceApi.createDevice(data as DeviceVO)
message.success(t('common.createSuccess'))
} else if (formType.value === 'update') {
const data: any = { id, deviceCode, deviceName, deviceModelId, sampleCycle, isEnable }
await DeviceApi.updateDevice(data)
message.success(t('common.updateSuccess'))
} else {
const data: any = { id, deviceCode, deviceName, deviceModelId, isEnable, topic }
await DeviceApi.updateDevice(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
deviceCode: undefined,
deviceName: undefined,
deviceType: undefined,
status: undefined,
readTopic: undefined,
writeTopic: undefined,
gatewayId: undefined,
deviceBrandId: undefined,
offLineDuration: undefined,
lastOnlineTime: undefined,
remark: undefined,
isEnable: false,
deviceModelId: undefined,
protocol: undefined,
sampleCycle: undefined,
url: undefined,
username: undefined,
password: undefined,
}
formRef.value?.resetFields()
}
/** 初始化 **/
</script>

@ -0,0 +1,295 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px" v-loading="formLoading">
<el-form-item :label="t('DataCollection.Device.attributeCode')" prop="attributeCode">
<el-input
v-model="formData.attributeCode" :placeholder="t('DataCollection.Device.placeholderAttributeCode')"
@input="handleAttributeCodeInput" :disabled="formType === 'update'" />
</el-form-item>
<el-form-item :label="t('DataCollection.Device.attributeName')" prop="attributeName">
<el-input v-model="formData.attributeName" :placeholder="t('DataCollection.Device.placeholderAttributeName')" />
</el-form-item>
<el-form-item :label="t('DataCollection.Device.attributeType')" prop="attributeType">
<el-select
v-model="formData.attributeType" clearable filterable
:placeholder="t('DataCollection.Device.placeholderAttributeType')" @change="handleAttributeTypeChange">
<el-option v-for="item in typeList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item :label="t('DataCollection.Device.dataType')" prop="dataType">
<el-select
v-model="formData.dataType" :placeholder="t('DataCollection.Device.placeholderDataType')"
:disabled="formType === 'update'">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_DEVICE_DATA_TYPE)" :key="dict.value"
:label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item :label="t('DataCollection.Device.address')" prop="address">
<el-input v-model="formData.address" :placeholder="t('DataCollection.Device.placeholderAddress')" />
</el-form-item>
<el-form-item :label="t('DataCollection.Device.dataUnit')" prop="dataUnit">
<el-select
v-model="formData.dataUnit" clearable
:placeholder="t('DataCollection.DeviceModel.placeholderDataUnit')" class="w-1/1">
<el-option v-for="unit in unitList" :key="unit.id" :label="unit.name" :value="unit.id" />
</el-select>
</el-form-item>
<el-form-item :label="t('DataCollection.Device.ratio')" prop="ratio">
<el-input-number
v-model="formData.ratio" :placeholder="t('DataCollection.Device.placeholderRatio')" :min="0.00"
:decision="2" :step="0.01" class="!w-full" :disabled="!ratioEnabled" />
</el-form-item>
<el-form-item :label="t('DataCollection.Device.remark')" prop="remark">
<el-input
v-model="formData.remark" :placeholder="t('DataCollection.Device.placeholderRemark')"
type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">{{ t('common.cancel') }}</el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading">
{{ t('common.ok') }}
</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
import { DeviceApi } from '@/api/iot/device'
import { DeviceAttributeTypeApi, DeviceAttributeTypeVO } from '@/api/iot/deviceattributetype'
import { ProductUnitApi, ProductUnitVO } from '@/api/erp/product/unit'
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const unitList = ref<ProductUnitVO[]>([]) //
const typeList = ref<DeviceAttributeTypeVO[]>([])
const loadTypeList = async () => {
if (typeList.value.length > 0) {
return
}
try {
const data = await DeviceAttributeTypeApi.getDeviceAttributeTypePage({ pageNo: 1, pageSize: 10 })
typeList.value = data?.list ?? []
} catch {
typeList.value = []
}
}
const formData = ref({
id: undefined as number | undefined,
attributeCode: undefined as string | undefined,
attributeName: undefined as string | undefined,
attributeType: undefined as number | undefined,
typeName: undefined as string | undefined,
dataType: undefined as string | undefined,
address: undefined as string | undefined,
dataUnit: undefined as string | undefined,
ratio: undefined as number | undefined,
remark: undefined as string | undefined,
deviceId: undefined as number | undefined
})
const ratioEnabledTypes = new Set([
'uint8',
'uint16',
'uint32',
'uint64',
'int8',
'int16',
'int32',
'float32',
'float64'
])
const ratioEnabled = computed(() => {
const v = formData.value.dataType
if (!v) return false
return ratioEnabledTypes.has(v)
})
watch(
() => formData.value.dataType,
() => {
if (!ratioEnabled.value) {
formData.value.ratio = undefined
}
}
)
const handleAttributeCodeInput = (val: string) => {
const sanitized = (val || '').replace(/[^A-Za-z0-9_]/g, '')
formData.value.attributeCode = sanitized.replace(/^[0-9]+/, '')
}
const handleAttributeTypeChange = (val: number | string) => {
const matched = typeList.value.find(
(item) => item.id === val || String(item.id) === String(val)
)
formData.value.typeName = matched?.name
}
const formRules = reactive({
attributeCode: [
{ required: true, message: t('DataCollection.Device.attributeValidatorCodeRequired'), trigger: 'blur' },
{
validator: (_rule: any, value: string, callback: any) => {
if (!value) {
callback()
return
}
if (/[\u4e00-\u9fa5]/.test(value)) {
callback(new Error(t('DataCollection.Device.attributeValidatorCodeNoChinese')))
return
}
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
callback(new Error('仅支持英文字母、数字和下划线,且必须以字母或下划线开头'))
return
}
callback()
},
trigger: ['blur', 'change']
}
],
attributeName: [
{ required: true, message: t('DataCollection.Device.attributeValidatorNameRequired'), trigger: 'blur' }
],
dataType: [
{ required: true, message: t('DataCollection.DeviceModel.validatorDataTypeRequired'), trigger: 'change' }
],
remark: [
{
validator: (_rule: any, value: string, callback: any) => {
if (value && value.length > 100) {
callback(new Error(t('DataCollection.Device.attributeValidatorRemarkTooLong')))
return
}
callback()
},
trigger: ['blur', 'change']
}
]
})
const formRef = ref() // Ref
const buildSubmitData = () => {
const {
id,
attributeCode,
attributeName,
attributeType,
typeName,
dataType,
address,
dataUnit,
ratio,
remark,
deviceId
} = formData.value
const data: any = {
attributeCode,
attributeName,
attributeType,
typeName,
dataType,
address,
dataUnit,
ratio: ratioEnabled.value ? ratio : undefined,
remark,
deviceId
}
if (formType.value === 'update') {
data.id = id
}
return data
}
/** 打开弹窗 */
const open = async (type: string, id?: number, deviceId: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
unitList.value = await ProductUnitApi.getProductUnitSimpleList()
formData.value.deviceId = deviceId
await loadTypeList()
//
if (id) {
formLoading.value = true
try {
formData.value = await DeviceApi.getDeviceAttribute(id)
if (!(formData.value as any)?.deviceId) {
; (formData.value as any).deviceId = deviceId
}
const currentType = (formData.value as any)?.attributeType
if (currentType !== undefined && currentType !== null && currentType !== '') {
const matched = typeList.value.find(
(item) =>
item.id === currentType ||
String(item.id) === String(currentType) ||
item.name === currentType ||
item.code === currentType
)
if (matched) {
; (formData.value as any).attributeType = matched.id
; (formData.value as any).typeName = matched.name
}
}
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = buildSubmitData()
if (formType.value === 'create') {
await DeviceApi.createDeviceAttribute(data)
message.success(t('common.createSuccess'))
} else {
await DeviceApi.updateDeviceAttribute(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
attributeCode: undefined,
attributeName: undefined,
attributeType: undefined,
typeName: undefined,
dataType: undefined,
address: undefined,
dataUnit: undefined,
ratio: undefined,
remark: undefined,
deviceId: undefined
}
formRef.value?.resetFields()
}
</script>

@ -0,0 +1,354 @@
<template>
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item :label="t('DataCollection.Device.attributeCode')" prop="attributeCode">
<el-input
v-model="queryParams.attributeCode"
:placeholder="t('DataCollection.Device.placeholderAttributeCode')"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item :label="t('DataCollection.Device.attributeName')" prop="attributeName">
<el-input
v-model="queryParams.attributeName"
:placeholder="t('DataCollection.Device.placeholderAttributeName')"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item :label="t('DataCollection.Device.attributeType')" prop="attributeType">
<el-select
v-model="queryParams.attributeType"
clearable
filterable
:placeholder="t('DataCollection.Device.placeholderAttributeType')"
class="!w-240px"
>
<el-option v-for="item in typeList" :key="item.id" :label="item.code + '-' + item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> {{ t('DataCollection.Device.search') }}
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('DataCollection.Device.reset') }}
</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['iot:device:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> {{ t('DataCollection.Device.create') }}
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['iot:device:export']"
>
<Icon icon="ep:download" class="mr-5px" /> {{ t('DataCollection.Device.export') }}
</el-button>
<el-button
type="danger"
plain
@click="handleBatchDelete"
v-hasPermi="['iot:device:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('DataCollection.Device.batchDelete') }}
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table
ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" row-key="id"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" reserve-selection />
<el-table-column
:label="t('DataCollection.Device.attributeCode')"
align="left"
prop="attributeCode"
width="150px"
sortable
/>
<el-table-column
:label="t('DataCollection.Device.attributeName')"
align="left"
prop="attributeName"
width="150px"
sortable
/>
<el-table-column
:label="t('DataCollection.Device.attributeType')"
align="center"
prop="typeName"
width="140px"
sortable />
<el-table-column
:label="t('DataCollection.Device.dataType')"
align="center"
prop="dataType"
width="120px"
sortable>
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_DATA_TYPE" :value="scope.row.dataType" />
</template>
</el-table-column>
<el-table-column
:label="t('DataCollection.Device.address')"
align="center"
prop="address"
min-width="140px"
/>
<el-table-column
:label="t('DataCollection.Device.attributeLatestValue')"
align="center"
prop="addressValue"
min-width="120px"
sortable
>
<template #default="scope">
{{ formatAddressValue(scope.row.addressValue) }}
</template>
</el-table-column>
<el-table-column
:label="t('DataCollection.Device.dataUnit')"
align="center"
prop="dataUnit"
width="80px"
/>
<el-table-column
:label="t('DataCollection.Device.ratio')"
align="center"
prop="ratio"
width="80px"
/>
<el-table-column
:label="t('DataCollection.Device.attributeLatestCollectionTime')"
align="center"
prop="latestCollectionTime"
:formatter="dateFormatter"
width="170px"
sortable
/>
<el-table-column
:label="t('DataCollection.Device.remark')"
align="center"
prop="remark"
min-width="160px"
/>
<el-table-column
:label="t('DataCollection.Device.operate')"
align="center"
width="150px"
fixed="right"
>
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['iot:device:update']"
>
{{ t('action.edit') }}
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['iot:device:delete']"
>
{{ t('action.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<DeviceAttributeForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { DeviceApi } from '@/api/iot/device'
import DeviceAttributeForm from './DeviceAttributeForm.vue'
import { DeviceAttributeTypeApi, DeviceAttributeTypeVO } from '@/api/iot/deviceattributetype'
const { t } = useI18n() //
const message = useMessage() //
const tableRef = ref()
const props = defineProps<{
deviceId?: number // id
}>()
const typeList = ref<DeviceAttributeTypeVO[]>([])
const loading = ref(false) //
const list = ref([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
deviceId: undefined as unknown,
attributeCode: undefined,
attributeName: undefined,
attributeType: undefined
})
const queryFormRef = ref()
const exportLoading = ref(false)
const formatAddressValue = (value: unknown) => {
if (value === null || value === undefined) {
return '-'
}
if (typeof value === 'string' || typeof value === 'number') {
return value
}
return ''
}
const selectedIds = ref<number[]>([])
const handleSelectionChange = (rows: any[]) => {
selectedIds.value = rows?.map((row) => row.id).filter((id) => id !== undefined) ?? []
}
const getAttributeTypeLabel = (attributeType: any) => {
if (attributeType === undefined || attributeType === null || attributeType === '') {
return ''
}
const matched = typeList.value.find(
(item) => item.name === attributeType || item.id === attributeType || item.code === attributeType
)
return matched?.name ?? String(attributeType)
}
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.deviceId,
(val: number) => {
if (!val) {
return
}
queryParams.deviceId = val
handleQuery()
},
{ immediate: true, deep: true }
)
/** 查询列表 */
async function getList() {
loading.value = true
try {
const data = await DeviceApi.getDeviceAttributePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
function handleQuery() {
if (!props.deviceId) {
return
}
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
if (!props.deviceId) {
return
}
queryFormRef.value?.resetFields()
queryParams.attributeCode = undefined
queryParams.attributeName = undefined
queryParams.attributeType = undefined
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
if (!props.deviceId) {
message.error(t('DataCollection.Device.messageSelectDeviceRequired'))
return
}
formRef.value.open(type, id, props.deviceId)
}
/** 删除按钮操作 */
const buildIdsParam = (ids: number | number[]) => {
return Array.isArray(ids) ? ids.join(',') : String(ids)
}
const handleDelete = async (ids: number | number[]) => {
try {
//
await message.delConfirm()
//
await DeviceApi.deleteDeviceAttribute(buildIdsParam(ids))
message.success(t('common.delSuccess'))
selectedIds.value = []
tableRef.value?.clearSelection?.()
//
await getList()
} catch { }
}
const handleBatchDelete = async () => {
if (!selectedIds.value.length) {
message.error(t('common.delNoData'))
return
}
await handleDelete(selectedIds.value)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const params: any = {}
if (selectedIds.value.length) {
params.ids = selectedIds.value.join(',')
}
const data = await DeviceApi.exportDeviceContactModel(params)
download.excel(data, t('DataCollection.Device.attributeExportFilename'))
} catch {
} finally {
exportLoading.value = false
}
}
onMounted(async () => {
try {
const data = await DeviceAttributeTypeApi.getDeviceAttributeTypeList()
typeList.value = data ?? []
} catch {
typeList.value = []
}
})
</script>

@ -1,286 +0,0 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="产品" prop="productId">
<el-select
v-model="formData.productId"
placeholder="请选择产品"
:disabled="formType === 'update'"
clearable
@change="handleProductChange"
>
<el-option
v-for="product in products"
:key="product.id"
:label="product.name"
:value="product.id"
/>
</el-select>
</el-form-item>
<el-form-item label="DeviceName" prop="deviceName">
<el-input
v-model="formData.deviceName"
placeholder="请输入 DeviceName"
:disabled="formType === 'update'"
/>
</el-form-item>
<el-collapse>
<el-collapse-item title="更多配置">
<el-form-item label="备注名称" prop="nickname">
<el-input v-model="formData.nickname" placeholder="请输入备注名称" />
</el-form-item>
<el-form-item label="设备图片" prop="picUrl">
<UploadImg v-model="formData.picUrl" :height="'120px'" :width="'120px'" />
</el-form-item>
<el-form-item label="设备分组" prop="groupIds">
<el-select v-model="formData.groupIds" placeholder="请选择设备分组" multiple clearable>
<el-option
v-for="group in deviceGroups"
:key="group.id"
:label="group.name"
:value="group.id"
/>
</el-select>
</el-form-item>
<el-form-item label="设备序列号" prop="serialNumber">
<el-input v-model="formData.serialNumber" placeholder="请输入设备序列号" />
</el-form-item>
<el-form-item label="设备位置" prop="longitude">
<div class="flex items-center gap-2 w-full">
<el-input v-model="formData.longitude" placeholder="经度" class="flex-1">
<template #prepend>经度</template>
</el-input>
<el-input v-model="formData.latitude" placeholder="纬度" class="flex-1">
<template #prepend>纬度</template>
</el-input>
<el-button type="primary" @click="openMapDialog"></el-button>
</div>
</el-form-item>
</el-collapse-item>
</el-collapse>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<!-- 地图选择弹窗 -->
<MapDialog ref="mapDialogRef" @confirm="handleMapConfirm" />
</template>
<script setup lang="ts">
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { DeviceGroupApi } from '@/api/iot/device/group'
import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product'
import { UploadImg } from '@/components/UploadFile'
import { MapDialog } from '@/components/Map'
import { ref } from 'vue'
/** IoT 设备表单 */
defineOptions({ name: 'IoTDeviceForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const mapDialogRef = ref() // Ref
const formData = ref({
id: undefined,
productId: undefined,
deviceName: undefined,
nickname: undefined,
picUrl: undefined,
deviceType: undefined as number | undefined,
serialNumber: undefined,
longitude: undefined as number | string | undefined,
latitude: undefined as number | string | undefined,
groupIds: [] as number[]
})
/** 打开地图选择弹窗 */
const openMapDialog = () => {
mapDialogRef.value?.open(
formData.value.longitude ? Number(formData.value.longitude) : undefined,
formData.value.latitude ? Number(formData.value.latitude) : undefined
)
}
/** 处理地图选择确认 */
const handleMapConfirm = (data: { longitude: string; latitude: string; address: string }) => {
formData.value.longitude = data.longitude
formData.value.latitude = data.latitude
}
const formRules = reactive({
productId: [{ required: true, message: '产品不能为空', trigger: 'blur' }],
deviceName: [
{ required: true, message: 'DeviceName 不能为空', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_.\-:@]{4,32}$/,
message:
'支持英文字母、数字、下划线_、中划线-)、点号(.)、半角冒号(:)和特殊字符@,长度限制为 4~32 个字符',
trigger: 'blur'
}
],
nickname: [
{
validator: (_rule, value: any, callback) => {
if (value === undefined || value === null) {
callback()
return
}
const length = value.replace(/[\u4e00-\u9fa5\u3040-\u30ff]/g, 'aa').length
if (length < 4 || length > 64) {
callback(new Error('备注名称长度限制为 4~64 个字符,中文及日文算 2 个字符'))
} else if (!/^[\u4e00-\u9fa5\u3040-\u30ff_a-zA-Z0-9]+$/.test(value)) {
callback(new Error('备注名称只能包含中文、英文字母、日文、数字和下划线_'))
} else {
callback()
}
},
trigger: 'blur'
}
],
serialNumber: [
{
pattern: /^[a-zA-Z0-9-_]+$/,
message: '序列号只能包含字母、数字、中划线和下划线',
trigger: 'blur'
}
],
longitude: [
{
validator: (_rule: any, value: any, callback: any) => {
if (value !== undefined && value !== null && value !== '') {
const num = Number(value)
if (isNaN(num)) {
callback(new Error('经度必须是有效数字'))
return
}
if (num < -180 || num > 180) {
callback(new Error('经度范围为 -180 到 180'))
return
}
if (!formData.value.latitude && formData.value.latitude !== 0) {
callback(new Error('请同时填写纬度'))
return
}
}
callback()
},
trigger: 'blur'
}
],
latitude: [
{
validator: (_rule: any, value: any, callback: any) => {
if (value !== undefined && value !== null && value !== '') {
const num = Number(value)
if (isNaN(num)) {
callback(new Error('纬度必须是有效数字'))
return
}
if (num < -90 || num > 90) {
callback(new Error('纬度范围为 -90 到 90'))
return
}
if (!formData.value.longitude && formData.value.longitude !== 0) {
callback(new Error('请同时填写经度'))
return
}
}
callback()
},
trigger: 'blur'
}
]
})
const formRef = ref() // Ref
const products = ref<ProductVO[]>([]) //
const deviceGroups = ref<any[]>([])
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await DeviceApi.getDevice(id)
} finally {
formLoading.value = false
}
}
//
products.value = await ProductApi.getSimpleProductList()
//
deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList()
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as DeviceVO
if (formType.value === 'create') {
await DeviceApi.createDevice(data)
message.success(t('common.createSuccess'))
} else {
await DeviceApi.updateDevice(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
productId: undefined,
deviceName: undefined,
nickname: undefined,
picUrl: undefined,
deviceType: undefined,
serialNumber: undefined,
longitude: undefined,
latitude: undefined,
groupIds: []
}
formRef.value?.resetFields()
}
/** 产品选择变化 */
const handleProductChange = (productId: number) => {
if (!productId) {
formData.value.deviceType = undefined
return
}
const product = products.value?.find((item) => item.id === productId)
formData.value.deviceType = product?.deviceType
}
</script>

@ -1,90 +0,0 @@
<template>
<Dialog :title="'添加设备到分组'" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="设备分组" prop="groupIds">
<el-select v-model="formData.groupIds" placeholder="请选择设备分组" multiple clearable>
<el-option
v-for="group in deviceGroups"
:key="group.id"
:label="group.name"
:value="group.id"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DeviceApi } from '@/api/iot/device/device'
import { DeviceGroupApi } from '@/api/iot/device/group'
defineOptions({ name: 'IoTDeviceGroupForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const formData = ref({
ids: [] as number[],
groupIds: [] as number[]
})
const formRules = reactive({
groupIds: [{ required: true, message: '设备分组不能为空', trigger: 'change' }]
})
const formRef = ref() // Ref
const deviceGroups = ref<any[]>([]) //
/** 打开弹窗 */
const open = async (ids: number[]) => {
dialogVisible.value = true
resetForm()
formData.value.ids = ids
//
try {
deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList()
} catch (error) {
console.error('加载设备分组列表失败:', error)
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
await DeviceApi.updateDeviceGroup(formData.value)
message.success(t('common.updateSuccess'))
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
ids: [],
groupIds: []
}
formRef.value?.resetFields()
}
</script>

@ -1,139 +0,0 @@
<template>
<Dialog v-model="dialogVisible" title="设备导入" width="400">
<el-upload
ref="uploadRef"
v-model:file-list="fileList"
:action="importUrl + '?updateSupport=' + updateSupport"
:auto-upload="false"
:disabled="formLoading"
:headers="uploadHeaders"
:limit="1"
:on-error="submitFormError"
:on-exceed="handleExceed"
:on-success="submitFormSuccess"
accept=".xlsx, .xls"
drag
>
<Icon icon="ep:upload" />
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="updateSupport" />
是否更新已经存在的设备数据
</div>
<span>仅允许导入 xlsxlsx 格式文件</span>
<el-link
:underline="false"
style="font-size: 12px; vertical-align: baseline"
type="primary"
@click="importTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { DeviceApi } from '@/api/iot/device/device'
import { getAccessToken, getTenantId } from '@/utils/auth'
import download from '@/utils/download'
defineOptions({ name: 'IoTDeviceImportForm' })
const message = useMessage() //
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const uploadRef = ref()
const importUrl =
import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/iot/device/import'
const uploadHeaders = ref() // Header
const fileList = ref([]) //
const updateSupport = ref(0) //
/** 打开弹窗 */
const open = () => {
dialogVisible.value = true
updateSupport.value = 0
fileList.value = []
resetForm()
}
defineExpose({ open }) // open
/** 提交表单 */
const submitForm = async () => {
if (fileList.value.length == 0) {
message.error('请上传文件')
return
}
//
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
formLoading.value = true
uploadRef.value!.submit()
}
/** 文件上传成功 */
const emits = defineEmits(['success'])
const submitFormSuccess = (response: any) => {
if (response.code !== 0) {
message.error(response.msg)
formLoading.value = false
return
}
//
const data = response.data
let text = '上传成功数量:' + data.createDeviceNames.length + ';'
for (let deviceName of data.createDeviceNames) {
text += '< ' + deviceName + ' >'
}
text += '更新成功数量:' + data.updateDeviceNames.length + ';'
for (const deviceName of data.updateDeviceNames) {
text += '< ' + deviceName + ' >'
}
text += '更新失败数量:' + Object.keys(data.failureDeviceNames).length + ';'
for (const deviceName in data.failureDeviceNames) {
text += '< ' + deviceName + ': ' + data.failureDeviceNames[deviceName] + ' >'
}
message.alert(text)
formLoading.value = false
dialogVisible.value = false
//
emits('success')
}
/** 上传错误提示 */
const submitFormError = (): void => {
message.error('上传失败,请您重新上传!')
resetForm()
}
/** 重置表单 */
const resetForm = async (): Promise<void> => {
//
formLoading.value = false
await nextTick()
uploadRef.value?.clearFiles()
}
/** 文件数超出提示 */
const handleExceed = (): void => {
message.error('最多只能上传一个文件!')
}
/** 下载模板操作 */
const importTemplate = async () => {
const res = await DeviceApi.importDeviceTemplate()
download.excel(res, '设备导入模版.xls')
}
</script>

@ -1,303 +0,0 @@
<!-- IoT 设备选择使用弹窗展示 -->
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" :appendToBody="true" width="60%">
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="100px"
>
<el-form-item v-if="!props.productId" label="产品" prop="productId">
<el-select
v-model="queryParams.productId"
placeholder="请选择产品"
clearable
class="!w-240px"
>
<el-option
v-for="product in products"
:key="product.id"
:label="product.name"
:value="product.id"
/>
</el-select>
</el-form-item>
<el-form-item label="DeviceName" prop="deviceName">
<el-input
v-model="queryParams.deviceName"
placeholder="请输入 DeviceName"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="备注名称" prop="nickname">
<el-input
v-model="queryParams.nickname"
placeholder="请输入备注名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="设备类型" prop="deviceType">
<el-select
v-model="queryParams.deviceType"
placeholder="请选择设备类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="设备状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择设备状态"
clearable
class="!w-240px"
>
<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>
<el-form-item label="设备分组" prop="groupId">
<el-select
v-model="queryParams.groupId"
placeholder="请选择设备分组"
clearable
class="!w-240px"
>
<el-option
v-for="group in deviceGroups"
:key="group.id"
:label="group.name"
:value="group.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
ref="tableRef"
v-loading="loading"
:data="list"
:show-overflow-tooltip="true"
:stripe="true"
@row-click="handleRowClick"
@selection-change="handleSelectionChange"
>
<el-table-column v-if="multiple" type="selection" width="55" />
<el-table-column v-else width="55">
<template #default="scope">
<el-radio
v-model="selectedId"
:value="scope.row.id"
@change="() => handleRadioChange(scope.row)"
>
&nbsp;
</el-radio>
</template>
</el-table-column>
<el-table-column label="DeviceName" align="center" prop="deviceName" />
<el-table-column label="备注名称" align="center" prop="nickname" />
<el-table-column label="所属产品" align="center" prop="productId">
<template #default="scope">
{{ products.find((p) => p.id === scope.row.productId)?.name || '-' }}
</template>
</el-table-column>
<el-table-column label="设备类型" align="center" prop="deviceType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
</template>
</el-table-column>
<el-table-column label="所属分组" align="center" prop="groupId">
<template #default="scope">
<template v-if="scope.row.groupIds?.length">
<el-tag v-for="id in scope.row.groupIds" :key="id" class="ml-5px" size="small">
{{ deviceGroups.find((g) => g.id === id)?.name }}
</el-tag>
</template>
</template>
</el-table-column>
<el-table-column label="设备状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="最后上线时间"
align="center"
prop="onlineTime"
:formatter="dateFormatter"
width="180px"
/>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { ProductApi, ProductVO } from '@/api/iot/product/product'
import { DeviceGroupApi, DeviceGroupVO } from '@/api/iot/device/group'
defineOptions({ name: 'IoTDeviceTableSelect' })
const props = defineProps({
multiple: {
type: Boolean,
default: false
},
productId: {
type: Number,
default: null
}
})
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('设备选择器')
const formLoading = ref(false)
const loading = ref(true) //
const list = ref<DeviceVO[]>([]) //
const total = ref(0) //
const selectedDevices = ref<DeviceVO[]>([]) //
const selectedId = ref<number>() // ID
const products = ref<ProductVO[]>([]) //
const deviceGroups = ref<DeviceGroupVO[]>([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
deviceName: undefined,
productId: undefined,
deviceType: undefined,
nickname: undefined,
status: undefined,
groupId: undefined
})
const queryFormRef = ref() //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
if (props.productId) {
queryParams.productId = props.productId as unknown as any
}
const data = await DeviceApi.getDevicePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true
//
selectedDevices.value = []
selectedId.value = undefined
if (!props.productId) {
//
products.value = await ProductApi.getSimpleProductList()
}
//
await getList()
}
defineExpose({ open })
/** 处理行点击事件 */
const tableRef = ref()
const handleRowClick = (row: DeviceVO) => {
if (props.multiple) {
tableRef.value?.toggleRowSelection(row)
} else {
selectedId.value = row.id
selectedDevices.value = [row]
}
}
/** 处理单选变更事件 */
const handleRadioChange = (row: DeviceVO) => {
selectedDevices.value = [row]
}
/** 处理选择变更事件 */
const handleSelectionChange = (selection: DeviceVO[]) => {
if (props.multiple) {
selectedDevices.value = selection
}
}
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
if (selectedDevices.value.length === 0) {
message.warning(props.multiple ? '请至少选择一个设备' : '请选择一个设备')
return
}
emit('success', props.multiple ? selectedDevices.value : selectedDevices.value[0])
dialogVisible.value = false
}
/** 初始化 **/
onMounted(async () => {
//
deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList()
})
</script>

@ -1,134 +0,0 @@
<!-- 设备配置 -->
<template>
<div>
<el-alert
title="支持远程更新设备的配置文件(JSON 格式),可以在下方编辑配置模板,对设备的系统参数、网络参数等进行远程配置。配置完成后,需点击「下发」按钮,设备即可进行远程配置。"
type="info"
show-icon
class="my-4"
description="如需编辑文件,请点击下方编辑按钮"
/>
<JsonEditor
v-model="config"
:mode="isEditing ? 'code' : 'view'"
height="600px"
@error="onError"
/>
<div class="mt-5 text-center">
<el-button v-if="isEditing" @click="cancelEdit"></el-button>
<el-button v-if="isEditing" type="primary" @click="saveConfig" :disabled="hasJsonError">
保存
</el-button>
<el-button v-else @click="enableEdit"></el-button>
<el-button v-if="!isEditing" type="success" @click="handleConfigPush" :loading="pushLoading">
配置推送
</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { IotDeviceMessageMethodEnum } from '@/views/iot/utils/constants'
import { jsonParse } from '@/utils'
import { isEmpty } from '@/utils/is'
defineOptions({ name: 'DeviceDetailConfig' })
const props = defineProps<{
device: DeviceVO
}>()
const emit = defineEmits<{
(e: 'success'): void // success
}>()
const message = useMessage()
const loading = ref(false) //
const pushLoading = ref(false) //
const config = ref<any>({}) // config
const hasJsonError = ref(false) // JSON
/** 监听 props.device 的变化,只更新 config 字段 */
watchEffect(() => {
config.value = jsonParse(props.device.config)
})
const isEditing = ref(false) //
/** 启用编辑模式的函数 */
const enableEdit = () => {
isEditing.value = true
hasJsonError.value = false //
}
/** 取消编辑的函数 */
const cancelEdit = () => {
config.value = jsonParse(props.device.config)
isEditing.value = false
hasJsonError.value = false //
}
/** 保存配置的函数 */
const saveConfig = async () => {
if (hasJsonError.value) {
message.error('JSON格式错误请修正后再提交')
return
}
await updateDeviceConfig()
isEditing.value = false
}
/** 配置推送处理函数 */
const handleConfigPush = async () => {
try {
//
await message.confirm('确定要推送配置到设备吗?此操作将远程更新设备配置。', '配置推送确认')
pushLoading.value = true
//
await DeviceApi.sendDeviceMessage({
deviceId: props.device.id,
method: IotDeviceMessageMethodEnum.CONFIG_PUSH.method,
params: config.value
})
message.success('配置推送成功!')
} catch (error) {
if (error !== 'cancel') {
message.error('配置推送失败!')
console.error('配置推送错误:', error)
}
} finally {
pushLoading.value = false
}
}
/** 更新设备配置 */
const updateDeviceConfig = async () => {
try {
//
loading.value = true
await DeviceApi.updateDevice({
id: props.device.id,
config: JSON.stringify(config.value)
} as DeviceVO)
message.success('更新成功!')
// success
emit('success')
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
/** 处理 JSON 编辑器错误的函数 */
const onError = (errors: any) => {
if (isEmpty(errors)) {
hasJsonError.value = false
return
}
hasJsonError.value = true
}
</script>

@ -1,74 +0,0 @@
<!-- 设备信息头部 -->
<template>
<div>
<div class="flex items-start justify-between">
<div>
<el-col>
<el-row>
<span class="text-xl font-bold">{{ device.deviceName }}</span>
</el-row>
</el-col>
</div>
<div>
<!-- 右上按钮 -->
<el-button
@click="openForm('update', device.id)"
v-hasPermi="['iot:device:update']"
v-if="product.status === 0"
>
编辑
</el-button>
</div>
</div>
</div>
<ContentWrap class="mt-10px">
<el-descriptions :column="5" direction="horizontal">
<el-descriptions-item label="产品">
<el-link @click="goToProductDetail(product.id)">{{ product.name }}</el-link>
</el-descriptions-item>
<el-descriptions-item label="ProductKey">
{{ product.productKey }}
<el-button @click="copyToClipboard(product.productKey)"></el-button>
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<DeviceForm ref="formRef" @success="emit('refresh')" />
</template>
<script setup lang="ts">
import DeviceForm from '@/views/iot/device/device/DeviceForm.vue'
import { ProductVO } from '@/api/iot/product/product'
import { DeviceVO } from '@/api/iot/device/device'
import { useClipboard } from '@vueuse/core'
const message = useMessage()
const { t } = useI18n() //
const router = useRouter()
const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>()
const emit = defineEmits(['refresh'])
/** 操作修改 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 复制到剪贴板方法 */
const copyToClipboard = async (text: string) => {
const { copy, copied, isSupported } = useClipboard({ legacy: true, source: text })
if (!isSupported) {
message.error(t('common.copyError'))
return
}
await copy()
if (unref(copied)) {
message.success(t('common.copySuccess'))
}
}
/** 跳转到产品详情页面 */
const goToProductDetail = (productId: number) => {
router.push({ name: 'IoTProductDetail', params: { id: productId } })
}
</script>

@ -1,160 +0,0 @@
<!-- 设备信息 -->
<template>
<div>
<ContentWrap>
<el-descriptions :column="3" border>
<el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item>
<el-descriptions-item label="ProductKey">{{ product.productKey }}</el-descriptions-item>
<el-descriptions-item label="设备类型">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
</el-descriptions-item>
<el-descriptions-item label="DeviceName">{{ device.deviceName }}</el-descriptions-item>
<el-descriptions-item label="备注名称">{{ device.nickname }}</el-descriptions-item>
<el-descriptions-item label="当前状态">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="device.state" />
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDate(device.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="激活时间">
{{ formatDate(device.activeTime) }}
</el-descriptions-item>
<el-descriptions-item label="最后上线时间">
{{ formatDate(device.onlineTime) }}
</el-descriptions-item>
<el-descriptions-item label="最后离线时间">
{{ formatDate(device.offlineTime) }}
</el-descriptions-item>
<el-descriptions-item label="设备位置">
<template v-if="hasLocation">
<span class="mr-2">{{ device.longitude }}, {{ device.latitude }}</span>
<el-button type="primary" link @click="openMapDialog">
<Icon icon="ep:location" class="mr-1" />
查看地图
</el-button>
</template>
<span v-else class="text-[var(--el-text-color-secondary)]">暂无位置信息</span>
</el-descriptions-item>
<el-descriptions-item label="认证信息">
<el-button type="primary" @click="handleAuthInfoDialogOpen" plain size="small">
查看
</el-button>
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
<!-- 认证信息弹框 -->
<Dialog
title="设备认证信息"
v-model="authDialogVisible"
width="640px"
:before-close="handleAuthInfoDialogClose"
>
<el-form :model="authInfo" label-width="120px">
<el-form-item label="clientId">
<el-input v-model="authInfo.clientId" readonly>
<template #append>
<el-button @click="copyToClipboard(authInfo.clientId)" type="primary">
<Icon icon="ph:copy" />
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="username">
<el-input v-model="authInfo.username" readonly>
<template #append>
<el-button @click="copyToClipboard(authInfo.username)" type="primary">
<Icon icon="ph:copy" />
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="password">
<el-input
v-model="authInfo.password"
readonly
:type="authPasswordVisible ? 'text' : 'password'"
>
<template #append>
<el-button @click="authPasswordVisible = !authPasswordVisible" type="primary">
<Icon :icon="authPasswordVisible ? 'ph:eye-slash' : 'ph:eye'" />
</el-button>
<el-button @click="copyToClipboard(authInfo.password)" type="primary">
<Icon icon="ph:copy" />
</el-button>
</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleAuthInfoDialogClose"></el-button>
</template>
</Dialog>
<!-- 地图弹窗 -->
<MapDialog ref="mapDialogRef" />
</div>
<!-- TODO 待开发设备标签 -->
</template>
<script setup lang="ts">
import { DICT_TYPE } from '@/utils/dict'
import { ProductVO } from '@/api/iot/product/product'
import { formatDate } from '@/utils/formatTime'
import { DeviceVO } from '@/api/iot/device/device'
import { DeviceApi, IotDeviceAuthInfoVO } from '@/api/iot/device/device'
import { MapDialog } from '@/components/Map'
import { ref, computed } from 'vue'
import { useClipboard } from '@vueuse/core'
const message = useMessage() //
const { t } = useI18n() //
const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>() // Props
const emit = defineEmits(['refresh']) // Emits
const authDialogVisible = ref(false) //
const authPasswordVisible = ref(false) //
const authInfo = ref<IotDeviceAuthInfoVO>({} as IotDeviceAuthInfoVO) //
const mapDialogRef = ref() // Ref
/** 是否有位置信息 */
const hasLocation = computed(() => {
return !!(device.longitude && device.latitude)
})
/** 打开地图弹窗 */
const openMapDialog = () => {
mapDialogRef.value?.open(device.longitude, device.latitude)
}
/** 复制到剪贴板方法 */
const copyToClipboard = async (text: string) => {
const { copy, copied, isSupported } = useClipboard({ legacy: true, source: text })
if (!isSupported) {
message.error(t('common.copyError'))
return
}
await copy()
if (unref(copied)) {
message.success(t('common.copySuccess'))
}
}
/** 打开设备认证信息弹框的方法 */
const handleAuthInfoDialogOpen = async () => {
try {
authInfo.value = await DeviceApi.getDeviceAuthInfo(device.id)
//
authDialogVisible.value = true
} catch (error) {
console.error('获取设备认证信息出错:', error)
message.error('获取设备认证信息失败,请检查网络连接或联系管理员')
}
}
/** 关闭设备认证信息弹框的方法 */
const handleAuthInfoDialogClose = () => {
authDialogVisible.value = false
}
</script>

@ -1,201 +0,0 @@
<!-- 设备消息列表 -->
<template>
<ContentWrap>
<!-- 搜索区域 -->
<el-form :model="queryParams" inline>
<el-form-item>
<el-select v-model="queryParams.method" placeholder="所有方法" class="!w-160px" clearable>
<el-option
v-for="item in methodOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-select
v-model="queryParams.upstream"
placeholder="上行/下行"
class="!w-160px"
clearable
>
<el-option label="上行" value="true" />
<el-option label="下行" value="false" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-switch
size="large"
width="80"
v-model="autoRefresh"
class="ml-20px"
inline-prompt
active-text="定时刷新"
inactive-text="定时刷新"
style="--el-switch-on-color: #13ce66"
/>
</el-form-item>
</el-form>
<!-- 消息列表 -->
<el-table v-loading="loading" :data="list" :stripe="true" class="whitespace-nowrap">
<el-table-column label="时间" align="center" prop="ts" width="180">
<template #default="scope">
{{ formatDate(scope.row.ts) }}
</template>
</el-table-column>
<el-table-column label="上行/下行" align="center" prop="upstream" width="140">
<template #default="scope">
<el-tag :type="scope.row.upstream ? 'primary' : 'success'">
{{ scope.row.upstream ? '上行' : '下行' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="是否回复" align="center" prop="reply" width="140">
<template #default="scope">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.reply" />
</template>
</el-table-column>
<el-table-column label="请求编号" align="center" prop="requestId" width="300" />
<el-table-column label="请求方法" align="center" prop="method" width="140">
<template #default="scope">
{{ methodOptions.find((item) => item.value === scope.row.method)?.label }}
</template>
</el-table-column>
<el-table-column
label="请求/响应数据"
align="center"
prop="params"
:show-overflow-tooltip="true"
>
<template #default="scope">
<span v-if="scope.row.reply">
{{ `{"code":${scope.row.code},"msg":"${scope.row.msg}","data":${scope.row.data}\}` }}
</span>
<span v-else>{{ scope.row.params }}</span>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="mt-10px flex justify-end">
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getMessageList"
/>
</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { DICT_TYPE } from '@/utils/dict'
import { DeviceApi } from '@/api/iot/device/device'
import { formatDate } from '@/utils/formatTime'
import { IotDeviceMessageMethodEnum } from '@/views/iot/utils/constants'
const props = defineProps<{
deviceId: number
}>()
//
const queryParams = reactive({
deviceId: props.deviceId,
method: undefined,
upstream: undefined,
pageNo: 1,
pageSize: 10
})
//
const loading = ref(false)
const total = ref(0)
const list = ref([])
const autoRefresh = ref(false) //
let autoRefreshTimer: any = null //
//
const methodOptions = computed(() => {
return Object.values(IotDeviceMessageMethodEnum).map((item) => ({
label: item.name,
value: item.method
}))
})
/** 查询消息列表 */
const getMessageList = async () => {
if (!props.deviceId) return
loading.value = true
try {
const data = await DeviceApi.getDeviceMessagePage(queryParams)
total.value = data.total
list.value = data.list
} finally {
loading.value = false
}
}
/** 搜索操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getMessageList()
}
/** 监听自动刷新 */
watch(autoRefresh, (newValue) => {
if (newValue) {
autoRefreshTimer = setInterval(() => {
getMessageList()
}, 5000)
} else {
clearInterval(autoRefreshTimer)
autoRefreshTimer = null
}
})
/** 监听设备标识变化 */
watch(
() => props.deviceId,
(newValue) => {
if (newValue) {
handleQuery()
}
}
)
/** 组件卸载时清除定时器 */
onBeforeUnmount(() => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer)
autoRefreshTimer = null
}
})
/** 初始化 */
onMounted(() => {
if (props.deviceId) {
getMessageList()
}
})
/** 刷新消息列表 */
const refresh = (delay = 0) => {
if (delay > 0) {
setTimeout(() => {
handleQuery()
}, delay)
} else {
handleQuery()
}
}
/** 暴露方法给父组件 */
defineExpose({
refresh
})
</script>

@ -1,424 +0,0 @@
<!-- 模拟设备 -->
<template>
<ContentWrap>
<el-row :gutter="20">
<!-- 左侧指令调试区域 -->
<el-col :span="12">
<el-tabs v-model="activeTab" type="border-card">
<!-- 上行指令调试 -->
<el-tab-pane label="上行指令调试" name="upstream">
<el-tabs v-if="activeTab === 'upstream'" v-model="upstreamTab">
<!-- 属性上报 -->
<el-tab-pane label="属性上报" :name="IotDeviceMessageMethodEnum.PROPERTY_POST.method">
<ContentWrap>
<el-table :data="propertyList" :show-overflow-tooltip="true" :stripe="true">
<el-table-column
fixed="left"
align="center"
label="功能名称"
prop="name"
width="120"
/>
<el-table-column
fixed="left"
align="center"
label="标识符"
prop="identifier"
width="120"
/>
<el-table-column align="center" label="数据类型" width="100">
<template #default="{ row }">
{{ row.property?.dataType ?? '-' }}
</template>
</el-table-column>
<el-table-column align="left" label="数据定义" min-width="200">
<template #default="{ row }">
<DataDefinition :data="row" />
</template>
</el-table-column>
<el-table-column fixed="right" align="center" label="值" width="150">
<template #default="scope">
<el-input
:model-value="getFormValue(scope.row.identifier)"
@update:model-value="setFormValue(scope.row.identifier, $event)"
placeholder="输入值"
size="small"
/>
</template>
</el-table-column>
</el-table>
<div class="flex justify-between items-center mt-4">
<span class="text-sm text-gray-600">
设置属性值后点击发送属性上报按钮
</span>
<el-button type="primary" @click="handlePropertyPost"></el-button>
</div>
</ContentWrap>
</el-tab-pane>
<!-- 事件上报 -->
<el-tab-pane label="事件上报" :name="IotDeviceMessageMethodEnum.EVENT_POST.method">
<ContentWrap>
<el-table :data="eventList" :show-overflow-tooltip="true" :stripe="true">
<el-table-column
fixed="left"
align="center"
label="功能名称"
prop="name"
width="120"
/>
<el-table-column
fixed="left"
align="center"
label="标识符"
prop="identifier"
width="120"
/>
<el-table-column align="center" label="数据类型" width="100">
<template #default="{ row }">
{{ row.event?.dataType ?? '-' }}
</template>
</el-table-column>
<el-table-column align="left" label="数据定义" min-width="200">
<template #default="{ row }">
<DataDefinition :data="row" />
</template>
</el-table-column>
<el-table-column align="center" label="值" width="200">
<template #default="scope">
<el-input
:model-value="getFormValue(scope.row.identifier)"
@update:model-value="setFormValue(scope.row.identifier, $event)"
type="textarea"
:rows="3"
placeholder="输入事件参数JSON格式"
size="small"
/>
</template>
</el-table-column>
<el-table-column fixed="right" align="center" label="操作" width="100">
<template #default="scope">
<el-button type="primary" size="small" @click="handleEventPost(scope.row)">
上报事件
</el-button>
</template>
</el-table-column>
</el-table>
</ContentWrap>
</el-tab-pane>
<!-- 状态变更 -->
<el-tab-pane label="状态变更" :name="IotDeviceMessageMethodEnum.STATE_UPDATE.method">
<ContentWrap>
<div class="flex gap-4">
<el-button type="primary" @click="handleDeviceState(DeviceStateEnum.ONLINE)">
设备上线
</el-button>
<el-button type="danger" @click="handleDeviceState(DeviceStateEnum.OFFLINE)">
设备下线
</el-button>
</div>
</ContentWrap>
</el-tab-pane>
</el-tabs>
</el-tab-pane>
<!-- 下行指令调试 -->
<el-tab-pane label="下行指令调试" name="downstream">
<el-tabs v-if="activeTab === 'downstream'" v-model="downstreamTab">
<!-- 属性调试 -->
<el-tab-pane label="属性设置" :name="IotDeviceMessageMethodEnum.PROPERTY_SET.method">
<ContentWrap>
<el-table :data="propertyList" :show-overflow-tooltip="true" :stripe="true">
<el-table-column
fixed="left"
align="center"
label="功能名称"
prop="name"
width="120"
/>
<el-table-column
fixed="left"
align="center"
label="标识符"
prop="identifier"
width="120"
/>
<el-table-column align="center" label="数据类型" width="100">
<template #default="{ row }">
{{ row.property?.dataType ?? '-' }}
</template>
</el-table-column>
<el-table-column align="left" label="数据定义" min-width="200">
<template #default="{ row }">
<DataDefinition :data="row" />
</template>
</el-table-column>
<el-table-column fixed="right" align="center" label="值" width="150">
<template #default="scope">
<el-input
:model-value="getFormValue(scope.row.identifier)"
@update:model-value="setFormValue(scope.row.identifier, $event)"
placeholder="输入值"
size="small"
/>
</template>
</el-table-column>
</el-table>
<div class="flex justify-between items-center mt-4">
<span class="text-sm text-gray-600">
设置属性值后点击发送属性设置按钮
</span>
<el-button type="primary" @click="handlePropertySet"></el-button>
</div>
</ContentWrap>
</el-tab-pane>
<!-- 服务调用 -->
<el-tab-pane
label="设备服务调用"
:name="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
>
<ContentWrap>
<el-table :data="serviceList" :show-overflow-tooltip="true" :stripe="true">
<el-table-column
fixed="left"
align="center"
label="服务名称"
prop="name"
width="120"
/>
<el-table-column
fixed="left"
align="center"
label="标识符"
prop="identifier"
width="120"
/>
<el-table-column align="left" label="输入参数" min-width="200">
<template #default="{ row }">
<DataDefinition :data="row" />
</template>
</el-table-column>
<el-table-column align="center" label="参数值" width="200">
<template #default="scope">
<el-input
:model-value="getFormValue(scope.row.identifier)"
@update:model-value="setFormValue(scope.row.identifier, $event)"
type="textarea"
:rows="3"
placeholder="输入服务参数JSON格式"
size="small"
/>
</template>
</el-table-column>
<el-table-column fixed="right" align="center" label="操作" width="100">
<template #default="scope">
<el-button
type="primary"
size="small"
@click="handleServiceInvoke(scope.row)"
>
服务调用
</el-button>
</template>
</el-table-column>
</el-table>
</ContentWrap>
</el-tab-pane>
</el-tabs>
</el-tab-pane>
</el-tabs>
</el-col>
<!-- 右侧设备日志区域 -->
<el-col :span="12">
<ContentWrap title="设备消息">
<DeviceDetailsMessage ref="deviceMessageRef" :device-id="device.id" />
</ContentWrap>
</el-col>
</el-row>
</ContentWrap>
</template>
<script lang="ts" setup>
import { ProductVO } from '@/api/iot/product/product'
import { ThingModelData } from '@/api/iot/thingmodel'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
import { DataDefinition } from '@/views/iot/thingmodel/components'
import {
DeviceStateEnum,
IotDeviceMessageMethodEnum,
IoTThingModelTypeEnum
} from '@/views/iot/utils/constants'
const props = defineProps<{
product: ProductVO
device: DeviceVO
thingModelList: ThingModelData[]
}>()
const message = useMessage() //
const activeTab = ref('upstream') // upstreamdownstream
const upstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_POST.method) //
const downstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_SET.method) //
const deviceMessageRef = ref() //
const deviceMessageRefreshDelay = 2000 // N
//
const formData = ref<Record<string, string>>({})
//
const getFilteredThingModelList = (type: number) => {
return props.thingModelList.filter((item) => item.type === type)
}
const propertyList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.PROPERTY))
const eventList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.EVENT))
const serviceList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.SERVICE))
/** 获取表单值的辅助函数 */
const getFormValue = (identifier: string | number | undefined) => {
if (!identifier) return ''
return formData.value[String(identifier)] || ''
}
/** 设置表单值的辅助函数 */
const setFormValue = (identifier: string | number | undefined, value: string) => {
if (!identifier) return
formData.value[String(identifier)] = value
}
/** 模拟属性上报 */
const handlePropertyPost = async () => {
const data: Record<string, any> = {}
propertyList.value.forEach((item) => {
const value = getFormValue(item.identifier)
if (value && item.identifier) {
data[String(item.identifier)] = value
}
})
if (Object.keys(data).length === 0) {
message.warning('请至少设置一个属性值')
return
}
try {
await DeviceApi.sendDeviceMessage({
deviceId: props.device.id,
method: IotDeviceMessageMethodEnum.PROPERTY_POST.method,
params: data
})
message.success('属性上报成功')
deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
} catch (error) {
message.error('属性上报失败')
}
}
/** 模拟事件上报 */
const handleEventPost = async (eventItem: ThingModelData) => {
const value = getFormValue(eventItem.identifier)
if (!value) {
message.warning('请输入事件参数')
return
}
let eventParams: any
try {
eventParams = JSON.parse(value)
} catch {
message.error('事件参数格式不正确请输入有效的JSON格式')
return
}
try {
await DeviceApi.sendDeviceMessage({
deviceId: props.device.id,
method: IotDeviceMessageMethodEnum.EVENT_POST.method,
params: {
identifier: String(eventItem.identifier),
value: eventParams,
time: Date.now()
}
})
message.success(`事件【${String(eventItem.name)}】上报成功`)
deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
} catch (error) {
message.error(`事件【${String(eventItem.name)}】上报失败`)
}
}
/** 模拟设备状态 */
const handleDeviceState = async (state: number) => {
try {
await DeviceApi.sendDeviceMessage({
deviceId: props.device.id,
method: IotDeviceMessageMethodEnum.STATE_UPDATE.method,
params: {
state: state
}
})
message.success(`设备${state === DeviceStateEnum.ONLINE ? '上线' : '下线'}成功`)
deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
} catch (error) {
message.error(`设备${state === DeviceStateEnum.ONLINE ? '上线' : '下线'}失败`)
}
}
/** 模拟属性设置 */
const handlePropertySet = async () => {
const data: Record<string, any> = {}
propertyList.value.forEach((item) => {
const value = getFormValue(item.identifier)
if (value && item.identifier) {
data[String(item.identifier)] = value
}
})
if (Object.keys(data).length === 0) {
message.warning('请至少设置一个属性值')
return
}
try {
await DeviceApi.sendDeviceMessage({
deviceId: props.device.id,
method: IotDeviceMessageMethodEnum.PROPERTY_SET.method,
params: data
})
message.success('属性设置成功')
deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
} catch (error) {
message.error('属性设置失败')
}
}
/** 模拟服务调用 */
const handleServiceInvoke = async (serviceItem: ThingModelData) => {
const value = getFormValue(serviceItem.identifier)
if (!value) {
message.warning('请输入服务参数')
return
}
let serviceParams: any
try {
serviceParams = JSON.parse(value)
} catch {
message.error('服务参数格式不正确请输入有效的JSON格式')
return
}
try {
await DeviceApi.sendDeviceMessage({
deviceId: props.device.id,
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
params: {
identifier: String(serviceItem.identifier),
inputParams: serviceParams
}
})
message.success(`服务【${String(serviceItem.name)}】调用成功`)
deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
} catch (error) {
message.error(`服务【${String(serviceItem.name)}】调用失败`)
}
}
</script>

@ -1,264 +0,0 @@
<!-- 子设备管理 -->
<template>
<ContentWrap>
<!-- 操作按钮 -->
<div class="mb-4">
<el-button type="primary" plain @click="openBindDialog" v-hasPermi="['iot:device:update']">
<Icon icon="ep:plus" class="mr-5px" /> 添加子设备
</el-button>
<el-button
type="danger"
plain
@click="handleUnbindBatch"
:disabled="selectedIds.length === 0"
v-hasPermi="['iot:device:update']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量解绑
</el-button>
</div>
<!-- 子设备列表 -->
<el-table
v-loading="loading"
:data="subDeviceList"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="DeviceName" align="center" prop="deviceName">
<template #default="{ row }">
<el-link type="primary" @click="openDeviceDetail(row.id)">{{ row.deviceName }}</el-link>
</template>
</el-table-column>
<el-table-column label="备注名称" align="center" prop="nickname" />
<el-table-column label="产品名称" align="center" prop="productName" />
<el-table-column label="设备状态" align="center" prop="state">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="row.state" />
</template>
</el-table-column>
<el-table-column
label="最后上线时间"
align="center"
prop="onlineTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" width="120px">
<template #default="{ row }">
<el-button link type="primary" @click="openDeviceDetail(row.id)"> </el-button>
<el-button
link
type="danger"
@click="handleUnbind(row.id)"
v-hasPermi="['iot:device:update']"
>
解绑
</el-button>
</template>
</el-table-column>
</el-table>
</ContentWrap>
<!-- 添加子设备弹窗 -->
<Dialog title="添加子设备" v-model="bindDialogVisible" width="900px">
<ContentWrap>
<!-- 搜索区域 -->
<el-form :model="bindQueryParams" ref="bindQueryFormRef" :inline="true" class="-mb-15px">
<el-form-item label="产品" prop="productId">
<ProductSelect
v-model="bindQueryParams.productId"
:device-type="DeviceTypeEnum.GATEWAY_SUB"
class="!w-200px"
/>
</el-form-item>
<el-form-item label="设备名称" prop="deviceName">
<el-input
v-model="bindQueryParams.deviceName"
placeholder="请输入设备名称"
clearable
class="!w-200px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getBindableDevicePage">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetBindQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<!-- 分页表格 -->
<el-table
ref="bindTableRef"
v-loading="bindFormLoading"
:data="bindableDevices"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleBindSelectionChange"
max-height="400px"
>
<el-table-column type="selection" width="55" />
<el-table-column label="DeviceName" align="center" prop="deviceName" />
<el-table-column label="备注名称" align="center" prop="nickname" />
<el-table-column label="产品名称" align="center" prop="productName" />
<el-table-column label="设备状态" align="center" prop="state">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="row.state" />
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<Pagination
v-model:page="bindQueryParams.pageNo"
v-model:limit="bindQueryParams.pageSize"
:total="bindTotal"
@pagination="getBindableDevicePage"
/>
</ContentWrap>
<template #footer>
<el-button type="primary" @click="handleBindSubmit" :loading="bindFormLoading">
确定已选 {{ bindSelectedIds.length }}
</el-button>
<el-button @click="bindDialogVisible = false">取消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { DeviceTypeEnum } from '@/api/iot/product/product'
import { DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import ProductSelect from '@/views/iot/product/product/components/ProductSelect.vue'
const props = defineProps<{
gatewayId: number
}>()
const message = useMessage()
const { push } = useRouter()
const loading = ref(false) //
const subDeviceList = ref<DeviceVO[]>([]) //
const selectedIds = ref<number[]>([]) // ID
const bindDialogVisible = ref(false) //
const bindFormLoading = ref(false) //
const bindTableRef = ref()
const bindQueryFormRef = ref()
const bindableDevices = ref<DeviceVO[]>([]) //
const bindSelectedIds = ref<number[]>([]) // ID
const bindTotal = ref(0) //
const bindQueryParams = reactive({
pageNo: 1,
pageSize: 10,
productId: undefined as number | undefined,
deviceName: ''
})
/** 获取子设备列表 */
const getSubDeviceList = async () => {
loading.value = true
try {
subDeviceList.value = await DeviceApi.getSubDeviceList(props.gatewayId)
} finally {
loading.value = false
}
}
/** 打开设备详情 */
const openDeviceDetail = (id: number) => {
push({ name: 'IoTDeviceDetail', params: { id } })
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: DeviceVO[]) => {
selectedIds.value = selection.map((item) => item.id)
}
/** 打开绑定弹窗 */
const openBindDialog = async () => {
bindSelectedIds.value = []
bindDialogVisible.value = true
await getBindableDevicePage()
}
/** 获取可绑定设备分页 */
const getBindableDevicePage = async () => {
bindFormLoading.value = true
try {
const result = await DeviceApi.getUnboundSubDevicePage(bindQueryParams)
bindableDevices.value = result.list
bindTotal.value = result.total
} finally {
bindFormLoading.value = false
}
}
/** 重置绑定弹窗搜索条件 */
const resetBindQuery = () => {
bindQueryParams.pageNo = 1
bindQueryParams.productId = undefined
bindQueryParams.deviceName = ''
getBindableDevicePage()
}
/** 绑定弹窗多选框选中数据 */
const handleBindSelectionChange = (selection: DeviceVO[]) => {
bindSelectedIds.value = selection.map((item) => item.id)
}
/** 提交绑定 */
const handleBindSubmit = async () => {
if (bindSelectedIds.value.length === 0) {
message.warning('请选择要绑定的子设备')
return
}
bindFormLoading.value = true
try {
await DeviceApi.bindDeviceGateway({
subIds: bindSelectedIds.value,
gatewayId: props.gatewayId
})
message.success('绑定成功')
bindDialogVisible.value = false
await getSubDeviceList()
} finally {
bindFormLoading.value = false
}
}
/** 解绑单个设备 */
const handleUnbind = async (id: number) => {
try {
await message.confirm('确定要解绑该子设备吗?')
await DeviceApi.unbindDeviceGateway({ subIds: [id], gatewayId: props.gatewayId })
message.success('解绑成功')
await getSubDeviceList()
} catch {}
}
/** 批量解绑 */
const handleUnbindBatch = async () => {
try {
await message.confirm(`确定要解绑选中的 ${selectedIds.value.length} 个子设备吗?`)
await DeviceApi.unbindDeviceGateway({ subIds: selectedIds.value, gatewayId: props.gatewayId })
message.success('批量解绑成功')
selectedIds.value = []
await getSubDeviceList()
} catch {}
}
/** 初始化 */
onMounted(async () => {
await getSubDeviceList()
})
</script>

@ -1,35 +0,0 @@
<!-- 设备物模型设备属性事件管理服务调用 -->
<template>
<ContentWrap>
<el-tabs v-model="activeTab">
<el-tab-pane label="设备属性(运行状态)" name="property">
<DeviceDetailsThingModelProperty :device-id="deviceId" />
</el-tab-pane>
<el-tab-pane label="设备事件上报" name="event">
<DeviceDetailsThingModelEvent
:device-id="props.deviceId"
:thing-model-list="props.thingModelList"
/>
</el-tab-pane>
<el-tab-pane label="设备服务调用" name="service">
<DeviceDetailsThingModelService
:device-id="deviceId"
:thing-model-list="props.thingModelList"
/>
</el-tab-pane>
</el-tabs>
</ContentWrap>
</template>
<script setup lang="ts">
import { ThingModelData } from '@/api/iot/thingmodel'
import DeviceDetailsThingModelProperty from './DeviceDetailsThingModelProperty.vue'
import DeviceDetailsThingModelEvent from './DeviceDetailsThingModelEvent.vue'
import DeviceDetailsThingModelService from './DeviceDetailsThingModelService.vue'
const props = defineProps<{
deviceId: number
thingModelList: ThingModelData[]
}>()
const activeTab = ref('property') //
</script>

@ -1,192 +0,0 @@
<!-- 设备事件管理 -->
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="80px"
@submit.prevent
>
<el-form-item label="标识符" prop="identifier">
<el-select
v-model="queryParams.identifier"
placeholder="请选择事件标识符"
clearable
class="!w-240px"
>
<el-option
v-for="event in eventThingModels"
:key="event.identifier"
:label="`${event.name}(${event.identifier})`"
:value="event.identifier!"
/>
</el-select>
</el-form-item>
<el-form-item label="时间范围" prop="times">
<el-date-picker
v-model="queryParams.times"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
class="!w-360px"
:shortcuts="defaultShortcuts"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<!-- 事件列表 -->
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="上报时间" align="center" prop="reportTime" width="180px">
<template #default="scope">
{{ scope.row.request?.reportTime ? formatDate(scope.row.request.reportTime) : '-' }}
</template>
</el-table-column>
<el-table-column label="标识符" align="center" prop="identifier" width="160px">
<template #default="scope">
<el-tag type="primary" size="small">
{{ scope.row.request?.identifier }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="事件名称" align="center" prop="eventName" width="160px">
<template #default="scope">
{{ getEventName(scope.row.request?.identifier) }}
</template>
</el-table-column>
<el-table-column label="事件类型" align="center" prop="eventType" width="100px">
<template #default="scope">
{{ getEventType(scope.row.request?.identifier) }}
</template>
</el-table-column>
<el-table-column label="输入参数" align="center" prop="params">
<template #default="scope"> {{ parseParams(scope.row.request.params) }} </template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
</template>
<script setup lang="ts">
import { DeviceApi } from '@/api/iot/device/device'
import { ThingModelData } from '@/api/iot/thingmodel'
import { formatDate, defaultShortcuts } from '@/utils/formatTime'
import {
getEventTypeLabel,
IotDeviceMessageMethodEnum,
IoTThingModelTypeEnum
} from '@/views/iot/utils/constants'
const props = defineProps<{
deviceId: number
thingModelList: ThingModelData[]
}>()
const loading = ref(false) //
const total = ref(0) //
const list = ref([] as any[]) //
const queryParams = reactive({
deviceId: props.deviceId,
method: IotDeviceMessageMethodEnum.EVENT_POST.method, //
identifier: '',
times: [] as any[],
pageNo: 1,
pageSize: 10
})
const queryFormRef = ref() //
/** 事件类型的物模型数据 */
const eventThingModels = computed(() => {
return props.thingModelList.filter(
(item: ThingModelData) => item.type === IoTThingModelTypeEnum.EVENT
)
})
/** 查询列表 */
const getList = async () => {
if (!props.deviceId) return
loading.value = true
try {
const data = await DeviceApi.getDeviceMessagePairPage(queryParams)
list.value = data.list
total.value = data.length
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields()
queryParams.identifier = ''
queryParams.times = []
handleQuery()
}
/** 获取事件名称 */
const getEventName = (identifier: string | undefined) => {
if (!identifier) return '-'
const event = eventThingModels.value.find(
(item: ThingModelData) => item.identifier === identifier
)
return event?.name || identifier
}
/** 获取事件类型 */
const getEventType = (identifier: string | undefined) => {
if (!identifier) return '-'
const event = eventThingModels.value.find(
(item: ThingModelData) => item.identifier === identifier
)
if (!event?.event?.type) return '-'
return getEventTypeLabel(event.event.type) || '-'
}
/** 解析参数 */
const parseParams = (params: string) => {
try {
const parsed = JSON.parse(params)
if (parsed.params) {
return parsed.params
}
return parsed
} catch (error) {
return {}
}
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>

@ -1,245 +0,0 @@
<!-- 设备属性管理 -->
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
@submit.prevent
>
<el-form-item label="" prop="keyword">
<el-input
v-model="queryParams.keyword"
placeholder="请输入属性名称、标志符"
clearable
class="!w-240px"
@keyup.enter="handleQuery"
@clear="handleQuery"
/>
</el-form-item>
<el-form-item class="float-right !mr-0 !mb-0">
<el-button-group>
<el-button :type="viewMode === 'card' ? 'primary' : 'default'" @click="viewMode = 'card'">
<Icon icon="ep:grid" />
</el-button>
<el-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
<Icon icon="ep:list" />
</el-button>
</el-button-group>
</el-form-item>
<!-- TODO @芋艿参考阿里云实时刷新 -->
<el-form-item>
<el-switch
size="large"
width="80"
v-model="autoRefresh"
class="-ml-15px"
inline-prompt
active-text="定时刷新"
inactive-text="定时刷新"
style="--el-switch-on-color: #13ce66"
/>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<!-- 卡片视图 -->
<template v-if="viewMode === 'card'">
<el-row :gutter="16" v-loading="loading">
<el-col
v-for="item in list"
:key="item.identifier"
:xs="24"
:sm="12"
:md="12"
:lg="6"
class="mb-4"
>
<el-card
class="h-full transition-colors relative overflow-hidden"
:body-style="{ padding: '0' }"
>
<!-- 添加渐变背景层 -->
<div
class="absolute top-0 left-0 right-0 h-[50px] pointer-events-none bg-gradient-to-b from-[#eefaff] to-transparent"
>
</div>
<div class="p-4 relative">
<!-- 标题区域 -->
<div class="flex items-center mb-3">
<div class="mr-2.5 flex items-center">
<Icon icon="ep:cpu" class="text-[18px] text-[#0070ff]" />
</div>
<div class="text-[16px] font-600 flex-1">{{ item.name }}</div>
<!-- 标识符 -->
<div class="inline-flex items-center mr-2">
<el-tag size="small" type="primary">
{{ item.identifier }}
</el-tag>
</div>
<!-- 数据类型标签 -->
<div class="inline-flex items-center mr-2">
<el-tag size="small" type="info">
{{ item.dataType }}
</el-tag>
</div>
<!-- 数据图标 - 可点击 -->
<div
class="cursor-pointer flex items-center justify-center w-8 h-8 rounded-full hover:bg-blue-50 transition-colors"
@click="openHistory(props.deviceId, item.identifier, item.dataType)"
>
<Icon icon="ep:data-line" class="text-[18px] text-[#0070ff]" />
</div>
</div>
<!-- 信息区域 -->
<div class="text-[14px]">
<div class="mb-2.5 last:mb-0">
<span class="text-[#717c8e] mr-2.5">属性值</span>
<span class="text-[var(--el-text-color-primary)] font-600">
{{ formatValueWithUnit(item) }}
</span>
</div>
<div class="mb-2.5 last:mb-0">
<span class="text-[#717c8e] mr-2.5">更新时间</span>
<span class="text-[var(--el-text-color-primary)] text-[12px]">
{{ item.updateTime ? formatDate(item.updateTime) : '-' }}
</span>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</template>
<!-- 列表视图 -->
<el-table v-else v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="属性标识符" align="center" prop="identifier" />
<el-table-column label="属性名称" align="center" prop="name" />
<el-table-column label="数据类型" align="center" prop="dataType" />
<el-table-column label="属性值" align="center" prop="value">
<template #default="scope">
{{ formatValueWithUnit(scope.row) }}
</template>
</el-table-column>
<el-table-column
label="更新时间"
align="center"
prop="updateTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openHistory(props.deviceId, scope.row.identifier, scope.row.dataType)"
>
查看数据
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 表单弹窗添加/修改 -->
<DeviceDetailsThingModelPropertyHistory ref="historyRef" :deviceId="props.deviceId" />
</ContentWrap>
</template>
<script setup lang="ts">
import { DeviceApi, IotDevicePropertyDetailRespVO } from '@/api/iot/device/device'
import { dateFormatter, formatDate } from '@/utils/formatTime'
import DeviceDetailsThingModelPropertyHistory from './DeviceDetailsThingModelPropertyHistory.vue'
const props = defineProps<{ deviceId: number }>()
const loading = ref(true) //
const list = ref<IotDevicePropertyDetailRespVO[]>([]) //
const filterList = ref<IotDevicePropertyDetailRespVO[]>([]) //
const queryParams = reactive({
keyword: '' as string
})
const autoRefresh = ref(false) //
let autoRefreshTimer: any = null //
const viewMode = ref<'card' | 'list'>('card') //
const queryFormRef = ref() //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const params = {
deviceId: props.deviceId,
identifier: undefined as string | undefined,
name: undefined as string | undefined
}
filterList.value = await DeviceApi.getLatestDeviceProperties(params)
handleFilter()
} finally {
loading.value = false
}
}
/** 前端筛选数据 */
const handleFilter = () => {
if (!queryParams.keyword.trim()) {
list.value = filterList.value
} else {
const keyword = queryParams.keyword.toLowerCase()
list.value = filterList.value.filter(
(item) =>
item.identifier?.toLowerCase().includes(keyword) ||
item.name?.toLowerCase().includes(keyword)
)
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
handleFilter()
}
/** 历史操作 */
const historyRef = ref()
const openHistory = (deviceId: number, identifier: string, dataType: string) => {
historyRef.value.open(deviceId, identifier, dataType)
}
/** 格式化属性值和单位 */
const formatValueWithUnit = (item: IotDevicePropertyDetailRespVO) => {
if (item.value === null || item.value === undefined || item.value === '') {
return '-'
}
const unitName = item.dataSpecs?.unitName
return unitName ? `${item.value} ${unitName}` : item.value
}
/** 监听自动刷新 */
watch(autoRefresh, (newValue) => {
if (newValue) {
autoRefreshTimer = setInterval(() => {
getList()
}, 5000) // 5
} else {
clearInterval(autoRefreshTimer)
autoRefreshTimer = null
}
})
/** 组件卸载时清除定时器 */
onBeforeUnmount(() => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer)
}
})
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

@ -1,216 +0,0 @@
<!-- 设备物模型 -> 运行状态 -> 查看数据设备的属性值历史-->
<template>
<Dialog title="查看数据" v-model="dialogVisible" width="1024px" :appendToBody="true">
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="" prop="createTime">
<el-date-picker
v-model="queryParams.times"
value-format="YYYY-MM-DD HH:mm:ss"
type="datetimerange"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-360px"
@change="handleTimeChange"
:shortcuts="defaultShortcuts"
/>
</el-form-item>
<el-form-item class="float-right !mr-0 !mb-0">
<el-button-group>
<el-button
:type="viewMode === 'chart' ? 'primary' : 'default'"
@click="viewMode = 'chart'"
:disabled="isComplexDataType"
>
<Icon icon="ep:histogram" />
</el-button>
<el-button
:type="viewMode === 'list' ? 'primary' : 'default'"
@click="viewMode = 'list'"
>
<Icon icon="ep:list" />
</el-button>
</el-button-group>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 数据展示区域 -->
<ContentWrap>
<!-- 图表模式 -->
<div v-if="viewMode === 'chart'" class="chart-container">
<div v-if="list.length === 0" class="text-center text-gray-500 py-20"> </div>
<Echart v-else :key="'erchart' + Date.now()" :options="echartsOption" height="400px" />
</div>
<!-- 表格模式 -->
<div v-else>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="时间" align="center" prop="time" width="180px">
<template #default="scope">
{{ formatDate(new Date(scope.row.updateTime)) }}
</template>
</el-table-column>
<el-table-column label="属性值" align="center" prop="value">
<template #default="scope">
{{ scope.row.value }}
</template>
</el-table-column>
</el-table>
</div>
</ContentWrap>
</Dialog>
</template>
<script setup lang="ts">
import { DeviceApi, IotDevicePropertyRespVO } from '@/api/iot/device/device'
import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
import { Echart } from '@/components/Echart'
import { IoTDataSpecsDataTypeEnum } from '@/views/iot/utils/constants'
defineProps<{ deviceId: number }>()
/** IoT 设备属性历史数据详情 */
defineOptions({ name: 'DeviceDetailsThingModelPropertyHistory' })
const dialogVisible = ref(false) //
const loading = ref(false)
const viewMode = ref<'chart' | 'list'>('chart') //
const list = ref<IotDevicePropertyRespVO[]>([]) //
const chartKey = ref(0) // key
const thingModelDataType = ref<string>('') //
const queryParams = reactive({
deviceId: -1,
identifier: '',
times: [
//
formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
formatDate(endOfDay(new Date()))
]
})
const queryFormRef = ref() //
// struct array
const isComplexDataType = computed(() => {
if (!thingModelDataType.value) return false
return [IoTDataSpecsDataTypeEnum.STRUCT, IoTDataSpecsDataTypeEnum.ARRAY].includes(
thingModelDataType.value as any
)
})
// Echarts
const echartsData = computed(() => {
if (!list.value || list.value.length === 0) return []
return list.value.map((item) => [item.updateTime, item.value])
})
// Echarts
const echartsOption = reactive<any>({
title: {
text: '设备属性值',
left: 'center'
},
grid: {
left: 60,
right: 40,
bottom: 80,
top: 80,
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
xAxis: {
type: 'time',
name: '时间',
axisLabel: {
formatter: (value: number) => formatDate(new Date(value), 'MM-DD HH:mm')
}
},
yAxis: {
type: 'value',
name: '属性值'
},
series: [
{
name: '属性值',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
width: 2,
color: '#1890FF'
},
itemStyle: {
color: '#1890FF'
},
data: []
}
],
dataZoom: [
{
type: 'inside'
},
{
type: 'slider',
height: 30
}
]
})
/** 获得设备历史数据 */
const getList = async () => {
loading.value = true
try {
const data = await DeviceApi.getHistoryDevicePropertyList(queryParams)
list.value = data || []
updateChartData()
} finally {
loading.value = false
}
}
/** 打开弹窗 */
const open = async (deviceId: number, identifier: string, dataType: string) => {
dialogVisible.value = true
queryParams.deviceId = deviceId
queryParams.identifier = identifier
thingModelDataType.value = dataType
// structarray使 list
if (isComplexDataType.value) {
viewMode.value = 'list'
} else {
viewMode.value = 'chart'
}
// key
chartKey.value = 0
//
await nextTick()
await getList()
}
/** 时间变化处理 */
const handleTimeChange = () => {
getList()
}
/** 更新图表数据 */
const updateChartData = () => {
if (echartsOption.series && echartsOption.series[0]) {
echartsOption.series[0].data = echartsData.value
}
}
defineExpose({ open }) // open
</script>

@ -1,208 +0,0 @@
<!-- 设备服务调用 -->
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="80px"
@submit.prevent
>
<el-form-item label="标识符" prop="identifier">
<el-select
v-model="queryParams.identifier"
placeholder="请选择服务标识符"
clearable
class="!w-240px"
>
<el-option
v-for="service in serviceThingModels"
:key="service.identifier"
:label="`${service.name}(${service.identifier})`"
:value="service.identifier!"
/>
</el-select>
</el-form-item>
<el-form-item label="时间范围" prop="times">
<el-date-picker
v-model="queryParams.times"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
class="!w-360px"
:shortcuts="defaultShortcuts"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<!-- 服务调用列表 -->
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="调用时间" align="center" prop="requestTime" width="180px">
<template #default="scope">
{{ scope.row.request?.reportTime ? formatDate(scope.row.request.reportTime) : '-' }}
</template>
</el-table-column>
<el-table-column label="响应时间" align="center" prop="responseTime" width="180px">
<template #default="scope">
{{ scope.row.reply?.reportTime ? formatDate(scope.row.reply.reportTime) : '-' }}
</template>
</el-table-column>
<el-table-column label="标识符" align="center" prop="identifier" width="160px">
<template #default="scope">
<el-tag type="primary" size="small">
{{ scope.row.request?.identifier }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="服务名称" align="center" prop="serviceName" width="160px">
<template #default="scope">
{{ getServiceName(scope.row.request?.identifier) }}
</template>
</el-table-column>
<el-table-column label="调用方式" align="center" prop="callType" width="100px">
<template #default="scope">
{{ getCallType(scope.row.request?.identifier) }}
</template>
</el-table-column>
<el-table-column label="输入参数" align="center" prop="inputParams">
<template #default="scope"> {{ parseParams(scope.row.request?.params) }} </template>
</el-table-column>
<el-table-column label="输出参数" align="center" prop="outputParams">
<template #default="scope">
<span v-if="scope.row.reply">
{{
`{"code":${scope.row.reply.code},"msg":"${scope.row.reply.msg}","data":${scope.row.reply.data}\}`
}}
</span>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
</template>
<script setup lang="ts">
import { DeviceApi } from '@/api/iot/device/device'
import { ThingModelData } from '@/api/iot/thingmodel'
import { formatDate, defaultShortcuts } from '@/utils/formatTime'
import {
getThingModelServiceCallTypeLabel,
IotDeviceMessageMethodEnum,
IoTThingModelTypeEnum
} from '@/views/iot/utils/constants'
const props = defineProps<{
deviceId: number
thingModelList: ThingModelData[]
}>()
const loading = ref(false) //
const total = ref(0) //
const list = ref([] as any[]) //
const queryParams = reactive({
deviceId: props.deviceId,
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method, //
identifier: '',
times: [] as any[],
pageNo: 1,
pageSize: 10
})
const queryFormRef = ref() //
/** 服务类型的物模型数据 */
const serviceThingModels = computed(() => {
return props.thingModelList.filter(
(item: ThingModelData) => item.type === IoTThingModelTypeEnum.SERVICE
)
})
/** 查询列表 */
const getList = async () => {
if (!props.deviceId) return
loading.value = true
try {
const data = await DeviceApi.getDeviceMessagePairPage(queryParams)
list.value = data.list
total.value = data.length
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields()
queryParams.identifier = ''
queryParams.times = []
handleQuery()
}
/** 获取服务名称 */
const getServiceName = (identifier: string | undefined) => {
if (!identifier) return '-'
const service = serviceThingModels.value.find(
(item: ThingModelData) => item.identifier === identifier
)
return service?.name || identifier
}
/** 获取调用方式 */
const getCallType = (identifier: string | undefined) => {
if (!identifier) return '-'
const service = serviceThingModels.value.find(
(item: ThingModelData) => item.identifier === identifier
)
if (!service?.service?.callType) return '-'
return getThingModelServiceCallTypeLabel(service.service.callType) || '-'
}
/** 解析参数 */
const parseParams = (params: string) => {
if (!params) return '-'
try {
const parsed = JSON.parse(params)
if (parsed.params) {
return JSON.stringify(parsed.params, null, 2)
}
return JSON.stringify(parsed, null, 2)
} catch (error) {
return params
}
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>

@ -1,292 +0,0 @@
<!-- Modbus 配置 -->
<template>
<div>
<!-- 连接配置区域 -->
<ContentWrap>
<div class="flex items-center justify-between mb-4">
<span class="text-lg font-medium">连接配置</span>
<el-button type="primary" @click="handleEditConfig" v-hasPermi="['iot:device:create']">
编辑
</el-button>
</div>
<!-- 详情展示 -->
<el-descriptions :column="3" border direction="horizontal">
<!-- Client 模式专有字段 -->
<template v-if="isClient">
<el-descriptions-item label="IP 地址">
{{ modbusConfig.ip || '-' }}
</el-descriptions-item>
<el-descriptions-item label="端口">
{{ modbusConfig.port || '-' }}
</el-descriptions-item>
</template>
<!-- 公共字段 -->
<el-descriptions-item label="从站地址">
{{ modbusConfig.slaveId || '-' }}
</el-descriptions-item>
<!-- Client 模式专有字段 -->
<template v-if="isClient">
<el-descriptions-item label="连接超时">
{{ modbusConfig.timeout ? `${modbusConfig.timeout} ms` : '-' }}
</el-descriptions-item>
<el-descriptions-item label="重试间隔">
{{ modbusConfig.retryInterval ? `${modbusConfig.retryInterval} ms` : '-' }}
</el-descriptions-item>
</template>
<!-- Server 模式专有字段 -->
<template v-if="isServer">
<el-descriptions-item label="工作模式">
<dict-tag :type="DICT_TYPE.IOT_MODBUS_MODE" :value="modbusConfig.mode" />
</el-descriptions-item>
<el-descriptions-item label="帧格式">
<dict-tag :type="DICT_TYPE.IOT_MODBUS_FRAME_FORMAT" :value="modbusConfig.frameFormat" />
</el-descriptions-item>
</template>
<!-- 公共字段 -->
<el-descriptions-item label="状态">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="modbusConfig.status" />
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
<!-- 点位配置区域 -->
<ContentWrap class="mt-4">
<div class="flex items-center justify-between mb-4">
<span class="text-lg font-medium">点位配置</span>
<el-button type="primary" @click="handleAddPoint" v-hasPermi="['iot:device:create']">
<Icon icon="ep:plus" class="mr-1" />
新增点位
</el-button>
</div>
<!-- 搜索栏 -->
<el-form :model="queryParams" :inline="true" class="-mb-15px">
<el-form-item label="属性名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入属性名称"
clearable
class="!w-200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="标识符" prop="identifier">
<el-input
v-model="queryParams.identifier"
placeholder="请输入标识符"
clearable
class="!w-200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
重置
</el-button>
</el-form-item>
</el-form>
<!-- 点位列表 -->
<el-table v-loading="pointLoading" :data="pointList" :stripe="true" class="mt-4">
<el-table-column label="属性名称" align="center" prop="name" min-width="100" />
<el-table-column label="标识符" align="center" prop="identifier" min-width="100">
<template #default="scope">
<el-tag size="small" type="primary">{{ scope.row.identifier }}</el-tag>
</template>
</el-table-column>
<el-table-column label="功能码" align="center" prop="functionCode" min-width="140">
<template #default="scope">
{{ formatFunctionCode(scope.row.functionCode) }}
</template>
</el-table-column>
<el-table-column label="寄存器地址" align="center" prop="registerAddress" min-width="100">
<template #default="scope">
{{ formatRegisterAddress(scope.row.registerAddress) }}
</template>
</el-table-column>
<el-table-column label="寄存器数量" align="center" prop="registerCount" min-width="90" />
<el-table-column label="数据类型" align="center" prop="rawDataType" min-width="90">
<template #default="scope">
<el-tag size="small" type="info">{{ scope.row.rawDataType }}</el-tag>
</template>
</el-table-column>
<el-table-column label="字节序" align="center" prop="byteOrder" min-width="80" />
<el-table-column label="缩放因子" align="center" prop="scale" min-width="80" />
<el-table-column label="轮询间隔" align="center" prop="pollInterval" min-width="90">
<template #default="scope"> {{ scope.row.pollInterval }} ms </template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" min-width="80">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="120">
<template #default="scope">
<el-button
link
type="primary"
@click="handleEditPoint(scope.row)"
v-hasPermi="['iot:device:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDeletePoint(scope.row.id, scope.row.name)"
v-hasPermi="['iot:device:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getPointPage"
/>
</ContentWrap>
<!-- 连接配置弹窗 -->
<DeviceModbusConfigForm
ref="configFormRef"
:device-id="device.id"
:protocol-type="product.protocolType"
@success="getModbusConfig"
/>
<!-- 点位表单弹窗 -->
<DeviceModbusPointForm
ref="pointFormRef"
:device-id="device.id"
:thing-model-list="thingModelList"
@success="getPointPage"
/>
</div>
</template>
<script lang="ts" setup>
import { DeviceVO } from '@/api/iot/device/device'
import { ProductVO, ProtocolTypeEnum } from '@/api/iot/product/product'
import { ThingModelData } from '@/api/iot/thingmodel'
import { DeviceModbusConfigApi, DeviceModbusConfigVO } from '@/api/iot/device/modbus/config'
import { DeviceModbusPointApi, DeviceModbusPointVO } from '@/api/iot/device/modbus/point'
import { ModbusFunctionCodeOptions } from '@/views/iot/utils/constants'
import { DICT_TYPE } from '@/utils/dict'
import DeviceModbusConfigForm from './DeviceModbusConfigForm.vue'
import DeviceModbusPointForm from './DeviceModbusPointForm.vue'
defineOptions({ name: 'DeviceModbusConfig' })
const props = defineProps<{
device: DeviceVO
product: ProductVO
thingModelList: ThingModelData[]
}>()
const message = useMessage()
// ======================= =======================
const isClient = computed(() => props.product.protocolType === ProtocolTypeEnum.MODBUS_TCP_CLIENT) // Client
const isServer = computed(() => props.product.protocolType === ProtocolTypeEnum.MODBUS_TCP_SERVER) // Server
const modbusConfig = ref<DeviceModbusConfigVO>({} as DeviceModbusConfigVO)
/** 获取连接配置 */
const getModbusConfig = async () => {
modbusConfig.value = await DeviceModbusConfigApi.getModbusConfig(props.device.id)
}
/** 编辑连接配置 */
const configFormRef = ref()
const handleEditConfig = () => {
configFormRef.value?.open(modbusConfig.value)
}
// ======================= =======================
const pointLoading = ref(false)
const pointList = ref<DeviceModbusPointVO[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
deviceId: props.device.id,
name: undefined as string | undefined,
identifier: undefined as string | undefined
})
/** 获取点位分页 */
const getPointPage = async () => {
pointLoading.value = true
try {
const data = await DeviceModbusPointApi.getModbusPointPage(queryParams)
pointList.value = data.list
total.value = data.total
} finally {
pointLoading.value = false
}
}
/** 搜索 */
const handleQuery = () => {
queryParams.pageNo = 1
getPointPage()
}
/** 重置搜索 */
const resetQuery = () => {
queryParams.name = undefined
queryParams.identifier = undefined
handleQuery()
}
/** 格式化功能码 */
const formatFunctionCode = (code: number) => {
const option = ModbusFunctionCodeOptions.find((item) => item.value === code)
return option ? option.label : `${code}`
}
/** 格式化寄存器地址为十六进制 */
const formatRegisterAddress = (address: number) => {
return '0x' + address.toString(16).toUpperCase().padStart(4, '0')
}
/** 新增点位 */
const pointFormRef = ref()
const handleAddPoint = () => {
pointFormRef.value?.open('create')
}
/** 编辑点位 */
const handleEditPoint = (row: DeviceModbusPointVO) => {
pointFormRef.value?.open('update', row.id)
}
/** 删除点位 */
const handleDeletePoint = async (id: number, name: string) => {
try {
//
await message.delConfirm('确定要删除点位【' + name + '】吗?')
//
await DeviceModbusPointApi.deleteModbusPoint(id)
message.success('删除成功')
//
await getPointPage()
} catch {}
}
/** 初始化 */
onMounted(async () => {
await getModbusConfig()
await getPointPage()
})
</script>

@ -1,205 +0,0 @@
<!-- Modbus 连接配置弹窗 -->
<template>
<Dialog title="编辑 Modbus 连接配置" v-model="dialogVisible" width="600px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<!-- Client 模式专有字段IP端口超时重试 -->
<template v-if="isClient">
<el-form-item label="IP 地址" prop="ip">
<el-input v-model="formData.ip" placeholder="请输入 Modbus 服务器 IP 地址" />
</el-form-item>
<el-form-item label="端口" prop="port">
<el-input-number
v-model="formData.port"
placeholder="请输入端口"
:min="1"
:max="65535"
controls-position="right"
class="!w-full"
/>
</el-form-item>
</template>
<!-- 公共字段从站地址 -->
<el-form-item label="从站地址" prop="slaveId">
<el-input-number
v-model="formData.slaveId"
:min="1"
:max="247"
controls-position="right"
placeholder="请输入从站地址,范围 1-247"
class="!w-full"
/>
</el-form-item>
<!-- Client 模式专有字段超时重试 -->
<template v-if="isClient">
<el-form-item label="连接超时(ms)" prop="timeout">
<el-input-number
v-model="formData.timeout"
:min="1000"
:step="1000"
controls-position="right"
placeholder="请输入连接超时时间"
class="!w-full"
/>
</el-form-item>
<el-form-item label="重试间隔(ms)" prop="retryInterval">
<el-input-number
v-model="formData.retryInterval"
:min="1000"
:step="1000"
controls-position="right"
placeholder="请输入重试间隔"
class="!w-full"
/>
</el-form-item>
</template>
<!-- Server 模式专有字段模式帧格式 -->
<template v-if="isServer">
<el-form-item label="工作模式" prop="mode">
<el-radio-group v-model="formData.mode">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_MODBUS_MODE)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="帧格式" prop="frameFormat">
<el-radio-group v-model="formData.frameFormat">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_MODBUS_FRAME_FORMAT)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</template>
<!-- 公共字段状态 -->
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="formLoading">确定</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { DeviceModbusConfigApi, DeviceModbusConfigVO } from '@/api/iot/device/modbus/config'
import { ProtocolTypeEnum } from '@/api/iot/product/product'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
import { ModbusModeEnum, ModbusFrameFormatEnum } from '@/views/iot/utils/constants'
defineOptions({ name: 'DeviceModbusConfigForm' })
const props = defineProps<{
deviceId: number
protocolType: string
}>()
const emit = defineEmits<{
(e: 'success'): void
}>()
const message = useMessage()
const dialogVisible = ref(false) //
const formLoading = ref(false) // loading
const isClient = computed(() => props.protocolType === ProtocolTypeEnum.MODBUS_TCP_CLIENT) // Client
const isServer = computed(() => props.protocolType === ProtocolTypeEnum.MODBUS_TCP_SERVER) // Server
const formData = ref<DeviceModbusConfigVO>({
deviceId: props.deviceId,
ip: '',
port: 502,
slaveId: 1,
timeout: 3000,
retryInterval: 10000,
mode: ModbusModeEnum.POLLING,
frameFormat: ModbusFrameFormatEnum.MODBUS_TCP,
status: CommonStatusEnum.ENABLE
})
const formRules = computed(() => {
const rules: Record<string, any[]> = {
slaveId: [{ required: true, message: '请输入从站地址', trigger: 'blur' }]
}
if (isClient.value) {
rules.ip = [{ required: true, message: '请输入 IP 地址', trigger: 'blur' }]
rules.port = [{ required: true, message: '请输入端口', trigger: 'blur' }]
rules.timeout = [{ required: true, message: '请输入连接超时时间', trigger: 'blur' }]
rules.retryInterval = [{ required: true, message: '请输入重试间隔', trigger: 'blur' }]
}
if (isServer.value) {
rules.mode = [{ required: true, message: '请选择工作模式', trigger: 'change' }]
rules.frameFormat = [{ required: true, message: '请选择帧格式', trigger: 'change' }]
}
return rules
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (data?: DeviceModbusConfigVO) => {
dialogVisible.value = true
resetForm()
//
if (data && data.id) {
formData.value = { ...data }
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
deviceId: props.deviceId,
ip: '',
port: 502,
slaveId: 1,
timeout: 3000,
retryInterval: 10000,
mode: ModbusModeEnum.POLLING,
frameFormat: ModbusFrameFormatEnum.MODBUS_TCP,
status: CommonStatusEnum.ENABLE
}
formRef.value?.resetFields()
}
/** 提交表单 */
const submitForm = async () => {
//
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
formData.value.deviceId = props.deviceId
await DeviceModbusConfigApi.saveModbusConfig(formData.value)
message.success('保存成功')
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
/** 暴露方法 */
defineExpose({ open })
</script>

@ -1,286 +0,0 @@
<!-- Modbus 点位表单弹窗 -->
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-form-item label="物模型属性" prop="thingModelId">
<el-select
v-model="formData.thingModelId"
placeholder="请选择物模型属性"
filterable
class="!w-full"
@change="handleThingModelChange"
>
<el-option
v-for="item in propertyList"
:key="item.id!"
:label="`${item.name} (${item.identifier})`"
:value="item.id!"
/>
</el-select>
</el-form-item>
<el-form-item label="功能码" prop="functionCode">
<el-select v-model="formData.functionCode" placeholder="请选择功能码" class="!w-full">
<el-option
v-for="item in ModbusFunctionCodeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="寄存器地址" prop="registerAddress">
<el-input
v-model.number="formData.registerAddress"
type="number"
:min="0"
:max="65535"
placeholder="请输入寄存器地址"
class="!w-full"
>
<template #suffix>
<span class="text-gray-400">{{ registerAddressHex }}</span>
</template>
</el-input>
</el-form-item>
<el-form-item label="寄存器数量" prop="registerCount">
<el-input-number
v-model="formData.registerCount"
:min="1"
:max="125"
controls-position="right"
placeholder="请输入寄存器数量"
class="!w-full"
/>
</el-form-item>
<el-form-item label="原始数据类型" prop="rawDataType">
<el-select
v-model="formData.rawDataType"
placeholder="请选择数据类型"
class="!w-full"
@change="handleRawDataTypeChange"
>
<el-option
v-for="item in ModbusRawDataTypeOptions"
:key="item.value"
:label="`${item.label} - ${item.description}`"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="字节序" prop="byteOrder">
<el-select v-model="formData.byteOrder" placeholder="请选择字节序" class="!w-full">
<el-option
v-for="item in currentByteOrderOptions"
:key="item.value"
:label="`${item.label} - ${item.description}`"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="缩放因子" prop="scale">
<el-input-number
v-model="formData.scale"
:precision="6"
:step="0.1"
controls-position="right"
placeholder="请输入缩放因子"
class="!w-full"
/>
</el-form-item>
<el-form-item label="轮询间隔(ms)" prop="pollInterval">
<el-input-number
v-model="formData.pollInterval"
:min="100"
:step="1000"
controls-position="right"
placeholder="请输入轮询间隔"
class="!w-full"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:label="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="formLoading">确定</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import { ThingModelData } from '@/api/iot/thingmodel'
import { DeviceModbusPointApi, DeviceModbusPointVO } from '@/api/iot/device/modbus/point'
import {
ModbusFunctionCodeOptions,
ModbusRawDataTypeOptions,
getByteOrderOptions,
IoTThingModelTypeEnum
} from '@/views/iot/utils/constants'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { CommonStatusEnum } from '@/utils/constants'
defineOptions({ name: 'DeviceModbusPointForm' })
const props = defineProps<{
deviceId: number
thingModelList: ThingModelData[]
}>()
const emit = defineEmits<{
(e: 'success'): void
}>()
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref<DeviceModbusPointVO>({
deviceId: props.deviceId,
thingModelId: undefined,
identifier: '',
name: '',
functionCode: undefined,
registerAddress: undefined,
registerCount: undefined,
byteOrder: undefined,
rawDataType: undefined,
scale: 1,
pollInterval: 5000,
status: CommonStatusEnum.ENABLE
})
const formRules = {
thingModelId: [{ required: true, message: '请选择物模型属性', trigger: 'change' }],
functionCode: [{ required: true, message: '请选择功能码', trigger: 'change' }],
registerAddress: [{ required: true, message: '请输入寄存器地址', trigger: 'blur' }],
registerCount: [{ required: true, message: '请输入寄存器数量', trigger: 'blur' }],
rawDataType: [{ required: true, message: '请选择数据类型', trigger: 'change' }],
pollInterval: [{ required: true, message: '请输入轮询间隔', trigger: 'blur' }]
}
const formRef = ref() // Ref
/** 寄存器地址十六进制显示 */
const registerAddressHex = computed(() => {
if (formData.value.registerAddress === undefined || formData.value.registerAddress === null) {
return ''
}
return '0x' + formData.value.registerAddress.toString(16).toUpperCase().padStart(4, '0')
})
/** 筛选属性类型的物模型 */
const propertyList = computed(() => {
return props.thingModelList.filter((item) => item.type === IoTThingModelTypeEnum.PROPERTY)
})
/** 当前字节序选项(根据数据类型动态变化) */
const currentByteOrderOptions = computed(() => {
if (!formData.value.rawDataType) {
return []
}
return getByteOrderOptions(formData.value.rawDataType)
})
/** 物模型属性变化 */
const handleThingModelChange = (thingModelId: number) => {
const thingModel = props.thingModelList.find((item) => item.id === thingModelId)
if (thingModel) {
formData.value.identifier = thingModel.identifier!
formData.value.name = thingModel.name!
}
}
/** 数据类型变化 */
const handleRawDataTypeChange = (rawDataType: string) => {
//
const option = ModbusRawDataTypeOptions.find((item) => item.value === rawDataType)
if (option && option.registerCount > 0) {
formData.value.registerCount = option.registerCount
}
//
const byteOrderOptions = getByteOrderOptions(rawDataType)
if (byteOrderOptions.length > 0) {
formData.value.byteOrder = byteOrderOptions[0].value
}
}
/** 打开弹窗 */
const open = async (type: 'create' | 'update', id?: number) => {
dialogVisible.value = true
formType.value = type
dialogTitle.value = t('action.' + type)
resetForm()
//
if (type === 'update' && id) {
formLoading.value = true
try {
formData.value = await DeviceModbusPointApi.getModbusPoint(id)
} finally {
formLoading.value = false
}
}
}
/** 提交表单 */
const submitForm = async () => {
//
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
//
formLoading.value = true
try {
if (formType.value === 'create') {
await DeviceModbusPointApi.createModbusPoint(formData.value)
message.success('创建成功')
} else {
await DeviceModbusPointApi.updateModbusPoint(formData.value)
message.success('更新成功')
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
deviceId: props.deviceId,
thingModelId: undefined,
identifier: '',
name: '',
functionCode: undefined,
registerAddress: undefined,
registerCount: undefined,
byteOrder: undefined,
rawDataType: undefined,
scale: 1,
pollInterval: 5000,
status: CommonStatusEnum.ENABLE
}
formRef.value?.resetFields()
}
/** 暴露方法 */
defineExpose({ open })
</script>

@ -1,132 +0,0 @@
<template>
<DeviceDetailsHeader
:loading="loading"
:product="product"
:device="device"
@refresh="getDeviceData"
/>
<el-col>
<el-tabs v-model="activeTab">
<el-tab-pane label="设备信息" name="info">
<DeviceDetailsInfo v-if="activeTab === 'info'" :product="product" :device="device" />
</el-tab-pane>
<el-tab-pane label="物模型数据" name="model">
<DeviceDetailsThingModel
v-if="activeTab === 'model'"
:device-id="device.id"
:thing-model-list="thingModelList"
/>
</el-tab-pane>
<el-tab-pane
label="子设备管理"
name="subDevice"
v-if="product.deviceType === DeviceTypeEnum.GATEWAY"
>
<DeviceDetailsSubDevice v-if="activeTab === 'subDevice'" :gateway-id="device.id" />
</el-tab-pane>
<el-tab-pane label="设备消息" name="log">
<DeviceDetailsMessage v-if="activeTab === 'log'" :device-id="device.id" />
</el-tab-pane>
<el-tab-pane label="模拟设备" name="simulator">
<DeviceDetailsSimulator
v-if="activeTab === 'simulator'"
:product="product"
:device="device"
:thing-model-list="thingModelList"
/>
</el-tab-pane>
<el-tab-pane label="设备配置" name="config">
<DeviceDetailConfig
v-if="activeTab === 'config'"
:device="device"
@success="getDeviceData"
/>
</el-tab-pane>
<el-tab-pane
label="Modbus 配置"
name="modbus"
v-if="
[ProtocolTypeEnum.MODBUS_TCP_CLIENT, ProtocolTypeEnum.MODBUS_TCP_SERVER].includes(
product.protocolType as ProtocolTypeEnum
)
"
>
<DeviceModbusConfig
v-if="activeTab === 'modbus'"
:device="device"
:product="product"
:thing-model-list="thingModelList"
/>
</el-tab-pane>
</el-tabs>
</el-col>
</template>
<script lang="ts" setup>
import { useTagsViewStore } from '@/store/modules/tagsView'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { DeviceTypeEnum, ProductApi, ProductVO, ProtocolTypeEnum } from '@/api/iot/product/product'
import { ThingModelApi, ThingModelData } from '@/api/iot/thingmodel'
import DeviceDetailsHeader from './DeviceDetailsHeader.vue'
import DeviceDetailsInfo from './DeviceDetailsInfo.vue'
import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue'
import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue'
import DeviceDetailConfig from './DeviceDetailConfig.vue'
import DeviceModbusConfig from './DeviceModbusConfig.vue'
import DeviceDetailsSubDevice from './DeviceDetailsSubDevice.vue'
defineOptions({ name: 'IoTDeviceDetail' })
const route = useRoute()
const message = useMessage()
const id = Number(route.params.id) //
const loading = ref(true) //
const product = ref<ProductVO>({} as ProductVO) //
const device = ref<DeviceVO>({} as DeviceVO) //
const activeTab = ref('info') //
const thingModelList = ref<ThingModelData[]>([]) //
/** 获取设备详情 */
const getDeviceData = async () => {
loading.value = true
try {
device.value = await DeviceApi.getDevice(id)
await getProductData(device.value.productId)
await getThingModelList(device.value.productId)
} finally {
loading.value = false
}
}
/** 获取产品详情 */
const getProductData = async (id: number) => {
product.value = await ProductApi.getProduct(id)
}
/** 获取物模型列表 */
const getThingModelList = async (productId: number) => {
try {
const data = await ThingModelApi.getThingModelList({
productId: productId
})
thingModelList.value = data || []
} catch (error) {
console.error('获取物模型列表失败:', error)
thingModelList.value = []
}
}
/** 初始化 */
const { delView } = useTagsViewStore() //
const router = useRouter() //
const { currentRoute } = router
onMounted(async () => {
if (!id) {
message.warning('参数错误,产品不能为空!')
delView(unref(currentRoute))
return
}
await getDeviceData()
activeTab.value = (route.query.tab as string) || 'info'
})
</script>

@ -1,530 +0,0 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="产品" prop="productId">
<el-select
v-model="queryParams.productId"
placeholder="请选择产品"
clearable
class="!w-240px"
>
<el-option
v-for="product in products"
:key="product.id"
:label="product.name"
:value="product.id"
/>
</el-select>
</el-form-item>
<el-form-item label="DeviceName" prop="deviceName">
<el-input
v-model="queryParams.deviceName"
placeholder="请输入 DeviceName"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="备注名称" prop="nickname">
<el-input
v-model="queryParams.nickname"
placeholder="请输入备注名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="设备类型" prop="deviceType">
<el-select
v-model="queryParams.deviceType"
placeholder="请选择设备类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="设备状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择设备状态"
clearable
class="!w-240px"
>
<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>
<el-form-item label="设备分组" prop="groupId">
<el-select
v-model="queryParams.groupId"
placeholder="请选择设备分组"
clearable
class="!w-240px"
>
<el-option
v-for="group in deviceGroups"
:key="group.id"
:label="group.name"
:value="group.id"
/>
</el-select>
</el-form-item>
<el-form-item class="float-right !mr-0 !mb-0">
<el-button-group>
<el-button :type="viewMode === 'card' ? 'primary' : 'default'" @click="viewMode = 'card'">
<Icon icon="ep:grid" />
</el-button>
<el-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
<Icon icon="ep:list" />
</el-button>
</el-button-group>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
重置
</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['iot:device:create']"
>
<Icon icon="ep:plus" class="mr-5px" />
新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['iot:device:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button type="warning" plain @click="handleImport" v-hasPermi="['iot:device:import']">
<Icon icon="ep:upload" /> 导入
</el-button>
<el-button
type="primary"
plain
@click="openGroupForm"
:disabled="selectedIds.length === 0"
v-hasPermi="['iot:device:update']"
>
<Icon icon="ep:folder-add" class="mr-5px" /> 添加到分组
</el-button>
<el-button
type="danger"
plain
@click="handleDeleteList"
:disabled="selectedIds.length === 0"
v-hasPermi="['iot:device:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<template v-if="viewMode === 'card'">
<el-row :gutter="16">
<el-col v-for="item in list" :key="item.id" :xs="24" :sm="12" :md="12" :lg="6" class="mb-4">
<el-card
class="h-full transition-colors relative overflow-hidden"
:body-style="{ padding: '0' }"
>
<!-- 添加渐变背景层 -->
<div
class="absolute top-0 left-0 right-0 h-[50px] pointer-events-none"
:class="[
item.state === DeviceStateEnum.ONLINE
? 'bg-gradient-to-b from-[#eefaff] to-transparent'
: 'bg-gradient-to-b from-[#fff1f1] to-transparent'
]"
>
</div>
<div class="p-4 relative">
<!-- 标题区域 -->
<div class="flex items-center mb-3">
<div class="mr-2.5 flex items-center">
<el-image :src="defaultIconUrl" class="w-[18px] h-[18px]" />
</div>
<div class="text-[16px] font-600 flex-1 overflow-hidden text-ellipsis whitespace-nowrap">{{ item.deviceName }}</div>
<!-- 添加设备状态标签 -->
<div class="inline-flex items-center">
<div
class="w-1 h-1 rounded-full mr-1.5"
:class="
item.state === DeviceStateEnum.ONLINE
? 'bg-[var(--el-color-success)]'
: 'bg-[var(--el-color-danger)]'
"
>
</div>
<el-text
class="!text-xs font-bold"
:type="item.state === DeviceStateEnum.ONLINE ? 'success' : 'danger'"
>
{{ getDictLabel(DICT_TYPE.IOT_DEVICE_STATE, item.state) }}
</el-text>
</div>
</div>
<!-- 信息区域 -->
<div class="flex items-center text-[14px]">
<div class="flex-1">
<div class="mb-2.5 last:mb-0">
<span class="text-[#717c8e] mr-2.5">所属产品</span>
<el-link class="text-[#0070ff]" @click="openProductDetail(item.productId)">
{{ products.find((p) => p.id === item.productId)?.name }}
</el-link>
</div>
<div class="mb-2.5 last:mb-0">
<span class="text-[#717c8e] mr-2.5">设备类型</span>
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="item.deviceType" />
</div>
<div class="mb-2.5 last:mb-0">
<span class="text-[#717c8e] mr-2.5">备注名称</span>
<span
class="text-[var(--el-text-color-primary)] inline-block align-middle overflow-hidden text-ellipsis whitespace-nowrap max-w-[130px]"
>
{{ item.nickname || item.deviceName }}
</span>
</div>
</div>
<div class="w-[100px] h-[100px]">
<el-image :src="defaultPicUrl" class="w-full h-full" />
</div>
</div>
<!-- 分隔线 -->
<el-divider class="!my-3" />
<!-- 按钮 -->
<div class="flex items-center px-0">
<el-button
class="flex-1 !px-2 !h-[32px] text-[13px]"
type="primary"
plain
@click="openForm('update', item.id)"
v-hasPermi="['iot:device:update']"
>
<Icon icon="ep:edit-pen" class="mr-1" />
编辑
</el-button>
<el-button
class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
type="warning"
plain
@click="openDetail(item.id)"
>
<Icon icon="ep:view" class="mr-1" />
详情
</el-button>
<el-button
class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
type="info"
plain
@click="openModel(item.id)"
>
<Icon icon="ep:tickets" class="mr-1" />
数据
</el-button>
<div class="mx-[10px] h-[20px] w-[1px] bg-[#dcdfe6]"></div>
<el-button
class="!px-2 !h-[32px] text-[13px]"
type="danger"
plain
@click="handleDelete(item.id)"
v-hasPermi="['iot:device:delete']"
>
<Icon icon="ep:delete" />
</el-button>
</div>
</div>
</el-card>
</el-col>
</el-row>
</template>
<!-- 列表视图 -->
<el-table
v-else
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="DeviceName" align="center" prop="deviceName">
<template #default="scope">
<el-link @click="openDetail(scope.row.id)">{{ scope.row.deviceName }}</el-link>
</template>
</el-table-column>
<el-table-column label="备注名称" align="center" prop="nickname" />
<el-table-column label="所属产品" align="center" prop="productId">
<template #default="scope">
<el-link @click="openProductDetail(scope.row.productId)">
{{ products.find((p) => p.id === scope.row.productId)?.name || '-' }}
</el-link>
</template>
</el-table-column>
<el-table-column label="设备类型" align="center" prop="deviceType">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
</template>
</el-table-column>
<el-table-column label="所属分组" align="center" prop="groupId">
<template #default="scope">
<template v-if="scope.row.groupIds?.length">
<el-tag v-for="id in scope.row.groupIds" :key="id" class="ml-5px" size="small">
{{ deviceGroups.find((g) => g.id === id)?.name }}
</el-tag>
</template>
</template>
</el-table-column>
<el-table-column label="设备状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="最后上线时间"
align="center"
prop="onlineTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openDetail(scope.row.id)"
v-hasPermi="['iot:product:query']"
>
查看
</el-button>
<el-button link type="primary" @click="openModel(scope.row.id)"> </el-button>
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['iot:device:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['iot:device:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<DeviceForm ref="formRef" @success="getList" />
<!-- 分组表单组件 -->
<DeviceGroupForm ref="groupFormRef" @success="getList" />
<!-- 导入表单组件 -->
<DeviceImportForm ref="importFormRef" @success="getList" />
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { DeviceStateEnum } from '@/views/iot/utils/constants'
import DeviceForm from './DeviceForm.vue'
import { ProductApi, ProductVO } from '@/api/iot/product/product'
import { DeviceGroupApi, DeviceGroupVO } from '@/api/iot/device/group'
import download from '@/utils/download'
import DeviceGroupForm from './DeviceGroupForm.vue'
import DeviceImportForm from './DeviceImportForm.vue'
/** IoT 设备列表 */
defineOptions({ name: 'IoTDevice' })
const message = useMessage() //
const { t } = useI18n() //
const route = useRoute() //
const loading = ref(true) //
const list = ref<DeviceVO[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
deviceName: undefined,
productId: undefined as number | undefined,
deviceType: undefined,
nickname: undefined,
status: undefined,
groupId: undefined
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
const products = ref<ProductVO[]>([]) //
const deviceGroups = ref<DeviceGroupVO[]>([]) //
const selectedIds = ref<number[]>([]) //
const viewMode = ref<'card' | 'list'>('card') //
const defaultPicUrl = ref('/src/assets/imgs/iot/device.png') //
const defaultIconUrl = ref('/src/assets/svgs/iot/card-fill.svg') //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await DeviceApi.getDevicePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
selectedIds.value = [] //
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 打开详情 */
const { push } = useRouter()
const openDetail = (id: number) => {
push({ name: 'IoTDeviceDetail', params: { id } })
}
/** 跳转到产品详情页面 */
const openProductDetail = (productId: number) => {
push({ name: 'IoTProductDetail', params: { id: productId } })
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await DeviceApi.deleteDevice(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 导出方法 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await DeviceApi.exportDeviceExcel(queryParams)
download.excel(data, '物联网设备.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: DeviceVO[]) => {
selectedIds.value = selection.map((item) => item.id)
}
/** 批量删除按钮操作 */
const handleDeleteList = async () => {
try {
await message.delConfirm()
//
await DeviceApi.deleteDeviceList(selectedIds.value)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 添加到分组操作 */
const groupFormRef = ref()
const openGroupForm = () => {
groupFormRef.value.open(selectedIds.value)
}
/** 打开物模型数据 */
const openModel = (id: number) => {
push({ name: 'IoTDeviceDetail', params: { id }, query: { tab: 'model' } })
}
/** 设备导入 */
const importFormRef = ref()
const handleImport = () => {
importFormRef.value.open()
}
/** 初始化 **/
onMounted(async () => {
// productId
const { productId } = route.query
if (productId) {
queryParams.productId = Number(productId)
}
await getList()
//
products.value = await ProductApi.getSimpleProductList()
//
deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList()
})
</script>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,111 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-form-item :label="t('DataCollection.DeviceAttributeType.code')" prop="code">
<el-input v-model="formData.code" :placeholder="t('DataCollection.DeviceAttributeType.placeholderCode')" :disabled = "formType === 'update'"/>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceAttributeType.name')" prop="name">
<el-input v-model="formData.name" :placeholder="t('DataCollection.DeviceAttributeType.placeholderName')" />
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceAttributeType.sort')" prop="sort">
<el-input-number v-model="formData.sort" :placeholder="t('DataCollection.DeviceAttributeType.placeholderSort')" :min="0" class="!w-full"/>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceAttributeType.remark')" prop="remark">
<el-input v-model="formData.remark" :placeholder="t('DataCollection.DeviceAttributeType.placeholderRemark')" type="textarea"/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">{{ t('DataCollection.DeviceAttributeType.dialogOk') }}</el-button>
<el-button @click="dialogVisible = false">{{ t('DataCollection.DeviceAttributeType.dialogCancel') }}</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DeviceAttributeTypeApi, DeviceAttributeTypeVO } from '@/api/iot/deviceattributetype'
/** 采集点分类 表单 */
defineOptions({ name: 'DeviceAttributeTypeForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
code: undefined,
name: undefined,
sort: 0,
remark: undefined,
})
const formRules = reactive({
code: [{ required: true, message: t('DataCollection.DeviceAttributeType.validatorCodeRequired'), trigger: 'blur' }],
name: [{ required: true, message: t('DataCollection.DeviceAttributeType.validatorNameRequired'), trigger: 'blur' }],
sort: [{ required: true, message: t('DataCollection.DeviceAttributeType.validatorSortRequired'), trigger: 'blur' }],
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await DeviceAttributeTypeApi.getDeviceAttributeType(id)
;(formData.value as any).sort = Number((formData.value as any).sort ?? 0)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as DeviceAttributeTypeVO
data.sort = Number((data as any).sort ?? 0)
if (formType.value === 'create') {
await DeviceAttributeTypeApi.createDeviceAttributeType(data)
message.success(t('common.createSuccess'))
} else {
await DeviceAttributeTypeApi.updateDeviceAttributeType(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
code: undefined,
name: undefined,
sort: 0,
remark: undefined,
}
formRef.value?.resetFields()
}
</script>

@ -0,0 +1,249 @@
<template>
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
min-label-width="80px"
>
<el-form-item :label="t('DataCollection.DeviceAttributeType.code')" prop="code">
<el-input
v-model="queryParams.code"
:placeholder="t('DataCollection.DeviceAttributeType.placeholderCode')"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceAttributeType.name')" prop="name">
<el-input
v-model="queryParams.name"
:placeholder="t('DataCollection.DeviceAttributeType.placeholderName')"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<!-- <el-form-item label="备注" prop="remark">
<el-input
v-model="queryParams.remark"
placeholder="请输入备注"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item> -->
<!-- <el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item> -->
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> {{ t('DataCollection.DeviceAttributeType.search') }}</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> {{ t('DataCollection.DeviceAttributeType.reset') }}</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['iot:device-attribute-type:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> {{ t('DataCollection.DeviceAttributeType.create') }}
</el-button>
<el-button
type="danger"
plain
@click="handleBatchDelete"
v-hasPermi="['iot:device-attribute-type:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> {{ t('DataCollection.DeviceAttributeType.batchDelete') }}
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['iot:device-attribute-type:export']"
>
<Icon icon="ep:download" class="mr-5px" /> {{ t('DataCollection.DeviceAttributeType.export') }}
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
ref="tableRef"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" reserve-selection />
<el-table-column :label="t('DataCollection.DeviceAttributeType.code')" align="center" prop="code" sortable />
<el-table-column :label="t('DataCollection.DeviceAttributeType.name')" align="center" prop="name" sortable/>
<el-table-column :label="t('DataCollection.DeviceAttributeType.sort')" align="center" prop="sort" sortable/>
<el-table-column :label="t('DataCollection.DeviceAttributeType.remark')" align="center" prop="remark" />
<el-table-column
:label="t('DataCollection.DeviceAttributeType.createTime')"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
sortable
/>
<el-table-column :label="t('DataCollection.DeviceAttributeType.operate')" align="center" width="150px" fixed="right">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['iot:device-attribute-type:update']"
>
{{ t('action.edit') }}
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['iot:device-attribute-type:delete']"
>
{{ t('action.del') }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<DeviceAttributeTypeForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { DeviceAttributeTypeApi, DeviceAttributeTypeVO } from '@/api/iot/deviceattributetype'
import DeviceAttributeTypeForm from './DeviceAttributeTypeForm.vue'
/** 采集点分类 列表 */
defineOptions({ name: 'DeviceAttributeType' })
const message = useMessage() //
const { t } = useI18n() //
const tableRef = ref()
const loading = ref(true) //
const list = ref<DeviceAttributeTypeVO[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
code: undefined,
name: undefined,
remark: undefined,
createTime: [],
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
const selectedIds = ref<number[]>([])
const handleSelectionChange = (rows: any[]) => {
selectedIds.value = rows?.map((row) => row.id).filter((id) => id !== undefined) ?? []
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await DeviceAttributeTypeApi.getDeviceAttributeTypePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const buildIdsParam = (ids: number | number[]) => {
return Array.isArray(ids) ? ids.join(',') : String(ids)
}
const handleDelete = async (ids: number | number[]) => {
try {
//
await message.delConfirm()
//
await DeviceAttributeTypeApi.deleteDeviceAttributeType(buildIdsParam(ids))
message.success(t('common.delSuccess'))
selectedIds.value = []
tableRef.value?.clearSelection?.()
//
await getList()
} catch {}
}
const handleBatchDelete = async () => {
if (!selectedIds.value.length) {
message.error('请选择需要删除的数据')
return
}
await handleDelete(selectedIds.value)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const params: any = {
...queryParams,
ids: selectedIds.value.length ? selectedIds.value.join(',') : undefined,
}
const data = await DeviceAttributeTypeApi.exportDeviceAttributeType(params)
download.excel(data, t('DataCollection.DeviceAttributeType.exportFilename'))
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

@ -0,0 +1,136 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-form-item :label="t('DataCollection.DeviceModel.code')" prop="code">
<el-input
v-model="formData.code"
:placeholder="t('DataCollection.DeviceModel.placeholderCode')"
:disabled = "formType === 'update'"
@input="onCodeInput"
/>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.name')" prop="name">
<el-input
v-model="formData.name"
:placeholder="t('DataCollection.DeviceModel.placeholderName')" />
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.protocol')" prop="protocol">
<el-select
v-model="formData.protocol"
:placeholder="t('DataCollection.DeviceModel.placeholderProtocol')">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_PROTOCOL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.remark')" prop="remark">
<el-input
v-model="formData.remark"
:placeholder="t('DataCollection.DeviceModel.placeholderRemark')"
type="textarea" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">{{ t('DataCollection.DeviceModel.dialogOk') }}</el-button>
<el-button @click="dialogVisible = false">{{ t('DataCollection.DeviceModel.dialogCancel') }}</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
import { DeviceModelApi, DeviceModelVO } from '@/api/iot/devicemodel'
/** 采集设备模型 表单 */
defineOptions({ name: 'DeviceModelForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
code: undefined,
name: undefined,
protocol: undefined,
remark: undefined,
})
const onCodeInput = (val: string) => {
if (typeof val !== 'string') return
const filtered = val.replace(/[^0-9a-zA-Z_\-]/g, '')
if (filtered !== val) {
formData.value.code = filtered
}
}
const formRules = reactive({
code: [{ required: true, message: t('DataCollection.DeviceModel.validatorCodeRequired'), trigger: 'blur' }],
name: [{ required: true, message: t('DataCollection.DeviceModel.validatorNameRequired'), trigger: 'blur' }],
protocol: [{ required: true, message: t('DataCollection.DeviceModel.validatorProtocolRequired'), trigger: 'blur' }],
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await DeviceModelApi.getDeviceModel(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = formData.value as unknown as DeviceModelVO
if (formType.value === 'create') {
await DeviceModelApi.createDeviceModel(data)
message.success(t('common.createSuccess'))
} else {
await DeviceModelApi.updateDeviceModel(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
code: undefined,
name: undefined,
protocol: undefined,
remark: undefined,
}
formRef.value?.resetFields()
}
</script>

@ -0,0 +1,270 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
v-loading="formLoading"
>
<el-form-item :label="t('DataCollection.DeviceModel.attributeCode')" prop="attributeCode">
<el-input
v-model="formData.attributeCode"
:placeholder="t('DataCollection.DeviceModel.placeholderAttributeCode')"
:disabled = "formType === 'update'"/>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.attributeName')" prop="attributeName">
<el-input
v-model="formData.attributeName"
:placeholder="t('DataCollection.DeviceModel.placeholderAttributeName')" />
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.attributeType')" prop="attributeType">
<el-select
v-model="formData.attributeType"
clearable
filterable
:placeholder="t('DataCollection.DeviceModel.placeholderAttributeType')"
@change="handleAttributeTypeChange"
>
<el-option v-for="item in typeList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.dataType')" prop="dataType">
<el-select
v-model="formData.dataType"
:placeholder="t('DataCollection.DeviceModel.placeholderDataType')">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_DEVICE_DATA_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.address')" prop="address">
<el-input
v-model="formData.address"
:placeholder="t('DataCollection.DeviceModel.placeholderAddress')" />
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.dataUnit')" prop="dataUnit">
<el-select v-model="formData.dataUnit" clearable :placeholder="t('DataCollection.DeviceModel.placeholderDataUnit')" class="w-1/1">
<el-option
v-for="unit in unitList"
:key="unit.id"
:label="unit.name"
:value="unit.id"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ratio')" prop="ratio">
<el-input-number
v-model="formData.ratio"
:placeholder="t('DataCollection.DeviceModel.placeholderRatio')"
:min="0.00"
:decision="2"
:step="0.01"
class="!w-full"
:disabled = "!ratioEnabled"/>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.remark')" prop="remark">
<el-input
v-model="formData.remark"
:placeholder="t('DataCollection.DeviceModel.placeholderAttributeRemark')"
type="textarea"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">{{ t('DataCollection.DeviceModel.dialogOk') }}</el-button>
<el-button @click="dialogVisible = false">{{ t('DataCollection.DeviceModel.dialogCancel') }}</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
import { DeviceModelAttributeApi } from '@/api/iot/devicemodelattribute'
import { DeviceAttributeTypeApi, DeviceAttributeTypeVO } from '@/api/iot/deviceattributetype'
import { ProductUnitApi, ProductUnitVO } from '@/api/erp/product/unit'
/** 采集设备模型-点位管理 表单 */
defineOptions({ name: 'DeviceModelAttributeForm' })
const { t } = useI18n() //
const message = useMessage() //
const typeList = ref<DeviceAttributeTypeVO[]>([])
const typeListLoaded = ref(false)
const loadTypeList = async () => {
if (typeListLoaded.value) {
return
}
try {
const data = await DeviceAttributeTypeApi.getDeviceAttributeTypePage({ pageNo: 1, pageSize: 10 })
typeList.value = data?.list ?? []
typeListLoaded.value = true
} catch {
typeList.value = []
}
}
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
attributeCode: undefined,
attributeName: undefined,
attributeType: undefined,
typeName: undefined,
dataType: undefined,
address: undefined,
dataUnit: undefined,
ratio: undefined,
remark: undefined,
deviceModelId: undefined,
})
const formRules = reactive({
attributeCode: [{ required: true, message: t('DataCollection.DeviceModel.validatorAttributeCodeRequired'), trigger: 'blur' }],
attributeName: [{ required: true, message: t('DataCollection.DeviceModel.validatorAttributeNameRequired'), trigger: 'blur' }],
dataType: [{ required: true, message: t('DataCollection.DeviceModel.validatorDataTypeRequired'), trigger: 'change' }]
})
const formRef = ref() // Ref
const unitList = ref<ProductUnitVO[]>([]) //
const ratioEnabledTypes = new Set([
'uint8',
'uint16',
'uint32',
'uint64',
'int8',
'int16',
'int32',
'float32',
'float64'
])
const ratioEnabled = computed(() => {
const v = formData.value.dataType
if (!v) return false
return ratioEnabledTypes.has(v)
})
const buildSubmitData = () => {
const {
id,
attributeCode,
attributeName,
attributeType,
typeName,
dataType,
address,
dataUnit,
ratio,
remark,
deviceModelId
} = formData.value
const data: any = {
attributeCode,
attributeName,
attributeType,
typeName,
dataType,
address,
dataUnit,
ratio: ratioEnabled.value ? ratio : undefined,
remark,
deviceModelId
}
if (formType.value === 'update') {
data.id = id
}
return data
}
/** 打开弹窗 */
const open = async (type: string, id: number, modelId: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
await loadTypeList()
//
unitList.value = await ProductUnitApi.getProductUnitSimpleList()
formData.value.deviceModelId = modelId
if (id) {
formLoading.value = true
try {
formData.value = await DeviceModelAttributeApi.getDeviceModelAttribute(id)
const currentType = (formData.value as any)?.attributeType
if (currentType !== undefined && currentType !== null && currentType !== '') {
const matched = typeList.value.find(
(item) =>
item.id === currentType ||
String(item.id) === String(currentType) ||
item.name === currentType ||
item.code === currentType
)
if (matched) {
;(formData.value as any).attributeType = matched.id
;(formData.value as any).typeName = matched.name
}
}
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = buildSubmitData()
if (formType.value === 'create') {
await DeviceModelAttributeApi.createDeviceModelAttribute(data)
message.success(t('common.createSuccess'))
} else {
await DeviceModelAttributeApi.updateDeviceModelAttribute(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
const handleAttributeTypeChange = (val: number | string) => {
const matched = typeList.value.find(
(item) => item.id === val || String(item.id) === String(val)
)
;(formData.value as any).typeName = matched?.name
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
attributeCode: undefined,
attributeName: undefined,
attributeType: undefined,
typeName: undefined,
dataType: undefined,
address: undefined,
dataUnit: undefined,
ratio: undefined,
remark: undefined,
deviceModelId: undefined,
}
formRef.value?.resetFields()
}
</script>

@ -0,0 +1,380 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item :label="t('DataCollection.DeviceModel.attributeCode')" prop="attributeCode">
<el-input
v-model="queryParams.attributeCode"
:placeholder="t('DataCollection.DeviceModel.placeholderAttributeCode')" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.attributeName')" prop="attributeName">
<el-input
v-model="queryParams.attributeName"
:placeholder="t('DataCollection.DeviceModel.placeholderAttributeName')" clearable @keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.attributeType')" prop="attributeType">
<el-select
v-model="queryParams.attributeType" clearable filterable
:placeholder="t('DataCollection.DeviceModel.placeholderAttributeType')" class="!w-240px">
<el-option v-for="item in typeList" :key="item.id" :label="item.code + '-' + item.name" :value="item.id" />
</el-select>
</el-form-item>
<!-- <el-form-item label="寄存器地址" prop="address">
<el-input
v-model="queryParams.address"
placeholder="请输入寄存器地址"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item> -->
<!-- <el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item> -->
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> {{ t('DataCollection.DeviceModel.search') }}
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('DataCollection.DeviceModel.reset') }}
</el-button>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['iot:device-model:create']">
<Icon icon="ep:plus" class="mr-5px" /> {{ t('DataCollection.DeviceModel.create') }}
</el-button>
<el-button type="warning" plain @click="openImport" v-hasPermi="['iot:device-model:import']">
<Icon icon="ep:upload" class="mr-5px" /> {{ t('action.import') }}
</el-button>
<el-button
type="success" plain @click="handleExport" :loading="exportLoading"
v-hasPermi="['iot:device-model:export']">
<Icon icon="ep:download" class="mr-5px" /> {{ t('DataCollection.DeviceModel.export') }}
</el-button>
<el-button type="danger" plain @click="handleBatchDelete" v-hasPermi="['iot:device-model:delete']">
<Icon icon="ep:delete" class="mr-5px" /> {{ t('DataCollection.DeviceModel.batchDelete') }}
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" row-key="id"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" reserve-selection />
<!-- <el-table-column label="ID" align="center" prop="id" /> -->
<el-table-column :label="t('DataCollection.DeviceModel.attributeCode')" align="center" prop="attributeCode" sortable/>
<el-table-column :label="t('DataCollection.DeviceModel.attributeName')" align="center" prop="attributeName" sortable/>
<el-table-column :label="t('DataCollection.DeviceModel.attributeType')" align="center" prop="typeName" sortable />
<el-table-column :label="t('DataCollection.DeviceModel.dataType')" align="center" prop="dataType" sortable />
<el-table-column :label="t('DataCollection.DeviceModel.address')" align="center" prop="address" />
<el-table-column :label="t('DataCollection.DeviceModel.dataUnit')" align="center" prop="dataUnitName" sortable />
<el-table-column :label="t('DataCollection.DeviceModel.ratio')" align="center" prop="ratio" />
<el-table-column :label="t('DataCollection.DeviceModel.remark')" align="center" prop="remark" />
<!-- <el-table-column label="采集设备模型id" align="center" prop="deviceModelId" /> -->
<el-table-column
:label="t('DataCollection.DeviceModel.createTime')" align="center" prop="createTime"
:formatter="dateFormatter" width="180px" sortable/>
<el-table-column :label="t('DataCollection.DeviceModel.operate')" align="center" width="150px" fixed="right">
<template #default="scope">
<el-button
link type="primary" @click="openForm('update', scope.row.id)"
v-hasPermi="['iot:device-model:update']">
{{ t('action.edit') }}
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['iot:device-model:delete']">
{{ t('action.del') }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<DeviceModelAttributeForm ref="formRef" @success="getList" />
<!-- 导入弹窗 -->
<Dialog v-model="importDialogVisible" :title="t('action.import')" width="400">
<el-upload
ref="uploadRef" v-model:file-list="importFileList" :action="importUrl + '?updateSupport=' + updateSupport + '&deviceModelId=' + props.id" :auto-upload="false"
:disabled="importLoading" :headers="uploadHeaders" :limit="1" :on-error="handleImportError"
:on-exceed="handleImportExceed" :on-success="handleImportSuccess" accept=".xlsx, .xls" drag>
<Icon icon="ep:upload" />
<div class="el-upload__text">
将文件拖到此处或点击上传
</div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="updateSupport" />
是否更新已存在的数据
</div>
<span>仅支持 .xlsx, .xls 格式</span>
<el-link
:underline="false" style="font-size: 12px; vertical-align: baseline" type="primary"
@click="importTemplate">
下载导入模板
</el-link>
</div>
</template>
</el-upload>
<template #footer>
<el-button :disabled="importLoading" type="primary" @click="submitImport">{{ t('common.ok') }}</el-button>
<el-button @click="importDialogVisible = false">{{ t('common.cancel') }}</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { DeviceModelAttributeApi, DeviceModelAttributeVO } from '@/api/iot/devicemodelattribute'
import DeviceModelAttributeForm from './DeviceModelAttributeForm.vue'
import { DeviceAttributeTypeApi, DeviceAttributeTypeVO } from '@/api/iot/deviceattributetype'
import { getAccessToken, getTenantId } from '@/utils/auth'
import { ElMessageBox } from 'element-plus'
const props = defineProps<{
id?: number // id
}>()
const typeList = ref<DeviceAttributeTypeVO[]>([]) //
const message = useMessage() //
const { t } = useI18n() //
const tableRef = ref()
const loading = ref(false) //
const list = ref<DeviceModelAttributeVO[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
id: undefined as unknown,
attributeCode: undefined,
attributeName: undefined,
attributeType: undefined,
dataType: undefined,
address: undefined,
dataUnit: undefined,
ratio: undefined,
remark: undefined,
deviceModelId: undefined,
createTime: [],
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
const importDialogVisible = ref(false)
const importLoading = ref(false)
const uploadRef = ref()
const importUrl =
import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/iot/device-model-attribute/import'
const uploadHeaders = ref()
const importFileList = ref([])
const updateSupport = ref(0)
const escapeHtml = (value: unknown) => {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
const selectedIds = ref<number[]>([])
const handleSelectionChange = (rows: any[]) => {
selectedIds.value = rows?.map((row) => row.id).filter((id) => id !== undefined) ?? []
}
/** 监听主表的关联字段的变化,加载对应的子表数据 */
watch(
() => props.id,
(val: number) => {
if (!val) {
return
}
queryParams.id = val
handleQuery()
},
{ immediate: true, deep: true }
)
/** 查询列表 */
async function getList() {
loading.value = true
try {
const data = await DeviceModelAttributeApi.getDeviceModelAttributePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
function handleQuery() {
if (!props.id) {
return
}
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
if (!props.id) {
return
}
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
if (!props.id) {
message.error('请选择一个采集设备模型')
return
}
formRef.value.open(type, id, props.id)
}
const openImport = () => {
if (!props.id) {
message.error('请选择一个采集设备模型')
return
}
importDialogVisible.value = true
importLoading.value = false
importFileList.value = []
updateSupport.value = 0
}
/** 删除按钮操作 */
const buildIdsParam = (ids: number | number[]) => {
return Array.isArray(ids) ? ids.join(',') : String(ids)
}
const handleDelete = async (ids: number | number[]) => {
try {
//
await message.delConfirm()
//
await DeviceModelAttributeApi.deleteDeviceModelAttribute(buildIdsParam(ids))
message.success(t('common.delSuccess'))
selectedIds.value = []
tableRef.value?.clearSelection?.()
//
await getList()
} catch { }
}
const handleBatchDelete = async () => {
if (!selectedIds.value.length) {
message.error('请选择需要删除的数据')
return
}
await handleDelete(selectedIds.value)
}
/** 导出按钮操作 */
const handleExport = async () => {
if (!selectedIds.value.length) {
message.error('请选择需要导出的数据')
return
}
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await DeviceModelAttributeApi.exportDeviceModelAttribute({ ids: selectedIds.value.join(',') })
download.excel(data, '采集设备模型-点位管理.xls')
} catch {
} finally {
exportLoading.value = false
}
}
const submitImport = async () => {
if (importFileList.value.length === 0) {
message.error('请选择导入文件')
return
}
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
tenantId: getTenantId(),
}
importLoading.value = true
uploadRef.value?.submit()
}
const handleImportSuccess = async (response: any) => {
if (!response || response.code !== 0) {
message.error(response?.msg || '导入失败')
importLoading.value = false
return
}
const failureCodes = response?.data?.failureCodes
if (failureCodes && typeof failureCodes === 'object') {
const entries = Object.entries(failureCodes).filter(([key]) => String(key).trim() !== '')
if (entries.length > 0) {
const detail = entries
.map(([code, err], index) => `${index + 1}. ${escapeHtml(code)}${escapeHtml(err)}`)
.join('<br />')
await ElMessageBox.alert(`以下点位编码导入失败:<br />${detail}`, t('common.confirmTitle'), {
type: 'error',
dangerouslyUseHTMLString: true,
})
} else {
message.success('导入成功')
}
} else {
message.success('导入成功')
}
importLoading.value = false
importDialogVisible.value = false
getList()
}
const handleImportError = () => {
message.error('导入失败')
importLoading.value = false
}
const handleImportExceed = () => {
message.error('只能上传一个文件')
}
const importTemplate = async () => {
const res = await DeviceModelAttributeApi.importDeviceModelAttributeTemplate()
download.excel(res, '采集设备模型-点位导入模板.xls')
}
/** 初始化 **/
onMounted(async () => {
typeList.value = await DeviceAttributeTypeApi.getDeviceAttributeTypeList()
// typeList.value = JSON.parse(JSON.stringify(await DeviceAttributeTypeApi.getDeviceAttributeTypePage(queryParams))).list
})
</script>

@ -0,0 +1,883 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" min-label-width="68px">
<el-form-item :label="t('DataCollection.DeviceModel.code')" prop="code">
<el-input
v-model="queryParams.code"
:placeholder="t('DataCollection.DeviceModel.placeholderCode')"
clearable
@keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.name')" prop="name">
<el-input
v-model="queryParams.name"
:placeholder="t('DataCollection.DeviceModel.placeholderName')"
clearable
@keyup.enter="handleQuery"
class="!w-240px" />
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.protocol')" prop="protocol">
<el-select
v-model="queryParams.protocol"
:placeholder="t('DataCollection.DeviceModel.placeholderProtocol')"
clearable
class="!w-240px">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_PROTOCOL)" :key="dict.value" :label="dict.label"
:value="dict.value" />
</el-select>
</el-form-item>
<!-- <el-form-item label="备注" prop="remark">
<el-input
v-model="queryParams.remark"
placeholder="请输入备注"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item> -->
<!-- <el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item> -->
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> {{ t('DataCollection.DeviceModel.search') }}
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('DataCollection.DeviceModel.reset') }}
</el-button>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['iot:device-model:create']">
<Icon icon="ep:plus" class="mr-5px" /> {{ t('DataCollection.DeviceModel.create') }}
</el-button>
<el-button type="danger" plain @click="handleBatchDelete" v-hasPermi="['iot:device-model:delete']">
<Icon icon="ep:delete" class="mr-5px" /> {{ t('DataCollection.DeviceModel.batchDelete') }}
</el-button>
<el-button
type="success" plain @click="handleExport" :loading="exportLoading"
v-hasPermi="['iot:device-model:export']">
<Icon icon="ep:download" class="mr-5px" /> {{ t('DataCollection.DeviceModel.export') }}
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"
highlight-current-row row-key="id" @selection-change="handleSelectionChange" @row-click="handleShowAttribute">
<el-table-column type="selection" width="55" reserve-selection />
<el-table-column :label="t('DataCollection.DeviceModel.code')" align="center" prop="code" sortable/>
<el-table-column :label="t('DataCollection.DeviceModel.name')" align="center" prop="name" sortable/>
<el-table-column align="center" :label="t('DataCollection.DeviceModel.protocol')" prop="protocol" min-width="150">
<template #default="scope">
<dict-tag :type="DICT_TYPE.IOT_PROTOCOL" :value="scope.row.protocol" />
</template>
</el-table-column>
<el-table-column :label="t('DataCollection.DeviceModel.remark')" align="center" prop="remark" />
<!-- <el-table-column label="创建时间" align="center" prop="createTime" :formatter="dateFormatter" width="180px" sortable /> -->
<el-table-column :label="t('DataCollection.DeviceModel.operate')" align="center" min-width="120px">
<template #default="scope">
<el-button link type="primary" @click.stop="handleShowAttribute(scope.row)">{{ t('DataCollection.DeviceModel.attributeModuleName') }}</el-button>
<el-button link type="primary" @click="handleCopy(scope.row.id)" v-hasPermi="['iot:device-model:create']">
{{ t('action.copy') }}
</el-button>
<el-button
link type="primary" @click="openForm('update', scope.row.id)"
v-hasPermi="['iot:device-model:update']">
{{ t('action.edit') }}
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)" v-hasPermi="['iot:device-model:delete']">
{{ t('action.del') }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<ContentWrap>
<template v-if="attributeModelId">
<div class="mb-10px text-sm text-gray-500">
{{ t('DataCollection.DeviceModel.moduleName') }}<span class="font-medium text-gray-700">{{ attributeModelName || '-' }}</span>
</div>
<el-tabs model-value="modelAttribute">
<el-tab-pane :label="t('DataCollection.DeviceModel.attributeModuleName')" name="modelAttribute">
<ModelAttributeList :id="attributeModelId" />
</el-tab-pane>
<el-tab-pane :label="t('DataCollection.DeviceModel.ruleTabLabel')" name="modelRule">
<el-form class="-mb-15px" :model="ruleQueryParams" ref="ruleQueryFormRef" :inline="true" label-width="120px">
<el-form-item :label="t('DataCollection.DeviceModel.ruleIdentifier')" prop="identifier">
<el-input
v-model="ruleQueryParams.identifier"
:placeholder="t('DataCollection.DeviceModel.ruleSearchIdentifierPlaceholder')"
clearable
@keyup.enter="handleRuleQuery"
class="!w-200px"
/>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleFieldName')" prop="fieldName">
<el-input
v-model="ruleQueryParams.fieldName"
:placeholder="t('DataCollection.DeviceModel.ruleSearchFieldNamePlaceholder')"
clearable
@keyup.enter="handleRuleQuery"
class="!w-200px"
/>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDefaultValue')" prop="defaultValue">
<el-input
v-model="ruleQueryParams.defaultValue"
:placeholder="t('DataCollection.DeviceModel.ruleSearchDefaultValuePlaceholder')"
clearable
@keyup.enter="handleRuleQuery"
class="!w-200px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleRuleQuery">
<Icon icon="ep:search" class="mr-5px" /> {{ t('DataCollection.DeviceModel.ruleSearch') }}
</el-button>
<el-button @click="resetRuleQuery">
<Icon icon="ep:refresh" class="mr-5px" /> {{ t('DataCollection.DeviceModel.ruleReset') }}
</el-button>
</el-form-item>
</el-form>
<div class="mb-10px text-right">
<el-button type="primary" @click="openCreateRuleForm">
{{ t('DataCollection.DeviceModel.ruleCreateButton') }}
</el-button>
</div>
<el-table
v-loading="ruleLoading"
:data="ruleList"
:stripe="true"
:show-overflow-tooltip="true"
row-key="id"
>
<el-table-column :label="t('DataCollection.DeviceModel.ruleIdentifier')" align="center" prop="identifier" />
<el-table-column :label="t('DataCollection.DeviceModel.ruleFieldName')" align="center" prop="fieldName" sortable />
<el-table-column :label="t('DataCollection.DeviceModel.ruleFieldRule')" align="center" prop="fieldRule" />
<el-table-column :label="t('DataCollection.DeviceModel.ruleDefaultValue')" align="center" prop="defaultValue" />
<el-table-column :label="t('DataCollection.DeviceModel.ruleCreateTime')" align="center" prop="createTime" :formatter="dateFormatter" width="180px" sortable />
<el-table-column :label="t('DataCollection.DeviceModel.ruleOperate')" align="center" width="160px">
<template #default="scope">
<el-button link type="primary" @click="openRuleForm(scope.row)">
{{ t('DataCollection.DeviceModel.ruleEditRuleButton') }}
</el-button>
<el-button
v-if="(scope.row.identifier || '').toString().toUpperCase() === 'ALARM'"
link
type="danger"
@click="handleRuleDelete(scope.row.id)"
>
{{ t('DataCollection.DeviceModel.ruleDeleteRuleButton') }}
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="ruleTotal"
v-model:page="ruleQueryParams.pageNo"
v-model:limit="ruleQueryParams.pageSize"
@pagination="getRuleList"
/>
<el-dialog v-model="ruleDialogVisible" :title="t('DataCollection.DeviceModel.ruleDialogTitle')" width="880px" draggable>
<el-form :model="ruleForm" ref="ruleFormRef" label-width="120px">
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogIdentifier')">
<el-input v-model="ruleForm.identifier" disabled />
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogFieldName')">
<el-input v-model="ruleForm.fieldName" disabled />
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogDefaultValue')">
<el-input v-model="ruleForm.defaultValue" disabled />
</el-form-item>
<el-form-item
v-if="(ruleForm.identifier || '').toString().toUpperCase() === 'ALARM'"
:label="t('DataCollection.DeviceModel.ruleDialogAlarmLevel')"
>
<el-select
v-model="ruleForm.alarmLevel"
:placeholder="t('DataCollection.DeviceModel.ruleDialogAlarmLevelPlaceholder')"
>
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_ALARM_REGISTRATION)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<div class="flex flex-col w-full">
<div class="border border-gray-200 dark:border-gray-600 rounded-md py-12px">
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogFieldRule')">
<el-select
v-model="ruleForm.fieldRule"
:placeholder="t('DataCollection.DeviceModel.ruleDialogFieldRulePlaceholder')"
class="!w-240px"
:disabled="!currentRuleOptions.length"
>
<el-option
v-for="item in currentRuleOptions"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="isRuleDisabled(item.value, ruleForm.fieldRule)"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogRule')">
<div class="flex items-center gap-8px">
<el-select
v-model="ruleForm.ruleAttributeId"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleAttributePlaceholder')"
class="!w-240px"
>
<el-option
v-for="item in ruleAttributeOptions"
:key="item.id"
:label="item.attributeName || item.attributeCode"
:value="item.id"
/>
</el-select>
<el-select
v-model="ruleForm.ruleOperator"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleOperatorPlaceholder')"
class="!w-160px"
>
<el-option
v-for="opt in ruleOperatorOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-input
v-if="ruleForm.ruleOperator !== 'TRUE' && ruleForm.ruleOperator !== 'FALSE'"
v-model="ruleForm.ruleValue"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleValuePlaceholder')"
class="!w-200px"
/>
</div>
</el-form-item>
</div>
<div
v-for="(item, index) in extraPointRules"
:key="index"
class="border border-gray-200 dark:border-gray-600 rounded-md px-16px py-12px"
>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogFieldRule')">
<el-select
v-model="item.rule"
:placeholder="t('DataCollection.DeviceModel.ruleDialogFieldRulePlaceholder')"
class="!w-240px"
>
<el-option
v-for="opt in currentRuleOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
:disabled="isRuleDisabled(opt.value, item.rule)"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogRule')">
<div class="flex items-center gap-8px">
<el-select
v-model="item.id"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleAttributePlaceholder')"
class="!w-240px"
>
<el-option
v-for="attr in ruleAttributeOptions"
:key="attr.id"
:label="attr.attributeName || attr.attributeCode"
:value="attr.id"
/>
</el-select>
<el-select
v-model="item.operator"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleOperatorPlaceholder')"
class="!w-160px"
>
<el-option
v-for="opt in ruleOperatorOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-input
v-if="item.operator !== 'TRUE' && item.operator !== 'FALSE'"
v-model="item.operatorRule"
:placeholder="t('DataCollection.DeviceModel.ruleDialogRuleValuePlaceholder')"
class="!w-200px"
/>
<el-button type="danger" link @click="handleRemovePointRule(index)">
{{ t('DataCollection.DeviceModel.ruleDeleteRuleButton') }}
</el-button>
</div>
</el-form-item>
</div>
<el-form-item v-if="isRunningIdentifier" label=" ">
<el-button type="primary" link @click="handleAddPointRule">
+ {{ t('DataCollection.DeviceModel.ruleCreateButton') }}
</el-button>
</el-form-item>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="ruleDialogVisible = false">{{ t('DataCollection.DeviceModel.dialogCancel') }}</el-button>
<el-button type="primary" :loading="ruleFormLoading" @click="handleRuleSubmit">{{ t('DataCollection.DeviceModel.dialogOk') }}</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="createRuleDialogVisible" :title="t('DataCollection.DeviceModel.ruleCreateButton')" width="520px" draggable>
<el-form :model="createRuleForm" ref="createRuleFormRef" label-width="120px">
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogIdentifier')">
<el-input v-model="createRuleForm.identifier" disabled />
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogFieldName')">
<el-input
v-model="createRuleForm.fieldName"
:placeholder="t('DataCollection.DeviceModel.ruleSearchFieldNamePlaceholder')"
/>
</el-form-item>
<el-form-item :label="t('DataCollection.DeviceModel.ruleDialogDefaultValue')">
<el-input v-model="createRuleForm.defaultValue" disabled />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="createRuleDialogVisible = false">{{ t('DataCollection.DeviceModel.dialogCancel') }}</el-button>
<el-button type="primary" :loading="createRuleFormLoading" @click="handleCreateRuleSubmit">
{{ t('DataCollection.DeviceModel.dialogOk') }}
</el-button>
</span>
</template>
</el-dialog>
</el-tab-pane>
</el-tabs>
</template>
<el-empty
v-else
:description="t('DataCollection.DeviceModel.moduleName') + t('-') + t('DataCollection.DeviceModel.attributeModuleName')"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<DeviceModelForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import request from '@/config/axios'
import { DeviceModelApi, DeviceModelVO } from '@/api/iot/devicemodel'
import { DeviceModelAttributeApi, DeviceModelAttributeVO } from '@/api/iot/devicemodelattribute'
import DeviceModelForm from './DeviceModelForm.vue'
import ModelAttributeList from './components/ModelAttributeList.vue'
/** 采集设备模型 列表 */
defineOptions({ name: 'DeviceModel' })
const message = useMessage() //
const { t } = useI18n() //
const tableRef = ref()
const loading = ref(true) //
const list = ref<DeviceModelVO[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
code: undefined,
name: undefined,
protocol: undefined,
remark: undefined,
createTime: [],
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
const selectedIds = ref<number[]>([])
const handleSelectionChange = (rows: any[]) => {
selectedIds.value = rows?.map((row) => row.id).filter((id) => id !== undefined) ?? []
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await DeviceModelApi.getDeviceModelPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const buildIdsParam = (ids: number | number[]) => {
return Array.isArray(ids) ? ids.join(',') : String(ids)
}
const handleDelete = async (ids: number | number[]) => {
try {
//
await message.delConfirm()
//
await DeviceModelApi.deleteDeviceModel(buildIdsParam(ids))
message.success(t('common.delSuccess'))
selectedIds.value = []
tableRef.value?.clearSelection?.()
//
await getList()
} catch { }
}
const handleBatchDelete = async () => {
if (!selectedIds.value.length) {
message.error('请选择需要删除的数据')
return
}
await handleDelete(selectedIds.value)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const params: any = {
...queryParams,
ids: selectedIds.value.length ? selectedIds.value.join(',') : undefined
}
const data = await DeviceModelApi.exportDeviceModel(params)
download.excel(data, '采集设备模型.xls')
} catch {
} finally {
exportLoading.value = false
}
}
const attributeModelId = ref<number | undefined>(undefined)
const attributeModelName = ref('')
const modelAttributeTabLabel = computed(() => {
return '采集点'
})
const modelRuleTabLabel = computed(() => {
return '点位规则'
})
interface DeviceModelRuleVO {
id: number
identifier: string
fieldName: string
fieldRule: string
defaultValue: string
modelId: number
createTime?: string | number | Date
ruleAttributeId?: number
ruleOperator?: string
ruleValue?: string | number
alarmLevel?: string
}
const ruleLoading = ref(false)
const ruleList = ref<DeviceModelRuleVO[]>([])
const ruleTotal = ref(0)
const ruleQueryParams = reactive({
pageNo: 1,
pageSize: 10,
identifier: undefined as string | undefined,
fieldName: undefined as string | undefined,
fieldRule: undefined as string | undefined,
defaultValue: undefined as string | undefined,
alarmLevel: undefined as string | undefined,
modelId: undefined as number | undefined,
})
const ruleQueryFormRef = ref()
const ruleAttributeOptions = ref<DeviceModelAttributeVO[]>([])
const getRuleList = async () => {
if (!attributeModelId.value) return
ruleLoading.value = true
try {
const params = {
...ruleQueryParams,
modelId: attributeModelId.value,
}
const data = await request.get({ url: '/iot/device-model-rules/page', params })
const listData = Array.isArray((data as any)?.list) ? (data as any).list : (Array.isArray(data) ? data : [])
const totalData = (data as any)?.total ?? listData.length
ruleList.value = listData as DeviceModelRuleVO[]
ruleTotal.value = totalData
} finally {
ruleLoading.value = false
}
}
const handleRuleQuery = () => {
if (!attributeModelId.value) return
ruleQueryParams.pageNo = 1
getRuleList()
}
const resetRuleQuery = () => {
if (!attributeModelId.value) return
ruleQueryFormRef.value?.resetFields?.()
handleRuleQuery()
}
const loadRuleAttributeOptions = async (modelId: number) => {
try {
const res = await DeviceModelAttributeApi.getDeviceModelAttributeList(modelId)
const data = Array.isArray(res) ? res : (res as any)?.list ?? []
ruleAttributeOptions.value = data as DeviceModelAttributeVO[]
} catch {
ruleAttributeOptions.value = []
}
}
watch(
() => attributeModelId.value,
async (val) => {
ruleQueryParams.modelId = val ?? undefined
if (!val) {
ruleList.value = []
ruleTotal.value = 0
ruleAttributeOptions.value = []
return
}
ruleQueryParams.pageNo = 1
await Promise.all([getRuleList(), loadRuleAttributeOptions(val)])
}
)
const ruleDialogVisible = ref(false)
const ruleFormLoading = ref(false)
const ruleFormRef = ref()
const ruleForm = reactive<Partial<DeviceModelRuleVO>>({
id: undefined,
identifier: '',
fieldName: '',
fieldRule: '',
defaultValue: '',
modelId: undefined,
ruleAttributeId: undefined,
ruleOperator: undefined,
ruleValue: undefined,
alarmLevel: '',
})
const createRuleDialogVisible = ref(false)
const createRuleFormLoading = ref(false)
const createRuleFormRef = ref()
const createRuleForm = reactive({
identifier: 'ALARM',
fieldName: '',
defaultValue: '报警',
})
const extraPointRules = ref<
Array<{
id?: number
rule?: string
operator?: string
operatorRule?: string | number
}>
>([])
const runningRuleOptions = [
{ value: '1', label: '运行' },
{ value: '2', label: '待机中(不运行、没故障)' },
{ value: '3', label: '故障中(故障且待机)' },
{ value: '4', label: '报警中(故障且运行)' },
]
const alarmRuleOptions = [
{ value: '5', label: '报警' },
]
const currentRuleOptions = computed(() => {
const id = (ruleForm.identifier || '').toString().toUpperCase()
if (id === 'RUNNING') return runningRuleOptions
if (id === 'ALARM') return alarmRuleOptions
return []
})
const isRuleDisabled = (value: string, selfRule?: string | number) => {
if (!value) return false
const v = String(value)
const self = selfRule != null ? String(selfRule) : undefined
if (self === v) return false
if (ruleForm.fieldRule && String(ruleForm.fieldRule) === v && self !== v) return true
if (
extraPointRules.value.some(
(item) => item.rule != null && String(item.rule) === v && String(item.rule) !== self
)
) {
return true
}
return false
}
const ruleOperatorOptions = computed(() => getStrDictOptions('czsb_rules_conditions'))
const isRunningIdentifier = computed(() => {
return (ruleForm.identifier || '').toString().toUpperCase() === 'RUNNING'
})
const openCreateRuleForm = () => {
if (!attributeModelId.value) {
message.error('请先选择模型')
return
}
createRuleForm.identifier = 'ALARM'
createRuleForm.fieldName = '报警'
createRuleForm.defaultValue = '报警'
createRuleDialogVisible.value = true
}
const openRuleForm = async (row: DeviceModelRuleVO & { pointRulesVOList?: any[] }) => {
const modelId = row.modelId || attributeModelId.value
if (!modelId) {
message.error('模型信息缺失,无法加载点位规则')
return
}
await loadRuleAttributeOptions(modelId)
ruleForm.id = row.id
ruleForm.identifier = row.identifier
ruleForm.fieldName = row.fieldName
ruleForm.defaultValue = row.defaultValue
ruleForm.modelId = row.modelId
ruleForm.alarmLevel = row.alarmLevel
extraPointRules.value = []
const list = Array.isArray(row.pointRulesVOList) ? row.pointRulesVOList : []
if (list.length) {
const first = list[0] as any
const firstRule = first.rule ?? row.fieldRule
const firstAttrId = (() => {
if (first && first.code != null) {
const target = ruleAttributeOptions.value.find(
(item) => item.attributeCode === first.code
)
return target?.id
}
if (first && first.id != null) return first.id
return undefined
})()
ruleForm.fieldRule = firstRule as any
ruleForm.ruleAttributeId = firstAttrId as any
ruleForm.ruleOperator = first.operator as any
ruleForm.ruleValue = first.operatorRule as any
extraPointRules.value = list.slice(1).map((item: any) => ({
id: (() => {
if (item && item.code != null) {
const target = ruleAttributeOptions.value.find(
(opt) => opt.attributeCode === item.code
)
return target?.id
}
if (item && item.id != null) return item.id
return undefined
})(),
rule: item.rule,
operator: item.operator,
operatorRule: item.operatorRule,
}))
} else {
ruleForm.fieldRule = row.fieldRule
ruleForm.ruleAttributeId = row.ruleAttributeId
ruleForm.ruleOperator = row.ruleOperator
ruleForm.ruleValue = row.ruleValue as any
}
const options = currentRuleOptions.value
if (options.length && !options.some((item) => item.value === ruleForm.fieldRule)) {
ruleForm.fieldRule = options[0].value
}
ruleDialogVisible.value = true
}
const handleRuleSubmit = async () => {
if (!ruleForm.id) return
try {
ruleFormLoading.value = true
const pointRulesVOList: any[] = []
if (ruleForm.ruleAttributeId && ruleForm.ruleOperator) {
const attr = ruleAttributeOptions.value.find(
(item) => item.id === ruleForm.ruleAttributeId
)
if (attr && attr.attributeCode) {
pointRulesVOList.push({
code: attr.attributeCode,
rule: ruleForm.fieldRule,
operator: ruleForm.ruleOperator,
operatorRule: ruleForm.ruleValue,
})
}
}
extraPointRules.value.forEach((item) => {
if (!item.id || !item.operator) return
const attr = ruleAttributeOptions.value.find((opt) => opt.id === item.id)
if (!attr || !attr.attributeCode) return
pointRulesVOList.push({
code: attr.attributeCode,
rule: item.rule ?? ruleForm.fieldRule,
operator: item.operator,
operatorRule: item.operatorRule,
})
})
const payload = {
id: ruleForm.id,
identifier: ruleForm.identifier,
fieldName: ruleForm.fieldName,
defaultValue: ruleForm.defaultValue,
modelId: ruleForm.modelId,
alarmLevel: ruleForm.alarmLevel,
pointRulesVOList,
}
await request.put({ url: '/iot/device-model-rules/update', data: payload })
message.success('保存成功')
ruleDialogVisible.value = false
await getRuleList()
} finally {
ruleFormLoading.value = false
}
}
const handleRuleDelete = async (id: number) => {
if (!id) return
try {
await message.delConfirm()
await request.delete({ url: '/iot/device-model-rules/delete?id=' + id })
message.success(t('common.delSuccess'))
await getRuleList()
} catch { }
}
const handleAddPointRule = () => {
if (!isRunningIdentifier.value) return
const allOptions = currentRuleOptions.value || []
const used = new Set<string>()
if (ruleForm.fieldRule) used.add(String(ruleForm.fieldRule))
extraPointRules.value.forEach((item) => {
if (item.rule != null) used.add(String(item.rule))
})
const next = allOptions.find((opt) => !used.has(String(opt.value)))
if (!next) {
message.warning('已没有可用的点位规则可选')
return
}
extraPointRules.value.push({
rule: next.value as any,
id: undefined,
operator: undefined,
operatorRule: undefined,
})
}
const handleCreateRuleSubmit = async () => {
if (!attributeModelId.value) return
if (!createRuleForm.fieldName) {
message.warning('请输入名称')
return
}
try {
createRuleFormLoading.value = true
const payload = {
identifier: createRuleForm.identifier,
fieldName: createRuleForm.fieldName,
defaultValue: createRuleForm.defaultValue,
modelId: attributeModelId.value,
}
await request.post({ url: '/iot/device-model-rules/create', data: payload })
message.success('新增成功')
createRuleDialogVisible.value = false
await getRuleList()
} finally {
createRuleFormLoading.value = false
}
}
const handleRemovePointRule = (index: number) => {
if (index < 0 || index >= extraPointRules.value.length) return
extraPointRules.value.splice(index, 1)
}
const handleShowAttribute = (row: any) => {
attributeModelId.value = row?.id
attributeModelName.value = row?.name ?? ''
}
const handleCopy = async (id: number) => {
try {
//
await message.confirm('是否复制模型?')
await DeviceModelApi.copyDeviceModel(id)
message.success('复制成功')
await getList()
} catch { }
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>
Loading…
Cancel
Save