refactor: 重构物模型操作

* feat: 添加读写类型插件

* feat: 添加读写类型插件

* feat: 优化规则

* feat: 抽离属性来源

* fix: 修改物模型

* feat: 属性来源

* feat: 物模型数据类型

* feat: 事件定义和标签定义

* fix: 物模型数据回显

* feat: 添加DataTable和CheckButton的按需引入

* feat: 添加指标配置

* feat: 添加指标配置和存储配置

* update: 功能定义组件抽离

* update: 事件定义组件抽离

* update: object其他配置

* feat: 添加物模型操作

* update: 修改物模型属性、功能、事件

* feat: 优化物模型操作

* feat: 优化物模型操作

* feat: 优化物模型Object类型

* feat: 优化物模型Object操作以及显示

* feat: 添加属性规则接口
This commit is contained in:
XieYongHong 2023-07-03 10:54:37 +08:00 committed by GitHub
parent 41daa5ea2a
commit 74747cd966
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 4167 additions and 492 deletions

View File

@ -1,3 +1,3 @@
#!/usr/bin/env bash
docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.1.0-SNAPSHOT .
docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.1.0-SNAPSHOT
docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.1.0-TEST .
docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.1.0-TEST

View File

@ -204,6 +204,10 @@ const matchComponents: IMatcher[] = [
pattern: /^Empty/,
styleDir: 'Empty'
},
{
pattern: /^PopconfirmModal/,
styleDir: 'PopconfirmModal'
},
{
pattern: /^Popconfirm/,
styleDir: 'Popconfirm'
@ -215,7 +219,15 @@ const matchComponents: IMatcher[] = [
{
pattern: /^Notification/,
styleDir: 'Notification'
}
},
{
pattern: /^DataTable/,
styleDir: 'DataTable'
},
{
pattern: /^CheckButton/,
styleDir: 'CheckButton'
},
]
export interface JetlinksVueResolverOptions {
@ -294,7 +306,19 @@ function getSideEffects(compName: string, options: JetlinksVueResolverOptions, _
}
const filterName = ['message', 'Notification']
const primitiveNames = ['AIcon','Affix', 'Anchor', 'AnchorLink', 'message', 'Notification', 'AutoComplete', 'AutoCompleteOptGroup', 'AutoCompleteOption', 'Alert', 'Avatar', 'AvatarGroup', 'BackTop', 'Badge', 'BadgeRibbon', 'Breadcrumb', 'BreadcrumbItem', 'BreadcrumbSeparator', 'Button', 'ButtonGroup', 'Calendar', 'Card', 'CardGrid', 'CardMeta', 'Collapse', 'CollapsePanel', 'Carousel', 'Cascader', 'Checkbox', 'CheckboxGroup', 'Col', 'Comment', 'ConfigProvider', 'DatePicker', 'MonthPicker', 'WeekPicker', 'RangePicker', 'QuarterPicker', 'Descriptions', 'DescriptionsItem', 'Divider', 'Dropdown', 'DropdownButton', 'Drawer', 'Empty', 'Form', 'FormItem', 'FormItemRest', 'Grid', 'Input', 'InputGroup', 'InputPassword', 'InputSearch', 'Textarea', 'Image', 'ImagePreviewGroup', 'InputNumber', 'Layout', 'LayoutHeader', 'LayoutSider', 'LayoutFooter', 'LayoutContent', 'List', 'ListItem', 'ListItemMeta', 'Menu', 'MenuDivider', 'MenuItem', 'MenuItemGroup', 'SubMenu', 'Mentions', 'MentionsOption', 'Modal', 'Statistic', 'StatisticCountdown', 'PageHeader', 'Pagination', 'Popconfirm', 'Popover', 'Progress', 'Radio', 'RadioButton', 'RadioGroup', 'Rate', 'Result', 'Row', 'Select', 'SelectOptGroup', 'SelectOption', 'Skeleton', 'SkeletonButton', 'SkeletonAvatar', 'SkeletonInput', 'SkeletonImage', 'Slider', 'Space', 'Spin', 'Steps', 'Step', 'Switch', 'Table', 'TableColumn', 'TableColumnGroup', 'TableSummary', 'TableSummaryRow', 'TableSummaryCell', 'Transfer', 'Tree', 'TreeNode', 'DirectoryTree', 'TreeSelect', 'TreeSelectNode', 'Tabs', 'TabPane', 'Tag', 'CheckableTag', 'TimePicker', 'TimeRangePicker', 'Timeline', 'TimelineItem', 'Tooltip', 'Typography', 'TypographyLink', 'TypographyParagraph', 'TypographyText', 'TypographyTitle', 'Upload', 'UploadDragger', 'LocaleProvider', 'ProTable', 'Search', 'AdvancedSearch', 'Ellipsis', 'MonacoEditor', 'ProLayout', 'ScrollTable', 'TableCard', 'Scrollbar', 'CardSelect', 'ColorPicker']
const primitiveNames = ['AIcon','Affix', 'Anchor', 'AnchorLink', 'message', 'Notification', 'AutoComplete', 'AutoCompleteOptGroup', 'AutoCompleteOption', 'Alert', 'Avatar', 'AvatarGroup', 'BackTop', 'Badge', 'BadgeRibbon', 'Breadcrumb', 'BreadcrumbItem', 'BreadcrumbSeparator', 'Button', 'ButtonGroup', 'Calendar', 'Card', 'CardGrid', 'CardMeta', 'Collapse', 'CollapsePanel', 'Carousel', 'Cascader', 'Checkbox', 'CheckboxGroup', 'Col', 'Comment', 'ConfigProvider', 'DatePicker', 'MonthPicker', 'WeekPicker', 'RangePicker', 'QuarterPicker', 'Descriptions', 'DescriptionsItem', 'Divider', 'Dropdown', 'DropdownButton', 'Drawer', 'Empty', 'Form', 'FormItem', 'FormItemRest', 'Grid', 'Input', 'InputGroup', 'InputPassword', 'InputSearch', 'Textarea', 'Image', 'ImagePreviewGroup', 'InputNumber', 'Layout', 'LayoutHeader', 'LayoutSider', 'LayoutFooter', 'LayoutContent', 'List', 'ListItem', 'ListItemMeta', 'Menu', 'MenuDivider', 'MenuItem', 'MenuItemGroup', 'SubMenu', 'Mentions', 'MentionsOption', 'Modal', 'Statistic', 'StatisticCountdown', 'PageHeader', 'Pagination', 'Popconfirm', 'Popover', 'Progress', 'Radio', 'RadioButton', 'RadioGroup', 'Rate', 'Result', 'Row', 'Select', 'SelectOptGroup', 'SelectOption', 'Skeleton', 'SkeletonButton', 'SkeletonAvatar', 'SkeletonInput', 'SkeletonImage', 'Slider', 'Space', 'Spin', 'Steps', 'Step', 'Switch', 'Table', 'TableColumn', 'TableColumnGroup', 'TableSummary', 'TableSummaryRow', 'TableSummaryCell', 'Transfer', 'Tree', 'TreeNode', 'DirectoryTree', 'TreeSelect', 'TreeSelectNode', 'Tabs', 'TabPane', 'Tag', 'CheckableTag', 'TimePicker', 'TimeRangePicker', 'Timeline', 'TimelineItem', 'Tooltip', 'Typography', 'TypographyLink', 'TypographyParagraph', 'TypographyText', 'TypographyTitle', 'Upload', 'UploadDragger', 'LocaleProvider', 'ProTable', 'Search', 'AdvancedSearch', 'Ellipsis', 'MonacoEditor', 'ProLayout', 'ScrollTable', 'TableCard', 'Scrollbar', 'CardSelect', 'ColorPicker', 'PopconfirmModal', 'DataTable',
'DataTableArray',
'DataTableString',
'DataTableInteger',
'DataTableDouble',
'DataTableBoolean',
'DataTableEnum',
'DataTableFile',
'DataTableDate',
'DataTableTypeSelect',
'DataTableObject',
'CheckButton',
]
const prefix = 'J'
let jetlinksNames: Set<string>

View File

@ -588,4 +588,11 @@ export const getInkingDevices = (data: string[]) => server.post('/plugin/mapping
export const getProtocolMetadata = (id: string, transport: string) => server.get(`/protocol/${id}/${transport}/metadata`)
/**
*
*/
export const saveDeviceVirtualProperty = (productId: string, deviceId: string, data: any[]) => server.patch(`/virtual/property/product/${productId}/${deviceId}/_batch`, data)
export const queryDeviceVirtualProperty = (productId: string, deviceId: string, propertyId: string) => server.get(`/virtual/property/product/${productId}/${deviceId}/${propertyId}`)

View File

@ -212,3 +212,12 @@ export const getMetadataDeviceConfig = (params: {
};
}) => server.get<Record<any, any>[]>(`/device/instance/${params.deviceId}/config-metadata/${params.metadata.type}/${params.metadata.id}/${params.metadata.dataType}`)
/**
*
*/
export const saveProductVirtualProperty = (productId: string, data: any[]) => server.patch(`/virtual/property/product/${productId}/_batch`, data)
export const queryProductVirtualProperty = (productId: string, propertyId: string) => server.get(`/virtual/property/product/${productId}/${propertyId}`)

View File

@ -0,0 +1,305 @@
<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>
<j-table :columns="columns" :data-source="property" :pagination="false" bordered size="small">
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'id'">
<j-auto-complete :options="options" v-model:value="record.id" size="small" style="width: 130px" />
</template>
<template v-if="column.key === 'current'">
<j-input v-model:value="record.current" size="small"></j-input>
</template>
<template v-if="column.key === 'last'">
<j-input v-model:value="record.last" size="small"></j-input>
</template>
<template v-if="column.key === 'action'">
<AIcon type="DeleteOutlined" @click="deleteItem(index)" />
</template>
</template>
</j-table>
<j-button type="dashed" block style="margin-top: 5px" @click="addItem">
<template #icon>
<AIcon type="PlusOutlined" />
</template>
添加条目
</j-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">
<j-descriptions>
<j-descriptions-item v-for="item in ruleEditorStore.state.log" :label="moment(item.time).format('HH:mm:ss')"
:key="item.time" :span="3">
<j-tooltip placement="top" :title="item.content">
{{ item.content }}
</j-tooltip>
</j-descriptions-item>
</j-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 'jetlinks-ui-components';
import { useRuleEditorStore } from '@/store/ruleEditor';
import moment from 'moment';
import { getWebSocket } from '@/utils/websocket';
import { PropertyMetadata } from '@/views/device/Product/typings';
const props = defineProps({
virtualRule: Object as PropType<Record<any, any>>,
id: String,
})
const isBeginning = ref(true)
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-${props.id}-${new Date().getTime()}`,
'/virtual-property-debug',
{
virtualId: `${virtualIdRef.value}-virtual-id`,
property: props.id,
virtualRule: {
...props.virtualRule,
},
properties: _properties || [],
})
ws.value.subscribe((data: any) => {
ruleEditorStore.state.log.push({ time: new Date().getTime(), content: JSON.stringify(data.payload) });
if (props.virtualRule?.type !== 'window') {
stopAction()
}
})
}
const wsAgain = ref<any>();
const runScriptAgain = async () => {
if (wsAgain.value) {
wsAgain.value.unsubscribe?.();
}
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 };
});
wsAgain.value = getWebSocket(`virtual-property-debug-${props.id}-${new Date().getTime()}`,
'/virtual-property-debug',
{
virtualId: `${virtualIdRef.value}-virtual-id`,
property: props.id,
virtualRule: {
...props.virtualRule,
},
properties: _properties || [],
})
wsAgain.value.subscribe((data: any) => { })
}
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?.();
}
clearAction()
})
const options = ref<{ label: string, value: string }[]>()
const getProperty = () => {
const metadata = productStore.current.metadata || '{}';
const _p: PropertyMetadata[] = JSON.parse(metadata).properties || [];
options.value = _p.filter((p) => p.id !== props.id).map((item) => ({
label: item.name,
value: item.id,
}));
}
getProperty()
</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,213 @@
<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="addOperatorValue(item.value)">
{{ item.value }}
</span>
<span>
<j-dropdown>
<AIcon type="MoreOutlined" />
<template #overlay>
<j-menu>
<j-menu-item v-for="item in symbolList.filter((t: SymbolType, i: number) => i > 6)" :key="item.key"
@click="addOperatorValue(item.value)">
{{ item.value }}
</j-menu-item>
</j-menu>
</template>
</j-dropdown>
</span>
</div>
<div class="right">
<span v-if="mode !== 'advance'">
<j-tooltip :title="!id ? '请先输入标识' : '设置属性规则'">
<AIcon type="FullscreenOutlined" :class="!id ? 'disabled' : ''" @click="fullscreenClick" />
</j-tooltip>
</span>
</div>
</div>
<div class="editor">
<j-monaco-editor v-if="loading" v-model:model-value="_value" theme="vs" ref="editor" language="javascript"/>
</div>
</div>
</template>
<script setup lang="ts" name="Editor">
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 addOperatorValue = (val: string) => {
editor.value?.insert(val)
}
const fullscreenClick = () => {
if (props.id) {
emit('change', 'advance');
}
}
defineExpose({
addOperatorValue
})
</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,161 @@
<template>
<div class="operator-box">
<j-input-search @search="search" allow-clear placeholder="搜索关键字" />
<div class="tree">
<j-tree @select="selectTree" :field-names="{ title: 'name', key: 'id', }" auto-expand-parent
:tree-data="data">
<template #title="node">
<div class="node">
<div style="max-width: 180px"><Ellipsis>{{ node.name }}</Ellipsis></div>
<div :class="node.children?.length > 0 ? 'parent' : 'add'">
<j-popover v-if="node.type === 'property'" placement="right" title="请选择使用值">
<template #content>
<j-space direction="vertical">
<j-tooltip placement="right" title="实时值为空时获取上一有效值补齐,实时值不为空则使用实时值">
<j-button type="text" @click="recentClick(node)">
$recent实时值
</j-button>
</j-tooltip>
<j-tooltip placement="right" title="实时值的上一有效值">
<j-button @click="lastClick(node)" type="text">
上一值
</j-button>
</j-tooltip>
</j-space>
</template>
<a>添加</a>
</j-popover>
<a v-else @click="addClick(node)">
添加
</a>
</div>
</div>
</template>
</j-tree>
</div>
<div class="explain">
<Markdown :source="item?.description || ''"></Markdown>
</div>
</div>
</template>
<script setup lang="ts" name="Operator">
import { useProductStore } from '@/store/product';
import type { OperatorItem } from './typings';
import { treeFilter } from '@/utils/tree'
import { PropertyMetadata } from '@/views/device/Product/typings';
import { getOperator } from '@/api/device/product'
import Markdown from 'vue3-markdown-it'
const props = defineProps({
id: String
})
interface Emits {
(e: 'addOperatorValue', data: string): void;
}
const emit = defineEmits<Emits>();
const item = ref<Partial<OperatorItem>>()
const data = ref<OperatorItem[]>([])
const dataRef = ref<OperatorItem[]>([])
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 recentClick = (node: OperatorItem) => {
emit('addOperatorValue', `$recent("${node.id}")`)
}
const lastClick = (node: OperatorItem) => {
emit('addOperatorValue', `$lastState("${node.id}")`)
}
const addClick = (node: OperatorItem) => {
emit('addOperatorValue', node.code)
}
const productStore = useProductStore()
const getData = async (id?: string) => {
const metadata = productStore.current.metadata || '{}';
const _properties = JSON.parse(metadata).properties || [] as PropertyMetadata[]
const properties = {
id: 'property',
name: '属性',
description: '',
code: '',
children: _properties
.filter((p: PropertyMetadata) => p.id !== id)
.map((p: PropertyMetadata) => ({
id: p.id,
name: p.name,
description: `### ${p.name}
\n 数据类型: ${p.valueType?.type}
\n 是否只读: ${p.expands?.readOnly || 'false'}
\n 可写数值范围: `,
type: 'property',
})),
};
const response = await getOperator();
if (response.status === 200) {
data.value = [properties as OperatorItem, ...response.result];
dataRef.value = [properties as OperatorItem, ...response.result];
}
};
watch(() => props.id,
(val) => {
getData(val)
},
{ immediate: 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,44 @@
<template>
<Editor key="simple" @change="change" v-model:value="_value" :id="id" />
<Advance v-if="ruleEditorStore.state.model === 'advance'" v-model:value="_value" :model="ruleEditorStore.state.model"
:virtualRule="virtualRule" :id="id" @change="change" />
</template>
<script setup lang="ts">
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

@ -1,73 +1,107 @@
<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 class="debug-container">
<div class="top">
<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>
<div class="top-bottom">
<j-table
:columns="columns"
:data-source="property"
:pagination="false"
bordered
size="small"
:scroll="{ y: 200 }"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'id'">
<j-auto-complete
:options="options"
v-model:value="record.id"
size="small"
style="width: 100%"
/>
</template>
<template v-if="column.key === 'current'">
<j-input
v-model:value="record.current"
size="small"
></j-input>
</template>
<template v-if="column.key === 'last'">
<j-input
v-model:value="record.last"
size="small"
></j-input>
</template>
<template v-if="column.key === 'action'">
<AIcon
type="DeleteOutlined"
@click="deleteItem(index)"
/>
</template>
</template>
</j-table>
<j-button
type="dashed"
block
style="margin-top: 5px"
@click="addItem"
>
<template #icon>
<AIcon type="PlusOutlined" />
</template>
添加条目
</j-button>
</div>
</div>
<div class="bottom">
<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">
<j-descriptions>
<j-descriptions-item
v-for="item in ruleEditorStore.state.log"
:label="moment(item.time).format('HH:mm:ss')"
:key="item.time"
:span="3"
>
<j-tooltip placement="top" :title="item.content">
{{ item.content }}
</j-tooltip>
</j-descriptions-item>
</j-descriptions>
</div>
</div>
</div>
<j-table :columns="columns" :data-source="property" :pagination="false" bordered size="small">
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'id'">
<j-auto-complete :options="options" v-model:value="record.id" size="small" style="width: 130px" />
</template>
<template v-if="column.key === 'current'">
<j-input v-model:value="record.current" size="small"></j-input>
</template>
<template v-if="column.key === 'last'">
<j-input v-model:value="record.last" size="small"></j-input>
</template>
<template v-if="column.key === 'action'">
<AIcon type="DeleteOutlined" @click="deleteItem(index)" />
</template>
</template>
</j-table>
<j-button type="dashed" block style="margin-top: 5px" @click="addItem">
<template #icon>
<AIcon type="PlusOutlined" />
</template>
添加条目
</j-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">
<j-descriptions>
<j-descriptions-item v-for="item in ruleEditorStore.state.log" :label="moment(item.time).format('HH:mm:ss')"
:key="item.time" :span="3">
<j-tooltip placement="top" :title="item.content">
{{ item.content }}
</j-tooltip>
</j-descriptions-item>
</j-descriptions>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="Debug">
import { PropType } from 'vue';
@ -79,227 +113,242 @@ import moment from 'moment';
import { getWebSocket } from '@/utils/websocket';
import { PropertyMetadata } from '@/views/device/Product/typings';
const props = defineProps({
virtualRule: Object as PropType<Record<any, any>>,
id: String,
})
virtualRule: Object as PropType<Record<any, any>>,
id: String,
});
const isBeginning = ref(true)
const isBeginning = ref(true);
type propertyType = {
id?: string,
current?: string,
last?: string
}
const property = ref<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 columns = [
{
title: '属性ID',
dataIndex: 'id',
key: 'id',
},
{
title: '当前值',
dataIndex: 'current',
key: 'current',
},
{
title: '上一值',
dataIndex: 'last',
key: 'last',
},
{
title: '操作',
key: 'action',
width: 50,
},
];
const addItem = () => {
property.value.push({})
}
property.value.push({});
};
const deleteItem = (index: number) => {
property.value.splice(index, 1)
}
property.value.splice(index, 1);
};
const ws = ref()
const ws = ref();
const virtualIdRef = ref(new Date().getTime());
const productStore = useProductStore()
const ruleEditorStore = useRuleEditorStore()
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 };
});
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-${props.id}-${new Date().getTime()}`,
'/virtual-property-debug',
{
virtualId: `${virtualIdRef.value}-virtual-id`,
property: props.id,
virtualRule: {
...props.virtualRule,
},
properties: _properties || [],
})
ws.value.subscribe((data: any) => {
ruleEditorStore.state.log.push({ time: new Date().getTime(), content: JSON.stringify(data.payload) });
if (props.virtualRule?.type !== 'window') {
stopAction()
if (ws.value) {
ws.value.unsubscribe?.();
}
})
}
if (!props.virtualRule?.script) {
isBeginning.value = true;
message.warning('请编辑规则');
return;
}
ws.value = getWebSocket(
`virtual-property-debug-${props.id}-${new Date().getTime()}`,
'/virtual-property-debug',
{
virtualId: `${virtualIdRef.value}-virtual-id`,
property: props.id,
virtualRule: {
...props.virtualRule,
},
properties: _properties || [],
},
);
ws.value.subscribe((data: any) => {
ruleEditorStore.state.log.push({
time: new Date().getTime(),
content: JSON.stringify(data.payload),
});
if (props.virtualRule?.type !== 'window') {
stopAction();
}
});
};
const wsAgain = ref<any>();
const runScriptAgain = async () => {
if (wsAgain.value) {
wsAgain.value.unsubscribe?.();
}
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 (wsAgain.value) {
wsAgain.value.unsubscribe?.();
}
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 };
});
wsAgain.value = getWebSocket(`virtual-property-debug-${props.id}-${new Date().getTime()}`,
'/virtual-property-debug',
{
virtualId: `${virtualIdRef.value}-virtual-id`,
property: props.id,
virtualRule: {
...props.virtualRule,
},
properties: _properties || [],
})
wsAgain.value.subscribe((data: any) => { })
}
wsAgain.value = getWebSocket(
`virtual-property-debug-${props.id}-${new Date().getTime()}`,
'/virtual-property-debug',
{
virtualId: `${virtualIdRef.value}-virtual-id`,
property: props.id,
virtualRule: {
...props.virtualRule,
},
properties: _properties || [],
},
);
wsAgain.value.subscribe((data: any) => {});
};
const beginAction = () => {
isBeginning.value = false;
runScript();
}
isBeginning.value = false;
runScript();
};
const stopAction = () => {
isBeginning.value = true;
if (ws.value) {
ws.value.unsubscribe?.();
}
}
isBeginning.value = true;
if (ws.value) {
ws.value.unsubscribe?.();
}
};
const clearAction = () => {
ruleEditorStore.set('log', []);
}
ruleEditorStore.set('log', []);
};
onUnmounted(() => {
if (ws.value) {
ws.value.unsubscribe?.();
}
clearAction()
})
if (ws.value) {
ws.value.unsubscribe?.();
}
clearAction();
});
const options = ref<{ label: string, value: string }[]>()
const options = ref<{ label: string; value: string }[]>();
const getProperty = () => {
const metadata = productStore.current.metadata || '{}';
const _p: PropertyMetadata[] = JSON.parse(metadata).properties || [];
options.value = _p.filter((p) => p.id !== props.id).map((item) => ({
label: item.name,
value: item.id,
}));
}
getProperty()
const metadata = productStore.current.metadata || '{}';
const _p: PropertyMetadata[] = JSON.parse(metadata).properties || [];
options.value = _p
.filter((p) => p.id !== props.id)
.map((item) => ({
label: item.name,
value: item.id,
}));
};
getProperty();
</script>
<style lang="less" scoped>
.debug-container {
display: flex;
width: 100%;
height: 340px;
margin-top: 20px;
// 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;
.top {
// min-width: 0;
// max-width: 550px;
// overflow-y: auto;
height: 350px;
border: 1px solid lightgray;
margin-bottom: 10px;
.header {
display: flex;
align-items: center;
width: 100%;
height: 40px;
border-bottom: 1px solid lightgray;
//justify-content: space-around;
.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%;
div {
display: flex;
//width: 100%;
align-items: center;
justify-content: flex-start;
height: 100%;
.title {
margin: 0 10px;
font-weight: 600;
font-size: 16px;
.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;
}
}
.description {
margin-left: 10px;
color: lightgray;
font-size: 12px;
.top-bottom {
padding: 10px;
}
}
.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;
.bottom {
border: 1px solid lightgray;
.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: 300px;
padding: 5px;
overflow: auto;
}
}
}
}
</style>

