Compare commits

...

39 Commits

Author SHA1 Message Date
YunaiV
782337952d 【代码评审】IoT:场景联动的 review 2025-03-30 10:18:33 +08:00
芋道源码
30c9f0b872
!757 【功能完善】IoT: 场景联动
Merge pull request !757 from puhui999/feature/iot
2025-03-30 02:05:29 +00:00
puhui999
1bc2978bc6 【功能完善】IoT: 场景联动执行器 array、struct 类型数据编辑 2025-03-29 23:02:56 +08:00
puhui999
f46540759f 【功能完善】IoT: 场景联动 2025-03-29 20:27:38 +08:00
puhui999
4eb7188ecf 【功能完善】IoT: 场景联动物模型属性参数输入组件 2025-03-29 17:40:21 +08:00
puhui999
04cfded36f 【代码优化】IoT: 场景联动执行器设备控制器信息回显 2025-03-29 14:35:33 +08:00
puhui999
c8fce1e254 【代码优化】IoT: 场景联动彻底修复索引重用问题 2025-03-29 14:34:56 +08:00
puhui999
f52f4bcc36 【功能完善】IoT: 场景联动执行器消息类型为服务的情况 2025-03-29 13:49:51 +08:00
puhui999
24cdcd10c1 【代码优化】IoT: 场景联动注释格式统一 2025-03-29 13:04:50 +08:00
puhui999
279d88e729 【功能完善】IoT: 场景联动执行器-数据桥梁选择 2025-03-29 12:52:26 +08:00
puhui999
6b99fdb41f 【功能完善】IoT: 场景联动产品设备信息回显 2025-03-29 12:07:27 +08:00
puhui999
5fa9e4e855 【功能完善】IoT: 场景联动执行器配置 2025-03-28 18:20:53 +08:00
puhui999
fe0d5f92f6 【功能完善】IoT: 场景联动解决索引重用问题 2025-03-28 17:52:41 +08:00
puhui999
c06f7f9ebd 【功能完善】IoT: 场景联动执行器配置 2025-03-28 17:22:52 +08:00
puhui999
eadf26dc3b 【功能完善】IoT: 场景联动执行器配置 2025-03-28 15:54:12 +08:00
puhui999
fe905721bd 【功能完善】修复 jsonEditor 编辑回显不生效的问题 2025-03-28 15:05:13 +08:00
puhui999
bff2327eba 【功能完善】IoT: ThingModelTSL 2025-03-28 14:05:24 +08:00
puhui999
73d83e8a88 【功能完善】IoT: ThingModelTSL 2025-03-28 14:03:43 +08:00
puhui999
314a1c2254 【功能完善】修复 JsonEditor mode 切换不生效的 bug 2025-03-28 14:03:02 +08:00
puhui999
6265d6d923 【功能新增】基于 https://github.com/josdejong/jsoneditor 二次封装 JsonEditor 组件,提供 JSON 编辑器功能 2025-03-28 13:54:16 +08:00
YunaiV
7cb4c48d47 【代码评审】IoT:数据桥梁的 review 2025-03-25 21:07:18 +08:00
芋道源码
966f1b8b7e
!754 IoT: 场景联动
Merge pull request !754 from puhui999/feature/iot
2025-03-25 13:02:16 +00:00
puhui999
0961df37dd 【功能新增】IoT: 场景联动增加定时触发配置 2025-03-25 17:39:53 +08:00
puhui999
aed4ff0718 【功能新增】IoT: 产品物模型 TSL 展示 2025-03-25 17:08:38 +08:00
puhui999
d6da0cbc46 【代码优化】IoT: 数据桥梁 2025-03-25 16:33:18 +08:00
puhui999
416c7f42ab 【代码优化】IoT: 场景联动 2025-03-25 16:19:56 +08:00
YunaiV
cbdcffb20c 【代码评审】IoT:场景联动的 review 2025-03-23 08:40:37 +08:00
芋道源码
eaefbabea2
!746 【功能新增】IoT: 规则场景联动
Merge pull request !746 from puhui999/feature/iot
2025-03-23 00:11:17 +00:00
puhui999
7680ff83d0 【功能新增】IoT: 获取产品物模型 TSL 2025-03-21 18:12:35 +08:00
puhui999
0743d372c2 【功能新增】IoT: 获取产品物模型 TSL 2025-03-21 17:06:12 +08:00
puhui999
a410fb40f6 【功能新增】IoT: 设备选择器 2025-03-21 15:54:04 +08:00
puhui999
d166f7063d 【功能新增】IoT: 产品选择器 2025-03-21 15:22:26 +08:00
puhui999
bb2b32b051 【功能完善】IoT: 规则场景触发器相关组件 2025-03-21 14:46:19 +08:00
puhui999
6745594e3a 【功能完善】IoT: 规则场景触发器相关组件 2025-03-21 14:03:05 +08:00
puhui999
73d2c2005c 【功能完善】IoT: 规则场景监听器相关组件 2025-03-21 13:30:22 +08:00
puhui999
07277a6efb 【功能完善】IoT: 规则场景监听器相关组件 2025-03-21 12:13:18 +08:00
puhui999
9389cf8863 【功能完善】IoT: 规则场景监听器相关组件 2025-03-20 18:46:59 +08:00
puhui999
14ffb6483f 【功能新增】IoT: 规则场景监听器相关组件 2025-03-20 18:02:58 +08:00
puhui999
477b2439c5 【功能新增】IoT: 规则场景基础 CRUD 2025-03-18 17:37:58 +08:00
39 changed files with 4150 additions and 1391 deletions

View File

@ -51,6 +51,7 @@
"fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2",
"jsoneditor": "^10.1.3",
"lodash-es": "^4.17.21",
"markdown-it": "^14.1.0",
"markmap-common": "^0.16.0",
@ -67,7 +68,6 @@
"sortablejs": "^1.15.3",
"steady-xml": "^0.1.0",
"url": "^0.11.3",
"v3-jsoneditor": "^0.0.6",
"video.js": "^7.21.5",
"vue": "3.5.12",
"vue-dompurify-html": "^4.1.4",
@ -85,6 +85,7 @@
"@iconify/json": "^2.2.187",
"@intlify/unplugin-vue-i18n": "^2.0.0",
"@purge-icons/generated": "^0.9.0",
"@types/jsoneditor": "^9.9.5",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.21",
"@types/nprogress": "^0.2.3",

File diff suppressed because it is too large Load Diff

View File

@ -162,8 +162,20 @@ export const DeviceApi = {
return await request.get({ url: `/iot/device/log/page`, params })
},
// 获取设备MQTT连接参数
// 获取设备 MQTT 连接参数
getMqttConnectionParams: async (deviceId: number) => {
return await request.get({ url: `/iot/device/mqtt-connection-params`, params: { deviceId } })
},
// 根据 ProductKey 和 DeviceNames 获取设备列表
// TODO @puhui999getDeviceListByProductKeyAndNames 哈。项目的风格统一~
getDevicesByProductKeyAndNames: async (productKey: string, deviceNames: string[]) => {
return await request.get({
url: `/iot/device/list-by-product-key-and-names`,
params: {
productKey,
deviceNames: deviceNames.join(',')
}
})
}
}

View File

@ -78,5 +78,10 @@ export const ProductApi = {
// 查询产品(精简)列表
getSimpleProductList() {
return request.get({ url: '/iot/product/simple-list' })
},
// 根据 ProductKey 获取产品信息
getProductByKey: async (productKey: string) => {
return await request.get({ url: `/iot/product/get-by-key`, params: { productKey } })
}
}

View File

@ -80,17 +80,21 @@ export interface RedisStreamMQConfig extends Config {
}
/** 数据桥梁类型 */
// TODO @puhui999枚举用 number 可以么?
export const IoTDataBridgeConfigType = {
HTTP: '1',
TCP: '2',
WEBSOCKET: '3',
MQTT: '10',
DATABASE: '20',
REDIS_STREAM: '21',
ROCKETMQ: '30',
RABBITMQ: '31',
KAFKA: '32'
HTTP: 1,
TCP: 2,
WEBSOCKET: 3,
MQTT: 10,
DATABASE: 20,
REDIS_STREAM: 21,
ROCKETMQ: 30,
RABBITMQ: 31,
KAFKA: 32
} as const
export const IotDataBridgeDirectionEnum = {
INPUT: 1, // 输入
OUTPUT: 2 // 输出
} as const
// 数据桥梁 API
@ -120,8 +124,9 @@ export const DataBridgeApi = {
return await request.delete({ url: `/iot/data-bridge/delete?id=` + id })
},
// 导出数据桥梁 Excel
exportDataBridge: async (params) => {
return await request.download({ url: `/iot/data-bridge/export-excel`, params })
// 查询数据桥梁(精简)列表
// TODO @puhui999getDataBridgeSimpleList 哈。项目的风格统一~ 之前有几个,我写错了。。。
getSimpleDataBridgeList() {
return request.get({ url: '/iot/data-bridge/simple-list' })
}
}

View File

