fix: 物模型属性组件开发

This commit is contained in:
wangshuaiswim 2023-02-22 20:47:47 +08:00
parent 7bd5c034e5
commit c201ebdbf4
14 changed files with 1025 additions and 28 deletions

View File

@ -0,0 +1,70 @@
<template>
<a-modal :mask-closable="false" visible width="70vw" title="设置属性规则" @cancel="handleCancel" @ok="handleOk">
<div class="advance-box">
<div class="left">
<Editor
mode="advance"
key="advance"
v-model:value="_value"
/>
<Debug
:virtualRule="{
...virtualRule,
script: _value,
}"
:id="id"
/>
</div>
<div class="right">
<Operator :id="id" />
</div>
</div>
</a-modal>
</template>
<script setup lang="ts" name="Advance">
import Editor from '../Editor/index.vue'
import Debug from '../Debug/index.vue'
import Operator from '../Operator/index.vue'
interface Emits {
(e: 'update:value', data: string | undefined): void;
(e: 'change', data: string): void;
}
const emit = defineEmits<Emits>();
const props = defineProps({
value: String,
id: String,
virtualRule: Object
})
const _value = ref<string | undefined>(props.value)
const handleCancel = () => {
emit('change', 'simple')
}
const handleOk = () => {
emit('update:value', _value.value)
emit('change', 'simple')
}
</script>
<style lang="less" scoped>
.advance-box {
display: flex;
justify-content: flex-start;
width: 100%;
.left {
width: 70%;
}
.right {
width: 30%;
margin-left: 10px;
padding-left: 10px;
border-left: 1px solid lightgray;
}
}
</style>

View File

