iot-ui-vue/src/views/DataCollect/Collector/Point/index.vue

929 lines
34 KiB
Vue

<template>
<j-spin :spinning="spinning">
<pro-search
:columns="columns"
target="search-point"
@search="handleSearch"
/>
<FullPage>
<j-scrollbar height="680">
<j-pro-table
ref="tableRef"
model="CARD"
:columns="columns"
:gridColumn="2"
:gridColumns="[1, 2]"
:request="queryPoint"
:defaultParams="defaultParams"
:rowSelection="
isCheck
? {
selectedRowKeys: _selectedRowKeys,
onSelectNone: () => (_selectedRowKeys = []),
}
: false
"
:params="params"
>
<template #headerTitle>
<j-space>
<PermissionButton
v-if="
[
'MODBUS_TCP',
'COLLECTOR_GATEWAY',
'snap7',
'iec104',
].includes(data?.provider)
"
type="primary"
@click="handleAdd"
hasPermission="DataCollect/Collector:add"
>
<template #icon
><AIcon type="PlusOutlined"
/></template>
新增点位
</PermissionButton>
<PermissionButton
v-if="
data?.provider === 'OPC_UA' ||
data?.provider === 'BACNetIp'
"
type="primary"
@click="handleScan"
hasPermission="DataCollect/Collector:add"
>
<template #icon
><AIcon type="PlusOutlined"
/></template>
扫描
</PermissionButton>
<PermissionButton
v-if="data?.id && data.id !== '*'"
type="primary"
@click="handleImport"
hasPermission="DataCollect/Collector:add"
>
批量导入
</PermissionButton>
<PermissionButton
v-if="data?.id && data.id !== '*'"
type="primary"
@click="handleExport"
hasPermission="DataCollect/Collector:add"
>
数据导出
</PermissionButton>
<BatchDropdown
v-if="data?.id && data.id !== '*'"
ref="batchRef"
v-model:isCheck="isCheck"
:actions="batchActions"
@change="onCheckChange"
/>
</j-space>
<div v-if="isCheck" 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="slotProps?.runningState?.value"
:statusText="slotProps?.runningState?.text"
:statusNames="
Object.fromEntries(colorMap.entries())
"
>
<template #title>
<slot name="title">
<Ellipsis style="width: calc(100% - 10px)">
<div class="card-box-title">
{{ slotProps.name }}
</div>
</Ellipsis>
</slot>
</template>
<template #action>
<div class="card-box-action">
<PermissionButton
type="text"
:tooltip="{
title: '删除',
}"
hasPermission="DataCollect/Collector:delete"
:popConfirm="{
title: `确认删除?`,
onConfirm: () =>
handleDelete(slotProps.id),
}"
>
<a
style="
font-size: 20px;
margin-top: -10px;
"
><AIcon type="DeleteOutlined"
/></a>
</PermissionButton>
<PermissionButton
type="text"
@click="handleEdit(slotProps)"
hasPermission="DataCollect/Collector:update"
>
<a style="font-size: 20px"
><AIcon type="FormOutlined"
/></a>
</PermissionButton>
</div>
</template>
<template #img>
<img :src="ImageMap.get(slotProps.provider)" />
</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"
>
{{
getParseData(slotProps)
}}
</j-ellipsis>
</div>
<div
class="ard-box-content-left-1-title"
v-else
>
<j-ellipsis
style="max-width: 150px"
>
{{
getReadParseData(
slotProps,
)
}}
</j-ellipsis>
</div>
<a
v-if="
getAccessModes(
slotProps,
).includes('write')
"
@click.stop="
clickEdit(slotProps)
"
><AIcon type="EditOutlined"
/></a>
<a
v-if="
getAccessModes(
slotProps,
).includes('read')
"
@click.stop="
clickRead(slotProps)
"
><AIcon type="RedoOutlined"
/></a>
</div>
<div
v-if="
propertyValue.has(slotProps.id)
"
class="card-box-content-right-2"
>
<Ellipsis>
{{
propertyValue.get(
slotProps.id,
)?.hex || ''
}}
</Ellipsis>
<p>
{{
dayjs(
propertyValue.get(
slotProps.id,
)?.timestamp,
).format(
'YYYY-MM-DD HH:mm:ss',
)
}}
</p>
</div>
</div>
<div class="card-box-content-right">
<Ellipsis
style="
width: calc(100% - 10px);
margin-bottom: 10px;
"
>
<div
v-if="getRight1(slotProps)"
class="card-box-content-right-1"
>
<span>
{{ getQuantity(slotProps) }}
</span>
<span>
{{ getAddress(slotProps) }}
</span>
<span>
{{
getScaleFactor(
slotProps,
)
}}
</span>
</div>
</Ellipsis>
<Ellipsis
style="
width: calc(100% - 10px);
margin-bottom: 10px;
"
>
<div
class="card-box-content-right-2"
>
<span>{{
getText(slotProps)
}}</span>
<span
v-if="
getInterval(slotProps)
"
>{{
getInterval(slotProps)
}}</span
>
</div>
</Ellipsis>
</div>
</div>
</template>
</PointCardBox>
</template>
</j-pro-table>
</j-scrollbar>
</FullPage>
<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"
:provider="data.provider"
@change="saveChange"
/>
<SaveS7 v-if="visible.saveS7" :data="current" @change="saveChange" />
<SaveIEC104
v-if="visible.saveIEC104"
:data="current"
@change="saveChange"
/>
<SaveBACNet
v-if="visible.saveBACNet"
:data="current"
@change="saveChange"
/>
<Scan v-if="visible.scan" :data="current" @change="saveChange" />
<ScanBacnet
v-if="visible.scanBacnet"
:data="current"
@change="saveChange"
/>
<Import
v-if="visible.import"
:data="current"
@close-import="closeImport"
/>
</j-spin>
</template>
<script lang="ts" setup name="PointPage">
import { getImage } from '@/utils/comm';
import {
queryPoint,
batchDeletePoint,
removePoint,
readPoint,
getProviders,
getStates,
exportPoint,
} 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 ScanBacnet from './ScanBacnet/index.vue';
import SaveBACNet from './Save/SaveBACNet.vue';
import { colorMap } from '../data';
import { cloneDeep, isBoolean, isNumber, throttle } from 'lodash-es';
import { getWebSocket } from '@/utils/websocket';
import { map } from 'rxjs/operators';
import dayjs from 'dayjs';
import SaveS7 from './Save/SaveS7.vue';
import SaveIEC104 from './Save/SaveIEC104.vue';
import Import from './components/Import/index.vue';
import { downloadFileByUrl } from '@/utils/utils';
import BatchDropdown from '@/components/BatchDropdown/index.vue';
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 s7Image = getImage('/DataCollect/s7.png');
const gatewayImage = getImage('/DataCollect/gateway.png');
const iecImage = getImage('/DataCollect/IEC104.png');
const ImageMap = new Map();
ImageMap.set('OPC_UA', opcImage);
ImageMap.set('MODBUS_TCP', modbusImage);
ImageMap.set('snap7', s7Image);
ImageMap.set('iec104', iecImage);
ImageMap.set('COLLECTOR_GATEWAY', gatewayImage);
const visible = reactive({
saveModBus: false,
saveOPCUA: false,
writePoint: false,
batchUpdate: false,
scan: false,
saveS7: false,
import: false,
saveIEC104: false,
scanBacnet: false,
saveBACNet: false,
});
const current: any = ref({});
const accessModesOption = ref();
const _selectedRowKeys = ref<string[]>([]);
const checkAll = ref(false);
const spinning = ref(false);
const ReadIdMap = new Map();
const isCheck = ref(false);
const batchRef = ref();
const defaultParams = ref({
sorts: [{ name: 'id', order: 'desc' }],
terms: [
{
terms: [
{
column: 'collectorId',
termType: 'eq',
value: !props.data?.id
? 'undefined'
: props.data.id === '*'
? undefined
: props.data.id,
},
],
},
],
});
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: async () => {
const resp: any = await getProviders();
if (resp.success) {
return resp.result.map((item: any) => ({
label: item.name,
value: item.id,
}));
} else {
return [];
}
},
},
},
{
title: '访问类型',
dataIndex: 'accessModes$in$any',
key: 'accessModes$in$any',
search: {
type: 'select',
options: accessModesOption,
},
},
{
title: '运行状态',
dataIndex: 'runningState',
key: 'runningState',
search: {
type: 'select',
options: async () => {
const resq: any = await getStates();
if (resq.status === 200) {
return resq.result.map((item: any) => ({
label: item.text,
value: item.value,
}));
} else {
return [];
}
},
},
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
search: {
type: 'string',
},
},
];
const subRef = ref();
const propertyValue = ref(new Map());
const batchActions = ref<any>([]);
const handleAdd = () => {
if (props.data?.provider === 'snap7') {
visible.saveS7 = true;
current.value = {
collectorId: props.data?.id,
provider: props.data?.provider,
deviceType: props.data?.configuration.type,
};
} else if (props.data?.provider === 'iec104') {
visible.saveIEC104 = true;
current.value = {
collectorId: props.data?.id,
provider: props.data?.provider,
};
} else {
visible.saveModBus = true;
current.value = {
collectorId: props.data?.id,
provider: props.data?.provider || 'MODBUS_TCP',
};
}
};
const handleEdit = (data: any) => {
if (data?.provider === 'OPC_UA') {
visible.saveOPCUA = true;
} else if (data?.provider === 'snap7') {
visible.saveS7 = true;
} else if (data?.provider === 'iec104') {
visible.saveIEC104 = true;
} else if (data?.provider === 'BACNetIp') {
visible.saveBACNet = true;
} else {
visible.saveModBus = true;
}
current.value = cloneDeep({
...data,
deviceType:
props.data?.configuration?.type ||
props.data?.configuration?.valueType,
});
};
const handleDelete = (id: string | undefined = undefined) => {
spinning.value = true;
const response = !id
? batchDeletePoint(_selectedRowKeys.value).catch(() => {})
: removePoint(id as string).catch(() => {});
response.then((res) => {
if (res?.status === 200) {
cancelSelect();
tableRef.value?.reload();
onlyMessage('操作成功', 'success');
}
spinning.value = false;
});
return response;
};
const onCheckChange = () => {
_selectedRowKeys.value = [];
checkAll.value = false;
};
const handleBatchDelete = () => {
if (!_selectedRowKeys.value.length) {
onlyMessage('至少选择一条数据', 'error');
return
}
handleDelete();
};
const handleBatchUpdate = () => {
if (!_selectedRowKeys.value.length) {
onlyMessage('至少选择一条数据', 'error');
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 handleScan = () => {
if (props.data?.provider === 'OPC_UA') {
visible.scan = true;
} else if (props.data?.provider === 'BACNetIp') {
visible.scanBacnet = true;
}
current.value = cloneDeep(props.data);
};
const handleImport = () => {
visible.import = true;
current.value = cloneDeep(props.data);
};
const handleExport = async () => {
const params =
props?.data?.provider === 'COLLECTOR_GATEWAY'
? props?.data?.configuration?.collectorProvider
: props?.data?.provider;
const res: any = await exportPoint(props.data.collectorId, params);
if (res) {
const blob = new Blob([res], { type: 'xlsx' });
const url = URL.createObjectURL(blob);
downloadFileByUrl(url, `${props?.data?.channelName}点位数据`, 'xlsx');
}
};
const clickEdit = async (data: object) => {
visible.writePoint = true;
current.value = cloneDeep(data);
};
// ReadIdMap
const clickRead = async (data: any) => {
const res: any = await readPoint(data?.collectorId, [data?.id]);
if (res.status === 200) {
const readData: any = res.result[0];
const _data = ReadIdMap.get(data?.id);
ReadIdMap.set(data?.id, { ..._data, ...readData });
console.log('====', ReadIdMap.get(data.id));
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 === 0 ? 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 getParseData = (item: any) => {
const { parseData, dataType } = propertyValue.value.get(item.id);
const data = isNumber(parseData) ? parseData || 0 : parseData;
const _data = `${data}(${dataType}) `;
return _data;
};
const getReadParseData = (item: any) => {
let _data = '--';
if (ReadIdMap.has(item.id)) {
const { parseData, dataType } = ReadIdMap.get(item.id);
if (isBoolean(parseData)) {
_data = `${parseData}(${dataType || '-'}) `;
} else {
_data = !!parseData ? `${parseData}(${dataType || '-'}) ` : '--';
}
}
return _data;
};
const saveChange = (value: object) => {
for (let key in visible) {
visible[key] = false;
}
current.value = {};
if (value) {
tableRef.value?.reload();
onlyMessage('操作成功', 'success');
}
};
const cancelSelect = () => {
_selectedRowKeys.value = [];
};
const handleClick = (dt: any) => {
if (!isCheck.value) {
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 handleSubscribeValue = throttle((payload: any) => {
propertyValue.value.set(payload.pointId, payload);
});
const subscribeProperty = (value: any) => {
const list = value.map((item: any) => item.id);
const id = `collector-${props.data?.channelId || 'channel'}-${
props.data?.id || (props.data && props.data.id === '*')
? 'point'
: props.data?.id
}-data-${list.join('-')}`;
const topic = `/collector/${props.data?.channelId || '*'}/${
props.data?.id || (props.data && props.data.id === '*')
? '*'
: props.data?.id
}/data`;
subRef.value = getWebSocket(id, topic, {
pointId: list.join(','),
})
?.pipe(map((res: any) => res.payload))
.subscribe((payload: any) => {
//防止刷新过快
handleSubscribeValue(payload);
});
};
const onCheckAllChange = (e: any) => {
if (e.target.checked) {
_selectedRowKeys.value = [
...tableRef.value?._dataSource.map((i: any) => i.id),
];
} else {
cancelSelect();
checkAll.value = false;
}
};
const closeImport = () => {
visible.import = false;
tableRef.value.reload();
};
watch(
() => tableRef?.value?._dataSource,
(value) => {
subRef.value?.unsubscribe();
if (value.length !== 0) {
setTimeout(() => {
subscribeProperty(value);
value.forEach((item: any) => {
item?.accessModes?.forEach((i: any) => {
if (i?.value === 'read') {
ReadIdMap.set(item.id, item);
}
});
});
}, 100);
}
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
? 'undefined'
: value.id === '*'
? undefined
: value.id;
tableRef?.value?.reload && tableRef?.value?.reload();
// cancelSelect();
checkAll.value = false;
batchRef.value?.reload();
batchActions.value =
props?.data?.provider === 'OPC_UA' ||
props?.data?.provider === 'BACNetIp'
? [
{
key: 'update',
text: '批量编辑',
permission: 'DataCollect/Collector:update',
icon: 'FormOutlined',
selected: {
onClick: handleBatchUpdate,
},
},
{
key: 'delete',
text: '批量删除',
danger: true,
permission: 'DataCollect/Collector:delete',
icon: 'DeleteOutlined',
selected: {
popConfirm: {
title: '确认删除?',
onConfirm: handleBatchDelete,
},
},
},
]
: [
{
key: 'delete',
text: '批量删除',
danger: true,
permission: 'DataCollect/Collector:delete',
icon: 'DeleteOutlined',
selected: {
popConfirm: {
title: '确认删除?',
onConfirm: handleBatchDelete,
},
},
},
];
}
},
{ immediate: true, deep: true },
);
onUnmounted(() => {
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: 90px;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: -10px;
}
.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;
}
}
.card-box-content-right-2 {
span {
margin: 0 5px 0 0;
padding: 3px 12px;
background: #f3f3f3;
color: #616161;
}
}
}
}
}
</style>