@ -0,0 +1,30 @@
import request from '@/config/axios'
import { IotRuleScene } from './scene.types'
// IoT 场景联动 API
export const RuleSceneApi = {
// 查询场景联动分页
getRuleScenePage: async (params: any) => {
return await request.get({ url: `/iot/rule-scene/page`, params })
},
// 查询场景联动详情
getRuleScene: async (id: number) => {
return await request.get({ url: `/iot/rule-scene/get?id=` + id })
},
// 新增场景联动
createRuleScene: async (data: IotRuleScene) => {
return await request.post({ url: `/iot/rule-scene/create`, data })
},
// 修改场景联动
updateRuleScene: async (data: IotRuleScene) => {
return await request.put({ url: `/iot/rule-scene/update`, data })
},
// 删除场景联动
deleteRuleScene: async (id: number) => {
return await request.delete({ url: `/iot/rule-scene/delete?id=` + id })
}
}

View File

@ -0,0 +1,134 @@
/**
* IoT
*/
// 枚举定义
const IotRuleSceneTriggerTypeEnum = {
DEVICE: 1, // 设备触发
TIMER: 2 // 定时触发
} as const
const IotRuleSceneActionTypeEnum = {
DEVICE_CONTROL: 1, // 设备执行
ALERT: 2, // 告警执行
DATA_BRIDGE: 3 // 桥接执行
} as const
const IotDeviceMessageTypeEnum = {
PROPERTY: 'property', // 属性
SERVICE: 'service', // 服务
EVENT: 'event' // 事件
} as const
const IotDeviceMessageIdentifierEnum = {
PROPERTY_SET: 'set', // 属性设置
SERVICE_INVOKE: '${identifier}' // 服务调用
} as const
const IotRuleSceneTriggerConditionParameterOperatorEnum = {
EQUALS: { name: '等于', value: '=' }, // 等于
NOT_EQUALS: { name: '不等于', value: '!=' }, // 不等于
GREATER_THAN: { name: '大于', value: '>' }, // 大于
GREATER_THAN_OR_EQUALS: { name: '大于等于', value: '>=' }, // 大于等于
LESS_THAN: { name: '小于', value: '<' }, // 小于
LESS_THAN_OR_EQUALS: { name: '小于等于', value: '<=' }, // 小于等于
IN: { name: '在...之中', value: 'in' }, // 在...之中
NOT_IN: { name: '不在...之中', value: 'not in' }, // 不在...之中
BETWEEN: { name: '在...之间', value: 'between' }, // 在...之间
NOT_BETWEEN: { name: '不在...之间', value: 'not between' }, // 不在...之间
LIKE: { name: '字符串匹配', value: 'like' }, // 字符串匹配
NOT_NULL: { name: '非空', value: 'not null' } // 非空
} as const
const IotAlertConfigReceiveTypeEnum = {
SMS: 1, // 短信
MAIL: 2, // 邮箱
NOTIFY: 3 // 通知
} as const
// 基础接口
interface TenantBaseDO {
createTime?: Date // 创建时间
updateTime?: Date // 更新时间
creator?: string // 创建者
updater?: string // 更新者
deleted?: boolean // 是否删除
tenantId?: number // 租户编号
}
// 触发条件参数
interface TriggerConditionParameter {
identifier0: string // 标识符(事件、服务)
identifier: string // 标识符(属性)
operator: string // 操作符
value: string // 比较值
}
// 触发条件
interface TriggerCondition {
type: string // 消息类型
identifier: string // 消息标识符
parameters: TriggerConditionParameter[] // 参数数组
}
// 触发器配置
interface TriggerConfig {
key: any // 解决组件索引重用
type: number // 触发类型
productKey: string // 产品标识
deviceNames: string[] // 设备名称数组
conditions?: TriggerCondition[] // 触发条件数组
cronExpression?: string // CRON 表达式
}
// 执行设备控制
interface ActionDeviceControl {
productKey: string // 产品标识
deviceNames: string[] // 设备名称数组
type: string // 消息类型
identifier: string // 消息标识符
data: Record<string, any> // 具体数据
}
// 告警执行配置
interface ActionAlert {
receiveType: number // 接收方式
phoneNumbers?: string[] // 手机号列表
emails?: string[] // 邮箱列表
content: string // 通知内容
}
// 执行器配置
interface ActionConfig {
key: any // 解决组件索引重用 TODO @puhui999看看有没更好的解决方案呢。
type: number // 执行类型
deviceControl?: ActionDeviceControl // 设备控制
alert?: ActionAlert // 告警执行
dataBridgeId?: number // 数据桥接编号
}
// 主接口
interface IotRuleScene extends TenantBaseDO {
id: number // 场景编号
name: string // 场景名称
description: string // 场景描述
status: number // 场景状态
triggers: TriggerConfig[] // 触发器数组
actions: ActionConfig[] // 执行器数组
}
export {
IotRuleScene,
TriggerConfig,
TriggerCondition,
TriggerConditionParameter,
ActionConfig,
ActionDeviceControl,
ActionAlert,
IotRuleSceneTriggerTypeEnum,
IotRuleSceneActionTypeEnum,
IotDeviceMessageTypeEnum,
IotDeviceMessageIdentifierEnum,
IotRuleSceneTriggerConditionParameterOperatorEnum,
IotAlertConfigReceiveTypeEnum
}

View File