@ -0,0 +1,266 @@
<template>
<div class="debug-container">
<div class="left">
<div class="header">
<div>
<div class="title">
属性赋值
<div class="description">请对上方规则使用的属性进行赋值</div>
</div>
<div v-if="!isBeginning && virtualRule?.type === 'window'" class="action" @click="runScriptAgain">
<a style="margin-left: 75px;">发送数据</a>
</div>
</div>
</div>
<a-table :columns="columns" :data-source="property" :pagination="false" bordered size="small">
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'id'">
<a-input v-model:value="record.id" size="small"></a-input>
</template>
<template v-if="column.key === 'current'">
<a-input v-model:value="record.current" size="small"></a-input>
</template>
<template v-if="column.key === 'last'">
<a-input v-model:value="record.last" size="small"></a-input>
</template>
<template v-if="column.key === 'action'">
<delete-outlined @click="deleteItem(index)" />
</template>
</template>
</a-table>
<a-button type="dashed" block style="margin-top: 5px" @click="addItem">
<template #icon>
<plus-outlined />
</template>
添加条目
</a-button>
</div>
<div class="right">
<div class="header">
<div class="title">
<div>运行结果</div>
</div>
<div class="action">
<div>
<a v-if="isBeginning" @click="beginAction">
开始运行
</a>
<a v-else @click="stopAction">
停止运行
</a>
</div>
<div>
<a @click="clearAction">
清空
</a>
</div>
</div>
</div>
<div class="log">
<a-descriptions>
<a-descriptions-item v-for="item in ruleEditorStore.state.log" :label="moment(item.time).format('HH:mm:ss')" :key="item.time"
:span="3">
<a-tooltip placement="top" :title="item.content">
{{ item.content }}
</a-tooltip>
</a-descriptions-item>
))}
</a-descriptions>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="Debug">
import { PropType } from 'vue';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
import { useProductStore } from '@/store/product';
import { message } from 'ant-design-vue';
import { useRuleEditorStore } from '@/store/ruleEditor';
import moment from 'moment';
import { getWebSocket } from '@/utils/websocket';
const props = defineProps({
virtualRule: Object as PropType<Record<any, any>>,
id: String,
})
const isBeginning = ref(true)
const runScriptAgain = () => { }
type propertyType = {
id?: string,
current?: string,
last?: string
}
const property = ref<propertyType[]>([])
const columns = [{
title: '属性ID',
dataIndex: 'id',
key: 'id'
}, {
title: '当前值',
dataIndex: 'current',
key: 'current'
}, {
title: '上一值',
dataIndex: 'last',
key: 'last'
}, {
title: '',
key: 'action'
}]
const addItem = () => {
property.value.push({})
}
const deleteItem = (index: number) => {
property.value.splice(index, 1)
}
const ws = ref()
const virtualIdRef = ref(new Date().getTime());
const productStore = useProductStore()
const ruleEditorStore = useRuleEditorStore()
const runScript = () => {
const metadata = productStore.current.metadata || '{}';
const propertiesList = JSON.parse(metadata).properties || [];
const _properties = property.value.map((item: any) => {
const _item = propertiesList.find((i: any) => i.id === item.id);
return { ...item, type: _item?.valueType?.type };
});
if (ws.value) {
ws.value.unsubscribe();
}
if (!props.virtualRule?.script) {
isBeginning.value = true;
message.warning('请编辑规则');
return;
}
ws.value = getWebSocket(`virtual-property-debug-${ruleEditorStore.state.property}-${new Date().getTime()}`,
'/virtual-property-debug',
{
virtualId: `${virtualIdRef.value}-virtual-id`,
property: ruleEditorStore.state.property,
virtualRule: {
...props.virtualRule,
},
properties: _properties || [],
})
ws.value.subscribe((data: any) => {
ruleEditorStore.state.log.push({ time: new Date().getTime(), content: JSON.stringify(data.payload) });
})
}
const beginAction = () => {
isBeginning.value = false;
runScript();
}
const stopAction = () => {
isBeginning.value = true;
if (ws.value) {
ws.value.unsubscribe();
}
}
const clearAction = () => {
ruleEditorStore.set('log', []);
}
onUnmounted(() => {
if (ws.value) {
ws.value.unsubscribe();
}
})
</script>
<style lang="less" scoped>
.debug-container {
display: flex;
width: 100%;
height: 340px;
margin-top: 20px;
.left {
flex: 1;
min-width: 0;
max-width: 550px;
overflow-y: auto;
border: 1px solid lightgray;
.header {
display: flex;
align-items: center;
width: 100%;
height: 40px;
border-bottom: 1px solid lightgray;
//justify-content: space-around;
div {
display: flex;
//width: 100%;
align-items: center;
justify-content: flex-start;
height: 100%;
.title {
margin: 0 10px;
font-weight: 600;
font-size: 16px;
}
.description {
margin-left: 10px;
color: lightgray;
font-size: 12px;
}
}
.action {
width: 150px;
font-size: 14px;
}
}
}
.right {
flex: 1;
min-width: 0;
border: 1px solid lightgray;
border-left: none;
.header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 40px;
border-bottom: 1px solid lightgray;
.title {
display: flex;
div {
margin: 0 10px;
}
}
.action {
display: flex;
div {
margin: 0 10px;
}
}
}
.log {
height: 290px;
padding: 5px;
overflow: auto;
}
}
}
</style>

View File

@ -0,0 +1,211 @@
<template>
<div class="editor-box">
<div class="top">
<div class="left">
<span v-for="item in symbolList.filter((t: SymbolType, i: number) => i <= 3)" :key="item.key"
@click="handleInsertCode(item.value)">
{{ item.value }}
</span>
<span>
<a-dropdown>
<more-outlined />
<template #overlay>
<a-menu>
<a-menu-item v-for="item in symbolList.filter((t: SymbolType, i: number) => i > 6)" :key="item.key"
@click="handleInsertCode(item.value)">
{{ item.value }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</span>
</div>
<div class="right">
<span v-if="mode !== 'advance'">
<a-tooltip :title="!id ? '请先输入标识' : '设置属性规则'">
<fullscreen-outlined :class="!id ? 'disabled' : ''" @click="fullscreenClick" />
</a-tooltip>
</span>
</div>
</div>
<div class="editor">
<MonacoEditor v-if="loading" v-model:model-value="_value" theme="vs" ref="editor" />
</div>
</div>
</template>
<script setup lang="ts" name="Editor">
import { FullscreenOutlined, MoreOutlined } from '@ant-design/icons-vue';
import MonacoEditor from '@/components/MonacoEditor/index.vue';
interface Props {
mode?: 'advance' | 'simple';
id?: string;
value?: string;
}
const props = defineProps<Props>()
interface Emits {
(e: 'change', data: string): void;
(e: 'update:value', data: string): void;
}
const emit = defineEmits<Emits>()
type editorType = {
insert(val: string): void
}
const editor = ref<editorType>()
type SymbolType = {
key: string,
value: string
}
const symbolList = [
{
key: 'add',
value: '+',
},
{
key: 'subtract',
value: '-',
},
{
key: 'multiply',
value: '*',
},
{
key: 'divide',
value: '/',
},
{
key: 'parentheses',
value: '()',
},
{
key: 'cubic',
value: '^',
},
{
key: 'dayu',
value: '>',
},
{
key: 'dayudengyu',
value: '>=',
},
{
key: 'dengyudengyu',
value: '==',
},
{
key: 'xiaoyudengyu',
value: '<=',
},
{
key: 'xiaoyu',
value: '<',
},
{
key: 'jiankuohao',
value: '<>',
},
{
key: 'andand',
value: '&&',
},
{
key: 'huohuo',
value: '||',
},
{
key: 'fei',
value: '!',
},
{
key: 'and',
value: '&',
},
{
key: 'huo',
value: '|',
},
{
key: 'bolang',
value: '~',
},
] as SymbolType[];
const _value = computed({
get: () => props.value || '',
set: (data: string) => {
emit('update:value', data);
}
})
const loading = ref(false)
onMounted(() => {
setTimeout(() => {
loading.value = true;
}, 100);
})
const handleInsertCode = (val: string) => {
editor.value?.insert(val)
}
const fullscreenClick = () => {
if (props.id) {
emit('change', 'advance');
}
}
</script>
<style lang="less" scoped>
.editor-box {
margin-bottom: 10px;
border: 1px solid lightgray;
.top {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
border-bottom: 1px solid lightgray;
.left {
display: flex;
align-items: center;
width: 60%;
margin: 0 5px;
span {
display: inline-block;
height: 40px;
margin: 0 10px;
line-height: 40px;
cursor: pointer;
}
}
.right {
display: flex;
align-items: center;
width: 10%;
margin: 0 5px;
span {
margin: 0 5px;
}
}
.disabled {
color: rgba(#000, 0.5);
cursor: not-allowed;
}
}
.editor {
height: 300px;
}
}
</style>

View File

@ -0,0 +1,119 @@
<template>
<div class="operator-box">
<a-input-search @search="search" allow-clear placeholder="搜索关键字" />
<a-tree class="tree" @select="selectTree" :field-names="{ title: 'name', key: 'id', }" auto-expand-parent
:tree-data="data">
<template #title="node">
<div class="node">
<div>{{ node.name }}</div>
<div :class="node.children?.length > 0 ? 'parent' : 'add'">
<a-popover v-if="node.type === 'property'" placement="right" title="请选择使用值" @visibleChange="setVisible">
<template #content>
<a-space direction="vertical">
<a-tooltip placement="right" title="实时值为空时获取上一有效值补齐,实时值不为空则使用实时值">
<a-button type="text" @click="recentClick(node)">
$recent实时值
</a-button>
</a-tooltip>
<a-tooltip placement="right" title="实时值的上一有效值">
<a-button @click="lastClick(node)" type="text">
上一值
</a-button>
</a-tooltip>
</a-space>
</template>
<a @click="setVisible(true)">添加</a>
</a-popover>
<a v-else @click="addClick(node)">
添加
</a>
</div>
</div>
</template>
</a-tree>
<div class="explain">
<ReactMarkdown>{{ item?.description || '' }}</ReactMarkdown>
</div>
</div>
</template>
<script setup lang="ts" name="Operator">
import type { OperatorItem } from './typings';
import { treeFilter } from '@/utils/tree'
import { Store } from 'jetlinks-store';
const item = ref<Partial<OperatorItem>>()
const data = ref<OperatorItem[]>([])
const dataRef = ref<OperatorItem[]>([])
const visible = ref(false)
const search = (value: string) => {
if (value) {
const nodes = treeFilter(dataRef.value, value, 'name') as OperatorItem[];
data.value = nodes;
} else {
data.value = dataRef.value;
}
};
const selectTree = (k: any, info: any) => {
item.value = info.node as unknown as OperatorItem;
}
const setVisible = (_visible: boolean) => {
visible.value = !!visible
}
const recentClick = (node: OperatorItem) => {
Store.set('add-operator-value', `$recent("${node.id}")`);
setVisible(!visible.value);
}
const lastClick = (node: OperatorItem) => {
Store.set('add-operator-value', `$lastState("${node.id}")`);
setVisible(!visible.value);
}
const addClick = (node: OperatorItem) => {
Store.set('add-operator-value', node.code);
setVisible(true);
}
</script>
<style lang="less" scoped>
.border {
margin-top: 10px;
padding: 10px;
border-top: 1px solid lightgray;
}
.operator-box {
width: 100%;
.explain {
.border;
}
.tree {
.border;
height: 350px;
overflow-y: auto;
.node {
display: flex;
justify-content: space-between;
width: 220px;
//.add {
// display: none;
//}
//
//&:hover .add {
// display: block;
//}
.parent {
display: none;
}
}
}
}
</style>

View File

@ -0,0 +1,10 @@
import type { TreeNode } from '@/utils/tree';
interface OperatorItem extends TreeNode {
id: string;
name: string;
key: string;
description: string;
code: string;
children: OperatorItem[];
}

View File

@ -0,0 +1,45 @@
<template>
<Editor key="simple" @change="change" v-model:value="_value" :id="id" />
{{ ruleEditorStore.state.model }}
<Advance v-if="ruleEditorStore.state.model === 'advance'" :model="ruleEditorStore.state.model"
:virtualRule="virtualRule" :id="id" @change="change" />
</template>
<script setup lang="ts" name="FRuleEditor">
import { useRuleEditorStore } from '@/store/ruleEditor'
import Editor from './Editor/index.vue'
import Advance from './Advance/index.vue'
interface Props {
value: string;
property?: string;
virtualRule?: any;
id?: string;
}
const props = defineProps<Props>()
interface Emits {
(e: 'update:value', data: string): void;
}
const emit = defineEmits<Emits>()
const _value = computed({
get: () => props.value,
set: (val: string) => {
emit('update:value', val)
}
})
const ruleEditorStore = useRuleEditorStore()
const change = (v: string) => {
ruleEditorStore.set('model', v);
}
onMounted(() => {
ruleEditorStore.set('property', props.property)
ruleEditorStore.set('code', props.value);
})
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,36 @@
<template>
<a-form-item :name="name.concat(['script'])">
<f-rule-editor v-model:value="value.script" :id="id" ></f-rule-editor>
</a-form-item>
</template>
<script setup lang="ts" name="VirtualRuleParam">
import { PropType } from 'vue';
import FRuleEditor from '@/components/FRuleEditor/index.vue'
const props = defineProps({
value: {
type: Object,
default: () => ({
type: 'script',
})
},
name: {
type: Array as PropType<string[]>,
default: () => ([])
},
id: String
})
interface Emits {
(e: 'update:value', data: Record<any, any>): void;
}
const emit = defineEmits<Emits>()
onMounted(() => {
emit('update:value', {
...props.value,
type: 'script'
})
})
</script>
<style lang="less" scoped></style>

View File

@ -73,6 +73,27 @@ watchEffect(() => {
editorFormat();
}, 300);
});
const insert = (val) => {
if (!instance) return
const position = instance.getPosition();
instance.executeEdits(instance.getValue(), [
{
range: new monaco.Range(
position?.lineNumber,
position?.column,
position?.lineNumber,
position?.column,
),
text: val,
},
]);
}
defineExpose({
editorFormat,
insert,
})
</script>
<style lang="less" scoped>

27
src/store/ruleEditor.ts Normal file
View File

@ -0,0 +1,27 @@
import { defineStore } from "pinia";
type RuleEditorType = {
model: 'simple' | 'advance';
code: string;
property?: string;
log: {
content: string;
time: number;
}[];
};
export const useRuleEditorStore = defineStore({
id: 'ruleEditor',
state: () => ({
state: {
model: 'simple',
code: '',
log: [],
} as RuleEditorType
}),
actions: {
set(key: string, value: any) {
this.state[key] = value
}
}
})

117
src/utils/tree.ts Normal file
View File

@ -0,0 +1,117 @@
/**
, , ,
如visible:true
对有标识的子集的父级添加标识visible:true
visible标识对数据进行递归过滤
*/
import _ from 'lodash';
export type TreeNode = {
id: string;
name: string;
children: TreeNode[];
visible?: boolean;
} & Record<string, any>;
/*
*
* data
* filter
* filterType
*/
export function treeFilter(data: TreeNode[], filter: string, filterType: string): TreeNode[] {
const _data = _.cloneDeep(data);
const traverse = (item: TreeNode[]) => {
item.forEach((child) => {
child.visible = filterMethod(filter, child, filterType);
if (child.children) traverse(child.children);
if (!child.visible && child.children?.length) {
const visible = !child.children.some((c) => c.visible);
child.visible = !visible;
}
});
};
traverse(_data);
return filterDataByVisible(_data);
}
// 根据传入的值进行数据匹配, 并返回匹配结果
function filterMethod(val: string, data: TreeNode, filterType: string | number) {
return data[filterType].includes(val);
}
// 递归过滤符合条件的数据
function filterDataByVisible(data: TreeNode[]) {
return data.filter((item) => {
if (item.children) {
item.children = filterDataByVisible(item.children);
}
return item.visible;
});
}
const mockData = [
{
children: [
{
children: [],
name: '加',
id: 'operator-1',
},
{
children: [],
name: '减',
id: 'operator-2',
},
{
children: [],
name: '乘',
id: 'operator-3',
},
{
children: [],
name: '除',
id: 'operator-4',
},
{
children: [],
name: '括号',
id: 'operator-5',
},
{
children: [],
name: '按位异或',
id: 'operator-6',
},
],
name: '操作符',
id: 'operator',
},
{
children: [
{
children: [],
name: 'if',
id: 'if',
},
{
children: [],
name: 'for',
id: 'for',
},
{
children: [],
name: 'while',
id: 'while',
},
],
name: '控制语句',
id: 'control',
},
];
const myTree = treeFilter(mockData, '操作', 'name');
console.log(JSON.stringify(myTree), 'mytree');

