feat: 新增场景联动-触发规则

This commit is contained in:
xieyonghong 2023-02-24 18:21:44 +08:00
parent 7d0fbc0e1c
commit be501e77fe
15 changed files with 996 additions and 109 deletions

View File

@ -98,7 +98,11 @@ const props = defineProps({
class: {
type: String,
default: ''
}
},
// defaultTerms: {
// type: Object,
// default: () => ({})
// }
})
const searchRef = ref(null)
@ -223,6 +227,7 @@ const handleParamsFormat = () => {
*/
const searchSubmit = () => {
emit('search', handleParamsFormat())
console.log('searchSubmit')
if (props.type === 'advanced') {
addUrlParams()
}

View File

@ -144,6 +144,10 @@ const JTable = defineComponent<JTableProps>({
pageSize: 12
}
}
},
scroll: {
type: Object,
default: () => { x: 1366 }
}
} as any,
setup(props: JTableProps, { slots, emit, expose }) {
@ -331,7 +335,7 @@ const JTable = defineComponent<JTableProps>({
pagination={false}
rowKey="id"
rowSelection={props.rowSelection}
scroll={{ x: 1366 }}
scroll={props.scroll}
v-slots={{
bodyCell: (dt: Record<string, any>) => {
const { column, record } = dt;

View File

@ -68,7 +68,7 @@ const defaultOptions = {
};
export const useSceneStore = defineStore('scene', () => {
const data = reactive<FormModelType | any>({
const data = reactive<FormModelType>({
trigger: { type: ''},
options: defaultOptions,
branches: defaultBranches,
@ -116,67 +116,3 @@ export const useSceneStore = defineStore('scene', () => {
getDetail
}
})
//
// export const useSceneStore = defineStore({
// id: 'scene',
// state: (): DataType => {
// return {
// data: {
// trigger: { type: ''},
// options: defaultOptions,
// branches: defaultBranches,
// description: ''
// },
// productCache: {}
// }
// },
// actions: {
// /**
// * 初始化数据
// */
// initData() {
//
// },
// /**
// * 获取详情
// * @param id
// */
// async getDetail(id: string) {
// const resp = await detail(id)
// if (resp.success) {
// const result = resp.result as SceneItem
// const triggerType = result.triggerType
// let branches: any[] = result.branches
//
// if (!branches) {
// branches = cloneDeep(defaultBranches)
// if (triggerType === 'device') {
// branches.push(null)
// }
// } else {
// const branchesLength = branches.length;
// if (
// triggerType === 'device' &&
// ((branchesLength === 1 && !!branches[0]?.when?.length) || // 有一组数据并且when有值
// (branchesLength > 1 && !branches[branchesLength - 1]?.when?.length)) // 有多组否则数据并且最后一组when有值
// ) {
// branches.push(null);
// }
// }
//
// this.data = {
// ...result,
// trigger: result.trigger || {},
// branches: cloneDeep(assignmentKey(branches)),
// options: {...defaultOptions, ...result.options },
// }
// }
// },
// getProduct() {
//
// }
// },
// getters: {
//
// }
// })

View File

@ -8,3 +8,11 @@ export const isUrl = (path: string): boolean => urlReg.test(path)
export const inputReg = /^[a-zA-Z0-9_\-]+$/
export const isInput = (value: string) => inputReg.test(value)
// cron 表达式
export const CronRegEx = new RegExp(
'^\\s*($|#|\\w+\\s*=|(\\?|\\*|(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?(?:,(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?)*)\\s+(\\?|\\*|(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?(?:,(?:[0-5]?\\d)(?:(?:-|\\/|\\,)(?:[0-5]?\\d))?)*)\\s+(\\?|\\*|(?:[01]?\\d|2[0-3])(?:(?:-|\\/|\\,)(?:[01]?\\d|2[0-3]))?(?:,(?:[01]?\\d|2[0-3])(?:(?:-|\\/|\\,)(?:[01]?\\d|2[0-3]))?)*)\\s+(\\?|\\*|(?:0?[1-9]|[12]\\d|3[01])(?:(?:-|\\/|\\,)(?:0?[1-9]|[12]\\d|3[01]))?(?:,(?:0?[1-9]|[12]\\d|3[01])(?:(?:-|\\/|\\,)(?:0?[1-9]|[12]\\d|3[01]))?)*)\\s+(\\?|\\*|(?:[1-9]|1[012])(?:(?:-|\\/|\\,)(?:[1-9]|1[012]))?(?:L|W)?(?:,(?:[1-9]|1[012])(?:(?:-|\\/|\\,)(?:[1-9]|1[012]))?(?:L|W)?)*|\\?|\\*|(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:(?:-)(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?(?:,(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:(?:-)(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)*)\\s+(\\?|\\*|(?:[0-6])(?:(?:-|\\/|\\,|#)(?:[0-6]))?(?:L)?(?:,(?:[0-6])(?:(?:-|\\/|\\,|#)(?:[0-6]))?(?:L)?)*|\\?|\\*|(?:MON|TUE|WED|THU|FRI|SAT|SUN)(?:(?:-)(?:MON|TUE|WED|THU|FRI|SAT|SUN))?(?:,(?:MON|TUE|WED|THU|FRI|SAT|SUN)(?:(?:-)(?:MON|TUE|WED|THU|FRI|SAT|SUN))?)*)(|\\s)+(\\?|\\*|(?:|\\d{4})(?:(?:-|\\/|\\,)(?:|\\d{4}))?(?:,(?:|\\d{4})(?:(?:-|\\/|\\,)(?:|\\d{4}))?)*))$',
);
export const isCron = (value: string) => CronRegEx.test(value)

View File

@ -6,7 +6,7 @@
@click='save'
@cancel='cancel'
>
<a-steps :current='addModel.stepNumber'>
<a-steps :current='addModel.stepNumber' @change='stepChange'>
<a-step>
<template #title>选择产品</template>
</a-step>
@ -17,19 +17,28 @@
<template #title>触发类型</template>
</a-step>
</a-steps>
<a-divider style='margin-bottom: 0px' />
<div class='steps-content'>
<Product :rowKey='addModel.productId' />
<Product v-if='addModel.stepNumber === 0' v-model:rowKey='addModel.productId' v-model:detail='addModel.productDetail' />
<DeviceSelect
v-else-if='addModel.stepNumber === 1'
:productId='addModel.productId'
v-model:deviceKeys='addModel.deviceKeys'
v-model:orgId='addModel.orgId'
v-model:selector='addModel.selector'
v-model:selectorValues='addModel.selectorValues'
/>
<Type
v-else-if='addModel.stepNumber === 2'
:metadata='addModel.metadata'
/>
</div>
<template #footer>
<div class='steps-action'>
<template>
<a-button v-if='addModel.stepNumber === 0' @click='cancel'>取消</a-button>
<a-button v-else>上一步</a-button>
</template>
<template>
<a-button type='primary' v-if='addModel.stepNumber < 2'>下一步</a-button>
<a-button type='primary' v-else>确定</a-button>
</template>
<a-button v-if='addModel.stepNumber === 0' @click='cancel'>取消</a-button>
<a-button v-else @click='prev'>上一步</a-button>
<a-button type='primary' v-if='addModel.stepNumber < 2' @click='saveClick'>下一步</a-button>
<a-button type='primary' v-else @click='saveClick'>确定</a-button>
</div>
</template>
</a-modal>
@ -37,10 +46,12 @@
<script setup lang='ts' name='AddModel'>
import type { PropType } from 'vue'
import { TriggerDevice } from '@/views/rule-engine/Scene/typings'
import type { metadataType, TriggerDevice } from '@/views/rule-engine/Scene/typings'
import { onlyMessage } from '@/utils/comm'
import { detail as deviceDetail } from '@/api/device/instance'
import Product from './Product.vue'
import DeviceSelect from './DeviceSelect.vue'
import Type from './Type.vue'
type Emit = {
(e: 'cancel'): void
@ -54,11 +65,7 @@ interface AddModelType extends Omit<TriggerDevice, 'selectorValues'> {
orgId: Array<{ label: string, value: string }>
productDetail: any
selectorValues: Array<{ label: string, value: string }>
metadata: {
properties?: any[]
functions?: any[]
events?: any[]
}
metadata: metadataType
}
const emit = defineEmits<Emit>()
@ -97,39 +104,56 @@ const handleOptions = () => {
}
const prev = () => {
addModel.stepNumber = addModel.stepNumber - 1
}
const cancel = () => {
emit("cancel")
}
const handleMetadata = (metadata: string) => {
const handleMetadata = (metadata?: string) => {
try {
addModel.metadata = JSON.parse(metadata)
addModel.metadata = JSON.parse(metadata || "{}")
} catch (e) {
console.warn('handleMetadata: ' + e)
}
}
const save = async () => {
if (addModel.stepNumber === 0) {
const save = async (step?: number) => {
let _step = step !== undefined ? step : addModel.stepNumber
if (_step === 0) {
addModel.productId ? addModel.stepNumber = 1 : onlyMessage('请选择产品', 'error')
} else if (addModel.stepNumber === 1) {
} else if (_step === 1) {
const isFixed = addModel.selector === 'fixed' //
if ((['fixed', 'org'].includes(addModel.selector) ) && addModel.selectorValues?.length) {
if ((['fixed', 'org'].includes(addModel.selector) ) && !addModel.selectorValues?.length) {
return onlyMessage(isFixed ? '请选择设备' : '请选择部门', 'error')
}
//
if (isFixed && addModel.selectorValues?.length === 1) {
const resp = await deviceDetail(addModel.selectorValues[0].value)
addModel.metadata
handleMetadata(resp.result.metadata)
} else {
handleMetadata(addModel.productDetail?.metadata)
}
//
addModel.stepNumber = 2
} else {
}
// handleOptions()
// emit('update:value', {})
}
const saveClick = () => save()
const stepChange = (step: number) => {
if (step !== 0) {
save(step - 1)
} else {
addModel.stepNumber = 0
}
}
</script>
<style scoped>

View File

@ -0,0 +1,187 @@
<template>
<Search
:columns="columns"
type='simple'
@search="handleSearch"
class='search'
target="scene-triggrt-device-device"
/>
<a-divider style='margin: 0' />
<j-table
ref='actionRef'
model='CARD'
:columns='columns'
:request='deviceQuery'
:gridColumn='2'
:params='params'
:bodyStyle='{
paddingRight: 0,
paddingLeft: 0
}'
>
<template #card="slotProps">
<CardBox
:value='slotProps'
:active="deviceRowKeys.includes(slotProps.id)"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:statusNames="{
online: 'success',
offline: 'error',
notActive: 'warning',
}"
@click="handleClick"
>
<template #img>
<slot name="img">
<img width='88' height='88' :src="slotProps.photoUrl || getImage('/device/instance/device-card.png')" />
</slot>
</template>
<template #content>
<Ellipsis style='width: calc(100% - 100px)'>
<span style="font-size: 16px;font-weight: 600" >
{{ slotProps.name }}
</span>
</Ellipsis>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>{{ slotProps.deviceType?.text }}</div>
</a-col>
<a-col :span="12">
<div class="card-item-content-text">
产品名称
</div>
<div>{{ slotProps.productName }}</div>
</a-col>
</a-row>
</template>
</CardBox>
</template>
</j-table>
</template>
<script setup lang='ts' name='DeviceSelectList'>
import type { PropType } from 'vue'
import { getImage } from '@/utils/comm'
import { query } from '@/api/device/instance'
import { cloneDeep } from 'lodash-es'
type Emit = {
(e: 'update', data: Array<{ name: string, value: string}>): void
}
const actionRef = ref()
const params = ref({})
const context = inject('SceneDeviceAddModel')
const props = defineProps({
rowKeys: {
type: Array as PropType<Array<{ name: string, value: string}>>,
default: () => ([])
},
productId: {
type: String,
default: ''
}
})
const emit = defineEmits<Emit>()
const deviceRowKeys = computed(() => {
return props.rowKeys.map(item => item.value)
})
const columns = [
{
title: 'ID',
dataIndex: 'id',
width: 300,
ellipsis: true,
fixed: 'left',
search: {
type: 'string'
}
},
{
title: '设备名称',
dataIndex: 'name',
width: 200,
ellipsis: true,
search: {
type: 'string',
first: true
}
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 200,
search: {
type: 'date'
}
},
{
title: '状态',
dataIndex: 'state',
width: 90,
search: {
type: 'select',
options: [
{ label: '禁用', value: 'notActive' },
{ label: '离线', value: 'offline' },
{ label: '在线', value: 'online' },
]
}
},
]
const handleSearch = (p: any) => {
params.value = p
}
const deviceQuery = (p: any) => {
const sorts: any = [];
if (props.rowKeys) {
props.rowKeys.forEach(rowKey => {
sorts.push({
name: 'id',
value: rowKey,
});
})
}
sorts.push({ name: 'createTime', order: 'desc' });
const terms = [
...p.terms,
{ terms: [{ column: "productId", value: props.productId }]}
]
return query({ ...p, terms, sorts })
}
const handleClick = (detail: any) => {
const cloneRowKeys = cloneDeep(props.rowKeys)
const indexOf = cloneRowKeys.findIndex(item => item.value === detail.id)
if (indexOf !== -1) {
cloneRowKeys.splice(indexOf, 1)
} else {
cloneRowKeys.push({
name: detail.name,
value: detail.id
})
}
console.log('cloneRowKeys', cloneRowKeys)
emit('update', cloneRowKeys)
}
</script>
<style scoped>
.search {
margin-bottom: 0;
padding-right: 0px;
padding-left: 0px;
}
</style>

View File

@ -0,0 +1,83 @@
<template>
<div class='device-select'>
<TopCard :options='typeList' v-model:value='selectorModel' @select='select' />
<DeviceList v-if='selectorModel === "fixed"' :productId='productId' :row-keys='devices' @update='updateDevice' />
<OrgList v-else-if='selectorModel === "org"' :productId='productId' :row-keys='orgIds' @update='updateOrg' />
</div>
</template>
<script setup lang='ts'>
import TopCard from '@/views/rule-engine/Scene/Save/components/TopCard.vue'
import DeviceList from './DeviceList.vue'
import OrgList from './OrgList.vue'
import { getImage } from '@/utils/comm'
import type { PropType } from 'vue'
type ItemType = {
name: string,
value: string
}
type Emit = {
(e: 'update:selector', data: string): void
(e: 'update:selectorValues', data: ItemType[]): void
(e: 'update:deviceKeys', data: ItemType[]): void
(e: 'update:orgId', data: ItemType[]): void
}
const emit = defineEmits<Emit>()
const props = defineProps({
productId: {
type: String,
default: ''
},
selector: {
type: String,
default: ''
},
device: {
type: Array as PropType<ItemType[]>,
default: () => []
},
orgId: {
type: Array as PropType<ItemType[]>,
default: () => []
}
})
const selectorModel = ref(props.selector)
const devices = ref(props.device)
const orgIds = ref(props.orgId)
const typeList = [
{ label: '自定义', value: 'fixed', tip: '自定义选择当前产品下的任意设备', img: getImage('/scene/device-custom.png')},
{ label: '全部', value: 'all', tip: '产品下的所有设备', img: getImage('/scene/trigger-device-all.png')},
{ label: '按组织', value: 'org', tip: '选择产品下归属于具体组织的设备', img: getImage('/scene/trigger-device-org.png')},
]
const select = (s: string) => {
selectorModel.value = s
emit('update:selector', s)
emit('update:selectorValues', [])
}
const updateDevice = (d: any[]) => {
devices.value = d
emit('update:deviceKeys', d)
emit('update:selectorValues', d)
}
const updateOrg = (d: any[]) => {
orgIds.value = d
emit('update:orgId', d)
emit('update:selectorValues', d)
}
</script>
<style scoped lang='less'>
.device-select{
margin-top: 24px;
}
</style>

View File

@ -0,0 +1,130 @@
<template>
<Search
:columns="columns"
type='simple'
@search="handleSearch"
class='search'
target="scene-triggrt-device-category"
/>
<a-divider style='margin: 0' />
<JTable
ref="instanceRef"
model='TABLE'
type='TREE'
:columns="columns"
:request="query"
:scroll="{
y: 350
}"
:expandable='{
expandedRowKeys: openKeys,
onExpandedRowsChange: expandedRowChange,
}'
:rowSelection='{
type: "radio",
selectedRowKeys: orgRowKeys,
onChange: selectedRowChange
}'
:onChange='tableChange'
>
</JTable>
</template>
<script setup lang='ts' name='OrgList'>
import type { PropType } from 'vue'
import { getExpandedRowById } from './util'
import { getTreeData_api } from '@/api/system/department'
type Emit = {
(e: 'update', data: Array<{ name: string, value: string}>): void
}
const props = defineProps({
rowKeys: {
type: Array as PropType<Array<{ name: string, value: string}>>,
default: () => ([])
},
productId: {
type: String,
default: ''
}
})
const emit = defineEmits<Emit>()
const params = ref()
const openKeys = ref<string[]>([])
const selectedRowKeys = ref(props.rowKeys.map(item => item.value))
const sortParam = ref<{ name:string, order: string }>({ name: 'sortIndex', order: 'asc' })
const iniPage = ref(true)
const orgRowKeys = computed(() => {
return props.rowKeys.map(item => item.value)
})
const columns = [
{
title: '名称',
width: 300,
ellipsis: true,
dataIndex: 'name',
},
{
title: '排序',
dataIndex: 'sortIndex',
sorter: true,
},
]
const handleSearch = (p: any) => {
params.value = p
}
const tableChange = (_: any, f: any, sorter: any) => {
if (sorter.order) {
sortParam.value = { name: sorter.columnKey, order: (sorter.order as string).replace('end', ''), }
} else {
sortParam.value = { name: 'sortIndex', order: 'asc' }
}
}
const query = async (p: any) => {
const _params: any = {
paging: false,
sorts: [sortParam.value],
}
if (p.terms && p.terms.length) {
_params.terms = p.terms
}
const resp = await getTreeData_api(_params)
if (iniPage.value && props.rowKeys.length) {
iniPage.value = false
openKeys.value = getExpandedRowById(props.rowKeys[0]?.value, resp.result as any[])
}
return resp
}
const selectedRowChange = (_: any, selectedRows: any[]) => {
const item = selectedRows[0]
console.log(selectedRows)
emit('update', item ? [{ name: item.name, value: item.id }] : [])
}
const expandedRowChange = (keys: string[]) => {
openKeys.value = keys
}
</script>
<style scoped>
.search {
margin-bottom: 0;
padding-right: 0px;
padding-left: 0px;
}
</style>

View File

@ -4,18 +4,25 @@
type='simple'
@search="handleSearch"
class='search'
target="scene-triggrt-device-device"
/>
<a-divider style='margin: 0' />
<j-table
:columns='columns'
ref='actionRef'
model='CARD'
:columns='columns'
:params='params'
:request='productQuery'
:gridColumn='2'
model='CARD'
:bodyStyle='{
paddingRight: 0,
paddingLeft: 0
}'
>
<template #card="slotProps">
<CardBox
:value='slotProps'
:active="selectedRowKeys.includes(slotProps.id)"
:active="rowKey === slotProps.id"
:status="slotProps.state"
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
:statusNames="{ 1: 'success', 0: 'error', }"
@ -23,13 +30,17 @@
>
<template #img>
<slot name="img">
<img :src="getImage('/device-product.png')" />
<img width='88' height='88' :src="slotProps.photoUrl || getImage('/device-product.png')" />
</slot>
</template>
<template #content>
<h3 style="font-weight: 600" >
{{ slotProps.name }}
</h3>
<div style='width: calc(100% - 100px)'>
<Ellipsis>
<span style="font-size: 16px;font-weight: 600" >
{{ slotProps.name }}
</span>
</Ellipsis>
</div>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
@ -51,16 +62,25 @@ import { getTreeData_api } from '@/api/system/department'
import { isNoCommunity } from '@/utils/utils'
import { getImage } from '@/utils/comm'
type Emit = {
(e: 'update:rowKey', data: string): void
(e: 'update:detail', data: string): void
}
const actionRef = ref()
const params = ref({})
const props = defineProps({
rowKey: {
type: String,
default: ''
},
detail: {
type: Object,
default: () => ({})
}
})
const selectedRowKeys = ref(props.rowKey)
const emit = defineEmits<Emit>()
const columns = [
{
@ -69,12 +89,19 @@ const columns = [
width: 300,
ellipsis: true,
fixed: 'left',
search: {
type: 'string',
},
},
{
title: '名称',
dataIndex: 'name',
width: 200,
ellipsis: true,
search: {
type: 'string',
first: true
}
},
{
title: '网关类型',
@ -199,7 +226,6 @@ const columns = [
const handleSearch = (p: any) => {
params.value = p
actionRef.value.required()
}
const productQuery = (p: any) => {
@ -217,12 +243,8 @@ const productQuery = (p: any) => {
}
const handleClick = (detail: any) => {
const _selected = new Set(selectedRowKeys.value)
if (_selected.has(detail.id)) {
_selected.delete(detail.id)
} else {
_selected.add(detail.id)
}
emit('update:rowKey', detail.id)
emit('update:detail', detail)
}
</script>
@ -230,5 +252,7 @@ const handleClick = (detail: any) => {
<style scoped lang='less'>
.search {
margin-bottom: 0;
padding-right: 0px;
padding-left: 0px;
}
</style>

View File

@ -0,0 +1,100 @@
<template>
<div class='type'>
<a-form ref='typeForm' :model='formModel' layout='vertical' :colon='false'>
<a-form-item
required
label='触发类型'
>
<TopCard
:label-bottom='true'
:options='options'
v-model:value='formModel.operator'
/>
</a-form-item>
<Timer v-if='showTimer' />
</a-form>
</div>
</template>
<script setup lang='ts'>
import TopCard from '@/views/rule-engine/Scene/Save/components/TopCard.vue'
import { getImage } from '@/utils/comm'
import { metadataType } from '@/views/rule-engine/Scene/typings'
import type { PropType } from 'vue'
import { TypeEnum } from '@/views/rule-engine/Scene/Save/Device/util'
import Timer from '../components/Timer.vue'
const props = defineProps({
metadata: {
type: Object as PropType<metadataType>,
default: () => ({})
}
})
const formModel = reactive({
operator: 'online',
})
const readProperties = ref<any[]>([])
const writeProperties = ref<any[]>([])
const options = computed(() => {
const baseOptions = [
{
label: '设备上线',
value: 'online',
img: getImage('/scene/online.png'),
},
{
label: '设备离线',
value: 'offline',
img: getImage('/scene/offline.png'),
},
]
if (props.metadata.events?.length) {
baseOptions.push(TypeEnum.reportEvent)
}
if (props.metadata.properties?.length) {
const _properties = props.metadata.properties
readProperties.value = _properties.filter((item: any) => item.expands.type?.includes('read'))
writeProperties.value = _properties.filter((item: any) => item.expands.type?.includes('write'))
const reportProperties = _properties.filter((item: any) => item.expands.type?.includes('report'))
if (readProperties.value.length) {
baseOptions.push(TypeEnum.readProperty)
}
if (writeProperties.value.length) {
baseOptions.push(TypeEnum.writeProperty)
}
if (reportProperties.length) {
baseOptions.push(TypeEnum.reportProperty)
}
}
if (props.metadata.functions?.length) {
baseOptions.push(TypeEnum.invokeFunction)
}
return baseOptions
})
const showTimer = computed(() => {
return ['readProperty', 'writeProperty', 'invokeFunction'].includes(formModel.operator)
})
</script>
<style scoped lang='less'>
.type {
max-height: calc(100vh - 350px);
overflow-x: hidden;
overflow-y: auto;
margin-top: 24px;
}
</style>

View File

@ -26,7 +26,8 @@ import AddButton from '../components/AddButton.vue'
import Title from '../components/Title.vue'
const sceneStore = useSceneStore()
const { data } = storeToRefs(sceneStore)
const { data } = storeToRefs<any>(sceneStore)
const visible = ref(false)
const rules = [{

View File

@ -0,0 +1,69 @@
import { getImage } from '@/utils/comm'
export const TypeName = {
online: '设备上线',
offline: '设备离线',
reportEvent: '事件上报',
reportProperty: '属性上报',
readProperty: '读取属性',
writeProperty: '修改属性',
invokeFunction: '功能调用',
};
export const TypeEnum = {
reportProperty: {
label: '属性上报',
value: 'reportProperty',
img: getImage('/scene/reportProperty.png'),
},
reportEvent: {
label: '事件上报',
value: 'reportEvent',
img: getImage('/scene/reportProperty.png'),
},
readProperty: {
label: '读取属性',
value: 'readProperty',
img: getImage('/scene/readProperty.png'),
},
writeProperty: {
label: '修改属性',
value: 'writeProperty',
img: getImage('/scene/writeProperty.png'),
},
invokeFunction: {
label: '功能调用',
value: 'invokeFunction',
img: getImage('/scene/invokeFunction.png'),
},
};
export const getExpandedRowById = (id: string, data: any[]): string[] => {
const expandedKeys:string[] = []
const dataMap = new Map()
const flatMapData = (flatData: any[]) => {
flatData.forEach(item => {
dataMap.set(item.id, { pid: item.parentId })
if (item.children && item.children.length) {
flatMapData(item.children)
}
})
}
const getExp = (_id: string) => {
const item = dataMap.get(_id)
if (item) {
expandedKeys.push(_id)
if (dataMap.has(dataMap)) {
getExp(item.pid)
}
}
}
flatMapData(data)
getExp(id)
return expandedKeys
}

View File

@ -0,0 +1,138 @@
<template>
<a-form
ref='timerForm'
:model='formModel'
layout='vertical'
:colon='false'
>
<a-form-item name='trigger'>
<a-radio-group
v-model:value='formModel.trigger'
:options='[
{ label: "按周", value: "week" },
{ label: "按月", value: "month" },
{ label: "cron表达式", value: "cron" },
]'
option-type='button'
button-style='solid'
/>
</a-form-item>
<a-form-item v-if='showCron' name='cron'>
<a-input placeholder='corn表达式' v-model='formModel.cron' />
</a-form-item>
<template v-else>
<a-form-item name='when'>
</a-form-item>
<a-form-item name='mod'>
<a-radio-group
v-model:value='formModel.mod'
:options='[
{ label: "周期执行", value: "period" },
{ label: "执行一次", value: "once" },
]'
option-type='button'
button-style='solid'
/>
</a-form-item>
</template>
<a-space v-if='showOnce' style='display: flex;gap: 24px'>
<a-form-item :name="['once', 'time']">
<a-time-picker valueFormat='HH:mm:ss' v-model:value='formModel.once.time' style='width: 100%' format='HH:mm:ss' />
</a-form-item>
<a-form-item> 执行一次 </a-form-item>
</a-space>
<a-space v-if='showPeriod' style='display: flex;gap: 24px'>
<a-form-item>
<a-time-range-picker
valueFormat='HH:mm:ss'
:value='[
formModel.period.from,
formModel.period.to,
]'
@change='(v) => {
formModel.period.from = v[0]
formModel.period.to = v[1]
}'
/>
</a-form-item>
<a-form-item></a-form-item>
<a-form-item
:name='["period", "every"]'
:rules='[{ required: true, message: "请输入时间" }]'
>
<a-input-number
placeholder='请输入时间'
style='max-width: 170px'
:precision='0'
:min='1'
:max='59'
v-model:value='formModel.period.every'
>
<template #addonAfter>
<a-select
v-model:value='formModel.period.unit'
:options='[
{ label: "秒", value: "seconds" },
{ label: "分", value: "minutes" },
{ label: "小时", value: "hours" },
]'
/>
</template>
</a-input-number>
</a-form-item>
<a-form-item>执行一次</a-form-item>
</a-space>
</a-form>
</template>
<script setup lang='ts' name='Timer'>
import type { PropType } from 'vue'
import moment from 'moment'
type NameType = string[] | string
const props = defineProps({
name: {
type: [String, Array] as PropType<NameType>,
default: ''
},
value: {
type: Object,
default: () => ({})
}
})
const formModel = reactive({
trigger: 'week',
when: [],
mod: 'period',
cron: undefined,
once: {
time: ''
},
period: {
from: moment(new Date()).startOf('day').format('HH:mm:ss'),
to: moment(new Date()).endOf('day').format('HH:mm:ss'),
every: 1,
unit: 'seconds'
}
})
const showCron = computed(() => {
return formModel.trigger === 'cron'
})
const showOnce = computed(() => {
return formModel.trigger !== 'cron' && formModel.mod === 'once'
})
const showPeriod = computed(() => {
return formModel.trigger !== 'cron' && formModel.mod === 'period'
})
</script>
<style scoped lang='less'>
</style>

View File

@ -0,0 +1,167 @@
<template>
<div :class='classNames'>
<div
v-for='item in options'
:key='item.value'
:class='[
"trigger-way-item",
value === item.value ? "active" : "",
labelBottom ? "label-bottom" : ""
]'
@click='() => click(item.value)'
>
<div class='way-item-title'>
<span class='label'>{{ item.label }}</span>
<a-popover v-if='item.tip' :content='item.tip'>
<AIcon type='QuestionCircleOutlined' class='icon' />
</a-popover>
</div>
<div class='way-item-image'>
<img
width='48'
v-bind='item.imgProps'
:src='item.img'
/>
</div>
</div>
</div>
</template>
<script setup lang='ts' name='TopCard'>
import type { PropType } from 'vue'
type optionsType = {
label: string
value: string
img?: string
tip?: string
imgProps: Record<string, any>
}
type Emit = {
(e: 'update:value', data: string): void
(e: 'select', data: string): void
}
const props = defineProps({
options: {
type: Array as PropType<optionsType[]>,
default: () => ([])
},
value: {
type: String,
default: ''
},
class: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
labelBottom: {
type: Boolean,
default: false
}
})
const classNames = computed(() => {
return [
props.class,
'trigger-way-warp',
props.disabled ? 'disabled' : ''
]
})
const emit = defineEmits<Emit>()
const click = (value: string) => {
emit('update:value', value)
emit('select', value)
}
</script>
<style scoped lang='less'>
.trigger-way-warp {
display: flex;
flex-wrap: wrap;
gap: 16px 24px;
width: 100%;
.trigger-way-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 237px;
//width: 100%;
padding: 12px 16px;
border: 1px solid #e0e4e8;
border-radius: 2px;
cursor: pointer;
transition: all 0.3s;
.way-item-title {
span {
font-size: 16px;
}
.label {
padding-right: 6px;
color: rgba(#000, 0.64);
}
.icon {
color: rgba(#000, 0.5);
}
}
.way-item-image {
margin: 0 !important;
opacity: 0.6;
}
&:hover {
//color: @primary-color-hover;
.way-item-image {
opacity: 0.8;
}
}
&.active {
border-color: @primary-color-active;
.way-item-image {
opacity: 1;
}
}
&.label-bottom {
flex-direction: column-reverse;
grid-gap: 16px;
gap: 0;
align-items: center;
width: auto;
padding: 8px 16px;
p {
margin: 0;
}
}
}
&.disabled {
.trigger-way-item {
cursor: not-allowed;
&:hover {
color: initial;
opacity: 0.6;
}
&.active {
opacity: 1;
}
}
}
}
</style>

View File

@ -9,6 +9,11 @@ type State = {
text: string;
};
export type optionItem = {
label: string
value: string
}
type Action = {
executor: string;
configuration: Record<string, unknown>;
@ -311,3 +316,9 @@ export interface FormModelType {
options?: Record<string, any>;
description?: string;
}
export type metadataType = {
properties?: any[]
functions?: any[]
events?: any[]
}