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:
parent
41daa5ea2a
commit
74747cd966
4
build.sh
4
build.sh
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}`)
|
||||
|
||||
|
||||
|
|
|
@ -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}`)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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[];
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
import Rule from './Rule.vue'
|
||||
|
||||
export default Rule
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
export { default as ReadType } from './ReadType.vue'
|
|
@ -21,7 +21,7 @@ export const useMetadataStore = defineStore({
|
|||
action: 'add',
|
||||
import: false,
|
||||
importMetadata: false,
|
||||
} as MetadataModelType
|
||||
} as MetadataModelType
|
||||
}),
|
||||
actions: {
|
||||
set(key: string, value: any) {
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
|||
import Metrics from './Metrics.vue'
|
||||
|
||||
export default Metrics
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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';
|
|
@ -0,0 +1,2 @@
|
|||
export { default as useMetadata } from './useMatadata'
|
||||
export { default as useOperateLimits } from './useOperateLimits'
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
export { default as Base } from './Base.vue'
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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');
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue