feat: 采集器 OPCUA扫描全部功能

This commit is contained in:
jackhoo_98 2023-03-12 15:27:39 +08:00
parent 4e51d021eb
commit 721409dfb0
6 changed files with 733 additions and 50 deletions

View File

@ -42,3 +42,13 @@ export const readPoint = (collectorId: string, data: string[]) =>
export const writePoint = (collectorId: string, data: string[]) =>
server.post(`/data-collect/collector/${collectorId}/points/_write`, data);
export const queryPointNoPaging = () =>
server.post(`/data-collect/point/_query/no-paging`, { paging: false });
export const scanOpcUAList = (data: any) =>
server.get(
`/data-collect/opc/channel/${data.id}/nodes?nodeId=${
data?.nodeId || ''
}`,
);

View File

@ -0,0 +1,274 @@
<template>
<j-form style="width: 80%" ref="formTableRef" :model="modelRef">
<j-table
:dataSource="modelRef.dataSource"
:columns="FormTableColumns"
:scroll="{ x: 1100, y: 500 }"
>
<template #bodyCell="{ column: { dataIndex }, record, index }">
<template v-if="dataIndex === 'name'">
<a-form-item
:name="['dataSource', index, 'name']"
:rules="[
{
required: true,
message: '请输入',
},
]"
>
<j-input
v-model:value="record[dataIndex]"
placeholder="请输入"
allowClear
></j-input>
</a-form-item>
</template>
<template v-if="dataIndex === 'id'">
<a-form-item :name="['dataSource', index, 'id']">
<j-input v-model:value="record[dataIndex]" disabled>
</j-input>
</a-form-item>
</template>
<template v-if="dataIndex === 'accessModes'">
<a-form-item
class="form-item"
:name="['dataSource', index, 'accessModes', 'value']"
:rules="[
{
required: true,
message: '请选择',
},
]"
>
<j-select
style="width: 75%"
v-model:value="record[dataIndex].value"
placeholder="请选择"
allowClear
mode="multiple"
:filter-option="filterOption"
:options="[
{ label: '读', value: 'read' },
{ label: '写', value: 'write' },
{ label: '订阅', value: 'subscribe' },
]"
:disabled="index !== 0 && record[dataIndex].check"
@change="changeValue(index, dataIndex)"
>
</j-select>
<j-checkbox
style="margin-left: 5px"
v-if="index !== 0"
v-model:checked="record[dataIndex].check"
@click="changeCheckbox(index, dataIndex)"
>同上</j-checkbox
>
</a-form-item>
</template>
<template v-if="dataIndex === 'interval'">
<a-form-item
class="form-item"
:name="[
'dataSource',
index,
'configuration',
'interval',
'value',
]"
:rules="[
{
required: true,
message: '请输入',
},
]"
>
<j-input
style="width: 70%"
v-model:value="
record.configuration[dataIndex].value
"
placeholder="请输入"
allowClear
addon-after="ms"
:disabled="
index !== 0 &&
record.configuration[dataIndex].check
"
@blur="changeValue(index, dataIndex)"
></j-input>
<j-checkbox
style="margin-left: 5px"
v-show="index !== 0"
v-model:checked="
record.configuration[dataIndex].check
"
@click="changeCheckbox(index, dataIndex)"
>同上</j-checkbox
>
</a-form-item>
</template>
<template v-if="dataIndex === 'features'">
<a-form-item
class="form-item"
:name="['dataSource', index, 'features', 'value']"
:rules="[
{
required: true,
message: '请选择',
},
]"
>
<j-select
style="width: 50%"
v-model:value="record[dataIndex].value"
placeholder="请选择"
allowClear
:filter-option="filterOption"
:options="[
{
label: '是',
value: true,
},
{
label: '否',
value: false,
},
]"
:disabled="index !== 0 && record[dataIndex].check"
@change="changeValue(index, dataIndex)"
>
</j-select>
<j-checkbox
style="margin-left: 5px"
v-show="index !== 0"
v-model:checked="record[dataIndex].check"
@click="changeCheckbox(index, dataIndex)"
>同上</j-checkbox
>
</a-form-item>
</template>
<template v-if="dataIndex === 'action'">
<a-tooltip title="删除">
<a-popconfirm
title="确认删除"
@confirm="clickDelete(record.id)"
>
<AIcon type="DeleteOutlined" />
</a-popconfirm>
</a-tooltip>
</template>
</template>
</j-table>
</j-form>
</template>
<script lang="ts" setup>
import { FormTableColumns } from '../../data';
const props = defineProps({
data: {
type: Array,
default: () => [],
},
});
const emits = defineEmits(['change']);
const formTableRef = ref();
const defaultType = ['accessModes', 'interval', 'features'];
const modelRef = reactive({
dataSource: [],
});
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const clickDelete = (value: string) => {
emits('change', value);
};
const getTargetData = (index: number, type: string) => {
const { dataSource } = modelRef;
const Interval = type === 'interval';
return !Interval
? dataSource[index][type]
: dataSource[index].configuration[type];
};
const changeValue = (index: number, type: string) => {
const { dataSource } = modelRef;
const originData = getTargetData(index, type);
for (let i = index + 1; i < dataSource.length; i++) {
const targetType = getTargetData(i, type);
if (!targetType.check) return;
targetType.value = originData.value;
}
};
const changeCheckbox = (index: number, type: string) => {
// console.log(1, getTargetData(index, type).check,getTargetData(index, type));
//使setTimeout
setTimeout(() => {
// console.log(2, getTargetData(index, type).check,getTargetData(index, type));
let startIndex = 0;
const { dataSource } = modelRef;
const currentCheck = getTargetData(index, type).check;
if (!currentCheck) return;
for (let i = index; i >= 0; i--) {
const preDatCheck = getTargetData(i, type).check;
if (!preDatCheck) {
startIndex = i;
break;
}
}
const originData = getTargetData(startIndex, type);
for (let i = startIndex; i < dataSource.length - 1; i++) {
const targetType = getTargetData(i + 1, type);
if (!targetType.check) return;
targetType.value = originData.value;
}
}, 0);
};
const validate = () => {
return new Promise((res, rej) => {
formTableRef.value
.validate()
.then(() => {
res(modelRef.dataSource);
})
.catch((err: any) => {
rej(err);
});
});
};
defineExpose({
validate,
});
watch(
() => props.data,
(value, preValue) => {
modelRef.dataSource = value;
//
const vlength = value.length,
plength = preValue.length;
if (plength !== 0 && plength < vlength) {
defaultType.forEach((type) => {
vlength === 2
? changeValue(0, type)
: changeCheckbox(vlength - 1, type);
});
}
},
{ deep: true },
);
</script>
<style lang="less" scoped>
.form-item {
display: flex;
}
</style>