View File

@ -0,0 +1,75 @@
<template>
<a-form-item label="来源" :name="name.concat(['source'])" v-if="type === 'product'" :rules="[
{ required: true, message: '请选择来源' },
]">
<a-select v-model:value="_value.source" :options="PropertySource" size="small" :disabled="metadataStore.model.action === 'edit'"></a-select>
</a-form-item>
<virtual-rule-param v-if="_value.source === 'rule'" v-model:value="_value.virtualRule" :name="name.concat(['virtualRule'])" :id="id"></virtual-rule-param>
<a-form-item label="读写类型" :name="name.concat(['type'])" :rules="[
{ required: true, message: '请选择读写类型' },
]">
<a-select v-model:value="_value.type" :options="options" mode="multiple" size="small"></a-select>
</a-form-item>
</template>
<script setup lang="ts" name="ExpandsForm">
import { useMetadataStore } from '@/store/metadata';
import { PropertySource } from '@/views/device/data';
import { PropType } from 'vue';
import VirtualRuleParam from '@/components/Metadata/VirtualRuleParam/index.vue';
type ValueType = Record<any, any>;
const props = defineProps({
value: {
type: Object as PropType<ValueType>,
default: () => ({})
},
type: {
type: String
},
name: {
type: Array as PropType<string[]>,
default: () => ([]),
required: true
},
id: {
type: String
},
})
interface Emits {
(e: 'update:value', data: ValueType): void;
}
const emit = defineEmits<Emits>()
const _value = computed({
get: () => props.value,
set: val => {
emit('update:value', val)
}
})
const options = [
{
label: '读',
value: 'read',
},
{
label: '写',
value: 'write',
},
{
label: '上报',
value: 'report',
},
]
const metadataStore = useMetadataStore()
onMounted(() => {
if (props.type === 'product' || !props.value.source) {
emit('update:value', { ...props.value, source: 'device' })
}
})
</script>
<style lang="less" scoped></style>

