perf:【IoT 物联网】场景联动目录结构优化

This commit is contained in:
puhui999
2025-07-26 22:35:59 +08:00
parent 52b9c1827e
commit 8549399ae8
29 changed files with 33 additions and 46 deletions

View File

@ -0,0 +1,104 @@
<!-- 执行器类型选择组件 -->
<template>
<div class="w-full">
<!-- TODO @puhui9991设备属性设置时貌似没选属性2服务调用时貌似也没的设置哈 -->
<el-form-item label="执行类型" required>
<el-select
v-model="localValue"
placeholder="请选择执行类型"
@change="handleChange"
class="w-full"
>
<el-option
v-for="option in actionTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
>
<div class="flex items-center justify-between w-full py-4px">
<div class="flex items-center gap-12px flex-1">
<Icon :icon="option.icon" class="text-18px text-[var(--el-color-primary)] flex-shrink-0" />
<div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ option.label }}</div>
<div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">{{ option.description }}</div>
</div>
</div>
<el-tag :type="option.tag" size="small">
{{ option.category }}
</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { IotRuleSceneActionTypeEnum } from '@/api/iot/rule/scene/scene.types'
/** 执行器类型选择组件 */
defineOptions({ name: 'ActionTypeSelector' })
interface Props {
modelValue: number
}
interface Emits {
(e: 'update:modelValue', value: number): void
(e: 'change', value: number): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const localValue = useVModel(props, 'modelValue', emit)
// 执行器类型选项
const actionTypeOptions = [
{
value: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
label: '设备属性设置',
description: '设置目标设备的属性值',
icon: 'ep:edit',
tag: 'primary',
category: '设备控制'
},
{
value: IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE,
label: '设备服务调用',
description: '调用目标设备的服务',
icon: 'ep:service',
tag: 'success',
category: '设备控制'
},
{
value: IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
label: '触发告警',
description: '触发系统告警通知',
icon: 'ep:warning',
tag: 'danger',
category: '告警通知'
},
{
value: IotRuleSceneActionTypeEnum.ALERT_RECOVER,
label: '恢复告警',
description: '恢复已触发的告警',
icon: 'ep:circle-check',
tag: 'warning',
category: '告警通知'
}
]
// 事件处理
const handleChange = (value: number) => {
emit('change', value)
}
</script>
<style scoped>
:deep(.el-select-dropdown__item) {
height: auto;
padding: 8px 20px;
}
</style>

View File

@ -0,0 +1,191 @@
<!-- 操作符选择器组件 -->
<template>
<div class="w-full">
<el-select
v-model="localValue"
placeholder="请选择操作符"
@change="handleChange"
class="w-full"
>
<el-option
v-for="operator in availableOperators"
:key="operator.value"
:label="operator.label"
:value="operator.value"
>
<div class="flex items-center justify-between w-full py-4px">
<div class="flex items-center gap-8px">
<div class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ operator.label }}</div>
<div class="text-12px text-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)] px-6px py-2px rounded-4px font-mono">{{ operator.symbol }}</div>
</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ operator.description }}</div>
</div>
</el-option>
</el-select>
<!-- 操作符说明 -->
<!-- TODO @puhui999这个去掉 -->
<div v-if="selectedOperator" class="mt-8px p-8px bg-[var(--el-fill-color-light)] rounded-4px border border-[var(--el-border-color-lighter)]">
<div class="flex items-center gap-6px">
<Icon icon="ep:info-filled" class="text-12px text-[var(--el-color-info)]" />
<span class="text-12px text-[var(--el-text-color-secondary)]">{{ selectedOperator.description }}</span>
</div>
<div v-if="selectedOperator.example" class="flex items-center gap-6px mt-4px">
<span class="text-12px text-[var(--el-text-color-secondary)]">示例</span>
<code class="text-12px text-[var(--el-color-primary)] bg-[var(--el-fill-color-blank)] px-4px py-2px rounded-2px font-mono">{{ selectedOperator.example }}</code>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
/** 操作符选择器组件 */
defineOptions({ name: 'OperatorSelector' })
interface Props {
modelValue?: string
propertyType?: string
}
interface Emits {
(e: 'update:modelValue', value: string): void
(e: 'change', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const localValue = useVModel(props, 'modelValue', emit)
// 所有操作符定义
const allOperators = [
{
value: '=',
label: '等于',
symbol: '=',
description: '值完全相等时触发',
example: 'temperature = 25',
supportedTypes: ['int', 'float', 'double', 'string', 'bool', 'enum']
},
{
value: '!=',
label: '不等于',
symbol: '≠',
description: '值不相等时触发',
example: 'power != false',
supportedTypes: ['int', 'float', 'double', 'string', 'bool', 'enum']
},
{
value: '>',
label: '大于',
symbol: '>',
description: '值大于指定值时触发',
example: 'temperature > 30',
supportedTypes: ['int', 'float', 'double', 'date']
},
{
value: '>=',
label: '大于等于',
symbol: '≥',
description: '值大于或等于指定值时触发',
example: 'humidity >= 80',
supportedTypes: ['int', 'float', 'double', 'date']
},
{
value: '<',
label: '小于',
symbol: '<',
description: '值小于指定值时触发',
example: 'temperature < 10',
supportedTypes: ['int', 'float', 'double', 'date']
},
{
value: '<=',
label: '小于等于',
symbol: '≤',
description: '值小于或等于指定值时触发',
example: 'battery <= 20',
supportedTypes: ['int', 'float', 'double', 'date']
},
{
value: 'in',
label: '包含于',
symbol: '∈',
description: '值在指定列表中时触发',
example: 'status in [1,2,3]',
supportedTypes: ['int', 'float', 'string', 'enum']
},
{
value: 'between',
label: '介于',
symbol: '⊆',
description: '值在指定范围内时触发',
example: 'temperature between 20,30',
supportedTypes: ['int', 'float', 'double', 'date']
},
{
value: 'contains',
label: '包含',
symbol: '⊃',
description: '字符串包含指定内容时触发',
example: 'message contains "error"',
supportedTypes: ['string']
},
{
value: 'startsWith',
label: '开始于',
symbol: '⊢',
description: '字符串以指定内容开始时触发',
example: 'deviceName startsWith "sensor"',
supportedTypes: ['string']
},
{
value: 'endsWith',
label: '结束于',
symbol: '⊣',
description: '字符串以指定内容结束时触发',
example: 'fileName endsWith ".log"',
supportedTypes: ['string']
}
]
// 计算属性
const availableOperators = computed(() => {
if (!props.propertyType) {
return allOperators
}
return allOperators.filter((op) => op.supportedTypes.includes(props.propertyType!))
})
const selectedOperator = computed(() => {
return allOperators.find((op) => op.value === localValue.value)
})
// 事件处理
const handleChange = (value: string) => {
emit('change', value)
}
// 监听属性类型变化
watch(
() => props.propertyType,
() => {
// 如果当前选择的操作符不支持新的属性类型,则清空选择
if (localValue.value && selectedOperator.value) {
if (!selectedOperator.value.supportedTypes.includes(props.propertyType || '')) {
localValue.value = ''
}
}
}
)
</script>
<style scoped>
:deep(.el-select-dropdown__item) {
height: auto;
padding: 8px 20px;
}
</style>

View File

@ -0,0 +1,295 @@
<!-- 产品设备选择器组件 -->
<template>
<div class="product-device-selector">
<el-row :gutter="16">
<!-- 产品选择 -->
<el-col :span="12">
<el-form-item label="选择产品" required>
<el-select
v-model="localProductId"
placeholder="请选择产品"
filterable
clearable
@change="handleProductChange"
class="w-full"
:loading="productLoading"
>
<el-option
v-for="product in productList"
:key="product.id"
:label="product.name"
:value="product.id"
>
<div class="flex items-center justify-between w-full py-4px">
<div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ product.name }}</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ product.productKey }}</div>
</div>
<!-- TODO @puhui999是不是用字典 -->
<el-tag size="small" :type="product.status === 0 ? 'success' : 'danger'">
{{ product.status === 0 ? '正常' : '禁用' }}
</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
</el-col>
<!-- 设备选择模式 -->
<el-col :span="12">
<el-form-item label="设备选择模式" required>
<el-radio-group v-model="deviceSelectionMode" @change="handleDeviceSelectionModeChange">
<el-radio value="all">全部设备</el-radio>
<el-radio value="specific">选择设备</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<!-- 具体设备选择 -->
<el-row v-if="deviceSelectionMode === 'specific'" :gutter="16">
<el-col :span="24">
<!-- TODO @puhui999貌似产品选择不上 -->
<el-form-item label="选择设备" required>
<!-- TODO @puhui999请先选择产品是不是改成请选择设备然后上面localProductId 为空未选择的时候禁用 deviceSelectionMode -->
<el-select
v-model="localDeviceId"
placeholder="请先选择产品"
filterable
clearable
@change="handleDeviceChange"
class="w-full"
:loading="deviceLoading"
:disabled="!localProductId"
>
<el-option
v-for="device in deviceList"
:key="device.id"
:label="device.deviceName"
:value="device.id"
>
<div class="flex items-center justify-between w-full py-4px">
<div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ device.deviceName }}</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ device.nickname || '无备注' }}</div>
</div>
<el-tag size="small" :type="getDeviceStatusTag(device.state)">
{{ getDeviceStatusText(device.state) }}
</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 选择结果展示 -->
<div v-if="localProductId && localDeviceId !== undefined" class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]">
<div class="flex items-center gap-6px mb-8px">
<Icon icon="ep:check" class="text-[var(--el-color-success)] text-16px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">已选择设备</span>
</div>
<div class="flex flex-col gap-6px ml-22px">
<div class="flex items-center gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-40px">产品</span>
<span class="text-12px text-[var(--el-text-color-primary)] font-500">{{ selectedProduct?.name }}</span>
<el-tag size="small" type="primary">{{ selectedProduct?.productKey }}</el-tag>
</div>
<div class="flex items-center gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-40px">设备</span>
<span v-if="deviceSelectionMode === 'all'" class="text-12px text-[var(--el-text-color-primary)] font-500">全部设备</span>
<span v-else class="text-12px text-[var(--el-text-color-primary)] font-500">{{ selectedDevice?.deviceName }}</span>
<el-tag v-if="deviceSelectionMode === 'all'" size="small" type="warning"> 全部 </el-tag>
<el-tag v-else size="small" :type="getDeviceStatusTag(selectedDevice?.state)">
{{ getDeviceStatusText(selectedDevice?.state) }}
</el-tag>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { ProductApi } from '@/api/iot/product/product'
import { DeviceApi } from '@/api/iot/device/device'
/** 产品设备选择器组件 */
defineOptions({ name: 'ProductDeviceSelector' })
interface Props {
productId?: number
deviceId?: number
}
interface Emits {
(e: 'update:productId', value?: number): void
(e: 'update:deviceId', value?: number): void
(e: 'change', value: { productId?: number; deviceId?: number }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const localProductId = useVModel(props, 'productId', emit)
const localDeviceId = useVModel(props, 'deviceId', emit)
// 设备选择模式
// TODO @puhui999默认选中 all
const deviceSelectionMode = ref<'specific' | 'all'>('all')
// 数据状态
const productLoading = ref(false)
const deviceLoading = ref(false)
const productList = ref<any[]>([])
const deviceList = ref<any[]>([])
// 计算属性
const selectedProduct = computed(() => {
return productList.value.find((p) => p.id === localProductId.value)
})
const selectedDevice = computed(() => {
return deviceList.value.find((d) => d.id === localDeviceId.value)
})
// TODO @puhui999字典下
// 设备状态映射
const getDeviceStatusText = (state?: number) => {
switch (state) {
case 0:
return '未激活'
case 1:
return '在线'
case 2:
return '离线'
default:
return '未知'
}
}
const getDeviceStatusTag = (state?: number) => {
switch (state) {
case 0:
return 'info'
case 1:
return 'success'
case 2:
return 'danger'
default:
return 'info'
}
}
// 事件处理
const handleProductChange = async (productId?: number) => {
localProductId.value = productId
localDeviceId.value = undefined
deviceList.value = []
if (productId) {
await getDeviceList(productId)
}
emitChange()
}
const handleDeviceChange = (deviceId?: number) => {
localDeviceId.value = deviceId
emitChange()
}
const handleDeviceSelectionModeChange = (mode: 'specific' | 'all') => {
deviceSelectionMode.value = mode
if (mode === 'all') {
// 全部设备时,设备 ID 设为 0
localDeviceId.value = 0
} else {
// 选择设备时,清空设备 ID
localDeviceId.value = undefined
}
emitChange()
}
const emitChange = () => {
emit('change', {
productId: localProductId.value,
deviceId: localDeviceId.value
})
}
// API 调用
const getProductList = async () => {
productLoading.value = true
try {
const data = await ProductApi.getSimpleProductList()
productList.value = data || []
} catch (error) {
console.error('获取产品列表失败:', error)
// 模拟数据
// TODO @puhui999移除下不太合理
productList.value = [
{ id: 1, name: '智能温度传感器', productKey: 'temp_sensor_001', status: 0 },
{ id: 2, name: '智能空调控制器', productKey: 'ac_controller_001', status: 0 },
{ id: 3, name: '智能门锁', productKey: 'smart_lock_001', status: 0 }
]
} finally {
productLoading.value = false
}
}
const getDeviceList = async (productId: number) => {
deviceLoading.value = true
try {
const data = await DeviceApi.getSimpleDeviceList(undefined, productId)
deviceList.value = data || []
} catch (error) {
console.error('获取设备列表失败:', error)
// 模拟数据
// TODO @puhui999移除下不太合理
deviceList.value = [
{ id: 1, deviceName: 'sensor_001', nickname: '客厅温度传感器', state: 1, productId },
{ id: 2, deviceName: 'sensor_002', nickname: '卧室温度传感器', state: 2, productId },
{ id: 3, deviceName: 'sensor_003', nickname: '厨房温度传感器', state: 1, productId }
]
} finally {
deviceLoading.value = false
}
}
// 初始化
onMounted(async () => {
await getProductList()
// 根据初始设备 ID 设置选择模式
if (localDeviceId.value === 0) {
deviceSelectionMode.value = 'all'
} else if (localDeviceId.value) {
deviceSelectionMode.value = 'specific'
}
if (localProductId.value) {
await getDeviceList(localProductId.value)
}
})
// 监听产品变化
watch(
() => localProductId.value,
async (newProductId) => {
if (newProductId && deviceList.value.length === 0) {
await getDeviceList(newProductId)
}
}
)
// TODO @puhui999是不是 unocss
</script>
<style scoped>
:deep(.el-select-dropdown__item) {
height: auto;
padding: 8px 20px;
}
</style>

View File

@ -0,0 +1,343 @@
<!-- 属性选择器组件 -->
<!-- TODO @yunai可能要在 review -->
<template>
<div class="w-full">
<el-select
v-model="localValue"
placeholder="请选择监控项"
filterable
clearable
@change="handleChange"
class="w-full"
:loading="loading"
>
<el-option-group v-for="group in propertyGroups" :key="group.label" :label="group.label">
<el-option
v-for="property in group.options"
:key="property.identifier"
:label="property.name"
:value="property.identifier"
>
<div class="flex items-center justify-between w-full py-4px">
<div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ property.name }}</div>
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ property.identifier }}</div>
</div>
<div class="flex-shrink-0">
<el-tag :type="getPropertyTypeTag(property.dataType)" size="small">
{{ getPropertyTypeName(property.dataType) }}
</el-tag>
</div>
</div>
</el-option>
</el-option-group>
</el-select>
<!-- 属性详情 -->
<div v-if="selectedProperty" class="mt-16px p-12px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]">
<div class="flex items-center gap-8px mb-12px">
<Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-16px" />
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">{{ selectedProperty.name }}</span>
<el-tag :type="getPropertyTypeTag(selectedProperty.dataType)" size="small">
{{ getPropertyTypeName(selectedProperty.dataType) }}
</el-tag>
</div>
<div class="space-y-8px ml-24px">
<div class="flex items-start gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">标识符</span>
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.identifier }}</span>
</div>
<div v-if="selectedProperty.description" class="flex items-start gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">描述</span>
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.description }}</span>
</div>
<div v-if="selectedProperty.unit" class="flex items-start gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">单位</span>
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.unit }}</span>
</div>
<div v-if="selectedProperty.range" class="flex items-start gap-8px">
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">取值范围</span>
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">{{ selectedProperty.range }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types'
import { ThingModelApi } from '@/api/iot/thingmodel'
import { IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
import type { IotThingModelTSLRespVO, PropertySelectorItem } from './types'
/** 属性选择器组件 */
defineOptions({ name: 'PropertySelector' })
interface Props {
modelValue?: string
triggerType: number
productId?: number
deviceId?: number
}
interface Emits {
(e: 'update:modelValue', value: string): void
(e: 'change', value: { type: string; config: any }): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const localValue = useVModel(props, 'modelValue', emit)
// 状态
const loading = ref(false)
const propertyList = ref<PropertySelectorItem[]>([])
const thingModelTSL = ref<IotThingModelTSLRespVO | null>(null)
// 计算属性
const propertyGroups = computed(() => {
const groups: { label: string; options: any[] }[] = []
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST) {
groups.push({
label: '设备属性',
options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.PROPERTY)
})
}
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
groups.push({
label: '设备事件',
options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.EVENT)
})
}
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) {
groups.push({
label: '设备服务',
options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.SERVICE)
})
}
return groups.filter((group) => group.options.length > 0)
})
const selectedProperty = computed(() => {
return propertyList.value.find((p) => p.identifier === localValue.value)
})
// 工具函数
const getPropertyTypeName = (dataType: string) => {
const typeMap = {
int: '整数',
float: '浮点数',
double: '双精度',
text: '字符串',
bool: '布尔值',
enum: '枚举',
date: '日期',
struct: '结构体',
array: '数组'
}
return typeMap[dataType] || dataType
}
const getPropertyTypeTag = (dataType: string) => {
const tagMap = {
int: 'primary',
float: 'success',
double: 'success',
text: 'info',
bool: 'warning',
enum: 'danger',
date: 'primary',
struct: 'info',
array: 'warning'
}
return tagMap[dataType] || 'info'
}
// 事件处理
const handleChange = (value: string) => {
const property = propertyList.value.find((p) => p.identifier === value)
if (property) {
emit('change', {
type: property.dataType,
config: property
})
}
}
// 获取物模型TSL数据
const getThingModelTSL = async () => {
if (!props.productId) {
thingModelTSL.value = null
propertyList.value = []
return
}
loading.value = true
try {
thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(props.productId)
parseThingModelData()
} catch (error) {
console.error('获取物模型TSL失败:', error)
// 如果TSL获取失败尝试获取物模型列表
await getThingModelList()
} finally {
loading.value = false
}
}
// 获取物模型列表(备用方案)
const getThingModelList = async () => {
if (!props.productId) {
propertyList.value = []
return
}
try {
const data = await ThingModelApi.getThingModelList({ productId: props.productId })
propertyList.value = data || []
} catch (error) {
console.error('获取物模型列表失败:', error)
propertyList.value = []
}
}
// 解析物模型TSL数据
const parseThingModelData = () => {
const tsl = thingModelTSL.value
const properties: PropertySelectorItem[] = []
if (tsl) {
// 解析属性
if (tsl.properties && Array.isArray(tsl.properties)) {
tsl.properties.forEach((prop) => {
properties.push({
identifier: prop.identifier,
name: prop.name,
description: prop.description,
dataType: prop.dataType,
type: IoTThingModelTypeEnum.PROPERTY,
accessMode: prop.accessMode,
required: prop.required,
unit: getPropertyUnit(prop),
range: getPropertyRange(prop),
property: prop
})
})
}
// 解析事件
if (tsl.events && Array.isArray(tsl.events)) {
tsl.events.forEach((event) => {
properties.push({
identifier: event.identifier,
name: event.name,
description: event.description,
dataType: 'struct',
type: IoTThingModelTypeEnum.EVENT,
eventType: event.type,
required: event.required,
outputParams: event.outputParams,
event: event
})
})
}
// 解析服务
if (tsl.services && Array.isArray(tsl.services)) {
tsl.services.forEach((service) => {
properties.push({
identifier: service.identifier,
name: service.name,
description: service.description,
dataType: 'struct',
type: IoTThingModelTypeEnum.SERVICE,
callType: service.callType,
required: service.required,
inputParams: service.inputParams,
outputParams: service.outputParams,
service: service
})
})
}
}
propertyList.value = properties
}
// 获取属性单位
const getPropertyUnit = (property: any) => {
if (!property) return undefined
// 数值型数据的单位
if (property.dataSpecs && property.dataSpecs.unit) {
return property.dataSpecs.unit
}
return undefined
}
// 获取属性范围描述
const getPropertyRange = (property: any) => {
if (!property) return undefined
// 数值型数据的范围
if (property.dataSpecs) {
const specs = property.dataSpecs
if (specs.min !== undefined && specs.max !== undefined) {
return `${specs.min}~${specs.max}`
}
}
// 枚举型和布尔型数据的选项
if (property.dataSpecsList && Array.isArray(property.dataSpecsList)) {
return property.dataSpecsList.map((item: any) => `${item.name}(${item.value})`).join(', ')
}
return undefined
}
// 获取数据范围描述(保留兼容性)
const getDataRange = (dataSpecs: any) => {
if (!dataSpecs) return undefined
if (dataSpecs.min !== undefined && dataSpecs.max !== undefined) {
return `${dataSpecs.min}~${dataSpecs.max}`
}
if (dataSpecs.dataSpecsList && Array.isArray(dataSpecs.dataSpecsList)) {
return dataSpecs.dataSpecsList.map((item: any) => `${item.name}(${item.value})`).join(', ')
}
return undefined
}
// 监听产品变化
watch(
() => props.productId,
() => {
getThingModelTSL()
},
{ immediate: true }
)
// 监听触发类型变化
watch(
() => props.triggerType,
() => {
localValue.value = ''
}
)
</script>
<style scoped>
:deep(.el-select-dropdown__item) {
height: auto;
padding: 8px 20px;
}
</style>