View File

@ -0,0 +1,240 @@
<template>
<div class="tree-content">
<div class="tree-header">
<div>数据源</div>
<j-checkbox v-model:checked="isSelected">隐藏已有节点</j-checkbox>
</div>
<j-spin :spinning="spinning">
<a-tree
v-model:checkedKeys="checkedKeys"
:tree-data="treeData"
default-expand-all
checkable
@check="onCheck"
:height="600"
>
<!-- <a-tree
:load-data="onLoadData"
:tree-data="treeData"
v-model:checkedKeys="checkedKeys"
checkable
@check="onCheck"
> -->
<template #title="{ name, key }">
<span
:class="[
selectKeys.includes(key)
? 'tree-selected'
: 'tree-title',
]"
>
{{ name }}
</span>
</template>
</a-tree>
</j-spin>
</div>
</template>
<script lang="ts" setup>
import type { TreeProps } from 'ant-design-vue';
import {
scanOpcUAList,
queryPointNoPaging,
} from '@/api/data-collect/collector';
import { cloneDeep } from 'lodash-es';
const props = defineProps({
data: {
type: Array,
default: () => [],
},
unSelectKeys: {
type: String,
default: '',
},
});
const emits = defineEmits(['change']);
// const channelId = '1610517801347788800'; //
const channelId = props.data?.channelId;
const checkedKeys = ref<string[]>([]);
const selectKeys = ref<string[]>([]);
const spinning = ref(false);
const isSelected = ref(false);
const treeData = ref<TreeProps['treeData']>();
const treeAllData = ref<TreeProps['treeData']>();
const onLoadData = (node: any) =>
new Promise<void>(async (resolve) => {
if ((node?.children && node.children?.length) || !node?.folder) {
resolve();
return;
}
const resp = await scanOpcUAList({
id: channelId,
nodeId: node.key,
});
if (resp.status === 200) {
const list = resp.result.map((item: any) => {
return {
...item,
key: item.id,
title: item.name,
disabled: item?.folder,
isLeaf: !item?.folder,
};
});
treeAllData.value = updateTreeData(
cloneDeep(treeAllData.value),
node.key,
[...list],
);
}
resolve();
});
const handleData = (arr: any[]): any[] => {
const data = arr.filter((item) => {
return (
(isSelected && !selectKeys.value.includes(item.id)) || !isSelected
);
});
return data.map((item) => {
if (item.children && item.children?.length) {
return {
...item,
children: handleData(item.children),
};
} else {
return item;
}
});
};
const onCheck = (checkedKeys, info) => {
const one: any = { ...info.node };
const list: any = [];
const last: any = list.length ? list[list.length - 1] : undefined;
if (list.map((i: any) => i?.id).includes(one.id)) {
return;
}
const item = {
features: {
value: last
? last?.features?.value
: (one?.features || []).includes('changedOnly'),
check: true,
},
id: one?.id || '',
name: one?.name || '',
accessModes: {
value: last ? last?.accessModes?.value : one?.accessModes || [],
check: true,
},
configuration: {
...one?.configuration,
interval: {
value: last
? last?.configuration?.interval?.value
: one?.configuration?.interval || 3000,
check: true,
},
nodeId: one?.id,
},
};
emits('change', item, info.checked);
};
const updateTreeData = (list: any[], key: string, children: any[]): any[] => {
const arr = list.map((node) => {
if (node.key === key) {
return {
...node,
children,
};
}
if (node?.children && node?.children?.length) {
return {
...node,
children: updateTreeData(node.children, key, children),
};
}
return node;
});
return arr;
};
const getPoint = async () => {
spinning.value = true;
const res = await queryPointNoPaging();
if (res.status === 200) {
selectKeys.value = res.result.map((item: any) => item.pointKey);
}
spinning.value = false;
};
getPoint();
const getScanOpcUAList = async () => {
const res = await scanOpcUAList({ id: channelId });
treeAllData.value = res.result.map((item: any) => ({
...item,
key: item.id,
title: item.name,
disabled: item?.folder || false,
}));
};
getScanOpcUAList();
watch(
() => isSelected.value,
(value) => {
if (value) {
treeData.value = handleData(treeAllData.value);
} else {
treeData.value = treeAllData.value;
}
},
{ deep: true },
);
watch(
() => treeAllData.value,
(value) => {
if (isSelected.value) {
treeData.value = handleData(value);
} else {
treeData.value = value;
}
},
{ deep: true },
);
watch(
() => props.unSelectKeys,
(value) => {
checkedKeys.value = checkedKeys.value.filter((i) => i !== value);
},
{ deep: true },
);
</script>
<style lang="less" scoped>
.tree-content {
padding: 16px;
padding-left: 0;
.tree-header {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
}
.tree-selected {
color: #1d39c4;
}
.tree-title {
color: black;
}
}
</style>

