feat: 新增场景联动设备规则新增Modal

This commit is contained in:
xieyonghong 2023-02-23 16:36:21 +08:00
parent dd3013ee04
commit 8ddd2562f1
14 changed files with 740 additions and 77 deletions

View File

@ -59,7 +59,16 @@ export const category = (data: any) => server.post('/device/category/_tree', dat
*
* @param data
*/
export const queryGatewayList = (data: any) => server.post('/gateway/device/_query/no-paging', data)
const defaultGatewayData = {
paging: false,
sorts: [
{
name: 'createTime',
order: 'desc',
},
],
}
export const queryGatewayList = (data: any = defaultGatewayData) => server.post('/gateway/device/_query/no-paging', data)
/**
* ()

View File

@ -235,7 +235,7 @@ const reset = () => {
urlParams.target = null
}
resetNumber.value += 1
emit('search', terms)
emit('search', { terms: []})
}
watch(width, (value) => {

View File

@ -1,21 +1,25 @@
<template>
<div class="title">
<div class="title" :style='style'>
<div class="title-before"></div>
<span>{{ data }}</span>
<slot name="extra"></slot>
</div>
</template>
<script>
export default {
name: "TitleComponent",
props: {
data: {
type: String,
default: ""
}
<script setup lang='ts' name='TitleComponent'>
import type { CSSProperties, PropType } from 'vue'
const props = defineProps({
data: {
type: String,
default: ""
},
};
style: {
type: Object as PropType<CSSProperties>,
default: () => ({})
}
})
</script>
<style lang="less" scoped>

View File

@ -67,66 +67,116 @@ const defaultOptions = {
],
};
export const useSceneStore = defineStore({
id: 'scene',
state: (): DataType => {
return {
data: {
trigger: { type: ''},
options: defaultOptions,
branches: defaultBranches,
description: ''
},
productCache: {}
}
},
actions: {
/**
*
*/
initData() {
export const useSceneStore = defineStore('scene', () => {
const data = reactive<FormModelType | any>({
trigger: { type: ''},
options: defaultOptions,
branches: defaultBranches,
description: '',
name: '',
id: undefined
})
const productCache = {}
},
/**
*
* @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
const getDetail = async (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);
}
if (!branches) {
branches = cloneDeep(defaultBranches)
if (triggerType === 'device') {
branches.push(null)
}
this.data = {
...result,
trigger: result.trigger || {},
branches: cloneDeep(assignmentKey(branches)),
options: {...defaultOptions, ...result.options },
} 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);
}
}
},
getProduct() {
Object.assign(data, {
...result,
trigger: result.trigger || {},
branches: cloneDeep(assignmentKey(branches)),
options: result.options ? {...defaultOptions, ...result.options } : defaultOptions,
})
}
},
getters: {
}
return {
data,
productCache,
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

@ -1,5 +1,6 @@
import type { Slots } from 'vue'
import { TOKEN_KEY } from '@/utils/variable'
import { message } from 'ant-design-vue'
/**
*
@ -96,3 +97,15 @@ export const modifySearchColumnValue = (e: any, column: object) => {
});
return e;
};
/**
* message
* @param msg
* @param type
*/
export const onlyMessage = (msg: string, type: 'success' | 'error' | 'warning' = 'success') => {
message[type]({
content: msg,
key: type
})
}

View File

@ -0,0 +1,137 @@
<template>
<a-modal
title='触发规则'
visible
:width='820'
@click='save'
@cancel='cancel'
>
<a-steps :current='addModel.stepNumber'>
<a-step>
<template #title>选择产品</template>
</a-step>
<a-step>
<template #title>选择设备</template>
</a-step>
<a-step>
<template #title>触发类型</template>
</a-step>
</a-steps>
<div class='steps-content'>
<Product :rowKey='addModel.productId' />
</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>
</div>
</template>
</a-modal>
</template>
<script setup lang='ts' name='AddModel'>
import type { PropType } from 'vue'
import { 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'
type Emit = {
(e: 'cancel'): void
(e: 'update:value', data: TriggerDevice): void
(e: 'update:options', data: any): void
}
interface AddModelType extends Omit<TriggerDevice, 'selectorValues'> {
stepNumber: number
deviceKeys: Array<{ label: string, value: string }>
orgId: Array<{ label: string, value: string }>
productDetail: any
selectorValues: Array<{ label: string, value: string }>
metadata: {
properties?: any[]
functions?: any[]
events?: any[]
}
}
const emit = defineEmits<Emit>()
const props = defineProps({
value: {
type: Object as PropType<TriggerDevice>,
default: () => ({
productId: '',
selector: 'fixed',
selectorValues: [],
})
},
options: {
type: Object as PropType<any>,
default: () => ({
})
}
})
const addModel = reactive<AddModelType>({
productId: '',
selector: 'fixed',
selectorValues: [],
stepNumber: 0,
deviceKeys: [],
orgId: [],
productDetail: {},
metadata: {}
})
Object.assign(addModel, props.value)
const handleOptions = () => {
}
const cancel = () => {
emit("cancel")
}
const handleMetadata = (metadata: string) => {
try {
addModel.metadata = JSON.parse(metadata)
} catch (e) {
console.warn('handleMetadata: ' + e)
}
}
const save = async () => {
if (addModel.stepNumber === 0) {
addModel.productId ? addModel.stepNumber = 1 : onlyMessage('请选择产品', 'error')
} else if (addModel.stepNumber === 1) {
const isFixed = addModel.selector === 'fixed' //
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
} else {
}
//
}
// handleOptions()
// emit('update:value', {})
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,231 @@
<template>
<Search
:columns="columns"
type='simple'
@search="handleSearch"
/>
<j-table
:columns='columns'
ref='actionRef'
:request='productQuery'
:gridColumn='2'
model='CARD'
>
<template #card="slotProps">
<CardBox
:value='slotProps'
:active="selectedRowKeys.includes(slotProps.id)"
:status="slotProps.state"
:statusText="slotProps.state === 1 ? '正常' : '禁用'"
:statusNames="{ 1: 'success', 0: 'error', }"
@click="handleClick"
>
<template #img>
<slot name="img">
<img :src="getImage('/device-product.png')" />
</slot>
</template>
<template #content>
<h3 style="font-weight: 600" >
{{ slotProps.name }}
</h3>
<a-row>
<a-col :span="12">
<div class="card-item-content-text">
设备类型
</div>
<div>直连设备</div>
</a-col>
</a-row>
</template>
</CardBox>
</template>
</j-table>
</template>
<script setup lang='ts' name='Product'>
import { getProviders, queryGatewayList, queryProductList } from '@/api/device/product'
import { queryTree } from '@/api/device/category'
import { getTreeData_api } from '@/api/system/department'
import { isNoCommunity } from '@/utils/utils'
import { getImage } from '@/utils/comm'
const actionRef = ref()
const params = ref({})
const props = defineProps({
rowKey: {
type: String,
default: ''
}
})
const selectedRowKeys = ref(props.rowKey)
const columns = [
{
title: 'ID',
dataIndex: 'id',
width: 300,
ellipsis: true,
fixed: 'left',
},
{
title: '名称',
dataIndex: 'name',
width: 200,
ellipsis: true,
},
{
title: '网关类型',
dataIndex: 'accessProvider',
width: 150,
ellipsis: true,
hideInTable: true,
search: {
type: 'select',
options: () => getProviders().then((resp: any) => {
if (isNoCommunity) {
return (resp?.result || []).map((item: any) => ({
label: item.name,
value: item.id
}))
} else {
return (resp?.result || []).filter((item: any) => [
'mqtt-server-gateway',
'http-server-gateway',
'mqtt-client-gateway',
'tcp-server-gateway',
].includes(item.id))
.map((item: any) => ({
label: item.name,
value: item.id,
}))
}
})
}
},
{
title: '接入方式',
dataIndex: 'accessName',
width: 150,
ellipsis: true,
search: {
type: 'select',
options: () => queryGatewayList().then((resp: any) =>
resp.result.map((item: any) => ({
label: item.name, value: item.id
}))
)
}
},
{
title: '设备类型',
dataIndex: 'deviceType',
width: 150,
search: {
type: 'select',
options: [
{ label: '直连设备', value: 'device' },
{ label: '网关子设备', value: 'childrenDevice' },
{ label: '网关设备', value: 'gateway' },
]
}
},
{
title: '状态',
dataIndex: 'state',
width: '90px',
search: {
type: 'select',
options: [
{ label: '禁用', value: 0 },
{ label: '正常', value: 1 },
]
}
},
{
title: '说明',
dataIndex: 'describe',
ellipsis: true,
width: 300,
},
{
dataIndex: 'classifiedId',
title: '分类',
hideInTable: true,
search: {
type: 'treeSelect',
options: queryTree({ paging: false }).then(resp => resp.result),
componentProps: {
fieldNames: {
label: 'name',
value: 'id',
}
}
}
},
{
dataIndex: 'id$dim-assets',
title: '所属组织',
hideInTable: true,
search: {
type: 'treeSelect',
options: getTreeData_api({ paging: false }).then((resp: any) => {
const formatValue = (list: any[]) => {
return list.map((item: any) => {
if (item.children) {
item.children = formatValue(item.children);
}
return {
...item,
value: JSON.stringify({
assetType: 'product',
targets: [
{
type: 'org',
id: item.id,
},
],
}),
}
})
}
return formatValue(resp.result)
}),
}
}
]
const handleSearch = (p: any) => {
params.value = p
actionRef.value.required()
}
const productQuery = (p: any) => {
const sorts: any = [];
if (props.rowKey) {
sorts.push({
name: 'id',
value: props.rowKey,
});
}
sorts.push({ name: 'createTime', order: 'desc' });
p.sorts = sorts
return queryProductList(p)
}
const handleClick = (detail: any) => {
const _selected = new Set(selectedRowKeys.value)
if (_selected.has(detail.id)) {
_selected.delete(detail.id)
} else {
_selected.add(detail.id)
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,45 @@
<template>
<div class='device'>
<a-form-item
:rules='rules'
name='device'
>
<template #label>
<TitleComponent data='触发规则' style='font-size: 14px;' />
</template>
<AddButton
style='width: 100%'
@click='visible = true'
>
<Title :options='data.options.trigger' />
</AddButton>
</a-form-item>
<AddModel v-if='visible' @cancel='visible = false' v-model='data.device' v-model:options='data.options.trigger' />
</div>
</template>
<script setup lang='ts' name='SceneSaveDevice'>
import { storeToRefs } from 'pinia';
import { useSceneStore } from '@/store/scene'
import AddModel from './AddModal.vue'
import AddButton from '../components/AddButton.vue'
import Title from '../components/Title.vue'
const sceneStore = useSceneStore()
const { data } = storeToRefs(sceneStore)
const visible = ref(false)
const rules = [{
validator(_: any, v: any) {
if (!v) {
return Promise.reject(new Error('请配置设备触发规则'));
}
return Promise.resolve();
},
}]
</script>
<style scoped lang='less'>
</style>

View File

@ -0,0 +1,13 @@
<template>
</template>
<script>
export default {
name: 'index'
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,13 @@
<template>
</template>
<script>
export default {
name: 'inex'
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,59 @@
<template>
<div class='rule-button-warp' :style='style'>
<div class='rule-button add-button' @click='click'>
<slot />
</div>
</div>
</template>
<script setup lang='ts' name='AddButton'>
import type { PropType, CSSProperties} from 'vue'
type Emit = {
(e: 'click'): void
}
const props = defineProps({
style: {
type: Object as PropType<CSSProperties>,
default: () => ({})
}
})
const emit = defineEmits<Emit>()
const click = () => {
emit('click')
}
</script>
<style scoped lang='less'>
.rule-button-warp {
display: inline-block;
padding: 14px 16px;
background-color: #fafafa;
border: 1px solid #f0f0f0;
border-radius: 2px;
cursor: pointer;
.rule-button {
display: inline-block;
padding: 6px 20px;
font-size: 14px;
line-height: 22px;
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 22px;
}
.add-button {
color: #bdbdbd;
&:hover,
&:active {
border-color: #d0d0d0;
}
}
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<div :class='["trigger-options-content", isAdd ? "is-add" : ""]'>
<span v-if='!isAdd'> 点击配置设备触发 </span>
<template v-else>
<div class='center-item'>
<AIcon v-if='options.selectorIcon' :type='options.selectorIcon' class='icon-padding-right' />
<span class='trigger-options-name'>
<Ellipsis style='width: 310px'>
{{ options.name }}
</Ellipsis>
</span>
<span v-if='options.extraName'>{{ options.extraName }}</span>
</div>
<template v-if='options.onlyName'>
<div v-if='options.productName' class='center-item'>
<AIcon type='icon-chanpin1' class='icon-padding-right' />
<span className='trigger-options-type'>{{ options.productName }}</span>
</div>
<div v-if='options.when'>
<span className='trigger-options-when'>{{ options.when }}</span>
</div>
<div v-if='options.time'>
<span className='trigger-options-time'>{{ options.time }}</span>
</div>
<div v-if='options.extraTime'>
<span className='trigger-options-extraTime'>{{ options.extraTime }}</span>
</div>
<div v-if='options.action' class='center-item'>
<AIcon :type='options.typeIcon' class='icon-padding-right' />
<span className='trigger-options-action'>{{ options.productName }}</span>
</div>
<div v-if='options.type' class='center-item'>
<AIcon :type='options.typeIcon' class='icon-padding-right' />
<span className='trigger-options-type'>{{ options.type }}</span>
</div>
</template>
</template>
</div>
</template>
<script setup lang='ts' name='DeviceTitle'>
const props = defineProps({
options: {
type: Object,
default: () => ({})
}
})
const isAdd = computed(() => {
console.log(props.options, Object.keys(props.options).length)
return !!Object.keys(props.options).length
})
</script>
<style scoped lang='less'>
.trigger-options-content {
.center-item {
display: flex;
align-items: center;
}
.icon-padding-right {
padding-right: 4px;
}
}
</style>

View File

@ -2,14 +2,18 @@
<page-container>
<div class='scene-warp'>
<div class='header'>
<Ellipsis :tooltip='data.name' style='max-width: 50%'>
<span class='title'>{{ data.name }}</span>
</Ellipsis>
<div class='type'>
<img :src='TriggerHeaderIcon[data.triggerType]' />
{{ keyByLabel[data.triggerType] }}
</div>
</div>
<a-form ref='sceneForm' :model='data'>
<a-form ref='sceneForm' :model='data' :colon='false' layout='vertical'>
<Device v-if='data.triggerType === "device"' />
<Manual v-else-if='data.triggerType === "manual"' />
<Timer v-else-if='data.triggerType === "timer"' />
</a-form>
<PermissionButton
type='primary'
@ -17,19 +21,26 @@
>
保存
</PermissionButton>
<!-- <a-button type='primary' :loading='loading'>保存</a-button>-->
</div>
</page-container>
</template>
<script setup lang='ts' name='Scene'>
import { storeToRefs } from 'pinia';
import { useSceneStore } from '@/store/scene'
import { TriggerHeaderIcon } from './asstes'
import { keyByLabel } from '../typings'
import Device from './Device/index.vue'
import Manual from './Manual/index.vue'
import Timer from './Timer/index.vue'
const sceneStore = useSceneStore()
const { data } = storeToRefs(sceneStore)
const { getDetail } = sceneStore
const { getDetail, data } = useSceneStore()
const route = useRoute();
const loading = ref(false)
console.log('data',data)
getDetail(route.query.id as string)
@ -45,17 +56,24 @@ getDetail(route.query.id as string)
align-items: center;
justify-content: flex-start;
margin-bottom: 16px;
.title {
font-size: 20px;
color: rgba(#000, .8);
font-weight: bold;
}
.type {
display: flex;
align-items: center;
min-width: 100px;
min-width: 80px;
margin-left: 16px;
padding: 4px 8px;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 2px;
img {
margin-right: 4px;
}
}
}
}

View File

@ -86,8 +86,8 @@ export default defineConfig(({ mode}) => {
// target: 'http://192.168.32.244:8881',
// target: 'http://47.112.135.104:5096', // opcua
// target: 'http://120.77.179.54:8844', // 120测试
target: 'http://47.108.63.174:8845', // 测试
// target: 'http://120.77.179.54:8844',
// target: 'http://47.108.63.174:8845', // 测试
target: 'http://120.77.179.54:8844',
ws: 'ws://120.77.179.54:8844',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')