View File

@ -0,0 +1,142 @@
<!-- 触发器类型选择组件 -->
<template>
<div class="w-full">
<el-form-item label="触发类型" required>
<el-select
v-model="localValue"
placeholder="请选择触发类型"
@change="handleChange"
class="w-full"
>
<el-option
v-for="option in triggerTypeOptions"
:key="option.value"
:label="option.label"
:value="option.value"
>
<div class="flex items-center justify-between w-full py-4px">
<div class="flex items-center gap-12px flex-1">
<!-- TODO @puhui999貌似没对齐 -->
<Icon :icon="option.icon" class="text-18px text-[var(--el-color-primary)] flex-shrink-0" />
<div class="flex-1">
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">{{ option.label }}</div>
<div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">{{ option.description }}</div>
</div>
</div>
<!-- TODO @puhui999这个要不去掉 -->
<el-tag :type="option.tag" size="small">
{{ option.category }}
</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
<!-- 类型说明 -->
<!-- TODO @puhui999这个去掉感觉没啥内容哈 -->
<div v-if="selectedOption" class="mt-16px p-16px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]">
<div class="flex items-center gap-8px mb-12px">
<Icon :icon="selectedOption.icon" class="text-20px text-[var(--el-color-primary)]" />
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">{{ selectedOption.label }}</span>
</div>
<div class="ml-28px">
<p class="text-14px text-[var(--el-text-color-regular)] m-0 mb-12px leading-relaxed">{{ selectedOption.description }}</p>
<div class="flex flex-col gap-6px">
<div v-for="feature in selectedOption.features" :key="feature" class="flex items-center gap-6px">
<Icon icon="ep:check" class="text-12px text-[var(--el-color-success)] flex-shrink-0" />
<span class="text-12px text-[var(--el-text-color-secondary)]">{{ feature }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import { IotRuleSceneTriggerTypeEnum } from '@/api/iot/rule/scene/scene.types'
/** 触发器类型选择组件 */
defineOptions({ name: 'TriggerTypeSelector' })
interface Props {
modelValue: number
}
interface Emits {
(e: 'update:modelValue', value: number): void
(e: 'change', value: number): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const localValue = useVModel(props, 'modelValue', emit)
// 触发器类型选项
const triggerTypeOptions = [
{
value: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
label: '设备状态变更',
description: '当设备上线、离线状态发生变化时触发',
icon: 'ep:connection',
tag: 'warning',
category: '设备状态',
features: ['监控设备连接状态', '实时响应设备变化', '无需配置额外条件']
},
{
value: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
label: '设备属性上报',
description: '当设备属性值满足指定条件时触发',
icon: 'ep:data-line',
tag: 'primary',
category: '数据监控',
features: ['监控设备属性变化', '支持多种比较条件', '可配置阈值范围']
},
{
value: IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
label: '设备事件上报',
description: '当设备上报特定事件时触发',
icon: 'ep:bell',
tag: 'success',
category: '事件监控',
features: ['监控设备事件', '支持事件参数过滤', '实时事件响应']
},
{
value: IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
label: '设备服务调用',
description: '当设备服务被调用时触发',
icon: 'ep:service',
tag: 'info',
category: '服务监控',
features: ['监控服务调用', '支持参数条件', '服务执行跟踪']
},
{
value: IotRuleSceneTriggerTypeEnum.TIMER,
label: '定时触发',
description: '按照设定的时间计划定时触发',
icon: 'ep:timer',
tag: 'danger',
category: '定时任务',
features: ['支持CRON表达式', '灵活的时间配置', '可视化时间设置']
}
]
// 计算属性
const selectedOption = computed(() => {
return triggerTypeOptions.find((option) => option.value === localValue.value)
})
// 事件处理
const handleChange = (value: number) => {
emit('change', value)
}
</script>
<style scoped>
/** TODO @puhui999unocss 哈 - 已完成转换 */
:deep(.el-select-dropdown__item) {
height: auto;
padding: 8px 20px;
}
</style>

View File

@ -0,0 +1,170 @@
// IoT物模型TSL数据类型定义
// TODO @puhui999看看这些里面是不是一些已经有了哈可以复用下~
/** 物模型TSL响应数据结构 */
export interface IotThingModelTSLRespVO {
productId: number
productKey: string
properties: ThingModelProperty[]
events: ThingModelEvent[]
services: ThingModelService[]
}
/** 物模型属性 */
export interface ThingModelProperty {
identifier: string
name: string
accessMode: string
required?: boolean
dataType: string
description?: string
dataSpecs?: ThingModelDataSpecs
dataSpecsList?: ThingModelDataSpecs[]
}
/** 物模型事件 */
export interface ThingModelEvent {
identifier: string
name: string
required?: boolean
type: string
description?: string
outputParams?: ThingModelParam[]
method?: string
}
/** 物模型服务 */
export interface ThingModelService {
identifier: string
name: string
required?: boolean
callType: string
description?: string
inputParams?: ThingModelParam[]
outputParams?: ThingModelParam[]
method?: string
}
/** 物模型参数 */
export interface ThingModelParam {
identifier: string
name: string
direction: string
paraOrder?: number
dataType: string
dataSpecs?: ThingModelDataSpecs
dataSpecsList?: ThingModelDataSpecs[]
}
/** 数值型数据规范 */
export interface ThingModelNumericDataSpec {
dataType: 'int' | 'float' | 'double'
max: string
min: string
step: string
precise?: string
defaultValue?: string
unit?: string
unitName?: string
}
/** 布尔/枚举型数据规范 */
export interface ThingModelBoolOrEnumDataSpecs {
dataType: 'bool' | 'enum'
name: string
value: number
}
/** 文本/时间型数据规范 */
export interface ThingModelDateOrTextDataSpecs {
dataType: 'text' | 'date'
length?: number
defaultValue?: string
}
/** 数组型数据规范 */
export interface ThingModelArrayDataSpecs {
dataType: 'array'
size: number
childDataType: string
dataSpecsList?: ThingModelDataSpecs[]
}
/** 结构体型数据规范 */
export interface ThingModelStructDataSpecs {
dataType: 'struct'
identifier: string
name: string
accessMode: string
required?: boolean
childDataType: string
dataSpecs?: ThingModelDataSpecs
dataSpecsList?: ThingModelDataSpecs[]
}
/** 数据规范联合类型 */
export type ThingModelDataSpecs =
| ThingModelNumericDataSpec
| ThingModelBoolOrEnumDataSpecs
| ThingModelDateOrTextDataSpecs
| ThingModelArrayDataSpecs
| ThingModelStructDataSpecs
/** 属性选择器内部使用的统一数据结构 */
export interface PropertySelectorItem {
identifier: string
name: string
description?: string
dataType: string
type: number // IoTThingModelTypeEnum
accessMode?: string
required?: boolean
unit?: string
range?: string
eventType?: string
callType?: string
inputParams?: ThingModelParam[]
outputParams?: ThingModelParam[]
property?: ThingModelProperty
event?: ThingModelEvent
service?: ThingModelService
}
/** 数据类型枚举 */
export enum DataTypeEnum {
INT = 'int',
FLOAT = 'float',
DOUBLE = 'double',
ENUM = 'enum',
BOOL = 'bool',
TEXT = 'text',
DATE = 'date',
STRUCT = 'struct',
ARRAY = 'array'
}
/** 访问模式枚举 */
export enum AccessModeEnum {
READ = 'r',
READ_write = 'rw'
}
/** 事件类型枚举 */
export enum EventTypeEnum {
INFO = 'info',
ALERT = 'alert',
ERROR = 'error'
}
/** 调用类型枚举 */
export enum CallTypeEnum {
ASYNC = 'async',
SYNC = 'sync'
}
/** 参数方向枚举 */
export enum ParamDirectionEnum {
INPUT = 'input',
OUTPUT = 'output'
}