View File

@ -0,0 +1,107 @@
<template lang="">
<j-modal title="扫描" :visible="true" width="90%" @cancel="handleCancel">
<div class="content">
<Tree
:data="treeData"
class="tree"
@change="changeTree"
:unSelectKeys="unSelectKeys"
></Tree>
<Table
:data="tableData"
class="table"
@change="changeTable"
ref="formTableRef"
></Table>
</div>
<template #footer>
<j-button key="back" @click="handleCancel">取消</j-button>
<PermissionButton
key="submit"
type="primary"
:loading="loading"
@click="handleOk"
style="margin-left: 8px"
:hasPermission="`DataCollect/Collector:update`"
>
确认
</PermissionButton>
</template>
</j-modal>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'ant-design-vue';
import { savePointBatch } from '@/api/data-collect/collector';
import { Rule } from 'ant-design-vue/lib/form';
import { cloneDeep } from 'lodash';
import Table from './Table.vue';
import Tree from './Tree.vue';
const props = defineProps({
data: {
type: Array,
default: () => [],
},
});
const treeData = ref(props.data);
const emit = defineEmits(['change']);
const loading = ref(false);
const formTableRef = ref<FormInstance>();
const tableData = ref();
const tableDataMap = new Map();
const unSelectKeys = ref();
const handleOk = async () => {
loading.value = true;
const data = await formTableRef.value?.validate();
const list = data.map((item: any) => {
return {
name: item.name,
provider: 'OPC_UA',
collectorId: props.data?.id,
collectorName: props.data?.name,
pointKey: item.id,
configuration: {
interval: item.configuration?.interval?.value,
},
features: !item.features?.value ? [] : ['changedOnly'],
accessModes: item.accessModes?.value || [],
};
});
console.log(1112, props.data, data, list);
const resp = await savePointBatch([...list]);
if (resp.status === 200) {
emit('change', true);
}
loading.value = false;
};
const handleCancel = () => {
emit('change', false);
};
const changeTree = (row: any, checked: boolean) => {
checked ? tableDataMap.set(row.id, row) : tableDataMap.delete(row.id);
tableData.value = [...tableDataMap.values()];
};
const changeTable = (value: string) => {
unSelectKeys.value = value;
tableDataMap.delete(value);
tableData.value = [...tableDataMap.values()];
};
</script>
<style lang="less" scoped>
.content {
display: flex;
min-height: 600px;
.tree {
width: 300px;
}
.table {
flex: 1;
}
}
</style>