View File

@ -16,12 +16,10 @@
]">
<a-input v-model:value="form.model.name" size="small"></a-input>
</a-form-item>
<ValueTypeForm :name="['valueType']" v-model:value="form.model.valueType" key="property"></ValueTypeForm>
<a-form-item label="读写类型" :name="['expands', 'type']" :rules="[
{ required: true, message: '请选择读写类型' },
]">
<a-select v-model:value="form.model.expands.type" :options="form.expandsType" mode="multiple" size="small"></a-select>
</a-form-item>
<value-type-form :name="['valueType']" v-model:value="form.model.valueType" key="property"></value-type-form>
<expands-form :name="['expands']" v-model:value="form.model.expands" :type="type" :id="form.model.id"></expands-form>
<a-form-item label="说明" name="description" :rules="[
{ max: 200, message: '最多可输入200个字符' },
]">
@ -30,8 +28,18 @@
</a-form>
</template>
<script setup lang="ts" name="PropertyForm">
import { PropType } from 'vue';
import ExpandsForm from './ExpandsForm.vue';
import ValueTypeForm from './ValueTypeForm.vue'
const props = defineProps({
type: {
type: String as PropType<'product' | 'device'>,
required: true,
default: 'product'
}
})
const form = reactive({
model: {
valueType: {
@ -39,20 +47,6 @@ const form = reactive({
},
expands: {}
} as any,
expandsType: [
{
label: '读',
value: 'read',
},
{
label: '写',
value: 'write',
},
{
label: '上报',
value: 'report',
},
]
})
</script>

View File

@ -4,7 +4,7 @@
<template #extra>
<a-button :loading="save.loading" type="primary" @click="save.saveMetadata">保存</a-button>
</template>
<PropertyForm v-if="metadataStore.model.type === 'properties'"></PropertyForm>
<PropertyForm v-if="metadataStore.model.type === 'properties'" :type="type"></PropertyForm>
</a-drawer>
</template>
<script lang="ts" setup name="Edit">
@ -20,12 +20,18 @@ import { SystemConst } from '@/utils/consts';
import { detail } from '@/api/device/instance';
import { DeviceInstance } from '@/views/device/Instance/typings';
import PropertyForm from './PropertyForm.vue';
import { PropType } from 'vue';
interface Props {
type: 'product' | 'device';
tabs?: string;
}
const props = defineProps<Props>()
const props = defineProps({
type: {
type: String as PropType<'product' | 'device'>,
required: true,
default: 'product'
},
tabs: {
type: String
}
})
const route = useRoute()
const instanceStore = useInstanceStore()

View File

@ -55,7 +55,7 @@
<script setup lang="ts" name="BaseMetadata">
import type { MetadataItem, MetadataType } from '@/views/device/Product/typings'
import MetadataMapping from './columns'
import JTable, { JColumnProps } from '@/components/Table'
import JTable from '@/components/Table'
import { useInstanceStore } from '@/store/instance'
import { useProductStore } from '@/store/product'
import { useMetadataStore } from '@/store/metadata'
@ -97,7 +97,7 @@ const expandsType = ref({
write: '写',
report: '上报',
});
const actions: JColumnProps[] = [
const actions = [
{
title: '操作',
align: 'left',