View File

@ -1,161 +1,200 @@
<template>
<div class="operator-box">
<j-input-search @search="search" allow-clear placeholder="搜索关键字" />
<div class="tree">
<j-tree @select="selectTree" :field-names="{ title: 'name', key: 'id', }" auto-expand-parent
:tree-data="data">
<template #title="node">
<div class="node">
<div style="max-width: 180px"><Ellipsis>{{ node.name }}</Ellipsis></div>
<div :class="node.children?.length > 0 ? 'parent' : 'add'">
<j-popover v-if="node.type === 'property'" placement="right" title="请选择使用值">
<template #content>
<j-space direction="vertical">
<j-tooltip placement="right" title="实时值为空时获取上一有效值补齐,实时值不为空则使用实时值">
<j-button type="text" @click="recentClick(node)">
$recent实时值
</j-button>
</j-tooltip>
<j-tooltip placement="right" title="实时值的上一有效值">
<j-button @click="lastClick(node)" type="text">
上一值
</j-button>
</j-tooltip>
</j-space>
</template>
<a>添加</a>
</j-popover>
<div class="operator-box">
<div class="left">
<j-input-search
@search="search"
allow-clear
placeholder="搜索关键字"
/>
<div class="tree">
<j-tree
@select="selectTree"
:field-names="{ title: 'name', key: 'id' }"
auto-expand-parent
:tree-data="data"
>
<template #title="node">
<div class="node">
<div style="max-width: 160px">
<Ellipsis>{{ node.name }}</Ellipsis>
</div>
<div
:class="
node.children?.length > 0 ? 'parent' : 'add'
"
>
<j-popover
v-if="node.type === 'property'"
placement="right"
title="请选择使用值"
>
<template #content>
<j-space direction="vertical">
<j-tooltip
placement="right"
title="实时值为空时获取上一有效值补齐,实时值不为空则使用实时值"
>
<j-button
type="text"
@click="recentClick(node)"
>
$recent实时值
</j-button>
</j-tooltip>
<j-tooltip
placement="right"
title="实时值的上一有效值"
>
<j-button
@click="lastClick(node)"
type="text"
>
上一值
</j-button>
</j-tooltip>
</j-space>
</template>
<a>添加</a>
</j-popover>
<a v-else @click="addClick(node)">
添加
</a>
<a v-else @click="addClick(node)"> 添加 </a>
</div>
</div>
</template>
</j-tree>
</div>
</div>
</template>
</j-tree>
</div>
<div class="right">
<Markdown :source="item?.description || ''"></Markdown>
</div>
</div>
<div class="explain">
<Markdown :source="item?.description || ''"></Markdown>
</div>
</div>
</template>
<script setup lang="ts" name="Operator">
import { useProductStore } from '@/store/product';
import type { OperatorItem } from './typings';
import { treeFilter } from '@/utils/tree'
import { treeFilter } from '@/utils/tree';
import { PropertyMetadata } from '@/views/device/Product/typings';
import { getOperator } from '@/api/device/product'
import Markdown from 'vue3-markdown-it'
import { getOperator } from '@/api/device/product';
import Markdown from 'vue3-markdown-it';
const props = defineProps({
id: String
})
id: String,
});
interface Emits {
(e: 'addOperatorValue', data: string): void;
(e: 'addOperatorValue', data: string): void;
}
const emit = defineEmits<Emits>();
const item = ref<Partial<OperatorItem>>()
const data = ref<OperatorItem[]>([])
const dataRef = ref<OperatorItem[]>([])
const item = ref<Partial<OperatorItem>>();
const data = ref<OperatorItem[]>([]);
const dataRef = ref<OperatorItem[]>([]);
const search = (value: string) => {
if (value) {
const nodes = treeFilter(dataRef.value, value, 'name') as OperatorItem[];
data.value = nodes;
} else {
data.value = dataRef.value;
}
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;
}
item.value = info.node as unknown as OperatorItem;
};
const recentClick = (node: OperatorItem) => {
emit('addOperatorValue', `$recent("${node.id}")`)
}
emit('addOperatorValue', `$recent("${node.id}")`);
};
const lastClick = (node: OperatorItem) => {
emit('addOperatorValue', `$lastState("${node.id}")`)
}
emit('addOperatorValue', `$lastState("${node.id}")`);
};
const addClick = (node: OperatorItem) => {
emit('addOperatorValue', node.code)
}
emit('addOperatorValue', node.code);
};
const productStore = useProductStore()
const productStore = useProductStore();
const getData = async (id?: string) => {
const metadata = productStore.current.metadata || '{}';
const _properties = JSON.parse(metadata).properties || [] as PropertyMetadata[]
const properties = {
id: 'property',
name: '属性',
description: '',
code: '',
children: _properties
.filter((p: PropertyMetadata) => p.id !== id)
.map((p: PropertyMetadata) => ({
id: p.id,
name: p.name,
description: `### ${p.name}
const metadata = productStore.current.metadata || '{}';
const _properties =
JSON.parse(metadata).properties || ([] as PropertyMetadata[]);
const properties = {
id: 'property',
name: '属性',
description: '',
code: '',
children: _properties
.filter((p: PropertyMetadata) => p.id !== id)
.map((p: PropertyMetadata) => ({
id: p.id,
name: p.name,
description: `### ${p.name}
\n 数据类型: ${p.valueType?.type}
\n 是否只读: ${p.expands?.readOnly || 'false'}
\n 可写数值范围: `,
type: 'property',
})),
};
const response = await getOperator();
if (response.status === 200) {
data.value = [properties as OperatorItem, ...response.result];
dataRef.value = [properties as OperatorItem, ...response.result];
}
type: 'property',
})),
};
const response = await getOperator();
if (response.status === 200) {
data.value = [properties as OperatorItem, ...response.result];
dataRef.value = [properties as OperatorItem, ...response.result];
}
};
watch(() => props.id,
(val) => {
getData(val)
},
{ immediate: true }
)
watch(
() => props.id,
(val) => {
getData(val);
},
{ immediate: true },
);
</script>
<style lang="less" scoped>
.border {
margin-top: 10px;
padding: 10px;
border-top: 1px solid lightgray;
margin-top: 10px;
padding: 10px;
border-top: 1px solid lightgray;
}
.operator-box {
width: 100%;
width: 100%;
display: flex;
.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;
}
.left,
.right {
width: 50%;
height: 350px;
border: 1px solid lightgray;
}
.left {
padding: 10px;
margin-right: 10px;
.tree {
height: 300px;
overflow-y: auto;
.node {
display: flex;
justify-content: space-between;
width: 190px;
.parent {
display: none;
}
}
}
}
.right {
padding: 20px;
}
}
}
</style>

View File

@ -1,44 +1,116 @@
<template>
<Editor key="simple" @change="change" v-model:value="_value" :id="id" />
<Advance v-if="ruleEditorStore.state.model === 'advance'" v-model:value="_value" :model="ruleEditorStore.state.model"
:virtualRule="virtualRule" :id="id" @change="change" />
<j-modal
:zIndex="1200"
:mask-closable="false"
visible
width="70vw"
title="编辑规则"
@cancel="handleCancel"
@ok="handleOk"
>
<div class="header" v-if="virtualRule?.windowType && virtualRule?.windowType !== 'undefined'">
<div class="header-item">
{{
virtualRule?.windowType === 'time' ? '时间窗口' : '频次窗口'
}}
</div>
<div class="header-item">
<div>聚合函数: <span>{{ aggType || '--' }}</span></div>
<div>窗口长度()<span>{{ virtualRule?.window?.span || '--' }}</span></div>
<div>步长(): <span>{{ virtualRule?.window?.every || '--' }}</span></div>
</div>
</div>
<div class="box">
<div class="left">
<div>
<Operator :id="id" @add-operator-value="addOperatorValue" />
</div>
<div style="margin-top: 10px;">
<Editor
ref="editor"
mode="advance"
key="advance"
v-model:value="_value"
/>
</div>
</div>
<div class="right">
<Debug
:virtualRule="{
...virtualRule,
script: _value,
}"
:id="id"
/>
</div>
</div>
</j-modal>
</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>()
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): void;
(e: 'save', data: string | undefined): void;
(e: 'close'): void;
}
const emit = defineEmits<Emits>();
const emit = defineEmits<Emits>()
const props = defineProps({
value: String,
id: String,
virtualRule: Object,
aggList: Array
});
const _value = computed({
get: () => props.value,
set: (val: string) => {
emit('update:value', val)
}
const _value = ref<string | undefined>(props.value);
const handleCancel = () => {
emit('close');
};
const handleOk = () => {
emit('save', _value.value);
};
const aggType = computed(() => {
const _item: any = (props?.aggList || []).find((item: any) => {
return item?.value === props.virtualRule?.aggType
})
return _item?.label
})
const ruleEditorStore = useRuleEditorStore()
const change = (v: string) => {
ruleEditorStore.set('model', v);
}
onMounted(() => {
ruleEditorStore.set('property', props.property)
ruleEditorStore.set('code', props.value);
})
const editor = ref();
const addOperatorValue = (val: string) => {
editor.value.addOperatorValue(val);
};
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.header {
margin-bottom: 20px;
.header-item {
display: flex;
gap: 24px;
div span {
color: rgba(0, 0, 0, 0.8);
}
}
}
.box {
display: flex;
justify-content: flex-start;
width: 100%;
.left {
width: 60%;
}
.right {
width: 40%;
margin-left: 10px;
padding-left: 10px;
border-left: 1px solid lightgray;
}
}
</style>

View File

@ -0,0 +1,26 @@
<template>
<j-modal
:mask-closable="false"
visible width="70vw"
title="设置属性规则"
@cancel="handleCancel"
@ok="handleOk"
>
</j-modal>
</template>
<script setup lang="ts" name="RuleModal">
const handleCancel = () => {
}
const handleOk = () => {
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,161 @@
<template>
<j-popconfirm-modal @confirm="confirm" bodyStyle="width: 450px;height: 300px">
<template #content>
<j-scrollbar>
<j-form ref="formRef" layout="vertical" :model="formData">
<ReadType v-model:value="formData.type" :disabled="true" />
<j-form-item name="promi">
<template #label>
触发属性
<j-popover>
<template #title>
<div>选择当前产品物模型下的属性作为触发属性</div>
<div>任意属性值更新时将触发下方计算规则</div>
</template>
<AIcon style="padding-left: 4px" type="icon-bangzhu" />
</j-popover>
</template>
<j-select />
</j-form-item>
<j-form-item label="计算规则">
<div class="rule-add" @click="showRuleWindow">
编辑规则
</div>
</j-form-item>
<j-form-item label="窗口" :name="['virtualRule', 'windowType']" required>
<j-select
v-model:value="formData.virtualRule.windowType"
:options="[
{ label: '无', value: 'undefined' },
{ label: '时间窗口', value: 'time' },
{ label: '频次窗口', value: 'num' },
]"
/>
</j-form-item>
<template v-if="showWindow">
<j-form-item label="聚合函数" :name="['virtualRule', 'aggType']">
<j-select
v-model:value="formData.virtualRule.aggType"
:options="[
{ label: '时间窗口', value: 'time' },
{ label: '频次窗口', value: 'num' },
]"
placeholder="请选择聚合函数"
/>
</j-form-item>
<j-form-item :name="['virtualRule', 'window', 'span']">
<template #label>
窗口长度({{ formData.virtualRule.aggType === 'num' ? '次' : 's' }})
</template>
<j-input-number v-model:value="formData.virtualRule.window.span" style="width: 100%" />
</j-form-item>
<j-form-item :name="['virtualRule', 'window', 'every']">
<template #label>
步长({{ formData.virtualRule.aggType === 'num' ? '次' : 's' }})
</template>
<j-input-number v-model:value="formData.virtualRule.window.every" style="width: 100%" />
</j-form-item>
</template>
</j-form>
</j-scrollbar>
</template>
<j-button style="padding: 4px 8px;">
<AIcon type="EditOutlined" />
</j-button>
</j-popconfirm-modal>
<Modal
v-if="visible"
@ok="ruleOk"
@cancel="ruleCancel"
/>
</template>
<script setup lang="ts" name="Rule">
import { ReadType } from '../components'
import Modal from './Modal.vue'
type Emit = {
(e: 'update:value', data: Record<string, any>): void
}
const props = defineProps({
value: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits<Emit>()
const formRef = ref<any>(null)
const visible = ref(false)
const formData = reactive<{
type?: string[]
virtualRule: Record<string, any>
}>({
type: ['report'],
virtualRule: {
windowType: 'undefined',
aggType: undefined,
isVirtualRule: false,
type: undefined,
window: {
every: undefined,
span: undefined
},
}
})
const showWindow = computed(() => {
const hasWindowType = formData.virtualRule.windowType !== 'undefined'
if (!hasWindowType) {
formData.virtualRule.window = {
every: undefined,
span: undefined
}
formData.virtualRule.aggType = undefined
}
formData.virtualRule.isVirtualRule = hasWindowType
formData.virtualRule.type = hasWindowType ? 'window' : 'script'
return hasWindowType
})
const confirm = () => {
return new Promise(async (resolve, reject) => {
const data = await formRef.value!.validate().catch(() => {
reject()
})
if (data) {
emit('update:value', formData)
resolve(true)
}
})
}
const showRuleWindow = () => {
visible.value = true
}
const ruleCancel = () => {
visible.value = false
}
const ruleOk = () => {
}
watch(() => props.value, () => {
Object.assign(formData, props.value)
}, { immediate: true })
</script>
<style scoped>
.rule-add {
padding: 8px;
width: 100%;
text-align: center;
border:1px solid rgba(0,0,0,.3);
}
</style>

View File

@ -0,0 +1,3 @@
import Rule from './Rule.vue'
export default Rule

View File

@ -24,12 +24,12 @@
<j-form-item :label="spanLabel" :name="name.concat(['window', 'span'])" :rules="[
{ required: true, message: '请输入窗口长度' },
]">
<j-input-number v-model:value="value.window.span" size="small" style="width: 100%;"></j-input-number>
<j-input-number stringMode v-model:value="value.window.span" size="small" style="width: 100%;"></j-input-number>
</j-form-item>
<j-form-item :label="everyLabel" :name="name.concat(['window', 'every'])" :rules="[
{ required: true, message: '请输入步长' },
]">
<j-input-number v-model:value="value.window.every" size="small" style="width: 100%;"></j-input-number>
<j-input-number stringMode :maxlength="10" v-model:value="value.window.every" size="small" style="width: 100%;"></j-input-number>
</j-form-item>
</template>
</template>

View File

@ -0,0 +1,56 @@
<template>
<j-form-item name="type" label="读写类型" required>
<j-select
v-model:value="myValue"
mode="multiple"
:options="options"
:disabled="disabled"
placeholder="请选择读写类型"
@change="onChange"
/>
</j-form-item>
</template>
<script setup lang="ts" name="ReadType">
import type {PropType} from "vue";
type Emit = {
(e: 'update:value', data: Array<string>): void
(e: 'change', data: Array<string>): void
}
const props = defineProps({
disabled: {
type: Boolean,
default: false
},
value: {
type: Array as PropType<Array<string>>,
default: () => []
},
options: {
type: Array as PropType<Array<{label: string, value: string}>>,
default: () => []
}
})
const emit = defineEmits<Emit>()
const myValue = ref<Array<string>>([])
const onChange = (keys: Array<string>) =>{
myValue.value = keys
emit('update:value', keys)
emit('change', keys)
}
watch(() => props.value, () => {
myValue.value = props.value
}, { immediate: true})
</script>
<style scoped>
</style>

View File

@ -0,0 +1 @@
export { default as ReadType } from './ReadType.vue'

View File

@ -21,7 +21,7 @@ export const useMetadataStore = defineStore({
action: 'add',
import: false,
importMetadata: false,
} as MetadataModelType
} as MetadataModelType
}),
actions: {
set(key: string, value: any) {

View File

@ -0,0 +1,307 @@
<template>
<j-data-table
ref="tableRef"
:data-source="dataSource"
:columns="columns"
row-key="id"
:height="560"
serial
>
<template #expand>
<PermissionButton
type="primary"
v-if="!dataSource.length"
:hasPermission="`${permission}:update`"
key="add"
@click="handleAddClick"
:disabled="hasOperate('add', type)"
:tooltip="{
title: hasOperate('add', type)
? '当前的存储方式不支持新增'
: '新增',
}"
>
新增
</PermissionButton>
<PermissionButton
type="primary"
:hasPermission="`${permission}:update`"
key="update"
v-else
@click="handleSaveClick"
:disabled="hasOperate('add', type)"
:tooltip="{
title: hasOperate('add', type)
? '当前的存储方式不支持新增'
: '保存',
}"
>
保存
</PermissionButton>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'id'">
<div style="width: 100px">
<j-ellipsis>{{ record.id || '-' }}</j-ellipsis>
</div>
</template>
<template v-if="column.dataIndex === 'name'">
<div style="width: 200px">
<j-ellipsis>{{ record.name || '-' }}</j-ellipsis>
</div>
</template>
<template v-if="column.dataIndex === 'description'">
<div style="width: 200px">
<j-ellipsis>{{ record.description || '-' }}</j-ellipsis>
</div>
</template>
<template v-if="column.dataIndex === 'level'">
{{ levelMap[record.expands?.level] || '-' }}
</template>
<template v-if="column.dataIndex === 'async'">
{{ record.async ? '是' : '否' }}
</template>
<template v-if="column.dataIndex === 'source'">
{{ sourceMap[record.expands?.source] }}
<Rule
v-if="record.expands?.source === 'rule'"
v-model:value="record.expands"
/>
<Source v-else v-model:value="record.expands" />
</template>
<template v-if="column.dataIndex === 'type'">
<j-tag v-for="item in record.expands?.type || []" :key="item">
{{ expandsType[item] }}
</j-tag>
</template>
<template v-if="column.dataIndex === 'action'"> </template>
</template>
<template #valueType="{ data }">
{{ data.record.valueType?.type }}
</template>
<template #expands="{ data }">
{{ sourceMap?.[data.record?.expands?.source] || '' }}
</template>
<template #inputs="{ data }">
{{ data.record.inputs?.map(item => item.name).join(',') }}
</template>
<template #output="{ data }">
{{ data.record.output?.type }}
</template>
<template #async="{ data }">
{{ data.record.async ? '是' : '否' }}
</template>
<template #expands="{ data }" v-if="type === 'events'">
{{ levelMap?.[data.record.expands?.level] || '-' }}
</template>
<template #properties="{ data }">
{{ data.record.valueType.properties?.map(item => item.name).join(',') }}
</template>
<template #other="{ data, }">
配置
</template>
<template #action="{data}">
<j-space>
<PermissionButton
:has-permission="`${permission}:add`"
type="link"
key="edit"
style="padding: 0"
:disabled="operateLimits('add', type)"
@click="copyItem(data.record, data.index)"
:tooltip="{
title: operateLimits('add', type) ? '当前的存储方式不支持复制' : '复制',
}"
>
<AIcon type="CopyOutlined" />
</PermissionButton>
<PermissionButton
:has-permission="`${permission}:update`"
type="link"
key="edit"
style="padding: 0"
:disabled="operateLimits('add', type)"
@click="handleAddClick(data.index)"
:tooltip="{
title: operateLimits('add', type) ? '当前的存储方式不支持新增' : '新增',
}"
>
<AIcon type="PlusSquareOutlined" />
</PermissionButton>
<PermissionButton
:has-permission="`${permission}:delete`"
type="link"
key="delete"
style="padding: 0"
danger
:pop-confirm="{
title: '确认删除?',
onConfirm: async () => {
await removeItem(data.record);
},
}"
:tooltip="{
title: '删除',
}"
>
<AIcon type="DeleteOutlined" />
</PermissionButton>
</j-space>
</template>
</j-data-table>
</template>
<script setup lang="ts" name="BaseMetadata">
import type {
MetadataItem,
MetadataType,
ProductItem,
} from '@/views/device/Product/typings';
import type { PropType } from 'vue';
import { useMetadata, useOperateLimits } from './hooks';
import MetadataMapping from './columns';
import { levelMap, sourceMap, expandsType, limitsMap } from './utils';
import Rule from '@/components/Metadata/Rule';
import { Source, OtherSetting } from './components';
import { saveProductVirtualProperty } from '@/api/device/product';
import { saveDeviceVirtualProperty } from '@/api/device/instance';
import { useInstanceStore } from '@/store/instance';
import { useProductStore } from '@/store/product';
import { asyncUpdateMetadata, updateMetadata } from '../metadata';
import { useMetadataStore } from '@/store/metadata';
import { DeviceInstance } from '@/views/device/Instance/typings';
import { onlyMessage } from '@/utils/comm';
import {omit} from "lodash-es";
import {useAction} from "@/views/device/components/Metadata/Base/hooks/useAction";
const props = defineProps({
// target: {
// type: String as PropType<'device' | 'product'>,
// default: 'product',
// },
type: {
type: String as PropType<MetadataType>,
default: undefined,
},
permission: {
type: [String, Array] as PropType<string | string[]>,
default: undefined,
},
});
const target = inject<'device' | 'product'>('_metadataType', 'product');
const { data: metadata } = useMetadata(target, props.type);
const { hasOperate } = useOperateLimits(target);
const metadataStore = useMetadataStore()
const instanceStore = useInstanceStore()
const productStore = useProductStore()
const dataSource = ref<MetadataItem[]>(metadata.value || []);
const tableRef = ref();
const columns = computed(() => MetadataMapping.get(props.type!));
const { addAction, copyAction, removeAction } = useAction(tableRef)
provide('_dataSource', dataSource.value)
const operateLimits = (action: 'add' | 'updata', types: MetadataType) => {
return (
target === 'device' &&
(instanceStore.detail.features || []).find((item: { id: string; name: string }) => item.id === limitsMap.get(`${types}-${action}`))
);
};
const handleSearch = (searchValue: string) => {
dataSource.value = searchValue
? metadata.value
.filter((item) => item.name!.indexOf(searchValue) > -1)
.sort((a, b) => b?.sortsIndex - a?.sortsIndex)
: metadata.value;
};
const handleAddClick = (index?: number) => {
const newObject = {
id: undefined,
name: undefined,
expands: {
source: 'device'
},
valueType: {
type: undefined
}
}
tableRef.value?.addItem?.(newObject, index)
};
const copyItem = (record: any, index: number) => {
copyAction(omit(record, ['_uuid']), index)
}
const removeItem = (index: number) => {
removeAction(index)
}
const handleSaveClick = async () => {
const resp = await tableRef.value.getData().finally(() => {
});
if(resp && resp.length) {
const virtual: any[] = [];
const arr = resp.map((item: any) => {
if(item.expands.virtualRule) {
virtual.push({
...item.expands.virtualRule,
propertyId: item.id
})
}
return {
...item,
expands: {
...item.expands,
virtualRule: undefined
}
}
})
//
if(virtual.length) {
let res = undefined
if(target === 'device') {
res = await saveDeviceVirtualProperty(instanceStore.current.productId, instanceStore.current.id, virtual)
} else {
res = await saveProductVirtualProperty(productStore.current.id, virtual)
}
}
//
const updateStore = (metadata: string) => {
if (target === 'device') {
const detail = instanceStore.current
detail.metadata = metadata
instanceStore.setCurrent(detail)
} else {
const detail = productStore.current || {} as ProductItem
detail.metadata = metadata
productStore.setCurrent(detail)
}
}
const _detail: ProductItem | DeviceInstance = target === 'device' ? instanceStore.detail : productStore.current
const _data = updateMetadata(props.type!, arr, _detail, updateStore)
const result = await asyncUpdateMetadata(target, _data)
if(result.success) {
onlyMessage('操作成功!')
}
}
};
</script>
<style scoped>
.table-header {
display: flex;
justify-content: space-between;
padding-bottom: 16px;
}
</style>

View File

@ -1,84 +0,0 @@
import { ColumnProps } from "ant-design-vue/es/table";
const SourceMap = {
device: '设备',
manual: '手动',
rule: '规则',
};
const type = {
read: '读',
write: '写',
report: '上报',
};
const BaseColumns: ColumnProps[] = [
{
title: '标识',
dataIndex: 'id',
width: 100
},
{
title: '名称',
dataIndex: 'name',
width: 200
},
{
title: '说明',
dataIndex: 'description',
width: 200
},
];
const EventColumns: ColumnProps[] = BaseColumns.concat([
{
title: '事件级别',
dataIndex: 'level',
},
]);
const FunctionColumns: ColumnProps[] = BaseColumns.concat([
{
title: '是否异步',
dataIndex: 'async',
},
// {
// title: '读写类型',
// dataIndex: 'expands',
// render: (text: any) => (text?.type || []).map((item: string | number) => <Tag>{type[item]}</Tag>),
// },
]);
const PropertyColumns: ColumnProps[] = BaseColumns.concat([
{
title: '数据类型',
dataIndex: 'valueType',
},
{
title: '属性来源',
dataIndex: 'source',
},
{
title: '读写类型',
dataIndex: 'type',
},
]);
const TagColumns: ColumnProps[] = BaseColumns.concat([
{
title: '数据类型',
dataIndex: 'valueType',
},
{
title: '读写类型',
dataIndex: 'type',
},
]);
const MetadataMapping = new Map<string, ColumnProps[]>();
MetadataMapping.set('properties', PropertyColumns);
MetadataMapping.set('events', EventColumns);
MetadataMapping.set('tags', TagColumns);
MetadataMapping.set('functions', FunctionColumns);
export default MetadataMapping;

View File

@ -0,0 +1,217 @@
import { ColumnProps } from "ant-design-vue/es/table";
import { DataType, Source, InputParams, OtherSetting, OutputParams, ConfigParams } from './components'
import SelectColumn from './components/Events/SelectColumn.vue';
import AsyncSelect from './components/Function/AsyncSelect.vue';
import { EventLevel } from "@/views/device/data";
interface DataTableColumnProps extends ColumnProps {
type?: string,
components?: {
name: any
[key: string]: any
}
form?: {
rules: any[]
}
}
const SourceMap = {
device: '设备',
manual: '手动',
rule: '规则',
};
const type = {
read: '读',
write: '写',
report: '上报',
};
const BaseColumns: DataTableColumnProps[] = [
{
title: '标识',
dataIndex: 'id',
width: 150,
type: 'text'
},
{
title: '名称',
dataIndex: 'name',
width: 200,
type: 'text'
},
];
const EventColumns: DataTableColumnProps[] = BaseColumns.concat([
{
title: '事件级别',
dataIndex: 'expands',
type: 'components',
components: {
name: SelectColumn,
props: {
options: EventLevel
}
}
},
{
title: '输出参数',
dataIndex: 'valueType',
},
{
title: '配置参数',
dataIndex: 'properties',
type: 'components',
components: {
name: ConfigParams,
}
},
{
title: '说明',
dataIndex: 'description',
type: 'text',
},
{
title: '操作',
dataIndex: 'action',
width: 120
}
]);
const FunctionColumns: DataTableColumnProps[] = BaseColumns.concat([
{
title: '是否异步',
dataIndex: 'async',
type: 'components',
components: {
name: AsyncSelect,
props: {
options: [
{ label: '是', value: true },
{ label: '否', value: false }
]
}
}
},
{
title: '输入参数',
dataIndex: 'inputs',
type: 'components',
components: {
name: InputParams,
}
},
{
title: '输出参数',
dataIndex: 'output',
type: 'components',
components: {
name: OutputParams
}
},
{
title: '说明',
dataIndex: 'description',
type: 'text',
},
{
title: '操作',
dataIndex: 'action',
width: 120
}
// {
// title: '读写类型',
// dataIndex: 'expands',
// render: (text: any) => (text?.type || []).map((item: string | number) => <Tag>{type[item]}</Tag>),
// },
]);
const PropertyColumns: DataTableColumnProps[] = BaseColumns.concat([
{
title: '数据类型',
dataIndex: 'valueType',
type: 'components',
components: {
name: DataType
}
},
{
title: '属性来源',
dataIndex: 'expands',
type: 'components',
components: {
name: Source
},
form: {
rules: [
{
validator: async (_: Record<string, any>, value: any) => {
if (value.source) {
if(value.source !== 'rule') {
if(value.type?.length) {
return Promise.resolve();
} else {
return Promise.reject('请选择读写类型');
}
} else {
if(value.virtualRule?.rule?.script) {
return Promise.resolve();
}else {
return Promise.reject('请配置规则');
}
}
} else {
return Promise.reject('请选择属性来源');
}
}
},
]
}
},
{
title: '其它配置',
dataIndex: 'other',
type: 'components',
width: 100,
components: {
name: OtherSetting
},
},
{
title: '操作',
dataIndex: 'action',
width: 120
}
]);
const TagColumns: DataTableColumnProps[] = BaseColumns.concat([
{
title: '数据类型',
dataIndex: 'valueType',
type: 'components',
components: {
name: DataType,
}
},
{
title: '读写类型',
dataIndex: 'type',
},
{
title: '说明',
dataIndex: 'description',
type: 'text',
},
{
title: '操作',
dataIndex: 'action',
width: 120
}
]);
const MetadataMapping = new Map<string, DataTableColumnProps[]>();
MetadataMapping.set('properties', PropertyColumns);
MetadataMapping.set('events', EventColumns);
MetadataMapping.set('tags', TagColumns);
MetadataMapping.set('functions', FunctionColumns);
export default MetadataMapping;

View File

@ -0,0 +1,152 @@
<template>
<div class="metadata-type">
<DataTableTypeSelect v-model:value="type" @change="typeChange" />
<DataTableArray
v-if="type === 'array'"
v-model:value="_valueType.elementType"
/>
<DataTableObject
v-else-if="type === 'object'"
v-model:value="_valueType.properties"
:columns="[
{ title: '参数标识', dataIndex: 'id', type: 'text', width: 100 },
{ title: '参数名称', dataIndex: 'name', type: 'text', width: 100 },
{
title: '数据类型',
type: 'components',
dataIndex: 'valueType',
components: {
name: ValueObject,
},
width: 100
},
{
title: '其他配置',
type: 'components',
dataIndex: 'config',
components: {
name: DataTypeObjectChild
},
width: 100
},
{
title: '操作',
dataIndex: 'action',
width: 60
}
]"
>
<template #valueType="{ data }">
{{ data.record.valueType?.type }}
</template>
<template #config="{ data }">
<OtherConfigInfo :value="data.record.valueType"></OtherConfigInfo>
</template>
</DataTableObject>
<DataTableEnum v-else-if="type === 'enum'" v-model:value="_valueType" />
<DataTableBoolean v-else-if="type === 'boolean'" v-model:value="_valueType" />
<DataTableDouble
v-else-if="['float', 'double'].includes(type)"
:options="options"
v-model:value="_valueType"
/>
<DataTableInteger
v-else-if="['int', 'long'].includes(type)"
:options="options"
v-model:value="_valueType.unit"
/>
<DataTableFile v-else-if="type === 'file'" v-model:value="_valueType.fileType"/>
<DataTableDate v-else-if="type === 'date'" v-model:value="_valueType.date"/>
<!-- <DataTableString
v-else-if="['string', 'password'].includes(type)"
v-model:value="data.expands.maxLength"
/> -->
</div>
</template>
<script setup lang="ts" name="MetadataDataType">
import { getUnit } from '@/api/device/instance';
import { InputParams, ValueObject, OtherConfigInfo } from '../components'
import {
DataTableTypeSelect,
DataTableArray,
DataTableString,
DataTableInteger,
DataTableDouble,
DataTableBoolean,
DataTableEnum,
DataTableFile,
DataTableDate,
DataTableObject,
} from 'jetlinks-ui-components';
import DataTypeObjectChild from './DataTypeObjectChild.vue';
import { cloneDeep } from 'lodash-es';
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
});
const options = ref<{ label: string; value: string }[]>([]);
const emit = defineEmits(['update:value']);
const type = ref(props.value?.valueType?.type);
const elements = ref(props.value?.valueType?.elements);
const _valueType = ref(cloneDeep(props.value.valueType));
const typeChange = (e: string) => {
console.log(e);
emit('update:value', {
...props.value,
valueType: { ..._valueType.value, type: e }
});
};
watch(
() => props.value,
() => {
type.value = props.value?.valueType.type;
// elements.value = props.value?.valueType.elements;
if (['float', 'double', 'int', 'long'].includes(type.value)) {
const res = getUnit().then((res) => {
if (res.success) {
options.value = res.result.map((item) => ({
label: item.description,
value: item.id,
}));
}
});
}
},
{ immediate: true, deep: true },
);
watch(
() => _valueType.value,
() => {
let result = {..._valueType.value};
// if(type.value == 'boolean') {
// result = {...data.value}
// }
emit('update:value', {
...props.value,
valueType: {...result, type: type.value},
});
},
{ deep: true },
);
</script>
<style scoped lang="less">
.metadata-type {
display: flex;
gap: 12px;
align-items: center;
.j-data-table-config--icon {
padding-right: 12px;
}
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<div class="values">
<div class="values-test">
{{ text }}
</div>
<!-- <OtherConfigInfo :value="formData"></OtherConfigInfo> -->
<DataTableEnum v-if="formData.type === 'enum'" v-model:value="formData" />
<DataTableBoolean v-else-if="formData.type === 'boolean'" v-model:value="formData" />
<DataTableDouble
v-else-if="['float', 'double'].includes(formData.type)"
:options="options"
v-model:value="formData"
/>
<DataTableArray
v-else-if="formData.type === 'array'"
v-model:value="formData.unit"
/>
<DataTableFile v-else-if="formData.type === 'file'" v-model:value="formData.fileType"/>
<DataTableDate v-else-if="formData.type === 'date'" v-model:value="formData.date"/>
<DataTableString
v-else-if="['string', 'password'].includes(formData.type)"
v-model:value="formData.expands.maxLength"
/>
</div>
</template>
<script setup lang="ts" name="DataTypeObjectChild">
import { getUnit } from '@/api/device/instance';
import { OtherConfigInfo } from '../components'
import {
DataTableString,
DataTableArray,
DataTableDouble,
DataTableBoolean,
DataTableEnum,
DataTableFile,
DataTableDate,
} from 'jetlinks-ui-components';
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
});
// const formData = computed({
// get() {
// let result:any = {type: props.value.valueType?.type};
// switch(true) {
// case ['float', 'double'].includes(props.value?.valueType?.type):
// result.scale = props.value?.valueType?.scale;
// result.unit = props.value?.valueType?.unit;
// break;
// case ['int', 'long'].includes(props.value?.valueType?.type):
// result.unit = props.value?.valueType?.unit;
// break;
// case props.value.valueType?.type === 'boolean':
// result.trueText = props.value?.valueType?.trueText;
// result.trueValue = props.value?.valueType?.trueValue;
// result.falseText = props.value?.valueType?.falseText;
// result.falseValue = props.value?.valueType?.falseValue;
// break;
// case ['string', 'password'].includes(props.value?.valueType?.type):
// result.maxLength = props.value?.valueType?.maxLength;
// break;
// case ['date'].includes(props.value?.valueType?.type):
// result.date = props.value?.valueType?.date;
// break;
// case ['enum'].includes(props.value?.valueType?.type):
// result.elements = props.value?.valueType?.elements;
// break;
// }
// return result;
// },
// set() {
// }
// });
const formData = ref(props.value?.valueType);
const type = ref(props.value?.valueType?.type);
console.log(props.value);
const emit = defineEmits(['update:value', 'cancel']);
const options = ref<{ label: string; value: string }[]>([]);
const text = computed(() => {
console.log(props.value);
const value = props.value?.valueType
if (value) {
switch(type.value) {
case 'float':
case 'double':
return value.scale;
case 'boolean':
return value?.trueText ? `${ value?.trueText }-${ value.trueValue }; ${ value.falseText }-${ value.falseValue }` : '';
case 'string':
case 'password':
return value?.expands?.maxLength;
case 'int':
case 'long':
return '无'
}
} else {
return ''
}
})
watch(
() => formData.value?.type,
() => {
if (formData.value?.type && ['float', 'double', 'int', 'long'].includes(formData.value.type)) {
const res = getUnit().then((res) => {
if (res.success) {
options.value = res.result.map((item) => ({
label: item.description,
value: item.id,
}));
}
});
}
},
{ immediate: true, deep: true },
);
watch(() => formData.value, () => {
emit('update:value', { ...props.value, valueType: { ...formData.value, type: type.value } });
}, {deep: true})
</script>
<style scoped lang="less">
.values {
display: flex;
gap: 12px;
align-items: center;
.values-test {
flex: 1;
}
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<div class="metadata-config-params">
{{ value?.map((item: any) => item.name).join(',') }}
<DataTableObject v-model:value="value" :columns="columns">
<template #valueType="{ data }">
<span>{{ data.data.record.valueType?.type }}</span>
</template>
<template #config="{ data }">
<OtherConfigInfo :value="data.data.record.valueType"></OtherConfigInfo>
</template>
</DataTableObject>
</div>
</template>
<script setup lang="ts" name="InputParams">
import type { PropType } from 'vue';
import DataTypeObjectChild from '../DataTypeObjectChild.vue'
import {
DataTableObject,
} from 'jetlinks-ui-components';
import { OtherConfigInfo, ValueObject } from '../index'
const columns = [
{ title: '参数标识', dataIndex: 'id', type: 'text' },
{ title: '参数名称', dataIndex: 'name', type: 'text' },
{
title: '数据类型',
type: 'components',
dataIndex: 'valueType',
components: {
name: ValueObject,
},
},
{
title: '其他配置',
dataIndex: 'config',
type: 'components',
components: {
name: DataTypeObjectChild
}
},
{
title: '操作',
},
];
type Emits = {
(e: 'update:value', data: Record<string, any>): void;
(e: 'change', data: string): void;
};
const emit = defineEmits<Emits>();
const props = defineProps({
value: {
type: Object as PropType<Record<string, any>>,
default: () => {},
},
placeholder: {
type: String,
default: '请选择',
},
options: {
type: Array as PropType<{ label: string; value: string }[]>,
default: () => [],
},
});
const value = ref(props.value.valueType?.properties);
const change = (v: string) => {
emit('update:value', { ...props.value, async: value.value });
emit('change', v);
};
watch(
() => props.value,
(newV) => {
value.value = props.value.valueType?.properties;
},
{ immediate: true },
);
watch(() => value.value, () => {
emit('update:value', {
...props.value,
valueType: {
properties: value.value,
type: props.value.valueType.type,
}
})
})
</script>
<style scoped></style>

View File

@ -0,0 +1,39 @@
<template>
<div class="values-text" v-if="['float', 'double'].includes(value.type)">{{ value.scale }}</div>
<div class="values-text" v-else-if="value.type == 'boolean'">{{ value.trueText }}-{{ value.trueValue }}; {{ value.falseText }}-{{ value.falseValue }}</div>
<div class="values-text" v-else-if="['string', 'password'].includes(value.type)">{{ value.expands?.maxLength }}</div>
<div class="values-text" v-else-if="value.type == 'date'">{{ value.date }}</div>
<div class="values-text" v-else-if="value.type == 'enum'">{{ value.elements?.map((item) => item.text).join(',') }}</div>
<div class="values-text" v-else-if="['int', 'long'].includes(value.type)"></div>
</template>
<script setup lang="ts" name="OtherConfigInfo">
const props = defineProps({
value: {
type: Object,
default: () => ({}),
}
})
const test = computed(() => {
const value = props.value
const type = props.value?.type
if (value.value) {
}
switch(type) {
case 'float':
case 'double':
return value.scale;
case 'boolean':
return value?.trueText ? `${ value?.trueText }-${ value.trueValue }; ${ value.falseText }-${ value.falseValue }` : '';
case 'string':
case 'password':
return value?.expands?.maxLength;
case 'int':
case 'long':
return '无'
}
})
</script>

View File

@ -0,0 +1,55 @@
<template>
<j-select
v-model:value="value"
:options="options"
:placeholder="placeholder"
@change="change"
/>
</template>
<script setup lang="ts" name="SelectColumnn">
import type { PropType } from 'vue';
import { ref, watch } from 'vue';
type Emits = {
(e: 'update:value', data: Record<string, any>): void;
(e: 'change', data: string): void;
};
type SizeType = 'small' | 'middle' | 'large' | undefined;
const emit = defineEmits<Emits>();
const props = defineProps({
value: {
type: Object as PropType<Record<string, any>>,
default: () => { },
},
placeholder: {
type: String,
default: '请选择'
},
options: {
type: Array as PropType<{label: string, value: string}[]>,
default: () => [],
},
});
const value = ref(props.value.expands?.level);
const change = (v: string) => {
emit('update:value', {...props.value, expands: {...props.value.expands, level: v}});
emit('change', v);
};
watch(
() => props.value,
(newV) => {
value.value = props.value.expands?.level;
},
{ immediate: true },
);
</script>
<style scoped></style>

View File

@ -0,0 +1,56 @@
<template>
<DataTableTypeSelect v-model:value="type" :filter="['object', 'array']">
</DataTableTypeSelect>
</template>
<script setup lang="ts" name="ValueObject">
import type { PropType } from 'vue';
import {
DataTableTypeSelect,
DataTableObject,
} from 'jetlinks-ui-components';
import { DataType } from '../index'
type Emits = {
(e: 'update:value', data: Record<string, any>): void;
(e: 'change', data: string): void;
};
const emit = defineEmits<Emits>();
const props = defineProps({
value: {
type: Object as PropType<Record<string, any>>,
default: () => {},
},
placeholder: {
type: String,
default: '请选择',
},
options: {
type: Array as PropType<{ label: string; value: string }[]>,
default: () => [],
},
});
const type = ref(props.value.valueType?.type || null);
watch(
() => props.value,
(newV) => {
type.value = props.value.valueType?.type;
},
{ immediate: true },
);
watch(() => type.value, () => {
emit('update:value', {
...props.value,
valueType: { type: type.value}
})
})
</script>
<style scoped></style>

View File

@ -0,0 +1,56 @@
<template>
<j-select
v-model:value="value"
:options="options"
:placeholder="placeholder"
@change="change"
/>
</template>
<script setup lang="ts" name="SelectColumnn">
import type { PropType } from 'vue';
import { ref, watch } from 'vue';
type Emits = {
(e: 'update:value', data: Record<string, any>): void;
(e: 'change', data: string): void;
};
type SizeType = 'small' | 'middle' | 'large' | undefined;
const emit = defineEmits<Emits>();
const props = defineProps({
value: {
type: Object as PropType<Record<string, any>>,
default: () => { },
},
placeholder: {
type: String,
default: '请选择'
},
options: {
type: Array as PropType<{label: string, value: string}[]>,
default: () => [],
},
});
const value = ref(props.value.async);
const change = (v: string) => {
emit('update:value', {...props.value, async: value.value});
emit('change', v);
};
watch(
() => props.value,
(newV) => {
value.value = props.value.async;
},
{ immediate: true },
);
</script>
<style scoped></style>

View File

@ -0,0 +1,90 @@
<template>
{{ value?.map((item) => item.name).join(',') }}
<DataTableObject v-model:value="value" :columns="columns">
<template #valueType="{ data }">
<span>{{ data.data.record.valueType?.type }}</span>
</template>
<template #config="{ data }">
<OtherConfigInfo :value="data.data.record.valueType"></OtherConfigInfo>
</template>
</DataTableObject>
</template>
<script setup lang="ts" name="InputParams">
import type { PropType } from 'vue';
import DataTypeObjectChild from '../DataTypeObjectChild.vue'
import {
DataTableObject,
} from 'jetlinks-ui-components';
import { DataType, OtherConfigInfo, ValueObject } from '../index'
const columns = [
{ title: '参数标识', dataIndex: 'id', type: 'text' },
{ title: '参数名称', dataIndex: 'name', type: 'text' },
{
title: '数据类型',
type: 'components',
dataIndex: 'valueType',
components: {
name: ValueObject,
},
},
{
title: '其他配置',
dataIndex: 'config',
type: 'components',
components: {
name: DataTypeObjectChild
}
},
{
title: '操作',
},
];
type Emits = {
(e: 'update:value', data: Record<string, any>): void;
(e: 'change', data: string): void;
};
const emit = defineEmits<Emits>();
const props = defineProps({
value: {
type: Object as PropType<Record<string, any>>,
default: () => {},
},
placeholder: {
type: String,
default: '请选择',
},
options: {
type: Array as PropType<{ label: string; value: string }[]>,
default: () => [],
},
});
const value = ref(props.value);
const change = (v: string) => {
emit('update:value', { ...props.value, async: value.value });
emit('change', v);
};
watch(
() => props.value,
(newV) => {
value.value = props.value.inputs;
},
{ immediate: true },
);
watch(() => value.value, () => {
console.log(value.value);
emit('update:value', {
...props.value,
inputs: value.value
})
})
</script>
<style scoped></style>

View File

@ -0,0 +1,129 @@
<template>
<div class="metadata-type">
<DataTableTypeSelect v-model:value="type" @change="typeChange" />
<DataTableArray
v-if="type === 'array'"
v-model:value="data.elementType"
/>
<DataTableObject
v-else-if="type === 'object'"
v-model:value="data.properties"
:columns="[
{ title: '参数标识', dataIndex: 'id', type: 'text' },
{ title: '参数名称', dataIndex: 'name', type: 'text' },
{
title: '数据类型',
type: 'components',
dataIndex: ['valueType', 'type'],
components: {
name: DataTableTypeSelect,
}
},
{
title: '其他配置',
type: 'components',
dataIndex: 'valueType',
components: {
name: DataTypeObjectChild
}
},
{
title: '操作'
}
]"
/>
<DataTableEnum v-else-if="type === 'enum'" v-model:value="data" />
<DataTableBoolean v-else-if="type === 'boolean'" v-model:value="data" />
<DataTableDouble
v-else-if="['float', 'double'].includes(type)"
:options="options"
v-model:value="data"
/>
<DataTableInteger
v-else-if="['int', 'long'].includes(type)"
:options="options"
v-model:value="data.unit"
/>
<DataTableFile v-else-if="type === 'file'" v-model:value="data.fileType"/>
<DataTableDate v-else-if="type === 'date'" v-model:value="data.date"/>
<DataTableString
v-else-if="['string', 'password'].includes(type)"
v-model:value="data.expands.maxLength"
/>
</div>
</template>
<script setup lang="ts" name="OutPutParams">
import { getUnit } from '@/api/device/instance';
import {
DataTableTypeSelect,
DataTableArray,
DataTableString,
DataTableInteger,
DataTableDouble,
DataTableBoolean,
DataTableEnum,
DataTableFile,
DataTableDate,
DataTableObject,
} from 'jetlinks-ui-components';
import DataTypeObjectChild from '../DataTypeObjectChild.vue';
import { cloneDeep } from 'lodash-es';
const props = defineProps({
value: {
type: Object,
default: () => ({}),
},
});
const options = ref<{ label: string; value: string }[]>([]);
const emit = defineEmits(['update:value']);
const type = ref(props.value?.output.type);
const data = ref(cloneDeep(props.value.output));
const typeChange = () => {
emit('update:value', {
...props.value,
output: { ...data.value, type: type.value }
});
};
watch(
() => props.value,
() => {
type.value = props.value?.output.type;
if (['float', 'double', 'int', 'long'].includes(type.value)) {
const res = getUnit().then((res) => {
if (res.success) {
options.value = res.result.map((item) => ({
label: item.description,
value: item.id,
}));
}
});
}
},
{ immediate: true, deep: true },
);
watch(
() => data.value,
(newVal) => {
emit('update:value', {
...props.value,
output: {...newVal, type: type.value},
});
},
{ deep: true },
);
</script>
<style scoped>
.metadata-type {
display: flex;
gap: 12px;
align-items: center;
}
</style>

View File

@ -0,0 +1,151 @@
<template>
<div>
<j-data-table
:dataSource="dataSource"
:columns="newColumns"
:showTool="false"
:serial="true"
ref="tableRef"
>
<template #range="{data}">
{{ data.record.range === 'true' ? '范围值' : '固定值'}}
</template>
<template #value="{data}">
{{ data.record.range === 'true' ? data.record.value.toString() : data.record.value }}
</template>
<template #action="{data}">
<j-button
type="link"
@click="() => deleteItem(data.index)"
>
<AIcon type="DeleteOutlined" />
</j-button>
</template>
</j-data-table>
<j-button style="width: 100%;margin-top: 16px;" @click="addItem">
<template #icon><AIcon type="PlusOutlined" /></template>
添加指标值
</j-button>
</div>
</template>
<script setup name="Metrics" lang="ts">
import { defineExpose, provide } from 'vue'
import MetricValueItem from './ValueItem.vue'
const props = defineProps({
value: {
type: Array,
default: () => []
},
type: {
type: String,
default: undefined
}
})
const emit = defineEmits(['update:value'])
const dataSource = ref<any[]>([])
const tableRef = ref()
provide('metricsType', props.type)
const columns: any = [
{
title: '指标标识',
dataIndex: 'id',
width: 120,
type: 'text'
},
{
title: '指标名称',
dataIndex: 'name',
type: 'text'
},
{
title: '指标配置',
dataIndex: 'value',
width: 100,
type: 'components',
components: {
name: MetricValueItem
}
},
{
title: '操作',
dataIndex: 'action',
width: 60,
},
]
const newColumns = computed(() => {
if (props.type && !['string', 'boolean', 'date'].includes(props.type)) {
const data = [...columns]
data.splice(1, 0, {
title: '指标值',
dataIndex: 'range',
width: 120,
type: 'booleanSelect',
components: {
props: {
trueText: '范围值',
trueValue: 'true',
falseText: '固定值',
falseValue: 'false',
}
}
})
console.log(data);
return data
}
return columns
})
const rules = []
const addItem = () => {
const data = {
id: undefined,
name: undefined,
range: 'false',
value: undefined,
}
tableRef.value.addItem(data)
}
const deleteItem = (index: number) => {
tableRef.value.removeItem(index)
}
const cancel = () => {
dataSource.value = props.value || []
}
const confirm = () => {
}
const getData = () => {
return new Promise((resolve, reject) => {
tableRef.value.getData().then((data: any) => {
resolve(data)
}).catch(() => {
reject(false)
})
})
}
watch(() => props.value, () => {
dataSource.value = props.value || []
}, { immediate: true, deep: true})
defineExpose({ getData })
</script>
<style scoped>
</style>

View File

@ -0,0 +1,92 @@
<template>
<j-popconfirm-modal
:show-cancel="false"
body-style="width: 300px"
@confirm="confirm"
>
<template #content>
<j-form ref="formRef" :model="formData">
<j-form-item v-if="value.range === 'false'" name="value" :rule="[{ required: true, message: '请输入指标值'}]">
<Item v-model:value="formData.value" />
</j-form-item>
<div v-else class="data-table-boolean-item">
<div class="data-table-boolean-item--value">
<j-form-item :name="['rangeValue', 0]" :rule="[{ required: true, message: '请输入指标值'}]">
<Item v-model:value="formData.rangeValue[0]" />
</j-form-item>
</div>
<div>-</div>
<div class="data-table-boolean-item--value">
<j-form-item :name="['rangeValue', 1]" :rule="[{ required: true, message: '请输入指标值'}]">
<Item v-model:value="formData.rangeValue[1]" />
</j-form-item>
</div>
</div>
</j-form>
</template>
<j-button my-icon="EditOutlined" style="padding: 4px 8px"></j-button>
</j-popconfirm-modal>
</template>
<script setup lang="ts" name="MetricValueItems">
import { reactive } from 'vue';
import type { PropType } from 'vue';
import Item from './item.vue'
type ValueType = number | Array<number | undefined> | undefined;
type Emit = {
(e: 'update:value', value: ValueType): void;
};
const props = defineProps({
value: {
type: Object as PropType<any>,
default: undefined,
},
});
const emit = defineEmits<Emit>();
const formData = reactive<{
value: ValueType;
rangeValue: ValueType;
}>({
value: props.value.range === 'false' ? props.value.value : undefined,
rangeValue: props.value.range === 'true'
? props.value.value || [undefined, undefined]
: [undefined, undefined],
});
const formRef = ref()
const confirm = () => {
return new Promise((resolve, reject) => {
formRef.value.validate().then(() => {
const value = props.value.range === 'true' ? formData.rangeValue : formData.value
emit('update:value', {
...props.value,
value: value
});
resolve(true)
}).catch(() => {
reject()
})
})
};
watch(() => props.value.range,(value, oldValue) => {
if (value !== oldValue ) {
if (value === 'false') {
formData.value = undefined
} else {
formData.rangeValue = [undefined, undefined]
}
}
})
</script>
<style scoped></style>

View File

@ -0,0 +1,3 @@
import Metrics from './Metrics.vue'
export default Metrics

View File

@ -0,0 +1,44 @@
<template>
<j-input
v-if="type === 'string'"
v-model:value="myValue"
:maxLength="64"
@change="change"
/>
<j-input-number
v-else-if="['int', 'long', 'float', 'double'].includes(type)"
v-model:value="myValue"
:precision="0"
style="width: 100%"
@change="change"
/>
<j-date-picker
v-else-if="type === 'date' "
v-model:value="myValue"
show-time
@change="change"
/>
</template>
<script setup name="MetricValueItem">
const props = defineProps({
value: {
type: [String, Number, Array],
default: undefined
}
})
const emit = defineEmits(['update:value'])
const type = inject('metricsType')
const myValue = ref(props.value)
const change = () => {
emit('update:value', myValue.value)
}
watch(() => props.value, () => {
myValue.value = props.value
})
</script>

View File

@ -0,0 +1,189 @@
<template>
<j-popconfirm-modal
body-style="padding-top:4px;width:600px;"
placement="topRight"
@confirm="confirm"
@cancel="cancel"
@visibleChange="visibleChange"
>
<template #content>
<j-scrollbar height="350">
<j-collapse v-model:activeKey="activeKey" >
<j-collapse-panel v-for="(item, index) in config" :key="'store_'+index" :header="item.name">
<j-table
:columns="columns"
:data-source="item.properties"
:pagination="false"
rowKey="id"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'type'">{{ record.type?.name }}</template>
<value-item
v-else-if="column.dataIndex === 'value'"
v-model:modelValue="configValue[record.property]"
:itemType="item.properties[index].type?.type"
:options="(item.properties[index].type?.elements || []).map((a:any) => ({
label: a.text,
value: a.value,
}))"
/>
</template>
</j-table>
</j-collapse-panel>
<j-collapse-panel key="metrics" v-if="showMetrics">
<template #header>
指标配置
<j-tooltip title="场景联动页面可引用指标配置作为触发条件">
<AIcon type="ExclamationCircleOutlined" style="padding-left: 12px;padding-top: 4px;" />
</j-tooltip>
</template>
<Metrics ref="metricsRef" :value="myValue.expands?.metrics" :type="props.value?.valueType?.type"/>
</j-collapse-panel>
</j-collapse>
</j-scrollbar>
</template>
<j-button>
<AIcon type="SettingOutlined" />
配置
</j-button>
</j-popconfirm-modal>
</template>
<script setup lang="ts" name="OtherSetting">
import Metrics from './Metrics/Metrics.vue'
import {watch} from "vue";
import {cloneDeep} from "lodash";
import {useProductStore} from "store/product";
import {useInstanceStore} from "store/instance";
import {getMetadataConfig, getMetadataDeviceConfig} from "@/api/device/product";
import {omit} from "lodash-es";
const props = defineProps({
value: {
type: Object,
default: () => ({})
}
})
const type = inject('_metadataType')
const productStore = useProductStore()
const deviceStore = useInstanceStore()
const emit = defineEmits(['update:value'])
const activeKey = ref()
const storageRef = ref()
const metricsRef = ref()
const myValue = ref(props.value)
const visible = ref(false)
const config = ref<any>([])
const configValue = ref(props.value?.expands)
const showMetrics = computed(() => {
return ['int', 'long', 'float', 'double', 'string', 'boolean', 'date'].includes(props.value?.valueType?.type as any)
})
const columns = ref([
{
title: '参数名称',
dataIndex: 'name',
width: 150,
ellipsis: true,
},
{
title: '输入类型',
dataIndex: 'type',
width: 150,
},
{
title: '值',
dataIndex: 'value',
},
]);
const getConfig = async () => {
const record = props.value
const id = type === 'product' ? productStore.current?.id : deviceStore.current.id
console.log(record.id, id, record.valueType)
if(!record.id || !id || !record.valueType.type) return
const params: any = {
deviceId: id,
metadata: {
id: record.id,
type: 'property',
dataType: record.valueType.type,
},
}
const resp = type === 'product' ? await getMetadataConfig(params) : await getMetadataDeviceConfig(params)
if (resp.status === 200) {
config.value = resp.result
if (resp.result.length && !configValue.value) {
resp.result.forEach(a => {
if (a.properties) {
a.properties.forEach(b => {
configValue.value[b.property] = undefined
})
}
})
}
}
}
const confirm = () => {
return new Promise(async (resolve, reject) => {
try {
let metrics: any
metrics = await metricsRef.value?.getData()
const expands = {
...(configValue.value || {}),
}
if (metrics) {
expands.metrics = metrics
}
console.log(expands)
emit('update:value', {
...props.value,
expands: {
...(props.value.expands || {}),
...expands
}
})
resolve(true)
} catch (err) {
console.log(err)
reject(false)
}
})
}
const visibleChange = (e: boolean) => {
if (e) {
configValue.value = omit(props.value?.expands, ['source', 'type', 'metrics'])
getConfig()
}
}
const cancel = () => {
console.log(props.value)
myValue.value = cloneDeep(props.value)
}
watch(() => props.value, () => {
console.log(props.value)
myValue.value = cloneDeep(props.value)
}, {immediate: true, deep: true})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,121 @@
<template>
<j-collapse-panel v-for="(item, index) in config" :key="'store_'+index" :header="item.name">
<j-table
:columns="columns"
:data-source="item.properties"
:pagination="false"
rowKey="id"
>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'type'">{{ record.type?.name }}</template>
<ValueItem
v-else-if="column.dataIndex === 'value'"
v-model:modelValue="configValue.value[record.property]"
:itemType="record.type?.type"
:options="(record?.type?.elements || []).map((a:any) => ({
label: a.text,
value: a.value,
}))"
/>
</template>
</j-table>
</j-collapse-panel>
</template>
<script setup lang="ts" name="StorageSetting">
import {useProductStore} from "store/product";
import {useInstanceStore} from "store/instance";
import type { PropType } from "vue";
import { defineExpose } from 'vue'
import {getMetadataConfig, getMetadataDeviceConfig} from "@/api/device/product";
import { omit } from 'lodash-es'
const props = defineProps({
type: {
type: String,
default: undefined
},
record: {
type: Object as PropType<any>,
default: undefined
},
visible: {
type: Boolean,
default: false
}
})
const productStore = useProductStore()
const deviceStore = useInstanceStore()
const config = ref<any>([])
const configValue = ref(props.record?.expands)
const columns = ref([
{
title: '参数名称',
dataIndex: 'name',
width: 150,
ellipsis: true,
},
{
title: '输入类型',
dataIndex: 'type',
width: 150,
},
{
title: '值',
dataIndex: 'value',
},
]);
const getConfig = async () => {
const { type, record } = props
const id = type === 'product' ? productStore.current?.id : deviceStore.current.id
console.log(record.id, id, record.valueType)
if(!record.id || !id || !record.valueType.type) return
const params: any = {
deviceId: id,
metadata: {
id: record.id,
type: 'property',
dataType: record.valueType.type,
},
}
const resp = type === 'product' ? await getMetadataConfig(params) : await getMetadataDeviceConfig(params)
if (resp.status === 200) {
config.value = resp.result
if (resp.result.length && !configValue.value) {
resp.result.forEach(a => {
if (a.properties) {
a.properties.forEach(b => {
configValue.value[b.property] = undefined
})
}
})
}
}
}
watch(() => props.visible, () => {
if (props.visible) {
configValue.value = omit(props.record?.expands, ['source', 'type', 'metrics'])
getConfig()
}
}, { immediate: true })
const getData = () => {
return configValue.value
}
defineExpose({
getData
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,139 @@
<template>
<div class="metadata-source">
<j-select
v-model:value="myValue"
:options="PropertySource"
placeholder="请选择来源"
@change="onChange"
:disabled="false"
>
</j-select>
<j-popconfirm-modal
v-if="myValue != 'manual'"
@confirm="confirm"
:bodyStyle="{width: '450px', height: myValue === 'rule' ? '300px' : '80px'}"
>
<template #content>
<j-scrollbar v-if="myValue">
<VirtualRule
:value="value"
:source="myValue"
:dataSource="dataSource"
ref="virtualRuleRef"
/>
</j-scrollbar>
</template>
<j-button :disabled="!myValue" type="link" style="padding: 4px 8px">
<AIcon type="EditOutlined" />
</j-button>
</j-popconfirm-modal>
</div>
</template>
<script setup lang="ts" name="MetadataSource">
import { isNoCommunity } from '@/utils/utils';
import VirtualRule from './VirtualRule/index.vue';
const PropertySource: { label: string; value: string }[] = isNoCommunity
? [
{
value: 'device',
label: '设备',
},
{
value: 'manual',
label: '手动',
},
{
value: 'rule',
label: '规则',
},
]
: [
{
value: 'device',
label: '设备',
},
{
value: 'manual',
label: '手动',
},
];
type SourceType = 'device' | 'manual' | 'rule' | '';
type Emit = {
(e: 'update:value', data: Record<string, any>): void;
};
const props = defineProps({
value: {
type: Object,
default: () => {},
},
dataSource: {
type: Array,
default: () => [],
},
});
const emit = defineEmits<Emit>();
const myValue = ref<SourceType>('');
const type = ref<string>('');
const virtualRuleRef = ref<any>(null);
const onChange = (keys: SourceType) => {
myValue.value = keys;
emit('update:value', {
...props.value,
expands: {
...props.value?.expands,
source: keys,
type:
keys === 'manual'
? ['write']
: keys === 'rule'
? ['report']
: [],
},
});
};
const confirm = async () => {
return new Promise(async (resolve, reject) => {
const data = await virtualRuleRef?.value.onSave().catch(() => {
reject();
});
if (data) {
const obj = {
...props.value,
expands: {
...props.value?.expands,
...data,
},
};
emit('update:value', obj);
resolve(true);
} else {
reject()
}
});
};
watch(
() => props.value,
() => {
myValue.value = props.value?.expands?.source || '';
type.value = props.value?.expands?.type || [];
},
{ immediate: true },
);
</script>
<style scoped>
.metadata-source {
display: flex;
align-items: center;
}
</style>

View File

@ -0,0 +1,35 @@
<template>
<j-button @click="visible = true" style="width: 100%" type="dashed">
编辑规则
</j-button>
<FRuleEditor :aggList="aggList" @close="onClose" v-if="visible" :value="value" @save="onChange" :id="id" :virtualRule="virtualRule" />
</template>
<script setup lang="ts" name="Rule">
import FRuleEditor from '@/components/FRuleEditor/index.vue';
interface Emits {
(e: 'update:value', data: string | undefined): void;
(e: 'change', data: string | undefined): void;
}
const emit = defineEmits<Emits>();
const props = defineProps({
value: String,
id: String,
virtualRule: Object,
aggList: Array
});
const visible = ref<boolean>(false);
const onChange = (val: string | undefined) => {
emit('change', val)
emit('update:value', val)
onClose()
}
const onClose = () => {
visible.value = false
}
</script>

View File

@ -0,0 +1,349 @@
<template>
<j-form ref="formRef" layout="vertical" :model="formData">
<ReadType
v-model:value="formData.type"
:disabled="source !== 'device'"
:options="typeOptions"
/>
<template v-if="source === 'rule'">
<j-form-item :name="['virtualRule', 'triggerProperties']" required>
<template #label>
触发属性
<j-tooltip>
<template #title>
<div>选择当前产品物模型下的属性作为触发属性</div>
<div>任意属性值更新时将触发下方计算规则</div>
</template>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</j-tooltip>
</template>
<j-select
v-model:value="formData.virtualRule.triggerProperties"
mode="multiple"
placeholder="请选择属性"
show-search
max-tag-count="responsive"
>
<j-select-option
:disabled="
formData.virtualRule?.triggerProperties?.length &&
!formData.virtualRule?.triggerProperties?.includes(
'*',
)
"
value="*"
>任意属性</j-select-option
>
<j-select-option
:disabled="
formData.virtualRule?.triggerProperties?.includes(
'*',
)
"
v-for="item in options"
:key="item?.id"
>{{ item?.name }}</j-select-option
>
</j-select>
</j-form-item>
<j-form-item
:name="['virtualRule', 'rule', 'script']"
label="计算规则"
required
>
<Rule
v-model:value="formData.virtualRule.rule.script"
:virtualRule="formData.virtualRule.rule"
:id="value.id"
:aggList="aggList"
/>
</j-form-item>
<j-form-item
label="窗口"
:name="['virtualRule', 'rule', 'windowType']"
required
>
<j-select
v-model:value="formData.virtualRule.rule.windowType"
:options="[
{ label: '无', value: 'undefined' },
{ label: '时间窗口', value: 'time' },
{ label: '频次窗口', value: 'num' },
]"
show-search
placeholder="请选择窗口类型"
/>
</j-form-item>
<template
v-if="formData.virtualRule?.rule?.windowType !== 'undefined'"
>
<j-form-item
label="聚合函数"
:name="['virtualRule', 'rule', 'aggType']"
required
>
<j-select
v-model:value="formData.virtualRule.rule.aggType"
:options="aggList"
placeholder="请选择聚合函数"
/>
</j-form-item>
<j-form-item
:label="
formData.virtualRule?.rule?.windowType === 'time'
? '窗口长度(s)'
: '窗口长度(次)'
"
:name="['virtualRule', 'rule', 'window', 'span']"
required
:rules="[
{
required: true,
message: '请输入窗口长度',
},
{
pattern: /^\d+$/,
message: '请输入0-999999之间的正整数',
},
]"
>
<j-input-number
v-model:value="formData.virtualRule.rule.window.span"
placeholder="请输入窗口长度"
style="width: 100%"
:max="999999"
:min="1"
/>
</j-form-item>
<j-form-item
:label="
formData.virtualRule?.rule?.windowType === 'time'
? '步长(s)'
: '步长(次)'
"
:name="['virtualRule', 'rule', 'window', 'every']"
required
:rules="[
{
required: true,
message: '请输入步长',
},
{
pattern: /^\d+$/,
message: '请输入0-999999之间的正整数',
},
]"
>
<j-input-number
style="width: 100%"
v-model:value="formData.virtualRule.rule.window.every"
placeholder="请输入步长"
:max="999999"
:min="1"
/>
</j-form-item>
</template>
</template>
</j-form>
</template>
<script setup lang="ts" name="VirtualRule">
import Rule from './Rule.vue';
import { queryDeviceVirtualProperty } from '@/api/device/instance';
import {
queryProductVirtualProperty,
getStreamingAggType,
} from '@/api/device/product';
import { useInstanceStore } from '@/store/instance';
import { useProductStore } from '@/store/product';
import { PropType } from 'vue';
import { ReadType } from '@/components/Metadata/components';
type SourceType = 'device' | 'manual' | 'rule';
const props = defineProps({
value: {
type: Object,
default: () => {},
},
dataSource: {
type: Array,
default: () => [],
},
source: {
type: String as PropType<SourceType>,
default: 'device',
},
});
const initData = {
triggerProperties: ['*'],
rule: {
type: undefined,
script: '',
isVirtualRule: false,
windowType: 'undefined',
aggType: undefined,
window: {
span: undefined,
every: undefined,
},
},
};
const instanceStore = useInstanceStore();
const productStore = useProductStore();
const aggList = ref<any[]>([]);
const formRef = ref<any>(undefined);
const target = inject<'device' | 'product'>('_metadataType', 'product');
const formData = reactive<{
type: string[];
virtualRule?: {
triggerProperties: string[];
rule: {
type: 'script' | 'window' | undefined;
script: string | undefined;
isVirtualRule: boolean;
windowType: string;
aggType: string | undefined;
window: {
span: number | undefined;
every: number | undefined;
};
};
};
}>({
type: [],
virtualRule: undefined,
});
const dataSource = inject<any[]>('_dataSource')
const typeOptions = computed(() => {
if (props.source === 'manual') {
return [{ value: 'write', label: '写' }];
} else if (props.source === 'rule') {
return [{ value: 'report', label: '上报' }];
} else {
return [
{ value: 'read', label: '读' },
{ value: 'write', label: '写' },
{ value: 'report', label: '上报' },
];
}
});
const options = computed(() => {
return dataSource?.filter((item: any) => item?.id !== props.value?.id);
});
const handleSearch = async () => {
let resp: any = undefined;
if (target === 'product') {
resp = await queryProductVirtualProperty(
productStore.current?.id,
props.value?.id,
);
} else {
resp = await queryDeviceVirtualProperty(
instanceStore.current?.productId,
instanceStore.current?.id,
props.value?.id,
);
}
if (resp && resp.status === 200) {
formData.virtualRule = {
triggerProperties: resp?.result?.triggerProperties?.length ? resp?.result?.triggerProperties : ['*'],
rule: resp?.result?.rule ? resp?.result?.rule : initData.rule,
}
}
};
const queryAggType = () => {
getStreamingAggType().then((resp) => {
if (resp.status === 200) {
aggList.value = resp.result.map((item) => {
return {
label: item?.text,
value: item?.value,
};
});
}
});
};
onMounted(() => {
queryAggType();
});
watch(
() => props.value,
() => {
formData.type = props.value.expands?.type;
if (props.value.virtualRule) {
}
},
{ immediate: true, deep: true },
);
watch(
() => props.source,
(newVal: SourceType) => {
if (newVal === 'rule') {
formData.virtualRule = initData;
console.log(formData.virtualRule);
handleSearch();
} else {
formData.virtualRule = undefined;
}
},
{
immediate: true,
deep: true
},
);
const onSave = () => {
return new Promise(async (resolve, reject) => {
const data = await formRef.value!.validate().catch(() => {
reject();
});
if (data) {
if (data.virtualRule) {
const flag = data.virtualRule.rule.windowType !== 'undefined'
resolve({
type: data.type,
virtualRule: {
type: flag ? 'window' : 'script',
rule: {
...data.virtualRule.rule,
isVirtualRule: flag,
type: flag ? 'window' : 'script',
},
triggerProperties:
data.virtualRule?.triggerProperties.includes('*')
? []
: data.virtualRule?.triggerProperties,
},
});
} else {
resolve({
type: data.type,
})
}
}
});
};
defineExpose({ onSave });
</script>
<style scoped>
</style>

View File

@ -0,0 +1,9 @@
export { default as Source } from './Source.vue'
export { default as DataType } from './DataType.vue'
export { default as InputParams } from './Function/InputParams.vue'
export { default as Metrics } from './Properties/Metrics/Metrics.vue'
export { default as OtherSetting } from './Properties/OtherSetting.vue'
export { default as OutputParams } from './Function/OutputParams.vue';
export { default as ValueObject } from './Events/ValueObject.vue';
export { default as OtherConfigInfo } from './Events/OtherConfigInfo.vue';
export { default as ConfigParams } from './Events/ConfigParams.vue';

View File

@ -0,0 +1,2 @@
export { default as useMetadata } from './useMatadata'
export { default as useOperateLimits } from './useOperateLimits'

View File

@ -0,0 +1,23 @@
export const useAction = (target: any) => {
const addAction = (data: any, index?: number) => {
console.log(target, index)
target.value?.addItem?.(data, index)
}
const copyAction = (data: any, index: number) => {
console.log(target)
addAction(data, index)
}
const removeAction = (index: number) => {
console.log(target)
target.value?.removeItem?.(index)
}
return {
addAction,
copyAction,
removeAction
}
}

View File

@ -0,0 +1,26 @@
import {DeviceMetadata, MetadataItem, MetadataType} from "@/views/device/Product/typings";
import {useInstanceStore} from "store/instance";
import {useProductStore} from "store/product";
import type { Ref, ComputedRef } from "vue";
const useMetadata = (type: 'device' | 'product', key?: MetadataType): {
data: ComputedRef<MetadataItem[]>,
metadata: Ref<Partial<DeviceMetadata>>
} => {
const instanceStore = useInstanceStore()
const productStore = useProductStore()
const metadata = ref<Partial<DeviceMetadata>>({})
const data = computed(() => {
const _metadataStr = type === 'product' ? productStore.current?.metadata : instanceStore.current.metadata
const _metadata = JSON.parse(_metadataStr || '{}')
console.log(_metadata)
return (key ? _metadata[key] : []) as MetadataItem[]
})
return {
data,
metadata
}
}
export default useMetadata

View File

@ -0,0 +1,27 @@
import {useInstanceStore} from "store/instance";
import type {MetadataType} from "@/views/device/Product/typings";
const limitsMap = new Map<string, any>();
limitsMap.set('events-add', 'eventNotInsertable');
limitsMap.set('events-updata', 'eventNotModifiable');
limitsMap.set('properties-add', 'propertyNotInsertable');
limitsMap.set('properties-updata', 'propertyNotModifiable');
const useOperateLimits = (target: 'device' | 'product') => {
const instanceStore = useInstanceStore()
const operates = computed(() => {
return target === 'device' ? (instanceStore.current?.features || []) : []
})
const hasOperate = (action: 'add' | 'update', types: MetadataType ) => {
return !!operates.value.find((item: { id: string; name: string }) => item.id === limitsMap.get(`${types}-${action}`))
}
return {
hasOperate
}
}
export default useOperateLimits

View File

@ -0,0 +1 @@
export { default as Base } from './Base.vue'

View File

@ -143,6 +143,7 @@ const refreshMetadata = () => {
// : await detail(route.params.id as string);
const result = target === 'product' ? productStore.current?.metadata : instanceStore.current.metadata
const item = JSON.parse(result || '{}') as MetadataItem[]
console.log(item)
data.value = item[type]?.sort((a: any, b: any) => b?.sortsIndex - a?.sortsIndex)
loading.value = false
}

View File

@ -0,0 +1,21 @@
export const levelMap = ref({
ordinary: '普通',
warn: '警告',
urgent: '紧急',
})
export const sourceMap = ref({
device: '设备',
manual: '手动',
rule: '规则',
});
export const expandsType = ref({
read: '读',
write: '写',
report: '上报',
});
export const limitsMap = new Map<string, any>();
limitsMap.set('events-add', 'eventNotInsertable');
limitsMap.set('events-updata', 'eventNotModifiable');
limitsMap.set('properties-add', 'propertyNotInsertable');
limitsMap.set('properties-updata', 'propertyNotModifiable');

View File

@ -51,7 +51,8 @@ import { message } from 'ant-design-vue'
import { useInstanceStore } from '@/store/instance'
import Import from './Import/index.vue'
import Cat from './Cat/index.vue'
import BaseMetadata from './Base/index.vue'
// import BaseMetadata from './Base/index.vue'
import BaseMetadata from './Base/Base.vue'
import { useMetadataStore } from '@/store/metadata'
const route = useRoute()
@ -67,6 +68,8 @@ const permission = computed(() => props.type === 'device' ? 'device/Instance' :
const visible = ref(false)
const cat = ref(false)
provide('_metadataType', props.type)
//
const resetMetadata = async () => {
const { id } = route.params

View File

@ -36,6 +36,7 @@ import type { DeviceMetadata, MetadataItem, MetadataType, ProductItem } from "..
} else {
console.warn('未触发物模型修改');
}
console.log('config', config, type)
// @ts-ignore
metadata[type] = config.sort((a, b) => b?.sortsIndex - a?.sortsIndex);
data.metadata = JSON.stringify(metadata);