View File

@ -13,19 +13,7 @@
:gridColumn="2"
:gridColumns="[1, 2]"
:request="queryPoint"
:defaultParams="{
sorts: [{ name: 'id', order: 'desc' }],
terms: [
{
terms: [
{
column: 'collectorId',
value: props.data.id,
},
],
},
],
}"
:defaultParams="defaultParams"
:params="params"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
@ -36,12 +24,23 @@
<template #headerTitle>
<j-space>
<PermissionButton
v-if="data?.provider !== 'OPC_UA'"
type="primary"
@click="handlAdd"
hasPermission="DataCollect/Collector:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
{{ data?.provider === 'OPC_UA' ? '扫描' : '新增点位' }}
新增点位
</PermissionButton>
<PermissionButton
v-if="data?.provider === 'OPC_UA'"
type="primary"
@click="handlScan"
hasPermission="DataCollect/Collector:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
扫描
</PermissionButton>
<j-dropdown v-if="data?.provider === 'OPC_UA'">
<j-button
@ -168,29 +167,29 @@
</template>
</j-pro-table>
<SaveModBus
v-if="visibleSaveModBus"
v-if="visible.saveModBus"
:data="current"
@change="saveChange"
/>
<SaveOPCUA
v-if="visibleSaveOPCUA"
v-if="visible.saveOPCUA"
:data="current"
@change="saveChange"
/>
<WritePoint
v-if="visibleWritePoint"
v-if="visible.writePoint"
:data="current"
@change="saveChange"
/>
<BatchUpdate
v-if="visibleBatchUpdate"
v-if="visible.batchUpdate"
:data="current"
@change="saveChange"
/>
<Scan v-if="visible.scan" :data="current" @change="saveChange" />
</j-spin>
</template>
<script lang="ts" setup name="PointPage">
import type { ActionsType } from '@/components/Table/index.vue';
import { getImage } from '@/utils/comm';
import {
queryPoint,
@ -205,6 +204,7 @@ import WritePoint from './components/WritePoint/index.vue';
import BatchUpdate from './components/BatchUpdate/index.vue';
import SaveModBus from './Save/SaveModBus.vue';
import SaveOPCUA from './Save/SaveOPCUA.vue';
import Scan from './Scan/index.vue';
import { colorMap, getState } from '../data.ts';
import { cloneDeep } from 'lodash-es';
@ -220,15 +220,34 @@ const tableRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const opcImage = getImage('/DataCollect/device-opcua.png');
const modbusImage = getImage('/DataCollect/device-modbus.png');
const visibleSaveModBus = ref(false);
const visibleSaveOPCUA = ref(false);
const visibleWritePoint = ref(false);
const visibleBatchUpdate = ref(false);
const visible = reactive({
saveModBus: false,
saveOPCUA: false,
writePoint: false,
batchUpdate: false,
scan: false,
});
const current = ref({});
const accessModesOption = ref();
const _selectedRowKeys = ref<string[]>([]);
const spinning = ref(false);
const collectorId = ref(props.data.id);
const defaultParams = ref({
sorts: [{ name: 'id', order: 'desc' }],
terms: [
{
terms: [
{
column: 'collectorId',
value: collectorId.value,
// value: '1610517928766550016', //
},
],
},
],
});
const accessModesMODBUS_TCP = [
{
@ -314,21 +333,21 @@ const columns = [
];
const handlAdd = () => {
visibleSaveModBus.value = true;
visible.saveModBus = true;
current.value = {
collectorId: props.data.id,
collectorId: collectorId.value,
provider: props.data?.provider || 'MODBUS_TCP',
};
};
const handlEdit = (data: Object) => {
if (data?.provider === 'OPC_UA') {
visibleSaveOPCUA.value = true;
visible.saveOPCUA = true;
} else {
visibleSaveModBus.value = true;
visible.saveModBus = true;
}
current.value = cloneDeep(data);
};
const handlDelete = async (data: string | undefined) => {
const handlDelete = async (data: string | undefined = undefined) => {
spinning.value = true;
const res = !data
? await batchDeletePoint(_selectedRowKeys.value)
@ -340,7 +359,6 @@ const handlDelete = async (data: string | undefined) => {
}
spinning.value = false;
};
const handlBatchUpdate = () => {
const dataSet = new Set(_selectedRowKeys.value);
const dataMap = new Map();
@ -348,10 +366,14 @@ const handlBatchUpdate = () => {
dataSet.has(i.id) && dataMap.set(i.id, i);
});
current.value = [...dataMap.values()];
visibleBatchUpdate.value = true;
visible.batchUpdate = true;
};
const handlScan = () => {
visible.scan = true;
current.value = cloneDeep(props.data);
};
const clickEdit = async (data: object) => {
visibleWritePoint.value = true;
visible.writePoint = true;
current.value = cloneDeep(data);
};
const clickRedo = async (data: object) => {
@ -389,20 +411,10 @@ const getAccessModes = (item: Partial<Record<string, any>>) => {
return item?.accessModes?.map((i) => i?.value);
};
const getaccessModesOption = () => {
return props.data?.provider !== 'MODBUS_TCP'
? accessModesMODBUS_TCP.concat({
label: '订阅',
value: 'subscribe',
})
: accessModesMODBUS_TCP;
};
const saveChange = (value: object) => {
visibleSaveModBus.value = false;
visibleSaveOPCUA.value = false;
visibleWritePoint.value = false;
visibleBatchUpdate.value = false;
for (let key in visible) {
visible[key] = false;
}
current.value = {};
if (value) {
tableRef.value?.reload();
@ -438,10 +450,12 @@ watch(
label: '订阅',
value: 'subscribe',
});
tableRef?.value?.reload();
defaultParams.value.terms[0].terms[0].value = value.id;
// defaultParams.value.terms[0].terms[0].value = '1610517928766550016'; //
tableRef?.value?.reload && tableRef?.value?.reload();
}
},
{ deep: true },
{ immediate: true, deep: true },
);
/**

View File

@ -117,27 +117,65 @@ export const OPCUARules = {
message: '最多可输入64个字符',
},
],
type: [
{
required: true,
message: '请选择数据类型',
},
],
accessModes: [
{
required: true,
message: '请选择访问类型',
},
],
interval: [
{
required: true,
message: '请输入采集频率',
},
],
description: [{ max: 200, message: '最多可输入200个字符' }],
};
export const FormTableColumns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 200,
fixed: 'left',
},
{
title: 'nodeId',
dataIndex: 'id',
key: 'id',
ellipsis: true,
},
{
title: '访问类型',
dataIndex: 'accessModes',
key: 'accessModes',
width: 300,
},
{
title: '采集频率',
key: 'interval',
dataIndex: 'interval',
width: 280,
},
{
title: '只推送变化的数据',
key: 'features',
dataIndex: 'features',
width: 200,
},
{
title: '操作',
key: 'action',
dataIndex: 'action',
fixed: 'right',
width: 80,
},
];