iot-ui-vue/src/views/DataCollect/Collector/Point/index.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>