@ -58,11 +58,10 @@ export const ThingModelApi = {
return await request.get({ url: `/iot/thing-model/list`, params })
},
// 获得产品物模型
getThingModelListByProductId: async (params: any) => {
// 获得产品物模型 TSL
getThingModelTSLByProductId: async (productId: number) => {
return await request.get({
url: `/iot/thing-model/list-by-product-id`,
params
url: `/iot/thing-model/get-tsl?productId=${productId}`
})
},

View File

@ -18,8 +18,8 @@ export const getSimpleDeptList = async (): Promise<DeptVO[]> => {
}
// 查询部门列表
export const getDeptPage = async (params: PageParam) => {
return await request.get({ url: '/system/dept/list', params })
export const getDeptList = async () => {
return await request.get({ url: '/system/dept/list' })
}
// 查询部门详情

View File

@ -12,11 +12,6 @@ export interface RoleVO {
createTime: Date
}
export interface UpdateStatusReqVO {
id: number
status: number
}
// 查询角色列表
export const getRolePage = async (params: PageParam) => {
return await request.get({ url: '/system/role/page', params })
@ -42,18 +37,13 @@ export const updateRole = async (data: RoleVO) => {
return await request.put({ url: '/system/role/update', data })
}
// 修改角色状态
export const updateRoleStatus = async (data: UpdateStatusReqVO) => {
return await request.put({ url: '/system/role/update-status', data })
}
// 删除角色
export const deleteRole = async (id: number) => {
return await request.delete({ url: '/system/role/delete?id=' + id })
}
// 导出角色
export const exportRole = (params) => {
export const exportRole = (params: any) => {
return request.download({
url: '/system/role/export-excel',
params

View File

@ -0,0 +1,3 @@
import JsonEditor from './src/JsonEditor.vue'
export { JsonEditor }

View File

@ -0,0 +1,126 @@
<template>
<div ref="jsonEditorContainer" class="json-editor" :style="{ height }"></div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import JSONEditor, { JSONEditorMode, JSONEditorOptions } from 'jsoneditor'
import 'jsoneditor/dist/jsoneditor.min.css'
import { JsonEditorEmits, JsonEditorExpose, JsonEditorProps } from '../types'
/** 基于 https://github.com/josdejong/jsoneditor 二次封装组件,提供 JSON 编辑器功能。 */
defineOptions({ name: 'JsonEditor' })
const props = withDefaults(defineProps<JsonEditorProps>(), {
mode: 'view' as JSONEditorMode,
height: '400px',
showModeSelection: false,
showNavigationBar: false,
showStatusBar: false,
showMainMenuBar: true
})
const emits = defineEmits<JsonEditorEmits>()
const jsonObj = useVModel(props, 'modelValue', emits) as Ref<any>
const jsonEditorContainer = ref<HTMLElement | null>(null)
let jsonEditor: JSONEditor | null = null
//
const height = props.height
// JSONEditor
const initJsonEditor = () => {
if (!jsonEditorContainer.value) return
//
const options: JSONEditorOptions = {
mode: props.mode,
modes: props.showModeSelection
? (['tree', 'code', 'form', 'text', 'view', 'preview'] as JSONEditorMode[])
: undefined,
navigationBar: props.showNavigationBar,
statusBar: props.showStatusBar,
mainMenuBar: props.showMainMenuBar,
onChange: () => {
jsonObj.value = jsonEditor?.get()
emits('change', jsonEditor?.get())
},
onValidationError: (errors: any) => {
emits('error', errors)
},
...props.options
} as JSONEditorOptions
// JSONEditor
jsonEditor = new JSONEditor(jsonEditorContainer.value, options)
//
if (jsonObj.value) {
jsonEditor.set(jsonObj.value)
}
if (props.mode === 'view') {
jsonEditor?.expandAll() //
}
}
//
watch(
() => jsonObj.value,
(newValue) => {
if (!jsonEditor) return
try {
//
const currentJson = jsonEditor.get()
if (JSON.stringify(currentJson) !== JSON.stringify(newValue)) {
jsonEditor.update(newValue)
}
} catch (error) {
console.error('JSON更新失败:', error)
}
},
{ deep: true }
)
//
watch(
() => props.mode,
(newMode) => {
if (!jsonEditor) return
try {
jsonEditor.setMode(newMode)
} catch (error) {
console.error('切换模式失败:', error)
}
}
)
//
onMounted(() => {
initJsonEditor()
})
onBeforeUnmount(() => {
if (jsonEditor) {
jsonEditor.destroy()
jsonEditor = null
}
})
//
defineExpose<JsonEditorExpose>({
// 便JSONEditor
getEditor: () => jsonEditor
})
</script>
<style lang="scss" scoped>
/* 隐藏 Ace 编辑器的 powered by ace 标记 */
:deep(.jsoneditor-menu) {
/* 隐藏 powered by ace 标记 */
.jsoneditor-poweredBy {
display: none !important;
}
}
</style>

View File

@ -0,0 +1,80 @@
import { JSONEditorOptions, JSONEditorMode } from 'jsoneditor'
export interface JsonEditorProps {
/**
* JSON数据
*/
modelValue: any
/**
*
* @default 'tree'
*/
mode?: JSONEditorMode
/**
*
* @default '400px'
*/
height?: string
/**
*
* @default false
*/
showModeSelection?: boolean
/**
*
* @default false
*/
showNavigationBar?: boolean
/**
*
* @default true
*/
showStatusBar?: boolean
/**
*
* @default true
*/
showMainMenuBar?: boolean
/**
* JSONEditor配置选项
* @see https://github.com/josdejong/jsoneditor/blob/develop/docs/api.md
*/
options?: Partial<JSONEditorOptions>
}
/**
* JsonEditor组件触发的事件
*/
export interface JsonEditorEmits {
/**
*
*/
(e: 'update:modelValue', value: any): void
/**
*
*/
(e: 'change', value: any): void
/**
*
*/
(e: 'error', errors: any): void
}
/**
* JsonEditor组件暴露的方法
*/
export interface JsonEditorExpose {
/**
* JSONEditor实例
*/
getEditor: () => any
}

View File

@ -245,5 +245,8 @@ export enum DICT_TYPE {
IOT_PLUGIN_STATUS = 'iot_plugin_status', // IOT 插件状态
IOT_PLUGIN_TYPE = 'iot_plugin_type', // IOT 插件类型
IOT_DATA_BRIDGE_DIRECTION_ENUM = 'iot_data_bridge_direction_enum', // 桥梁方向
IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum' // 桥梁类型
IOT_DATA_BRIDGE_TYPE_ENUM = 'iot_data_bridge_type_enum', // 桥梁类型
IOT_DEVICE_MESSAGE_TYPE_ENUM = 'iot_device_message_type_enum', // IoT 设备消息类型枚举
IOT_RULE_SCENE_TRIGGER_TYPE_ENUM = 'iot_rule_scene_trigger_type_enum', // IoT 场景流转的触发类型枚举
IOT_RULE_SCENE_ACTION_TYPE_ENUM = 'iot_rule_scene_action_type_enum' // IoT 规则场景的触发类型枚举
}

View File

@ -0,0 +1,303 @@
<!-- 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>

View File

@ -8,24 +8,10 @@
class="my-4"
description="如需编辑文件,请点击下方编辑按钮"
/>
<!-- JSON 编辑器读模式 -->
<Vue3Jsoneditor
v-if="isEditing"
<JsonEditor
v-model="config"
:options="editorOptions"
height="500px"
currentMode="code"
@error="onError"
/>
<!-- JSON 编辑器写模式 -->
<Vue3Jsoneditor
v-else
v-model="config"
:options="editorOptions"
height="500px"
currentMode="view"
v-loading.fullscreen.lock="loading"
:mode="isEditing ? 'code' : 'view'"
height="600px"
@error="onError"
/>
<div class="mt-5 text-center">
@ -40,9 +26,11 @@
</template>
<script lang="ts" setup>
import Vue3Jsoneditor from 'v3-jsoneditor/src/Vue3Jsoneditor.vue'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { jsonParse } from '@/utils'
import { isEmpty } from '@/utils/is'
defineOptions({ name: 'DeviceDetailConfig' })
const props = defineProps<{
device: DeviceVO
@ -63,12 +51,6 @@ watchEffect(() => {
})
const isEditing = ref(false) //
const editorOptions = computed(() => ({
mainMenuBar: false,
navigationBar: false,
statusBar: false
})) // JSON
/** 启用编辑模式的函数 */
const enableEdit = () => {
isEditing.value = true
@ -112,8 +94,11 @@ const updateDeviceConfig = async () => {
}
/** 处理 JSON 编辑器错误的函数 */
const onError = (e: any) => {
console.log('onError', e)
const onError = (errors: any) => {
if (isEmpty(errors)) {
hasJsonError.value = false
return
}
hasJsonError.value = true
}
</script>

View File

@ -0,0 +1,220 @@
<!-- 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="68px"
>
<el-form-item label="产品名称" prop="name">
<el-input
v-model="queryParams.name"
class="!w-240px"
clearable
placeholder="请输入产品名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="ProductKey" prop="productKey">
<el-input
v-model="queryParams.productKey"
class="!w-240px"
clearable
placeholder="请输入产品标识"
@keyup.enter="handleQuery"
/>
</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 align="center" label="名称" prop="name" />
<el-table-column align="center" label="ProductKey" prop="productKey" />
<el-table-column align="center" label="品类" prop="categoryName" />
<el-table-column align="center" label="设备类型" 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 align="center" label="产品图标" prop="icon">
<template #default="scope">
<el-image
v-if="scope.row.icon"
:preview-src-list="[scope.row.icon]"
:src="scope.row.icon"
class="w-40px h-40px"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column align="center" label="产品图片" prop="picture">
<template #default="scope">
<el-image
v-if="scope.row.picUrl"
:preview-src-list="[scope.row.picture]"
:src="scope.row.picUrl"
class="w-40px h-40px"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column
:formatter="dateFormatter"
align="center"
label="创建时间"
prop="createTime"
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 } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { ProductApi, ProductVO } from '@/api/iot/product/product'
defineOptions({ name: 'IoTProductTableSelect' })
const props = defineProps({
multiple: {
type: Boolean,
default: false
}
})
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('产品选择器')
const formLoading = ref(false)
const loading = ref(true) //
const list = ref<ProductVO[]>([]) //
const total = ref(0) //
const selectedProducts = ref<ProductVO[]>([]) //
const selectedId = ref<number>() // ID
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
productKey: undefined
})
const queryFormRef = ref() //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ProductApi.getProductPage(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
//
selectedProducts.value = []
selectedId.value = undefined
await getList()
}
defineExpose({ open })
/** 处理行点击事件 */
const tableRef = ref()
const handleRowClick = (row: ProductVO) => {
if (props.multiple) {
tableRef.value?.toggleRowSelection(row)
} else {
selectedId.value = row.id
selectedProducts.value = [row]
}
}
/** 处理单选变更事件 */
const handleRadioChange = (row: ProductVO) => {
selectedProducts.value = [row]
}
/** 处理选择变更事件 */
const handleSelectionChange = (selection: ProductVO[]) => {
if (props.multiple) {
selectedProducts.value = selection
}
}
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
if (selectedProducts.value.length === 0) {
message.warning(props.multiple ? '请至少选择一个产品' : '请选择一个产品')
return
}
emit('success', props.multiple ? selectedProducts.value : selectedProducts.value[0])
dialogVisible.value = false
}
</script>

View File

@ -46,7 +46,7 @@
v-if="showConfig(IoTDataBridgeConfigType.RABBITMQ)"
v-model="formData.config"
/>
<RedisStreamMQConfigForm
<RedisStreamConfigForm
v-if="showConfig(IoTDataBridgeConfigType.REDIS_STREAM)"
v-model="formData.config"
/>
@ -73,13 +73,19 @@
</template>
<script lang="ts" setup>
import { DICT_TYPE, getDictObj, getIntDictOptions } from '@/utils/dict'
import { DataBridgeApi, DataBridgeVO, IoTDataBridgeConfigType } from '@/api/iot/rule/databridge'
import { CommonStatusEnum } from '@/utils/constants'
import {
DataBridgeApi,
DataBridgeVO,
IoTDataBridgeConfigType,
IotDataBridgeDirectionEnum
} from '@/api/iot/rule/databridge'
import {
HttpConfigForm,
KafkaMQConfigForm,
MqttConfigForm,
RabbitMQConfigForm,
RedisStreamMQConfigForm,
RedisStreamConfigForm,
RocketMQConfigForm
} from './config'
@ -94,9 +100,9 @@ const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref<DataBridgeVO>({
status: 0,
direction: 1, // TODO @puhui999:
type: 1, // TODO @puhui999:
status: CommonStatusEnum.ENABLE,
direction: IotDataBridgeDirectionEnum.INPUT,
type: IoTDataBridgeConfigType.HTTP,
config: {} as any
})
const formRules = reactive({
@ -139,9 +145,9 @@ const formRules = reactive({
})
const formRef = ref() // Ref
const showConfig = computed(() => (val: string) => {
const showConfig = computed(() => (val: number) => {
const dict = getDictObj(DICT_TYPE.IOT_DATA_BRIDGE_TYPE_ENUM, formData.value.type)
return dict && dict.value + '' === val
return dict && dict.value + '' === val + ''
}) // Config
/** 打开弹窗 */
@ -196,10 +202,9 @@ const handleTypeChange = (val: number) => {
/** 重置表单 */
const resetForm = () => {
formData.value = {
// TODO @puhui999
status: 0,
direction: 1,
type: 1,
status: CommonStatusEnum.ENABLE,
direction: IotDataBridgeDirectionEnum.INPUT,
type: IoTDataBridgeConfigType.HTTP,
config: {} as any
}
formRef.value?.resetFields()

View File

@ -73,7 +73,7 @@ onMounted(() => {
}
config.value = {
type: IoTDataBridgeConfigType.HTTP,
type: IoTDataBridgeConfigType.HTTP + '', // 使
url: '',
method: 'POST',
headers: {},

View File

@ -34,7 +34,7 @@ onMounted(() => {
return
}
config.value = {
type: IoTDataBridgeConfigType.KAFKA,
type: IoTDataBridgeConfigType.KAFKA + '', // 使
bootstrapServers: '',
username: '',
password: '',

View File

@ -34,7 +34,7 @@ onMounted(() => {
return
}
config.value = {
type: IoTDataBridgeConfigType.MQTT,
type: IoTDataBridgeConfigType.MQTT + '', // 使
url: '',
username: '',
password: '',

View File

@ -49,7 +49,7 @@ onMounted(() => {
return
}
config.value = {
type: IoTDataBridgeConfigType.RABBITMQ,
type: IoTDataBridgeConfigType.RABBITMQ + '', // 使
host: '',
port: 5672,
virtualHost: '/',

View File

@ -1,4 +1,3 @@
<!-- TODO @puhui999去掉 MQ 关键字哈 -->
<template>
<el-form-item label="主机地址" prop="config.host">
<el-input v-model="config.host" placeholder="请输入主机地址localhost" />
@ -47,7 +46,7 @@ onMounted(() => {
return
}
config.value = {
type: IoTDataBridgeConfigType.REDIS_STREAM,
type: IoTDataBridgeConfigType.REDIS_STREAM + '', // 使
host: '',
port: 6379,
password: '',

View File

@ -45,7 +45,7 @@ onMounted(() => {
return
}
config.value = {
type: IoTDataBridgeConfigType.ROCKETMQ,
type: IoTDataBridgeConfigType.ROCKETMQ + '', // 使
nameServer: '',
accessKey: '',
secretKey: '',

View File

@ -3,7 +3,7 @@ import MqttConfigForm from './MqttConfigForm.vue'
import RocketMQConfigForm from './RocketMQConfigForm.vue'
import KafkaMQConfigForm from './KafkaMQConfigForm.vue'
import RabbitMQConfigForm from './RabbitMQConfigForm.vue'
import RedisStreamMQConfigForm from './RedisStreamMQConfigForm.vue'
import RedisStreamConfigForm from './RedisStreamConfigForm.vue'
export {
HttpConfigForm,
@ -11,5 +11,5 @@ export {
RocketMQConfigForm,
KafkaMQConfigForm,
RabbitMQConfigForm,
RedisStreamMQConfigForm
RedisStreamConfigForm
}

View File

@ -0,0 +1,204 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="1080px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-row>
<el-col :span="12">
<el-form-item label="场景名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入场景名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<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"
:value="dict.value"
>
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="场景描述" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入场景描述" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-divider content-position="left">触发器配置</el-divider>
<device-listener
v-for="(trigger, index) in formData.triggers"
:key="trigger.key"
:model-value="trigger"
@update:model-value="(val) => (formData.triggers[index] = val)"
class="mb-10px"
>
<el-button type="danger" round size="small" @click="removeTrigger(index)">
<Icon icon="ep:delete" />
</el-button>
</device-listener>
<el-button class="ml-10px!" type="primary" size="small" @click="addTrigger">
添加触发器
</el-button>
</el-col>
<el-col :span="24">
<el-divider content-position="left">执行器配置</el-divider>
<action-executor
v-for="(action, index) in formData.actions"
:key="action.key"
:model-value="action"
@update:model-value="(val) => (formData.actions[index] = val)"
class="mb-10px"
>
<el-button type="danger" round size="small" @click="removeAction(index)">
<Icon icon="ep:delete" />
</el-button>
</action-executor>
<el-button class="ml-10px!" type="primary" size="small" @click="addAction">
添加执行器
</el-button>
</el-col>
</el-row>
</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 { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { RuleSceneApi } from '@/api/iot/rule/scene'
import DeviceListener from './components/listener/DeviceListener.vue'
import { CommonStatusEnum } from '@/utils/constants'
import {
ActionConfig,
IotDeviceMessageIdentifierEnum,
IotDeviceMessageTypeEnum,
IotRuleScene,
IotRuleSceneActionTypeEnum,
IotRuleSceneTriggerTypeEnum,
TriggerConfig
} from '@/api/iot/rule/scene/scene.types'
import ActionExecutor from './components/action/ActionExecutor.vue'
import { generateUUID } from '@/utils'
/** IoT 场景联动表单 */
defineOptions({ name: 'IotRuleSceneForm' })
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<IotRuleScene>({
status: CommonStatusEnum.ENABLE,
triggers: [] as TriggerConfig[],
actions: [] as ActionConfig[]
} as IotRuleScene)
const formRules = reactive({
name: [{ required: true, message: '场景名称不能为空', trigger: 'blur' }],
status: [{ required: true, message: '场景状态不能为空', trigger: 'blur' }],
triggers: [{ required: true, message: '触发器数组不能为空', trigger: 'blur' }],
actions: [{ required: true, message: '执行器数组不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 添加触发器 */
const addTrigger = () => {
formData.value.triggers.push({
key: generateUUID(), //
type: IotRuleSceneTriggerTypeEnum.DEVICE,
productKey: '',
deviceNames: [],
conditions: [
{
type: IotDeviceMessageTypeEnum.PROPERTY,
identifier: IotDeviceMessageIdentifierEnum.PROPERTY_SET,
parameters: []
}
]
})
}
/** 移除触发器 */
const removeTrigger = (index: number) => {
formData.value.triggers.splice(index, 1)
}
/** 添加执行器 */
const addAction = () => {
formData.value.actions.push({
key: generateUUID(), //
type: IotRuleSceneActionTypeEnum.DEVICE_CONTROL
} as ActionConfig)
}
/** 移除执行器 */
const removeAction = (index: number) => {
formData.value.actions.splice(index, 1)
}
/** 打开弹窗 */
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 {
const data = (await RuleSceneApi.getRuleScene(id)) as IotRuleScene
//
data.triggers?.forEach((item) => (item.key = generateUUID()))
data.actions?.forEach((item) => (item.key = generateUUID()))
formData.value = data
} 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 IotRuleScene
if (formType.value === 'create') {
await RuleSceneApi.createRuleScene(data)
message.success(t('common.createSuccess'))
} else {
await RuleSceneApi.updateRuleScene(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
status: CommonStatusEnum.ENABLE,
triggers: [] as TriggerConfig[],
actions: [] as ActionConfig[]
} as IotRuleScene
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,81 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" :appendToBody="true" v-loading="loading">
<div class="flex h-600px">
<!-- 左侧物模型属性view 模式 -->
<div class="w-1/2 border-r border-gray-200 pr-2 overflow-auto">
<JsonEditor :model-value="thingModel" mode="view" height="600px" />
</div>
<!-- 右侧 JSON 编辑器code 模式 -->
<div class="w-1/2 pl-2 overflow-auto">
<JsonEditor v-model="editableModelTSL" mode="code" height="600px" @error="handleError" />
</div>
</div>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave" :disabled="hasJsonError">保存</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { isEmpty } from '@/utils/is'
defineOptions({ name: 'ThingModelDualView' })
const props = defineProps<{
modelValue: any //
thingModel: any[] //
}>()
const emits = defineEmits(['update:modelValue', 'change'])
const message = useMessage()
const dialogVisible = ref(false) //
const dialogTitle = ref('物模型编辑器') //
const editableModelTSL = ref([
{
identifier: '对应左侧 identifier 属性值',
value: '如果 identifier 是 int 类型则输入数字,具体查看产品物模型定义'
}
]) //
const hasJsonError = ref(false) // JSON
const loading = ref(false) //
/** 打开弹窗 */
const open = () => {
try {
//
if (props.modelValue) {
editableModelTSL.value = JSON.parse(props.modelValue)
}
} catch (e) {
message.error('物模型编辑器参数')
console.error(e)
} finally {
dialogVisible.value = true
//
hasJsonError.value = false
}
}
defineExpose({ open }) //
/** 保存修改 */
const handleSave = async () => {
try {
await message.confirm('确定要保存物模型参数吗?')
emits('update:modelValue', JSON.stringify(editableModelTSL.value))
message.success('保存成功')
dialogVisible.value = false
} catch {}
}
/** 处理 JSON 编辑器错误的函数 */
const handleError = (errors: any) => {
if (isEmpty(errors)) {
hasJsonError.value = false
return
}
hasJsonError.value = true
}
</script>

View File

@ -0,0 +1,138 @@
<template>
<div class="flex items-center">
<!-- 数值类型输入框 -->
<template v-if="isNumeric">
<el-input
v-model="value"
class="w-1/1!"
:placeholder="`请输入${dataSpecs.unitName ? dataSpecs.unitName : '数值'}`"
>
<template #append> {{ dataSpecs.unit }} </template>
</el-input>
</template>
<!-- 布尔类型使用开关 -->
<template v-else-if="isBool">
<el-switch
v-model="value"
size="large"
:active-text="dataSpecsList[1].name"
:active-value="dataSpecsList[1].value"
:inactive-text="dataSpecsList[0].name"
:inactive-value="dataSpecsList[0].value"
/>
</template>
<!-- 枚举类型使用下拉选择 -->
<template v-else-if="isEnum">
<el-select class="w-1/1!" v-model="value">
<el-option
v-for="(item, index) in dataSpecsList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</template>
<!-- 时间类型使用时间选择器 -->
<template v-else-if="isDate">
<el-date-picker
class="w-1/1!"
v-model="value"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择日期时间"
/>
</template>
<!-- 文本类型使用文本输入框 -->
<template v-else-if="isText">
<el-input
class="w-1/1!"
v-model="value"
:maxlength="dataSpecs?.length"
:show-word-limit="true"
placeholder="请输入文本"
/>
</template>
<!-- arraystruct 直接输入 -->
<template v-else>
<el-input class="w-1/1!" :model-value="value" disabled placeholder="请输入值">
<template #append>
<el-button type="primary" @click="openJsonEditor">编辑</el-button>
</template>
</el-input>
<!-- arraystruct 类型数据编辑 -->
<ThingModelDualView
ref="thingModelDualViewRef"
v-model="value"
:thing-model="dataSpecsList"
/>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useVModel } from '@vueuse/core'
import { DataSpecsDataType } from '@/views/iot/thingmodel/config'
import ThingModelDualView from './ThingModelDualView.vue'
/** 物模型属性参数输入组件 */
defineOptions({ name: 'ThingModelParamInput' })
const props = defineProps<{
modelValue: any //
thingModel: any //
}>()
const emits = defineEmits(['update:modelValue', 'change'])
const value = useVModel(props, 'modelValue', emits)
const thingModelDualViewRef = ref<InstanceType<typeof ThingModelDualView>>()
const openJsonEditor = () => {
thingModelDualViewRef.value?.open()
}
/** 计算属性:判断数据类型 */
const isNumeric = computed(() =>
[DataSpecsDataType.INT, DataSpecsDataType.FLOAT, DataSpecsDataType.DOUBLE].includes(
props.thingModel?.dataType as any
)
)
const isBool = computed(() => props.thingModel?.dataType === DataSpecsDataType.BOOL)
const isEnum = computed(() => props.thingModel?.dataType === DataSpecsDataType.ENUM)
const isDate = computed(() => props.thingModel?.dataType === DataSpecsDataType.DATE)
const isText = computed(() => props.thingModel?.dataType === DataSpecsDataType.TEXT)
/** 获取数据规格 */
const dataSpecs = computed(() => {
if (isNumeric.value || isDate.value || isText.value) {
return props.thingModel?.dataSpecs || {}
}
return {}
})
const dataSpecsList = computed(() => {
if (
isBool.value ||
isEnum.value ||
[DataSpecsDataType.ARRAY, DataSpecsDataType.STRUCT].includes(props.thingModel?.dataType)
) {
return props.thingModel?.dataSpecsList || []
}
return []
})
/** 物模型切换重置值 */
watch(
() => props.thingModel?.dataType,
(_, oldValue) => {
if (!oldValue) {
return
}
value.value = undefined
},
{ deep: true }
)
</script>

View File

@ -0,0 +1,248 @@
<template>
<div>
<div class="m-10px">
<!-- 产品设备回显区域 -->
<div class="relative bg-[#eff3f7] h-50px flex items-center px-10px">
<div class="flex items-center mr-60px">
<span class="mr-10px">执行动作</span>
<el-select
v-model="actionConfig.type"
class="!w-240px"
clearable
placeholder="请选择执行类型"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_RULE_SCENE_ACTION_TYPE_ENUM)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</div>
<div
v-if="actionConfig.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL"
class="flex items-center mr-60px"
>
<span class="mr-10px">产品</span>
<el-button type="primary" @click="handleSelectProduct" size="small" plain>
{{ product ? product.name : '选择产品' }}
</el-button>
</div>
<div
v-if="actionConfig.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL"
class="flex items-center mr-60px"
>
<span class="mr-10px">设备</span>
<el-button type="primary" @click="handleSelectDevice" size="small" plain>
{{ isEmpty(deviceList) ? '选择设备' : deviceList.map((d) => d.deviceName).join(',') }}
</el-button>
</div>
<!-- 删除执行器 -->
<div class="absolute top-auto right-16px bottom-auto">
<el-tooltip content="删除执行器" placement="top">
<slot></slot>
</el-tooltip>
</div>
</div>
<!-- 设备控制执行器 -->
<DeviceControlAction
v-if="actionConfig.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL"
:model-value="actionConfig.deviceControl"
:product-id="product?.id"
:product-key="product?.productKey"
@update:model-value="(val) => (actionConfig.deviceControl = val)"
/>
<!-- 告警执行器 -->
<AlertAction
v-else-if="actionConfig.type === IotRuleSceneActionTypeEnum.ALERT"
:model-value="actionConfig.alert"
@update:model-value="(val) => (actionConfig.alert = val)"
/>
<!-- 数据桥接执行器 -->
<DataBridgeAction
v-else-if="actionConfig.type === IotRuleSceneActionTypeEnum.DATA_BRIDGE"
:model-value="actionConfig.dataBridgeId"
@update:model-value="(val) => (actionConfig.dataBridgeId = val)"
/>
</div>
<!-- 产品设备的选择 -->
<ProductTableSelect ref="productTableSelectRef" @success="handleProductSelect" />
<DeviceTableSelect
ref="deviceTableSelectRef"
multiple
:product-id="product?.id"
@success="handleDeviceSelect"
/>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { isEmpty } from '@/utils/is'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import ProductTableSelect from '@/views/iot/product/product/components/ProductTableSelect.vue'
import DeviceTableSelect from '@/views/iot/device/device/components/DeviceTableSelect.vue'
import DeviceControlAction from './DeviceControlAction.vue'
import AlertAction from './AlertAction.vue'
import DataBridgeAction from './DataBridgeAction.vue'
import { ProductApi, ProductVO } from '@/api/iot/product/product'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import {
ActionAlert,
ActionConfig,
ActionDeviceControl,
IotDeviceMessageIdentifierEnum,
IotDeviceMessageTypeEnum,
IotRuleSceneActionTypeEnum
} from '@/api/iot/rule/scene/scene.types'
/** 场景联动之执行器组件 */
defineOptions({ name: 'ActionExecutor' })
const props = defineProps<{ modelValue: any }>()
const emits = defineEmits(['update:modelValue'])
const actionConfig = useVModel(props, 'modelValue', emits) as Ref<ActionConfig>
const message = useMessage()
/** 初始化执行器结构 */
const initActionConfig = () => {
if (!actionConfig.value) {
actionConfig.value = { type: IotRuleSceneActionTypeEnum.DEVICE_CONTROL } as ActionConfig
}
//
if (
actionConfig.value.type === IotRuleSceneActionTypeEnum.DEVICE_CONTROL &&
!actionConfig.value.deviceControl
) {
actionConfig.value.deviceControl = {
productKey: '',
deviceNames: [],
type: IotDeviceMessageTypeEnum.PROPERTY,
identifier: IotDeviceMessageIdentifierEnum.PROPERTY_SET,
data: {}
} as ActionDeviceControl
}
//
if (actionConfig.value.type === IotRuleSceneActionTypeEnum.ALERT && !actionConfig.value.alert) {
actionConfig.value.alert = {} as ActionAlert
}
//
if (
actionConfig.value.type === IotRuleSceneActionTypeEnum.DATA_BRIDGE &&
!actionConfig.value.dataBridgeId
) {
actionConfig.value.dataBridgeId = undefined
}
}
/** 产品和设备选择 */
const productTableSelectRef = ref<InstanceType<typeof ProductTableSelect>>()
const deviceTableSelectRef = ref<InstanceType<typeof DeviceTableSelect>>()
const product = ref<ProductVO>()
const deviceList = ref<DeviceVO[]>([])
/** 处理选择产品 */
const handleSelectProduct = () => {
productTableSelectRef.value?.open()
}
/** 处理选择设备 */
const handleSelectDevice = () => {
if (!product.value) {
message.warning('请先选择一个产品')
return
}
deviceTableSelectRef.value?.open()
}
/** 处理产品选择成功 */
const handleProductSelect = (val: ProductVO) => {
product.value = val
if (actionConfig.value.deviceControl) {
actionConfig.value.deviceControl.productKey = val.productKey
}
//
deviceList.value = []
if (actionConfig.value.deviceControl) {
actionConfig.value.deviceControl.deviceNames = []
}
}
/** 处理设备选择成功 */
const handleDeviceSelect = (val: DeviceVO[]) => {
deviceList.value = val
if (actionConfig.value.deviceControl) {
actionConfig.value.deviceControl.deviceNames = val.map((item) => item.deviceName)
}
}
/** 监听执行类型变化,初始化对应配置 */
watch(
() => actionConfig.value.type,
() => {
initActionConfig()
},
{ immediate: true }
)
/** 初始化产品回显信息 */
const initProductInfo = async () => {
if (!actionConfig.value.deviceControl?.productKey) {
return
}
try {
const productData = await ProductApi.getProductByKey(
actionConfig.value.deviceControl.productKey
)
if (productData) {
product.value = productData
}
} catch (error) {
console.error('获取产品信息失败:', error)
}
}
/**
* 初始化设备回显信息
*/
const initDeviceInfo = async () => {
if (
!actionConfig.value.deviceControl?.productKey ||
!actionConfig.value.deviceControl?.deviceNames?.length
) {
return
}
try {
const deviceData = await DeviceApi.getDevicesByProductKeyAndNames(
actionConfig.value.deviceControl.productKey,
actionConfig.value.deviceControl.deviceNames
)
if (deviceData && deviceData.length > 0) {
deviceList.value = deviceData
}
} catch (error) {
console.error('获取设备信息失败:', error)
}
}
/** 初始化 */
onMounted(async () => {
initActionConfig()
//
if (actionConfig.value.deviceControl) {
await initProductInfo()
await initDeviceInfo()
}
})
</script>

View File

@ -0,0 +1,91 @@
<template>
<div class="bg-[#dbe5f6] p-10px">
<div class="flex items-center mb-10px">
<span class="mr-10px w-80px">接收方式</span>
<el-select
v-model="alertConfig.receiveType"
class="!w-160px"
clearable
placeholder="选择接收方式"
>
<!-- TODO @芋艿后续搞成字典 -->
<!-- TODO @puhui999这里好像是 1/2/3 -->
<el-option
v-for="(value, key) in IotAlertConfigReceiveTypeEnum"
:key="value"
:label="key === 'SMS' ? '短信' : key === 'MAIL' ? '邮箱' : '通知'"
:value="value"
/>
</el-select>
</div>
<div
v-if="alertConfig.receiveType === IotAlertConfigReceiveTypeEnum.SMS"
class="flex items-center mb-10px"
>
<span class="mr-10px w-80px">手机号码</span>
<el-select
v-model="alertConfig.phoneNumbers"
class="!w-360px"
multiple
filterable
allow-create
default-first-option
placeholder="请输入手机号码"
/>
</div>
<div
v-if="alertConfig.receiveType === IotAlertConfigReceiveTypeEnum.MAIL"
class="flex items-center mb-10px"
>
<span class="mr-10px w-80px">邮箱地址</span>
<el-select
v-model="alertConfig.emails"
class="!w-360px"
multiple
filterable
allow-create
default-first-option
placeholder="请输入邮箱地址"
/>
</div>
<div class="flex items-center">
<span class="mr-10px w-80px align-self-start">通知内容</span>
<el-input
v-model="alertConfig.content"
type="textarea"
:rows="4"
class="!w-360px"
placeholder="请输入通知内容"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { ActionAlert, IotAlertConfigReceiveTypeEnum } from '@/api/iot/rule/scene/scene.types'
/** 告警执行器组件 */
defineOptions({ name: 'AlertAction' })
const props = defineProps<{ modelValue: any }>()
const emits = defineEmits(['update:modelValue'])
const alertConfig = useVModel(props, 'modelValue', emits) as Ref<ActionAlert>
/** 初始化告警执行器结构 */
const initAlertConfig = () => {
if (!alertConfig.value) {
alertConfig.value = {
receiveType: IotAlertConfigReceiveTypeEnum.NOTIFY,
phoneNumbers: [],
emails: [],
content: ''
} as ActionAlert
}
}
/** 初始化 */
onMounted(() => {
initAlertConfig()
})
</script>

View File

@ -0,0 +1,38 @@
<template>
<div class="bg-[#dbe5f6] p-10px">
<div class="flex items-center">
<span class="mr-10px w-80px">数据桥梁</span>
<el-select v-model="dataBridgeId" class="!w-240px" clearable placeholder="选择数据桥接">
<el-option
v-for="bridge in dataBridgeList"
:key="bridge.id"
:label="bridge.name"
:value="bridge.id"
/>
</el-select>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { DataBridgeApi } from '@/api/iot/rule/databridge'
/** 数据桥接执行器组件 */
defineOptions({ name: 'DataBridgeAction' })
const props = defineProps<{ modelValue: any }>()
const emits = defineEmits(['update:modelValue'])
const dataBridgeId = useVModel(props, 'modelValue', emits)
const dataBridgeList = ref<any[]>([]) //
/** 获取数据桥接列表 */
const getDataBridgeList = async () => {
dataBridgeList.value = await DataBridgeApi.getSimpleDataBridgeList()
}
onMounted(() => {
getDataBridgeList()
})
</script>

View File

@ -0,0 +1,230 @@
<template>
<div class="bg-[#dbe5f6] flex p-10px">
<div class="flex flex-col items-center justify-center mr-10px h-a">
<el-select v-model="deviceControlConfig.type" class="!w-160px" clearable placeholder="">
<el-option label="属性" :value="IotDeviceMessageTypeEnum.PROPERTY" />
<el-option label="服务" :value="IotDeviceMessageTypeEnum.SERVICE" />
</el-select>
</div>
<div class="">
<div
class="flex items-center justify-around mb-10px last:mb-0"
v-for="(parameter, index) in parameters"
:key="index"
>
<!-- 选择服务 -->
<el-select
v-if="IotDeviceMessageTypeEnum.SERVICE === deviceControlConfig.type"
v-model="parameter.identifier0"
class="!w-240px mr-10px"
clearable
placeholder="请选择服务"
>
<el-option
v-for="thingModel in getThingModelTSLServices"
:key="thingModel.identifier"
:label="thingModel.name"
:value="thingModel.identifier"
/>
</el-select>
<el-select
v-model="parameter.identifier"
class="!w-240px mr-10px"
clearable
placeholder="请选择物模型"
>
<el-option
v-for="thingModel in thingModels(parameter?.identifier0)"
:key="thingModel.identifier"
:label="thingModel.name"
:value="thingModel.identifier"
/>
</el-select>
<ThingModelParamInput
class="!w-240px mr-10px"
v-model="parameter.value"
:thing-model="
thingModels(parameter?.identifier0)?.find(
(item) => item.identifier === parameter.identifier
)
"
/>
<el-tooltip content="删除参数" placement="top">
<el-button type="danger" circle size="small" @click="removeParameter(index)">
<Icon icon="ep:delete" />
</el-button>
</el-tooltip>
</div>
</div>
<!-- 添加参数 -->
<div class="flex flex-1 flex-col items-center justify-center w-60px h-a">
<el-tooltip content="添加参数" placement="top">
<el-button type="primary" circle size="small" @click="addParameter">
<Icon icon="ep:plus" />
</el-button>
</el-tooltip>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { isEmpty } from '@/utils/is'
import { ThingModelApi } from '@/api/iot/thingmodel'
import {
ActionDeviceControl,
IotDeviceMessageIdentifierEnum,
IotDeviceMessageTypeEnum
} from '@/api/iot/rule/scene/scene.types'
import ThingModelParamInput from '../ThingModelParamInput.vue'
/** 设备控制执行器组件 */
defineOptions({ name: 'DeviceControlAction' })
const props = defineProps<{
modelValue: any
productId?: number
productKey?: string
}>()
const emits = defineEmits(['update:modelValue'])
const deviceControlConfig = useVModel(props, 'modelValue', emits) as Ref<ActionDeviceControl>
const message = useMessage()
/** 执行器参数 */
const parameters = ref<{ identifier: string; value: any; identifier0?: string }[]>([])
const addParameter = () => {
if (!props.productId) {
message.warning('请先选择一个产品')
return
}
if (parameters.value.length >= thingModels.value().length) {
message.warning(`该产品只有${thingModels.value().length}个物模型!!!`)
return
}
parameters.value.push({ identifier: '', value: undefined })
}
const removeParameter = (index: number) => {
parameters.value.splice(index, 1)
}
watch(
() => parameters.value,
(newVal) => {
if (isEmpty(newVal)) {
return
}
for (const parameter of newVal) {
if (isEmpty(parameter.identifier)) {
break
}
//
if (IotDeviceMessageTypeEnum.SERVICE === deviceControlConfig.value.type) {
if (!parameter.identifier0) {
continue
}
deviceControlConfig.value.data[parameter.identifier0] = {
identifier: parameter.identifier,
value: parameter.value
}
continue
}
deviceControlConfig.value.data[parameter.identifier] = parameter.value
}
},
{ deep: true }
)
/** 初始化设备控制执行器结构 */
const initDeviceControlConfig = () => {
if (!deviceControlConfig.value) {
deviceControlConfig.value = {
productKey: '',
deviceNames: [],
type: IotDeviceMessageTypeEnum.PROPERTY,
identifier: IotDeviceMessageIdentifierEnum.PROPERTY_SET,
data: {}
} as ActionDeviceControl
} else {
//
if (IotDeviceMessageTypeEnum.SERVICE === deviceControlConfig.value.type) {
//
parameters.value = Object.entries(deviceControlConfig.value.data).map(([key, value]) => ({
identifier0: key,
identifier: value.identifier,
value: value.value
}))
return
}
//
parameters.value = Object.entries(deviceControlConfig.value.data).map(([key, value]) => ({
identifier: key,
value: value
}))
}
// data
if (!deviceControlConfig.value.data) {
deviceControlConfig.value.data = {}
}
}
/** 获取产品物模型 */
const thingModelTSL = ref<any>()
const getThingModelTSL = async () => {
if (!props.productId) {
return
}
thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(props.productId)
}
const thingModels = computed(() => (identifier?: string): any[] => {
if (isEmpty(thingModelTSL.value)) {
return []
}
switch (deviceControlConfig.value.type) {
case IotDeviceMessageTypeEnum.PROPERTY:
return thingModelTSL.value?.properties || []
case IotDeviceMessageTypeEnum.SERVICE:
const service = thingModelTSL.value.services?.find(
(item: any) => item.identifier === identifier
)
return service?.inputParams || []
}
return []
})
/** 获取物模型服务 */
const getThingModelTSLServices = computed(() => thingModelTSL.value?.services || [])
/** 监听 productId 变化 */
watch(
() => props.productId,
() => {
getThingModelTSL()
if (deviceControlConfig.value && deviceControlConfig.value.productKey === props.productKey) {
return
}
// ID
deviceControlConfig.value.data = {}
parameters.value = []
}
)
/** 监听消息类型变化 */
watch(
() => deviceControlConfig.value.type,
() => {
//
deviceControlConfig.value.data = {}
parameters.value = []
if (deviceControlConfig.value.type === IotDeviceMessageTypeEnum.PROPERTY) {
deviceControlConfig.value.identifier = IotDeviceMessageIdentifierEnum.PROPERTY_SET
} else if (deviceControlConfig.value.type === IotDeviceMessageTypeEnum.SERVICE) {
deviceControlConfig.value.identifier = IotDeviceMessageIdentifierEnum.SERVICE_INVOKE
}
}
)
//
onMounted(() => {
initDeviceControlConfig()
})
</script>

View File

@ -0,0 +1,106 @@
<template>
<el-select v-model="selectedOperator" class="w-1/1" clearable :placeholder="placeholder">
<!-- 根据属性类型展示不同的可选条件 -->
<el-option
v-for="(item, key) in filteredOperators"
:key="key"
:label="item.name"
:value="item.value"
/>
</el-select>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { IotRuleSceneTriggerConditionParameterOperatorEnum } from '@/api/iot/rule/scene/scene.types'
/** 条件选择器 */
defineOptions({ name: 'ConditionSelector' })
const props = defineProps({
placeholder: {
type: String,
default: '请选择条件'
},
modelValue: {
type: String,
default: ''
},
dataType: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
const selectedOperator = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
//
const filteredOperators = computed(() => {
//
if (!props.dataType) {
return IotRuleSceneTriggerConditionParameterOperatorEnum
}
const operatorMap = new Map()
//
operatorMap.set('NOT_NULL', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_NULL)
//
switch (props.dataType) {
case 'int':
case 'float':
case 'double':
//
operatorMap.set('EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS)
operatorMap.set('NOT_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS)
operatorMap.set('GREATER_THAN', IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN)
operatorMap.set('GREATER_THAN_OR_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS)
operatorMap.set('LESS_THAN', IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN)
operatorMap.set('LESS_THAN_OR_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS)
operatorMap.set('IN', IotRuleSceneTriggerConditionParameterOperatorEnum.IN)
operatorMap.set('NOT_IN', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN)
operatorMap.set('BETWEEN', IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN)
operatorMap.set('NOT_BETWEEN', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN)
break
case 'enum':
//
operatorMap.set('EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS)
operatorMap.set('NOT_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS)
operatorMap.set('IN', IotRuleSceneTriggerConditionParameterOperatorEnum.IN)
operatorMap.set('NOT_IN', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN)
break
case 'bool':
//
operatorMap.set('EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS)
operatorMap.set('NOT_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS)
break
case 'text':
//
operatorMap.set('EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS)
operatorMap.set('NOT_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS)
operatorMap.set('LIKE', IotRuleSceneTriggerConditionParameterOperatorEnum.LIKE)
break
case 'date':
//
operatorMap.set('EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS)
operatorMap.set('NOT_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS)
operatorMap.set('GREATER_THAN', IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN)
operatorMap.set('GREATER_THAN_OR_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS)
operatorMap.set('LESS_THAN', IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN)
operatorMap.set('LESS_THAN_OR_EQUALS', IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS)
operatorMap.set('BETWEEN', IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN)
operatorMap.set('NOT_BETWEEN', IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN)
break
// struct array
default:
return IotRuleSceneTriggerConditionParameterOperatorEnum
}
return Object.fromEntries(operatorMap)
})
</script>

View File

@ -0,0 +1,306 @@
<template>
<div>
<div class="m-10px">
<div class="relative bg-[#eff3f7] h-50px flex items-center px-10px">
<div class="flex items-center mr-60px">
<span class="mr-10px">触发条件</span>
<el-select
v-model="triggerConfig.type"
class="!w-240px"
clearable
placeholder="请选择触发条件"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_RULE_SCENE_TRIGGER_TYPE_ENUM)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</div>
<div
v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE"
class="flex items-center mr-60px"
>
<span class="mr-10px">产品</span>
<el-button type="primary" @click="productTableSelectRef?.open()" size="small" plain>
{{ product ? product.name : '选择产品' }}
</el-button>
</div>
<div
v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE"
class="flex items-center mr-60px"
>
<span class="mr-10px">设备</span>
<el-button type="primary" @click="openDeviceSelect" size="small" plain>
{{ isEmpty(deviceList) ? '选择设备' : triggerConfig.deviceNames.join(',') }}
</el-button>
</div>
<!-- 删除触发器 -->
<div class="absolute top-auto right-16px bottom-auto">
<el-tooltip content="删除触发器" placement="top">
<slot></slot>
</el-tooltip>
</div>
</div>
<!-- 设备触发器条件 -->
<template v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE">
<div
class="bg-[#dbe5f6] flex p-10px"
v-for="(condition, index) in triggerConfig.conditions"
:key="index"
>
<div class="flex flex-col items-center justify-center mr-10px h-a">
<el-select
v-model="condition.type"
@change="condition.parameters = []"
class="!w-160px"
clearable
placeholder=""
>
<el-option label="属性" :value="IotDeviceMessageTypeEnum.PROPERTY" />
<el-option label="服务" :value="IotDeviceMessageTypeEnum.SERVICE" />
<el-option label="事件" :value="IotDeviceMessageTypeEnum.EVENT" />
</el-select>
</div>
<div class="w-70%">
<DeviceListenerCondition
v-for="(parameter, index2) in condition.parameters"
:key="index2"
:model-value="parameter"
:condition-type="condition.type"
:thingModels="thingModels(condition)"
@update:model-value="(val) => (condition.parameters[index2] = val)"
class="mb-10px last:mb-0"
>
<el-tooltip content="删除参数" placement="top">
<el-button
type="danger"
circle
size="small"
@click="removeConditionParameter(condition.parameters, index2)"
>
<Icon icon="ep:delete" />
</el-button>
</el-tooltip>
</DeviceListenerCondition>
</div>
<!-- 添加参数 -->
<div class="flex flex-1 flex-col items-center justify-center w-60px h-a">
<el-tooltip content="添加参数" placement="top">
<el-button
type="primary"
circle
size="small"
@click="addConditionParameter(condition.parameters)"
>
<Icon icon="ep:plus" />
</el-button>
</el-tooltip>
</div>
<!-- 删除条件 -->
<div
class="device-listener-condition flex flex-1 flex-col items-center justify-center w-a h-a"
>
<el-tooltip content="删除条件" placement="top">
<el-button type="danger" size="small" @click="removeCondition(index)">
<Icon icon="ep:delete" />
</el-button>
</el-tooltip>
</div>
</div>
</template>
<!-- 定时触发 -->
<div
v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.TIMER"
class="bg-[#dbe5f6] flex items-center justify-between p-10px"
>
<span class="w-120px">CRON 表达式</span>
<crontab v-model="triggerConfig.cronExpression" />
</div>
<!-- 设备触发才可以设置多个触发条件 -->
<el-text
v-if="triggerConfig.type === IotRuleSceneTriggerTypeEnum.DEVICE"
class="ml-10px!"
type="primary"
@click="addCondition"
>
添加触发条件
</el-text>
</div>
<!-- 产品设备的选择 -->
<ProductTableSelect ref="productTableSelectRef" @success="handleProductSelect" />
<DeviceTableSelect
ref="deviceTableSelectRef"
multiple
:product-id="product?.id"
@success="handleDeviceSelect"
/>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { isEmpty } from '@/utils/is'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import DeviceListenerCondition from './DeviceListenerCondition.vue'
import ProductTableSelect from '@/views/iot/product/product/components/ProductTableSelect.vue'
import DeviceTableSelect from '@/views/iot/device/device/components/DeviceTableSelect.vue'
import { ProductApi, ProductVO } from '@/api/iot/product/product'
import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
import { ThingModelApi } from '@/api/iot/thingmodel'
import {
IotDeviceMessageIdentifierEnum,
IotDeviceMessageTypeEnum,
IotRuleSceneTriggerTypeEnum,
TriggerCondition,
TriggerConditionParameter,
TriggerConfig
} from '@/api/iot/rule/scene/scene.types'
/** 场景联动之监听器组件 */
defineOptions({ name: 'DeviceListener' })
const props = defineProps<{ modelValue: any }>()
const emits = defineEmits(['update:modelValue'])
const triggerConfig = useVModel(props, 'modelValue', emits) as Ref<TriggerConfig>
const message = useMessage()
/** 添加触发条件 */
const addCondition = () => {
triggerConfig.value.conditions?.push({
type: IotDeviceMessageTypeEnum.PROPERTY,
identifier: IotDeviceMessageIdentifierEnum.PROPERTY_SET,
parameters: []
})
}
/** 移除触发条件 */
const removeCondition = (index: number) => {
triggerConfig.value.conditions?.splice(index, 1)
}
/** 添加参数 */
const addConditionParameter = (conditionParameters: TriggerConditionParameter[]) => {
if (!product.value) {
message.warning('请先选择一个产品')
return
}
conditionParameters.push({} as TriggerConditionParameter)
}
/** 移除参数 */
const removeConditionParameter = (
conditionParameters: TriggerConditionParameter[],
index: number
) => {
conditionParameters.splice(index, 1)
}
/** 产品和设备选择引用 */
const productTableSelectRef = ref<InstanceType<typeof ProductTableSelect>>()
const deviceTableSelectRef = ref<InstanceType<typeof DeviceTableSelect>>()
const product = ref<ProductVO>()
const deviceList = ref<DeviceVO[]>([])
/** 处理产品选择 */
const handleProductSelect = (val: ProductVO) => {
product.value = val
triggerConfig.value.productKey = val.productKey
deviceList.value = []
getThingModelTSL()
}
/** 处理设备选择 */
const handleDeviceSelect = (val: DeviceVO[]) => {
deviceList.value = val
triggerConfig.value.deviceNames = val.map((item) => item.deviceName)
}
/** 打开设备选择器 */
const openDeviceSelect = () => {
if (!product.value) {
message.warning('请先选择一个产品')
return
}
deviceTableSelectRef.value?.open()
}
/**
* 初始化产品回显信息
*/
const initProductInfo = async () => {
if (!triggerConfig.value.productKey) {
return
}
try {
// 使APIproductKey
const productData = await ProductApi.getProductByKey(triggerConfig.value.productKey)
if (productData) {
product.value = productData
//
await getThingModelTSL()
}
} catch (error) {
console.error('获取产品信息失败:', error)
}
}
/**
* 初始化设备回显信息
*/
const initDeviceInfo = async () => {
if (!triggerConfig.value.productKey || !triggerConfig.value.deviceNames?.length) {
return
}
try {
// 使APIproductKeydeviceNames
const deviceData = await DeviceApi.getDevicesByProductKeyAndNames(
triggerConfig.value.productKey,
triggerConfig.value.deviceNames
)
if (deviceData && deviceData.length > 0) {
deviceList.value = deviceData
}
} catch (error) {
console.error('获取设备信息失败:', error)
}
}
/** 获取产品物模型 */
const thingModelTSL = ref<any>()
const thingModels = computed(() => (condition: TriggerCondition) => {
if (isEmpty(thingModelTSL.value)) {
return []
}
switch (condition.type) {
case IotDeviceMessageTypeEnum.PROPERTY:
return thingModelTSL.value?.properties || []
case IotDeviceMessageTypeEnum.SERVICE:
return thingModelTSL.value?.services || []
case IotDeviceMessageTypeEnum.EVENT:
return thingModelTSL.value?.events || []
}
return []
})
const getThingModelTSL = async () => {
if (!product.value) {
return
}
thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(product.value.id)
}
/** 初始化 */
onMounted(async () => {
//
if (triggerConfig.value) {
// conditions
if (!triggerConfig.value.conditions) {
triggerConfig.value.conditions = []
}
await initProductInfo()
await initDeviceInfo()
}
})
</script>

View File

@ -0,0 +1,87 @@
<template>
<div class="flex items-center w-1/1">
<!-- 选择服务 -->
<el-select
v-if="
[IotDeviceMessageTypeEnum.SERVICE, IotDeviceMessageTypeEnum.EVENT].includes(conditionType)
"
v-model="conditionParameter.identifier0"
class="!w-150px mr-10px"
clearable
placeholder="请选择服务"
>
<el-option
v-for="thingModel in thingModels"
:key="thingModel.identifier"
:label="thingModel.name"
:value="thingModel.identifier"
/>
</el-select>
<el-select
v-model="conditionParameter.identifier"
class="!w-150px mr-10px"
clearable
placeholder="请选择物模型"
>
<el-option
v-for="thingModel in getThingModels"
:key="thingModel.identifier"
:label="thingModel.name"
:value="thingModel.identifier"
/>
</el-select>
<ConditionSelector
v-model="conditionParameter.operator"
:data-type="model?.dataType"
class="!w-150px mr-10px"
/>
<ThingModelParamInput
v-if="
conditionParameter.operator !==
IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_NULL.value
"
class="!w-200px mr-10px"
v-model="conditionParameter.value"
:thing-model="model"
/>
<!-- 按钮插槽 -->
<slot></slot>
</div>
</template>
<script setup lang="ts">
import ConditionSelector from './ConditionSelector.vue'
import {
IotDeviceMessageTypeEnum,
IotRuleSceneTriggerConditionParameterOperatorEnum,
TriggerConditionParameter
} from '@/api/iot/rule/scene/scene.types'
import { useVModel } from '@vueuse/core'
import ThingModelParamInput from '@/views/iot/rule/scene/components/ThingModelParamInput.vue'
/** 设备触发条件 */
defineOptions({ name: 'DeviceListenerCondition' })
const props = defineProps<{ modelValue: any; conditionType: any; thingModels: any }>()
const emits = defineEmits(['update:modelValue'])
const conditionParameter = useVModel(props, 'modelValue', emits) as Ref<TriggerConditionParameter>
/** 属性就是 thingModels服务和事件取对应的 outputParams */
const getThingModels = computed(() => {
switch (props.conditionType) {
case IotDeviceMessageTypeEnum.PROPERTY:
return props.thingModels || []
case IotDeviceMessageTypeEnum.SERVICE:
case IotDeviceMessageTypeEnum.EVENT:
return (
props.thingModels.find(
(item: any) => item.identifier === conditionParameter.value.identifier0
)?.outputParams || []
)
}
})
/** 获得物模型属性、类型 */
const model = computed(() =>
getThingModels.value.find((item: any) => item.identifier === conditionParameter.value.identifier)
)
</script>

View File

@ -0,0 +1,192 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="场景名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入场景名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</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.COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</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" /> 搜索</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:rule-scene:create']"
>
<Icon icon="ep:plus" 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="id" />
<el-table-column label="场景名称" align="center" prop="name" />
<el-table-column label="场景描述" align="center" prop="description" />
<el-table-column label="场景状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="触发器" align="center" prop="triggers">
<template #default="{ row }"> {{ row.triggers?.length }} </template>
</el-table-column>
<el-table-column label="执行器" align="center" prop="actions">
<template #default="{ row }"> {{ row.actions?.length }} </template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['iot:rule-scene:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['iot:rule-scene: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>
<!-- 表单弹窗添加/修改 -->
<RuleSceneForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { RuleSceneApi } from '@/api/iot/rule/scene'
import RuleSceneForm from './RuleSceneForm.vue'
import { IotRuleScene } from '@/api/iot/rule/scene/scene.types'
/** IoT 场景联动 列表 */
defineOptions({ name: 'IotRuleScene' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const list = ref<IotRuleScene[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
description: undefined,
status: undefined,
createTime: []
})
const queryFormRef = ref() //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await RuleSceneApi.getRuleScenePage(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 handleDelete = async (id: number) => {
try {
//
await message.delConfirm()
//
await RuleSceneApi.deleteRuleScene(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@ -0,0 +1,50 @@
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle">
<JsonEditor
v-model="thingModelTSL"
:mode="viewMode === 'editor' ? 'code' : 'view'"
height="600px"
/>
<template #footer>
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="code">代码视图</el-radio-button>
<el-radio-button label="editor">编辑器视图</el-radio-button>
</el-radio-group>
</template>
</Dialog>
</template>
<script setup lang="ts">
import hljs from 'highlight.js' //
import 'highlight.js/styles/github.css' //
import json from 'highlight.js/lib/languages/json'
import { ThingModelApi } from '@/api/iot/thingmodel'
import { ProductVO } from '@/api/iot/product/product'
import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
defineOptions({ name: 'ThingModelTSL' })
const dialogVisible = ref(false) //
const dialogTitle = ref('物模型 TSL') //
const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) //
const viewMode = ref('code') // code-editor-
/** 打开弹窗 */
const open = () => {
dialogVisible.value = true
}
defineExpose({ open })
/** 获取 TSL */
const thingModelTSL = ref({})
const getTsl = async () => {
thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(product?.value?.id || 0)
}
/** 初始化 **/
onMounted(async () => {
//
hljs.registerLanguage('json', json)
await getTsl()
})
</script>

View File

@ -42,6 +42,9 @@
<Icon class="mr-5px" icon="ep:plus" />
添加功能
</el-button>
<el-button v-hasPermi="[`iot:thing-model:query`]" plain type="primary" @click="openTSL">
TSL
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
@ -99,11 +102,13 @@
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<ThingModelForm ref="formRef" @success="getList" />
<ThingModelTSL ref="thingModelTSLRef" />
</template>
<script lang="ts" setup>
import { ThingModelApi, ThingModelData } from '@/api/iot/thingmodel'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import ThingModelForm from './ThingModelForm.vue'
import ThingModelTSL from './ThingModelTSL.vue'
import { ProductVO } from '@/api/iot/product/product'
import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
import { getDataTypeOptionsLabel } from './config'
@ -160,6 +165,12 @@ const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 展示物模型 TSL */
const thingModelTSLRef = ref()
const openTSL = () => {
thingModelTSLRef.value?.open()
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {

View File

@ -134,7 +134,7 @@ const userList = ref<UserApi.UserVO[]>([]) // 用户列表
const getList = async () => {
loading.value = true
try {
const data = await DeptApi.getDeptPage(queryParams)
const data = await DeptApi.getDeptList(queryParams)
list.value = handleTree(data)
} finally {
loading.value = false