【功能完善】IoT: 规则场景监听器相关组件
This commit is contained in:
@ -1,12 +1,13 @@
|
||||
import request from '@/config/axios'
|
||||
import { IotRuleSceneTriggerConfig } from '@/api/iot/rule/scene/scene.types'
|
||||
|
||||
// IoT 规则场景(场景联动) VO
|
||||
export interface RuleSceneVO {
|
||||
id?: number // 场景编号
|
||||
name?: string // 场景名称
|
||||
name: string // 场景名称
|
||||
description?: string // 场景描述
|
||||
status?: number // 场景状态
|
||||
triggers?: any[] // 触发器数组
|
||||
status: number // 场景状态
|
||||
triggers: IotRuleSceneTriggerConfig[] // 触发器数组
|
||||
actions?: any[] // 执行器数组
|
||||
}
|
||||
|
||||
|
||||
@ -9,11 +9,11 @@ export interface IotRuleSceneTriggerConfig {
|
||||
*/
|
||||
type: number
|
||||
/** 产品标识 */
|
||||
productKey?: string
|
||||
productKey: string
|
||||
/** 设备名称数组 */
|
||||
deviceNames?: string[]
|
||||
deviceNames: string[]
|
||||
/** 触发条件数组。条件之间是"或"的关系 */
|
||||
conditions?: IotRuleSceneTriggerCondition[]
|
||||
conditions: IotRuleSceneTriggerCondition[]
|
||||
/** CRON 表达式。当 type = 2 时必填 */
|
||||
cronExpression?: string
|
||||
}
|
||||
@ -29,7 +29,7 @@ export interface IotRuleSceneTriggerCondition {
|
||||
*/
|
||||
type: string
|
||||
/** 消息标识符 */
|
||||
identifier: string
|
||||
identifier?: string
|
||||
/** 参数数组。参数之间是"或"的关系 */
|
||||
parameters: IotRuleSceneTriggerConditionParameter[]
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@ export const useAppStore = defineStore('app', {
|
||||
tagsView: true, // 标签页
|
||||
tagsViewImmerse: false, // 标签页沉浸
|
||||
tagsViewIcon: true, // 是否显示标签图标
|
||||
logo: true, // logo
|
||||
logo: false, // logo
|
||||
fixedHeader: true, // 固定toolheader
|
||||
footer: true, // 显示页脚
|
||||
greyMode: false, // 是否开始灰色模式,用于特殊悼念日
|
||||
|
||||
@ -35,10 +35,19 @@
|
||||
<el-divider content-position="left">触发器配置</el-divider>
|
||||
<device-listener
|
||||
v-for="(trigger, index) in formData.triggers"
|
||||
:model-value="trigger"
|
||||
:key="index"
|
||||
:model-value="trigger"
|
||||
@update:model-value="(val) => (formData.triggers[index] = val)"
|
||||
class="mb-10px"
|
||||
/>
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
round
|
||||
:icon="Delete"
|
||||
size="small"
|
||||
@click="removeTrigger(index)"
|
||||
/>
|
||||
</device-listener>
|
||||
<el-text class="ml-10px!" type="primary" @click="addTrigger">添加触发器</el-text>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
@ -59,6 +68,8 @@
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { RuleSceneApi, RuleSceneVO } from '@/api/iot/rule/scene'
|
||||
import DeviceListener from './components/DeviceListener.vue'
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
import { IotRuleSceneTriggerConfig } from '@/api/iot/rule/scene/scene.types'
|
||||
|
||||
/** IoT 规则场景(场景联动) 表单 */
|
||||
defineOptions({ name: 'RuleSceneForm' })
|
||||
@ -72,7 +83,7 @@ const formLoading = ref(false) // 表单的加载中:1)修改时的数据加
|
||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||
const formData = ref<RuleSceneVO>({
|
||||
status: 0,
|
||||
triggers: []
|
||||
triggers: [] as IotRuleSceneTriggerConfig[]
|
||||
} as RuleSceneVO)
|
||||
const formRules = reactive({
|
||||
name: [{ required: true, message: '场景名称不能为空', trigger: 'blur' }],
|
||||
@ -82,10 +93,26 @@ const formRules = reactive({
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
/** 添加触发器 */
|
||||
const addTrigger = () => {
|
||||
formData.value.triggers?.push({})
|
||||
formData.value.triggers.push({
|
||||
type: 1,
|
||||
productKey: '',
|
||||
deviceNames: [],
|
||||
conditions: [
|
||||
{
|
||||
type: 'property',
|
||||
parameters: []
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
/** 移除触发器 */
|
||||
const removeTrigger = (index: number) => {
|
||||
const newTriggers = [...formData.value.triggers]
|
||||
newTriggers.splice(index, 1)
|
||||
formData.value.triggers = newTriggers
|
||||
}
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
@ -132,7 +159,7 @@ const submitForm = async () => {
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
status: 0,
|
||||
triggers: []
|
||||
triggers: [] as IotRuleSceneTriggerConfig[]
|
||||
} as RuleSceneVO
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
@ -3,7 +3,12 @@
|
||||
<div class="device-listener-header h-50px flex items-center px-10px">
|
||||
<div class="flex items-center mr-60px">
|
||||
<span class="mr-10px">触发条件</span>
|
||||
<el-select v-model="triggerType" class="!w-240px" clearable placeholder="请选择触发条件">
|
||||
<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"
|
||||
@ -20,12 +25,19 @@
|
||||
<span class="mr-10px">设备</span>
|
||||
<el-button type="primary">选择设备</el-button>
|
||||
</div>
|
||||
<!-- 添加规则 -->
|
||||
<el-button class="device-listener-delete" type="danger" round :icon="Delete" size="small" />
|
||||
<!-- 删除触发器 -->
|
||||
<div class="device-listener-delete">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-listener-condition flex p-10px">
|
||||
<!-- 触发器条件 -->
|
||||
<div
|
||||
class="device-listener-condition 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="messageType" class="!w-160px" clearable placeholder="">
|
||||
<el-select v-model="condition.type" class="!w-160px" clearable placeholder="">
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.IOT_DEVICE_MESSAGE_TYPE_ENUM)"
|
||||
:key="dict.value"
|
||||
@ -36,62 +48,74 @@
|
||||
</div>
|
||||
<div class="">
|
||||
<DeviceListenerCondition
|
||||
v-for="(conditionParameter, index) in conditionParameters"
|
||||
:key="index"
|
||||
:model-value="conditionParameter"
|
||||
@update:model-value="(val) => (conditionParameters[index] = val)"
|
||||
v-for="(parameter, index2) in condition.parameters"
|
||||
:key="index2"
|
||||
:model-value="parameter"
|
||||
@update:model-value="(val) => (condition.parameters[index2] = val)"
|
||||
class="mb-10px last:mb-0"
|
||||
>
|
||||
<!-- 添加规则 -->
|
||||
<!-- 删除规则 -->
|
||||
<el-button
|
||||
class="device-listener-delete"
|
||||
type="danger"
|
||||
circle
|
||||
:icon="Delete"
|
||||
size="small"
|
||||
@click="removeConditionParameter(index)"
|
||||
@click="removeConditionParameter(condition.parameters, index2)"
|
||||
/>
|
||||
</DeviceListenerCondition>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col items-center justify-center w-a h-a">
|
||||
<!-- 添加规则 -->
|
||||
<el-button type="primary" circle :icon="Plus" size="small" @click="addConditionParameter" />
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
:icon="Plus"
|
||||
size="small"
|
||||
@click="addConditionParameter(condition.parameters)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-text class="ml-10px!" type="primary" @click="addCondition">添加触发条件</el-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
|
||||
import { ref } from 'vue'
|
||||
import DeviceListenerCondition from './DeviceListenerCondition.vue'
|
||||
import { IotRuleSceneTriggerConditionParameter } from '@/api/iot/rule/scene/scene.types'
|
||||
import {
|
||||
IotRuleSceneTriggerConditionParameter,
|
||||
IotRuleSceneTriggerConfig
|
||||
} from '@/api/iot/rule/scene/scene.types'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
|
||||
/** 场景联动之监听器组件 */
|
||||
defineOptions({ name: 'DeviceListener' })
|
||||
|
||||
defineProps<{
|
||||
modelValue: any
|
||||
}>()
|
||||
const props = defineProps<{ modelValue: any }>()
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
const triggerConfig = useVModel(props, 'modelValue', emits) as Ref<IotRuleSceneTriggerConfig>
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// 添加响应式变量
|
||||
const triggerType = ref()
|
||||
const messageType = ref('property')
|
||||
const conditionParameters = ref<IotRuleSceneTriggerConditionParameter[]>([])
|
||||
/** 添加触发条件 */
|
||||
const addConditionParameter = () => {
|
||||
conditionParameters.value?.push({} as IotRuleSceneTriggerConditionParameter)
|
||||
const addCondition = () => {
|
||||
triggerConfig.value.conditions.push({
|
||||
type: 'property',
|
||||
parameters: []
|
||||
})
|
||||
}
|
||||
/** 移除触发条件 */
|
||||
const removeConditionParameter = (index: number) => {
|
||||
conditionParameters.value?.splice(index, 1)
|
||||
|
||||
/** 添加参数 */
|
||||
const addConditionParameter = (conditionParameters: IotRuleSceneTriggerConditionParameter[]) => {
|
||||
conditionParameters.push({} as IotRuleSceneTriggerConditionParameter)
|
||||
}
|
||||
/** 移除参数 */
|
||||
const removeConditionParameter = (
|
||||
conditionParameters: IotRuleSceneTriggerConditionParameter[],
|
||||
index: number
|
||||
) => {
|
||||
conditionParameters.splice(index, 1)
|
||||
}
|
||||
onMounted(() => {
|
||||
addConditionParameter()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -1,379 +0,0 @@
|
||||
<template>
|
||||
<div class="trigger-conditions">
|
||||
<div class="conditions-header mb-3">
|
||||
<el-button type="primary" @click="addCondition" :disabled="!productKey">
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 添加条件
|
||||
</el-button>
|
||||
<div class="conditions-tips" v-if="modelValue && modelValue.length > 0">
|
||||
注:多个条件之间为"或"关系
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!modelValue || modelValue.length === 0" description="暂无触发条件" />
|
||||
|
||||
<div class="conditions-list" v-else>
|
||||
<div v-for="(condition, index) in modelValue" :key="index" class="condition-item mb-3">
|
||||
<el-card class="box-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>条件 {{ index + 1 }}</span>
|
||||
<el-button type="danger" link @click="removeCondition(index)"> 删除 </el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="condition-content">
|
||||
<el-form label-width="100px" :model="condition">
|
||||
<el-form-item label="消息类型">
|
||||
<el-select
|
||||
v-model="condition.type"
|
||||
placeholder="请选择消息类型"
|
||||
@change="handleMessageTypeChange(index)"
|
||||
>
|
||||
<el-option label="属性上报" :value="IotDeviceMessageTypeEnum.PROPERTY" />
|
||||
<el-option label="事件上报" :value="IotDeviceMessageTypeEnum.EVENT" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="消息标识符">
|
||||
<el-select
|
||||
v-model="condition.identifier"
|
||||
placeholder="请选择消息标识符"
|
||||
filterable
|
||||
:loading="thingModelLoading"
|
||||
@change="handleIdentifierChange(index)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in getThingModelOptions(condition.type)"
|
||||
:key="item.identifier"
|
||||
:label="item.name"
|
||||
:value="item.identifier"
|
||||
>
|
||||
<div class="thing-model-option">
|
||||
<span>{{ item.name }}</span>
|
||||
<span class="thing-model-identifier">{{ item.identifier }}</span>
|
||||
</div>
|
||||
<div class="thing-model-desc" v-if="item.description">{{
|
||||
item.description
|
||||
}}</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<div class="parameters-area mt-3 mb-2">
|
||||
<div class="parameters-header">
|
||||
<div>参数列表(多个参数之间为"或"关系)</div>
|
||||
<el-button type="primary" link @click="addParameter(index)"> 添加参数 </el-button>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
v-if="!condition.parameters || condition.parameters.length === 0"
|
||||
description="暂无参数"
|
||||
/>
|
||||
|
||||
<div class="parameters-list mt-2" v-else>
|
||||
<div
|
||||
v-for="(param, pIndex) in condition.parameters"
|
||||
:key="pIndex"
|
||||
class="parameter-item mb-2"
|
||||
>
|
||||
<el-card shadow="hover">
|
||||
<div class="parameter-item-header">
|
||||
<span>参数 {{ pIndex + 1 }}</span>
|
||||
<el-button type="danger" link @click="removeParameter(index, pIndex)">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-form label-width="90px" :model="param" class="mt-2">
|
||||
<el-form-item label="标识符">
|
||||
<el-select
|
||||
v-model="param.identifier"
|
||||
placeholder="请选择参数标识符"
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in getParameterOptions(condition)"
|
||||
:key="item.identifier"
|
||||
:label="item.name"
|
||||
:value="item.identifier"
|
||||
>
|
||||
<div class="thing-model-option">
|
||||
<span>{{ item.name }}</span>
|
||||
<span class="thing-model-identifier">{{ item.identifier }}</span>
|
||||
</div>
|
||||
<div class="thing-model-desc" v-if="item.description">{{
|
||||
item.description
|
||||
}}</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="条件">
|
||||
<condition-selector
|
||||
v-model="param.condition"
|
||||
:placeholder="'请选择条件'"
|
||||
:value-placeholder="'请输入比较值'"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineEmits, defineProps, onMounted, ref, watch } from 'vue'
|
||||
import {
|
||||
IotDeviceMessageIdentifierEnum,
|
||||
IotDeviceMessageTypeEnum,
|
||||
IotRuleSceneTriggerCondition,
|
||||
IotRuleSceneTriggerConditionParameter
|
||||
} from '@/api/iot/rule/scene/scene.types'
|
||||
import { ThingModelApi, ThingModelData } from '@/api/iot/thingmodel'
|
||||
import ConditionSelector from './ConditionSelector.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as () => IotRuleSceneTriggerCondition[],
|
||||
required: true
|
||||
},
|
||||
productKey: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// 物模型数据
|
||||
const thingModelList = ref<ThingModelData[]>([])
|
||||
const thingModelLoading = ref(false)
|
||||
|
||||
// 加载物模型数据
|
||||
const loadThingModelData = async () => {
|
||||
if (!props.productKey) return
|
||||
|
||||
try {
|
||||
thingModelLoading.value = true
|
||||
const result = await ThingModelApi.getThingModelListByProductId({
|
||||
productKey: props.productKey
|
||||
})
|
||||
thingModelList.value = result || []
|
||||
} catch (error) {
|
||||
console.error('获取物模型数据失败', error)
|
||||
} finally {
|
||||
thingModelLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取物模型选项
|
||||
const getThingModelOptions = (type: string) => {
|
||||
if (!thingModelList.value) return []
|
||||
|
||||
return thingModelList.value.filter((item) => {
|
||||
if (type === IotDeviceMessageTypeEnum.PROPERTY) {
|
||||
return item.property
|
||||
} else if (type === IotDeviceMessageTypeEnum.EVENT) {
|
||||
return item.event
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// 获取参数选项
|
||||
const getParameterOptions = (condition: IotRuleSceneTriggerCondition) => {
|
||||
if (!condition || !condition.identifier) return []
|
||||
|
||||
const model = thingModelList.value?.find((item) => item.identifier === condition.identifier)
|
||||
if (!model) return []
|
||||
|
||||
if (condition.type === IotDeviceMessageTypeEnum.PROPERTY) {
|
||||
return [model] // 属性本身就是参数
|
||||
} else if (condition.type === IotDeviceMessageTypeEnum.EVENT) {
|
||||
// TODO: 获取事件的输出参数列表
|
||||
return []
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// 添加条件
|
||||
const addCondition = () => {
|
||||
const newCondition: IotRuleSceneTriggerCondition = {
|
||||
type: IotDeviceMessageTypeEnum.PROPERTY,
|
||||
identifier: IotDeviceMessageIdentifierEnum.PROPERTY_REPORT,
|
||||
parameters: []
|
||||
}
|
||||
|
||||
const newValue = [...(props.modelValue || []), newCondition]
|
||||
emit('update:modelValue', newValue)
|
||||
}
|
||||
|
||||
// 移除条件
|
||||
const removeCondition = (index: number) => {
|
||||
const newValue = [...props.modelValue]
|
||||
newValue.splice(index, 1)
|
||||
emit('update:modelValue', newValue)
|
||||
}
|
||||
|
||||
// 消息类型变更
|
||||
const handleMessageTypeChange = (index: number) => {
|
||||
const newValue = [...props.modelValue]
|
||||
// 更新标识符
|
||||
if (newValue[index].type === IotDeviceMessageTypeEnum.PROPERTY) {
|
||||
newValue[index].identifier = IotDeviceMessageIdentifierEnum.PROPERTY_REPORT
|
||||
} else if (newValue[index].type === IotDeviceMessageTypeEnum.EVENT) {
|
||||
newValue[index].identifier = IotDeviceMessageIdentifierEnum.EVENT_REPORT
|
||||
}
|
||||
// 清空参数
|
||||
newValue[index].parameters = []
|
||||
emit('update:modelValue', newValue)
|
||||
}
|
||||
|
||||
// 标识符变更
|
||||
const handleIdentifierChange = (index: number) => {
|
||||
const newValue = [...props.modelValue]
|
||||
// 清空参数
|
||||
newValue[index].parameters = []
|
||||
emit('update:modelValue', newValue)
|
||||
}
|
||||
|
||||
// 添加参数
|
||||
const addParameter = (conditionIndex: number) => {
|
||||
const newValue = [...props.modelValue]
|
||||
if (!newValue[conditionIndex].parameters) {
|
||||
newValue[conditionIndex].parameters = []
|
||||
}
|
||||
|
||||
const newParameter: IotRuleSceneTriggerConditionParameter = {
|
||||
identifier: '',
|
||||
condition: {
|
||||
operator: 'eq',
|
||||
value: ''
|
||||
}
|
||||
}
|
||||
|
||||
newValue[conditionIndex].parameters.push(newParameter)
|
||||
emit('update:modelValue', newValue)
|
||||
}
|
||||
|
||||
// 移除参数
|
||||
const removeParameter = (conditionIndex: number, paramIndex: number) => {
|
||||
const newValue = [...props.modelValue]
|
||||
newValue[conditionIndex].parameters.splice(paramIndex, 1)
|
||||
emit('update:modelValue', newValue)
|
||||
}
|
||||
|
||||
// 监听 productKey 变化
|
||||
watch(
|
||||
() => props.productKey,
|
||||
(newVal) => {
|
||||
if (!newVal) {
|
||||
// 清空条件
|
||||
if (props.modelValue?.length > 0) {
|
||||
emit('update:modelValue', [])
|
||||
}
|
||||
// 清空物模型数据
|
||||
thingModelList.value = []
|
||||
} else {
|
||||
// 加载物模型数据
|
||||
loadThingModelData()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
if (props.productKey) {
|
||||
loadThingModelData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trigger-conditions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.conditions-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.conditions-tips {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.condition-item {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.parameters-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.parameter-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.value-tips {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.thing-model-option {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.thing-model-identifier {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.thing-model-desc {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mr-5px {
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user