655 lines
22 KiB
Vue
655 lines
22 KiB
Vue
<template>
|
|
<j-spin :spinning="spinning">
|
|
<pro-search :columns="columns" target="search" @search="handleSearch" />
|
|
<j-scrollbar height="680">
|
|
<j-pro-table
|
|
ref="tableRef"
|
|
model="CARD"
|
|
:columns="columns"
|
|
:gridColumn="2"
|
|
:gridColumns="[1, 2]"
|
|
:request="queryPoint"
|
|
:defaultParams="defaultParams"
|
|
:params="params"
|
|
:rowSelection="{
|
|
selectedRowKeys: _selectedRowKeys,
|
|
onChange: onSelectChange,
|
|
}"
|
|
@cancelSelect="cancelSelect"
|
|
>
|
|
<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>
|
|
新增点位
|
|
</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
|
|
>批量操作 <AIcon type="DownOutlined"
|
|
/></j-button>
|
|
<template #overlay>
|
|
<j-menu>
|
|
<j-menu-item>
|
|
<PermissionButton
|
|
hasPermission="DataCollect/Collector:update"
|
|
@click="handlBatchUpdate()"
|
|
>
|
|
<template #icon
|
|
><AIcon type="FormOutlined"
|
|
/></template>
|
|
编辑
|
|
</PermissionButton>
|
|
</j-menu-item>
|
|
<j-menu-item>
|
|
<PermissionButton
|
|
hasPermission="DataCollect/Collector:delete"
|
|
:popConfirm="{
|
|
title: `确定删除?`,
|
|
onConfirm: () => handlDelete(),
|
|
}"
|
|
>
|
|
<template #icon
|
|
><AIcon type="EditOutlined"
|
|
/></template>
|
|
删除
|
|
</PermissionButton>
|
|
</j-menu-item>
|
|
</j-menu>
|
|
</template>
|
|
</j-dropdown>
|
|
</j-space>
|
|
<div
|
|
v-if="data?.provider === 'OPC_UA'"
|
|
style="margin-top: 15px"
|
|
>
|
|
<j-checkbox
|
|
v-model:checked="checkAll"
|
|
@change="onCheckAllChange"
|
|
>全选</j-checkbox
|
|
>
|
|
</div>
|
|
</template>
|
|
<template #card="slotProps">
|
|
<PointCardBox
|
|
:showStatus="true"
|
|
:value="slotProps"
|
|
@click="handleClick"
|
|
:active="_selectedRowKeys.includes(slotProps.id)"
|
|
class="card-box"
|
|
:status="getState(slotProps).value"
|
|
:statusText="getState(slotProps)?.text"
|
|
:statusNames="Object.fromEntries(colorMap.entries())"
|
|
>
|
|
<template #title>
|
|
<slot name="title">
|
|
<div class="card-box-title">
|
|
{{ slotProps.name }}
|
|
</div>
|
|
</slot>
|
|
</template>
|
|
<template #action>
|
|
<div class="card-box-action">
|
|
<j-popconfirm
|
|
title="确定删除?"
|
|
@confirm="handlDelete(slotProps.id)"
|
|
>
|
|
<a><AIcon type="DeleteOutlined" /></a>
|
|
</j-popconfirm>
|
|
<a @click="handlEdit(slotProps)"
|
|
><AIcon type="FormOutlined"
|
|
/></a>
|
|
</div>
|
|
</template>
|
|
<template #img>
|
|
<img
|
|
:src="
|
|
slotProps.provider === 'OPC_UA'
|
|
? opcImage
|
|
: modbusImage
|
|
"
|
|
/>
|
|
</template>
|
|
<template #content>
|
|
<div class="card-box-content">
|
|
<div class="card-box-content-left">
|
|
<div class="card-box-content-left-1">
|
|
<div
|
|
class="ard-box-content-left-1-title"
|
|
v-if="
|
|
propertyValue.has(slotProps.id)
|
|
"
|
|
>
|
|
<j-ellipsis
|
|
style="max-width: 150px"
|
|
>
|
|
{{
|
|
propertyValue.get(
|
|
slotProps.id,
|
|
)?.parseData[0] || 0
|
|
}}({{
|
|
propertyValue.get(
|
|
slotProps.id,
|
|
)?.dataType
|
|
}})
|
|
</j-ellipsis>
|
|
</div>
|
|
<span v-else>--</span>
|
|
<a
|
|
v-if="
|
|
getAccessModes(
|
|
slotProps,
|
|
).includes('write')
|
|
"
|
|
@click.stop="clickEdit(slotProps)"
|
|
><AIcon type="EditOutlined"
|
|
/></a>
|
|
<a
|
|
v-if="
|
|
getAccessModes(
|
|
slotProps,
|
|
).includes('read')
|
|
"
|
|
@click.stop="clickRedo(slotProps)"
|
|
><AIcon type="RedoOutlined"
|
|
/></a>
|
|
</div>
|
|
<div
|
|
v-if="propertyValue.has(slotProps.id)"
|
|
class="card-box-content-right-2"
|
|
>
|
|
<p>
|
|
{{
|
|
propertyValue.get(slotProps.id)
|
|
?.hex || ''
|
|
}}
|
|
</p>
|
|
<p>
|
|
{{
|
|
moment(
|
|
propertyValue.get(
|
|
slotProps.id,
|
|
)?.timestamp,
|
|
).format('YYYY-MM-DD HH:mm:ss')
|
|
}}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-box-content-right">
|
|
<div
|
|
v-if="getRight1(slotProps)"
|
|
class="card-box-content-right-1"
|
|
>
|
|
<span>{{
|
|
getQuantity(slotProps)
|
|
}}</span>
|
|
<span>{{ getAddress(slotProps) }}</span>
|
|
<span>{{
|
|
getScaleFactor(slotProps)
|
|
}}</span>
|
|
</div>
|
|
<div class="card-box-content-right-2">
|
|
<span>{{ getText(slotProps) }}</span>
|
|
<span>{{
|
|
getInterval(slotProps)
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</PointCardBox>
|
|
</template>
|
|
</j-pro-table>
|
|
</j-scrollbar>
|
|
<SaveModBus
|
|
v-if="visible.saveModBus"
|
|
:data="current"
|
|
@change="saveChange"
|
|
/>
|
|
<SaveOPCUA
|
|
v-if="visible.saveOPCUA"
|
|
:data="current"
|
|
@change="saveChange"
|
|
/>
|
|
<WritePoint
|
|
v-if="visible.writePoint"
|
|
:data="current"
|
|
@change="saveChange"
|
|
/>
|
|
<BatchUpdate
|
|
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 { getImage } from '@/utils/comm';
|
|
import {
|
|
queryPoint,
|
|
batchDeletePoint,
|
|
removePoint,
|
|
readPoint,
|
|
} from '@/api/data-collect/collector';
|
|
import { onlyMessage } from '@/utils/comm';
|
|
import PointCardBox from './components/PointCardBox/index.vue';
|
|
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';
|
|
import { getWebSocket } from '@/utils/websocket';
|
|
import { map } from 'rxjs/operators';
|
|
import moment from 'moment';
|
|
|
|
const props = defineProps({
|
|
data: {
|
|
type: Object,
|
|
default: {},
|
|
},
|
|
});
|
|
|
|
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 visible = reactive({
|
|
saveModBus: false,
|
|
saveOPCUA: false,
|
|
writePoint: false,
|
|
batchUpdate: false,
|
|
scan: false,
|
|
});
|
|
const current: any = ref({});
|
|
const accessModesOption = ref();
|
|
const _selectedRowKeys = ref<string[]>([]);
|
|
const checkAll = ref(false);
|
|
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,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
const accessModesMODBUS_TCP = [
|
|
{
|
|
label: '读',
|
|
value: 'read',
|
|
},
|
|
{
|
|
label: '写',
|
|
value: 'write',
|
|
},
|
|
];
|
|
|
|
const columns = [
|
|
{
|
|
title: '点位名称',
|
|
dataIndex: 'name',
|
|
key: 'name',
|
|
search: {
|
|
type: 'string',
|
|
},
|
|
},
|
|
{
|
|
title: '通讯协议',
|
|
dataIndex: 'provider',
|
|
key: 'provider',
|
|
search: {
|
|
type: 'select',
|
|
options: [
|
|
{
|
|
label: 'OPC_UA',
|
|
value: 'OPC_UA',
|
|
},
|
|
{
|
|
label: 'MODBUS_TCP',
|
|
value: 'MODBUS_TCP',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
title: '访问类型',
|
|
dataIndex: 'accessModes$in$any',
|
|
key: 'accessModes$in$any',
|
|
search: {
|
|
type: 'select',
|
|
options: accessModesOption,
|
|
},
|
|
},
|
|
{
|
|
title: '运行状态',
|
|
dataIndex: 'runningState',
|
|
key: 'runningState',
|
|
search: {
|
|
type: 'select',
|
|
options: [
|
|
{
|
|
label: '运行中',
|
|
value: 'running',
|
|
},
|
|
{
|
|
label: '部分错误',
|
|
value: 'partialError',
|
|
},
|
|
{
|
|
label: '错误',
|
|
value: 'failed',
|
|
},
|
|
{
|
|
label: '已停止',
|
|
value: 'stopped',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
title: '说明',
|
|
dataIndex: 'description',
|
|
key: 'description',
|
|
search: {
|
|
type: 'string',
|
|
},
|
|
},
|
|
];
|
|
|
|
const subRef = ref();
|
|
const propertyValue = ref(new Map());
|
|
|
|
const handlAdd = () => {
|
|
visible.saveModBus = true;
|
|
current.value = {
|
|
collectorId: collectorId.value,
|
|
provider: props.data?.provider || 'MODBUS_TCP',
|
|
};
|
|
};
|
|
const handlEdit = (data: any) => {
|
|
if (data?.provider === 'OPC_UA') {
|
|
visible.saveOPCUA = true;
|
|
} else {
|
|
visible.saveModBus = true;
|
|
}
|
|
current.value = cloneDeep(data);
|
|
};
|
|
const handlDelete = async (id: string | undefined = undefined) => {
|
|
spinning.value = true;
|
|
const res = !id
|
|
? await batchDeletePoint(_selectedRowKeys.value).catch(() => {})
|
|
: await removePoint(id as string).catch(() => {});
|
|
if (res?.status === 200) {
|
|
cancelSelect();
|
|
tableRef.value?.reload();
|
|
onlyMessage('操作成功', 'success');
|
|
}
|
|
spinning.value = false;
|
|
};
|
|
const handlBatchUpdate = () => {
|
|
if (_selectedRowKeys.value.length === 0) {
|
|
onlyMessage('请先选择', 'warning');
|
|
return;
|
|
}
|
|
const dataSet = new Set(_selectedRowKeys.value);
|
|
const dataMap = new Map();
|
|
tableRef?.value?._dataSource.forEach((i: any) => {
|
|
dataSet.has(i.id) && dataMap.set(i.id, i);
|
|
});
|
|
current.value = [...dataMap.values()];
|
|
visible.batchUpdate = true;
|
|
};
|
|
const handlScan = () => {
|
|
visible.scan = true;
|
|
current.value = cloneDeep(props.data);
|
|
};
|
|
const clickEdit = async (data: object) => {
|
|
visible.writePoint = true;
|
|
current.value = cloneDeep(data);
|
|
};
|
|
const clickRedo = async (data: any) => {
|
|
const res = await readPoint(data?.collectorId, [data?.id]);
|
|
if (res.status === 200) {
|
|
cancelSelect();
|
|
tableRef.value?.reload();
|
|
onlyMessage('操作成功', 'success');
|
|
}
|
|
};
|
|
|
|
const getQuantity = (item: Partial<Record<string, any>>) => {
|
|
const { quantity } = item.configuration?.parameter || '';
|
|
return !!quantity ? quantity + '(读取寄存器)' : '';
|
|
};
|
|
const getAddress = (item: Partial<Record<string, any>>) => {
|
|
const { address } = item.configuration?.parameter || '';
|
|
return !!address ? address + '(地址)' : '';
|
|
};
|
|
const getScaleFactor = (item: Partial<Record<string, any>>) => {
|
|
const { scaleFactor } = item.configuration?.codec?.configuration || '';
|
|
return !!scaleFactor ? scaleFactor + '(缩放因子)' : '';
|
|
};
|
|
const getRight1 = (item: Partial<Record<string, any>>) => {
|
|
return !!getQuantity(item) || getAddress(item) || getScaleFactor(item);
|
|
};
|
|
const getText = (item: Partial<Record<string, any>>) => {
|
|
return (item?.accessModes || []).map((i: any) => i?.text).join(',');
|
|
};
|
|
const getInterval = (item: Partial<Record<string, any>>) => {
|
|
const { interval } = item.configuration || '';
|
|
return !!interval ? '采集频率' + interval + 'ms' : '';
|
|
};
|
|
const getAccessModes = (item: Partial<Record<string, any>>) => {
|
|
return item?.accessModes?.map((i: any) => i?.value);
|
|
};
|
|
|
|
const saveChange = (value: object) => {
|
|
for (let key in visible) {
|
|
visible[key] = false;
|
|
}
|
|
current.value = {};
|
|
if (value) {
|
|
tableRef.value?.reload();
|
|
onlyMessage('操作成功', 'success');
|
|
}
|
|
};
|
|
|
|
const onSelectChange = (keys: string[]) => {
|
|
_selectedRowKeys.value = [...keys];
|
|
};
|
|
|
|
const cancelSelect = () => {
|
|
_selectedRowKeys.value = [];
|
|
};
|
|
|
|
const handleClick = (dt: any) => {
|
|
if (props.data?.provider !== 'OPC_UA') return;
|
|
if (_selectedRowKeys.value.includes(dt.id)) {
|
|
const _index = _selectedRowKeys.value.findIndex((i) => i === dt.id);
|
|
_selectedRowKeys.value.splice(_index, 1);
|
|
checkAll.value = false;
|
|
} else {
|
|
_selectedRowKeys.value = [..._selectedRowKeys.value, dt.id];
|
|
if (
|
|
_selectedRowKeys.value.length === tableRef.value?._dataSource.length
|
|
) {
|
|
checkAll.value = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
const subscribeProperty = (value: any) => {
|
|
const list = value.map((item: any) => item.id);
|
|
const id = `collector-${props.data?.channelId || 'channel'}-${
|
|
props.data?.id || 'point'
|
|
}-data-${list.join('-')}`;
|
|
const topic = `/collector/${props.data?.channelId || '*'}/${
|
|
props.data?.id || '*'
|
|
}/data`;
|
|
subRef.value = getWebSocket(id, topic, {
|
|
pointId: list.join(','),
|
|
})
|
|
?.pipe(map((res: any) => res.payload))
|
|
.subscribe((payload: any) => {
|
|
propertyValue.value.set(payload.pointId, payload);
|
|
});
|
|
};
|
|
|
|
const onCheckAllChange = (e: any) => {
|
|
if (e.target.checked) {
|
|
_selectedRowKeys.value = [
|
|
...tableRef.value?._dataSource.map((i: any) => i.id),
|
|
];
|
|
} else {
|
|
cancelSelect();
|
|
checkAll.value = false;
|
|
}
|
|
};
|
|
|
|
watch(
|
|
() => tableRef?.value?._dataSource,
|
|
(value) => {
|
|
if (value.length !== 0) {
|
|
subscribeProperty(value);
|
|
}
|
|
cancelSelect();
|
|
checkAll.value = false;
|
|
},
|
|
);
|
|
watch(
|
|
() => _selectedRowKeys.value,
|
|
(value) => {
|
|
if (value.length === 0) {
|
|
checkAll.value = false;
|
|
}
|
|
},
|
|
);
|
|
|
|
watch(
|
|
() => props.data,
|
|
(value) => {
|
|
if (!!value) {
|
|
accessModesOption.value =
|
|
value?.provider === 'MODBUS_TCP'
|
|
? accessModesMODBUS_TCP
|
|
: accessModesMODBUS_TCP.concat({
|
|
label: '订阅',
|
|
value: 'subscribe',
|
|
});
|
|
defaultParams.value.terms[0].terms[0].value = value.id;
|
|
tableRef?.value?.reload && tableRef?.value?.reload();
|
|
cancelSelect();
|
|
checkAll.value = false;
|
|
}
|
|
},
|
|
{ immediate: true, deep: true },
|
|
);
|
|
|
|
onUnmounted(() => {
|
|
if (subRef.value) {
|
|
subRef.value?.unsubscribe();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 搜索
|
|
* @param params
|
|
*/
|
|
const handleSearch = (e: any) => {
|
|
params.value = e;
|
|
};
|
|
</script>
|
|
<style lang="less" scoped>
|
|
.card-box {
|
|
min-width: 480px;
|
|
a {
|
|
color: #474747;
|
|
z-index: 1;
|
|
}
|
|
a:hover {
|
|
color: #315efb;
|
|
z-index: 1;
|
|
}
|
|
.card-box-title {
|
|
font-size: 18px;
|
|
color: #474747;
|
|
}
|
|
.card-box-action {
|
|
width: 50px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
font-size: 20px;
|
|
}
|
|
.card-box-content {
|
|
margin-top: 20px;
|
|
display: flex;
|
|
.card-box-content-left {
|
|
max-width: 220px;
|
|
border-right: 1px solid #e0e4e8;
|
|
height: 68px;
|
|
padding-right: 10px;
|
|
.card-box-content-left-1 {
|
|
display: flex;
|
|
justify-content: flex-start;
|
|
.card-box-content-left-1-title {
|
|
color: #000;
|
|
font-size: 20px;
|
|
opacity: 0.85;
|
|
}
|
|
}
|
|
a {
|
|
margin-left: 10px;
|
|
}
|
|
}
|
|
.card-box-content-right {
|
|
flex: 0.8;
|
|
margin-left: 20px;
|
|
.card-box-content-right-1 {
|
|
span {
|
|
margin: 0 5px 0 0;
|
|
}
|
|
margin-bottom: 10px;
|
|
}
|
|
.card-box-content-right-2 {
|
|
span {
|
|
margin: 0 5px 0 0;
|
|
padding: 3px 12px;
|
|
background: #f3f3f3;
|
|
color: #616161;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|