fix: bug#21601、20621、21711、21712

* fix: bug#21601、#20621

* fix: bug#21711

* fix: bug#21712
This commit is contained in:
FuHao 2024-01-23 16:19:18 +08:00 committed by GitHub
parent 8600bbee98
commit aaa0e78343
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 338 additions and 60 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -58,4 +58,5 @@ export const protocolList = [
{ label: 'MODBUS_TCP', value: 'MODBUS_TCP', alias: 'Modbus/TCP' }, { label: 'MODBUS_TCP', value: 'MODBUS_TCP', alias: 'Modbus/TCP' },
{ label: 'COLLECTOR_GATEWAY', value: 'COLLECTOR_GATEWAY', alias: 'GATEWAY' }, { label: 'COLLECTOR_GATEWAY', value: 'COLLECTOR_GATEWAY', alias: 'GATEWAY' },
{ label: 'S7', value: 'snap7', alias: 'snap7' }, { label: 'S7', value: 'snap7', alias: 'snap7' },
{ label: 'IEC104', value: 'iec104', alias: 'IEC104' }
] ]

View File

@ -249,6 +249,8 @@ const handleOk = async () => {
params.configuration={ params.configuration={
connect : false connect : false
} }
} else {
params.configuration = {}
} }
params.circuitBreaker = { params.circuitBreaker = {
@ -316,7 +318,7 @@ const getProvidersList = async () => {
if (resp.status === 200) { if (resp.status === 200) {
const arr = resp.result const arr = resp.result
.filter( .filter(
(item: any) => ['GATEWAY', 'Modbus/TCP', 'opc-ua','snap7'].includes(item.name), (item: any) => ['GATEWAY', 'Modbus/TCP', 'opc-ua','snap7', 'IEC104'].includes(item.name),
) )
.map((it: any) => it.name); .map((it: any) => it.name);
const providers: any = protocolList.filter((item: any) => const providers: any = protocolList.filter((item: any) =>

View File

@ -67,9 +67,9 @@
<div class="card-item-content-text"> <div class="card-item-content-text">
<j-tooltip> <j-tooltip>
<template #title>{{ <template #title>{{
slotProps.provider protocolList.find(item => item.value === slotProps.provider)?.label
}}</template> }}</template>
{{ slotProps.provider }} {{ protocolList.find(item => item.value === slotProps.provider)?.label }}
</j-tooltip> </j-tooltip>
</div> </div>
</j-col> </j-col>
@ -103,9 +103,11 @@
<div class="card-item-content-text"> <div class="card-item-content-text">
说明 说明
</div> </div>
<div class="card-item-content-text"> <j-ellipsis>
<j-ellipsis>{{slotProps.description}}</j-ellipsis> <div class="explain">
{{slotProps.description}}
</div> </div>
</j-ellipsis>
</j-col> </j-col>
</j-row> </j-row>
</div> </div>
@ -161,10 +163,12 @@ const opcImage = getImage('/DataCollect/device-opcua.png');
const modbusImage = getImage('/DataCollect/device-modbus.png'); const modbusImage = getImage('/DataCollect/device-modbus.png');
const s7Image = getImage('/DataCollect/s7.png') const s7Image = getImage('/DataCollect/s7.png')
const gatewayImage = getImage('/DataCollect/gateway.png') const gatewayImage = getImage('/DataCollect/gateway.png')
const iecImage = getImage('/DataCollect/IEC104.png')
const ImageMap = new Map() const ImageMap = new Map()
ImageMap.set('OPC_UA',opcImage) ImageMap.set('OPC_UA',opcImage)
ImageMap.set('MODBUS_TCP',modbusImage) ImageMap.set('MODBUS_TCP',modbusImage)
ImageMap.set('snap7',s7Image) ImageMap.set('snap7',s7Image)
ImageMap.set('iec104',iecImage)
ImageMap.set('COLLECTOR_GATEWAY',gatewayImage) ImageMap.set('COLLECTOR_GATEWAY',gatewayImage)
@ -367,6 +371,9 @@ const handleSearch = (e: any) => {
text-overflow: ellipsis; // text-overflow: ellipsis; //
white-space: nowrap; // white-space: nowrap; //
} }
.explain {
margin-top: 10px;
}
} }
.details-text { .details-text {
font-weight: 700; font-weight: 700;

View File

@ -0,0 +1,263 @@
<template>
<j-modal :title="data.id ? '编辑' : '新增'" visible @cancel="handleCancel">
<j-form :model="formData" layout="vertical" ref="formRef">
<j-form-item
label="点位名称"
name="name"
:rules="rules.name"
>
<j-input
placeholder="请输入点位名称"
v-model:value="formData.name"
:maxlength="64"
/>
</j-form-item>
<j-form-item
label="类型标识"
:name="['configuration', 'typeIdentifierName']"
:rules="rules.configuration.typeIdentifierName"
>
<j-select
v-model:value="formData.configuration.typeIdentifierName"
:options="dataTypeList"
placeholder="请选择类型标识"
allowClear
show-search
/>
</j-form-item>
<j-form-item
label="地址"
:name="['configuration', 'pointAddress']"
:rules="rules.configuration.pointAddress"
>
<j-input
v-model:value="formData.configuration.pointAddress"
placeholder="请输入地址"
:min="1"
:max="65535"
:precision="0"
/>
</j-form-item>
<j-form-item
label="访问类型"
name="accessModes"
:rules="rules.accessModes"
>
<j-card-select
multiple
:showImage="false"
v-model:value="formData.accessModes"
:options="[
{ label: '读', value: 'read' },
{ label: '写', value: 'write' },
{ label: '订阅', value: 'subscribe' },
]"
:column="3"
/>
</j-form-item>
<j-form-item :name="['configuration', 'terms']" :rules="[{
validator: Area,
trigger: 'change',
}]">
<template #label>
<j-space>
<span>点位死区</span><span class="explain">点位死区范围内的异常数据将被过滤请勿配置非数值类型</span>
</j-space>
</template>
<DeathArea v-model:value="formData.configuration.terms" />
</j-form-item>
<j-form-item label="轮询任务" :name="['configuration', 'interval']" :rules="rules.configuration.interval">
<p>
采集频率<span
style="
margin-left: 5px;
color: #9d9ea1;
font-size: 12px;
"
>采集频率为0时不执行轮询任务</span
>
</p>
<j-input-number
style="width: 100%"
placeholder="请输入采集频率"
v-model:value="formData.configuration.interval"
addon-after="ms"
:max="2147483648"
:min="0"
/>
</j-form-item>
<j-form-item name="features">
<j-checkbox-group v-model:value="formData.features">
<j-checkbox value="changedOnly"
>只推送变化的数据</j-checkbox
>
</j-checkbox-group>
</j-form-item>
<j-form-item label="说明" name="description">
<j-textarea
placeholder="请输入说明"
v-model:value="formData.description"
:maxlength="200"
:rows="3"
showCount
/>
</j-form-item>
</j-form>
<template #footer>
<j-button @click="handleCancel">取消</j-button>
<PermissionButton
key="submit"
type="primary"
:loading="loading"
@click="handleOk"
style="margin-left: 8px"
:hasPermission="`DataCollect/Collector:${
data.id ? 'update' : 'add'
}`"
>
确认
</PermissionButton>
</template>
</j-modal>
</template>
<script setup lang="ts">
import {
savePoint,
updatePoint,
} from '@/api/data-collect/collector';
import { randomString } from '@/utils/utils';
import DeathArea from './DeathArea.vue';
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['change']);
const loading = ref(false);
const formRef = ref()
const dataTypeList = ref([
{ label: '单点开关量', value: 'onePointTelecontrol' },
{ label: '双点开关量', value: 'twoPointTelecontrol' },
{ label: '归一化值', value: 'prefabActivationOneParameter' },
]);
const formData = ref({
name: props.data.name,
configuration: props.data.configuration || {
typeIdentifierName: undefined,
pointAddress: '',
interval: 3000,
},
accessModes: [],
features: [],
description: props.data.description || '',
});
const rules = {
name: [
{
required: true,
message: '请输入名称',
trigger: 'blur',
},
],
configuration: {
typeIdentifierName: [
{
required: true,
message: '请输入类型标识',
trigger: 'change',
},
],
pointAddress: [
{
required: true,
message: '请输入地址',
trigger: 'blur',
},
],
interval: [
{
required: true,
message: '请输入采集频率',
trigger: 'change',
},
],
},
accessModes: [
{
required: true,
message: '请选择访问类型',
trigger: 'change',
},
],
};
const Area = (_: any, value: any): Promise<any> =>
new Promise(async (resolve, reject) => {
if (!value) {
return resolve('')
}
if (value?.length === 0) {
return resolve('')
} else if (value?.length === 1) {
return value[0].value && value[0].termType ? resolve('') : reject('请配置点位死区');
} else {
if (value?.[0].column === 'currentValue') {
// value.forEach((item:any) => {
// if(item.termType && item.value){
// return resolve('')
// }else{
// return reject('')
// }
// });
const pass = value.every((item: any) => item.termType && item.value)
return pass ? resolve('') : reject('请配置点位死区')
} else {
value.forEach((item: any) => {
if (item.column === `this['currentValue'] - this['lastValue']*init/100`) {
return reject('请配置点位死区')
} else {
return resolve('')
}
});
}
}
});
const handleOk = async () => {
const res: any = await formRef.value?.validate();
const params = {
...res,
configuration: {
...res.configuration,
},
inheritBreaker: true,
pointKey: props.data.pointKey || randomString(9),
provider: props.data.provider,
collectorId: props.data.collectorId,
accessModes: res?.accessModes.filter((item: any) => item)
}
loading.value = true;
const response = !props.data.id
? await savePoint(params).catch(() => { })
: await updatePoint(props.data.id, { ...props.data, ...params }).catch(() => { });
emit('change', response?.status === 200);
loading.value = false;
};
const handleCancel = () => {
emit('change', false);
};
onMounted(() => {
formData.value.features = props.data.features?.map((item: any) => item.value)
if (props.data.accessModes?.length !== 0) {
formData.value.accessModes = props.data.accessModes?.map((item: any) => item.value)
}
})
</script>

View File

@ -97,20 +97,14 @@
<p> <p>
采集频率<span style="margin-left: 5px; color: #9d9ea1; font-size: 12px">采集频率为0时不执行轮询任务</span> 采集频率<span style="margin-left: 5px; color: #9d9ea1; font-size: 12px">采集频率为0时不执行轮询任务</span>
</p> </p>
<j-radio-group v-model:value="form.configuration.interval"> <j-input-number
<j-space> style="width: 100%"
<j-radio-button :value="3000">3000ms</j-radio-button> placeholder="请输入采集频率"
<j-radio-button :value="6000">6000ms</j-radio-button> v-model:value="form.configuration.interval"
<j-radio-button :value="9000">9000ms</j-radio-button> addon-after="ms"
<j-radio-button :value="Number(form.configuration.interval)" @click="intervalRef.visible = true"> :max="2147483648"
{{ :min="0"
![3000, 6000, 9000].includes(form.configuration.interval) />
? form.configuration.interval + 'ms'
: '自定义'
}}
</j-radio-button>
</j-space>
</j-radio-group>
</j-form-item> </j-form-item>
<j-form-item name="features"> <j-form-item name="features">
<j-checkbox-group v-model:value="form.features"> <j-checkbox-group v-model:value="form.features">
@ -121,17 +115,6 @@
<j-textarea placeholder="请输入说明" v-model:value="form.description" :maxlength="200" :rows="3" showCount /> <j-textarea placeholder="请输入说明" v-model:value="form.description" :maxlength="200" :rows="3" showCount />
</j-form-item> </j-form-item>
</j-form> </j-form>
<j-modal title="采集频率" :visible="intervalRef.visible" @cancel="handleCancelInterval" @ok="handleInterval">
<j-form ref="formRef2" name="virtual-form" layout="vertical" :model="intervalRef">
<j-form-item label="采集频率" name="interval" :rules="[
{ required: true, message: '请输入采集频率' },
]">
<j-input-number type="tel" addonAfter="ms" v-model:value="intervalRef.interval" placeholder="请输入采集频率"
:controls="false" :precision="0" :maxlength="64" />
</j-form-item>
</j-form>
</j-modal>
<template #footer> <template #footer>
<j-button key="back" @click="handleCancel">取消</j-button> <j-button key="back" @click="handleCancel">取消</j-button>
<PermissionButton key="submit" type="primary" :loading="loading" @click="handleOk" style="margin-left: 8px" <PermissionButton key="submit" type="primary" :loading="loading" @click="handleOk" style="margin-left: 8px"
@ -181,8 +164,7 @@ const dataAreaFilter = {
const emit = defineEmits(['change']); const emit = defineEmits(['change']);
const loading = ref(false); const loading = ref(false);
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const formRef2 = ref<FormInstance>(); const deviceType = ref<string>(props. data.deviceType);
const deviceType = ref<string>(props.data.deviceType);
const dataTypesList = ref<any[]>([]); const dataTypesList = ref<any[]>([]);
const daveAreaList = ref<any>([]); const daveAreaList = ref<any>([]);
@ -203,10 +185,6 @@ const form = ref<any>({
// inheritBreaker: true, // inheritBreaker: true,
// pointKey: randomString(9) // pointKey: randomString(9)
}); });
const intervalRef = reactive({
visible: false,
interval: 3000,
})
/**选择S7点位数据类型 */ /**选择S7点位数据类型 */
@ -259,19 +237,6 @@ const dataAreaFilterList = computed(() => {
return result; return result;
}); });
const handleInterval = async () => {
const res = await formRef2.value?.validate()
if (res) {
form.value.configuration.interval = res.interval
intervalRef.visible = false
}
};
const handleCancelInterval = () => {
intervalRef.visible = false
intervalRef.interval = 3000
}
const handleOk = async () => { const handleOk = async () => {
const res: any = await formRef.value?.validate(); const res: any = await formRef.value?.validate();

View File

@ -20,7 +20,7 @@
<template #headerTitle> <template #headerTitle>
<j-space> <j-space>
<PermissionButton <PermissionButton
v-if="['MODBUS_TCP', 'COLLECTOR_GATEWAY','snap7'].includes(data?.provider)" v-if="['MODBUS_TCP', 'COLLECTOR_GATEWAY','snap7', 'iec104'].includes(data?.provider)"
type="primary" type="primary"
@click="handlAdd" @click="handlAdd"
hasPermission="DataCollect/Collector:add" hasPermission="DataCollect/Collector:add"
@ -31,7 +31,7 @@
新增点位 新增点位
</PermissionButton> </PermissionButton>
<PermissionButton <PermissionButton
v-if="['MODBUS_TCP', 'COLLECTOR_GATEWAY','snap7'].includes(data?.provider)" v-if="['MODBUS_TCP', 'COLLECTOR_GATEWAY','snap7', 'iec104'].includes(data?.provider)"
type="primary" type="primary"
@click="handleImport" @click="handleImport"
hasPermission="DataCollect/Collector:add" hasPermission="DataCollect/Collector:add"
@ -324,6 +324,7 @@
@change="saveChange" @change="saveChange"
/> />
<SaveS7 v-if="visible.saveS7" :data="current" @change="saveChange"/> <SaveS7 v-if="visible.saveS7" :data="current" @change="saveChange"/>
<SaveIEC104 v-if="visible.saveIEC104" :data="current" @change="saveChange"/>
<Scan v-if="visible.scan" :data="current" @change="saveChange" /> <Scan v-if="visible.scan" :data="current" @change="saveChange" />
<Import v-if="visible.import" :data="current" @close-import="closeImport"/> <Import v-if="visible.import" :data="current" @close-import="closeImport"/>
</j-spin> </j-spin>
@ -352,6 +353,7 @@ import { map } from 'rxjs/operators';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { responsiveArray } from 'ant-design-vue/lib/_util/responsiveObserve'; import { responsiveArray } from 'ant-design-vue/lib/_util/responsiveObserve';
import SaveS7 from './Save/SaveS7.vue'; import SaveS7 from './Save/SaveS7.vue';
import SaveIEC104 from './Save/SaveIEC104.vue';
import Import from './components/Import/index.vue' import Import from './components/Import/index.vue'
const props = defineProps({ const props = defineProps({
data: { data: {
@ -366,10 +368,12 @@ const opcImage = getImage('/DataCollect/device-opcua.png');
const modbusImage = getImage('/DataCollect/device-modbus.png'); const modbusImage = getImage('/DataCollect/device-modbus.png');
const s7Image = getImage('/DataCollect/s7.png') const s7Image = getImage('/DataCollect/s7.png')
const gatewayImage = getImage('/DataCollect/gateway.png') const gatewayImage = getImage('/DataCollect/gateway.png')
const iecImage = getImage('/DataCollect/IEC104.png')
const ImageMap = new Map() const ImageMap = new Map()
ImageMap.set('OPC_UA',opcImage) ImageMap.set('OPC_UA',opcImage)
ImageMap.set('MODBUS_TCP',modbusImage) ImageMap.set('MODBUS_TCP',modbusImage)
ImageMap.set('snap7',s7Image) ImageMap.set('snap7',s7Image)
ImageMap.set('iec104',iecImage)
ImageMap.set('COLLECTOR_GATEWAY',gatewayImage) ImageMap.set('COLLECTOR_GATEWAY',gatewayImage)
@ -380,7 +384,8 @@ const visible = reactive({
batchUpdate: false, batchUpdate: false,
scan: false, scan: false,
saveS7:false, saveS7:false,
import:false import:false,
saveIEC104: false
}); });
const current: any = ref({}); const current: any = ref({});
const accessModesOption = ref(); const accessModesOption = ref();
@ -500,7 +505,13 @@ const handlAdd = () => {
provider: props.data?.provider, provider: props.data?.provider,
deviceType:props.data?.configuration.type, deviceType:props.data?.configuration.type,
} }
}else{ }else if (props.data?.provider === 'iec104'){
visible.saveIEC104 = true;
current.value = {
collectorId: props.data?.id,
provider: props.data?.provider,
}
} else {
visible.saveModBus = true; visible.saveModBus = true;
current.value = { current.value = {
collectorId: props.data?.id, collectorId: props.data?.id,
@ -514,7 +525,9 @@ const handlEdit = (data: any) => {
visible.saveOPCUA = true; visible.saveOPCUA = true;
} else if(data?.provider === 'snap7'){ } else if(data?.provider === 'snap7'){
visible.saveS7 = true visible.saveS7 = true
}else{ } else if(data?.provider === 'iec104') {
visible.saveIEC104 = true
} else {
visible.saveModBus = true; visible.saveModBus = true;
} }
current.value = cloneDeep({ current.value = cloneDeep({

View File

@ -67,6 +67,20 @@
<j-radio-button :value="true">串行</j-radio-button> <j-radio-button :value="true">串行</j-radio-button>
</j-radio-group> </j-radio-group>
</j-form-item> </j-form-item>
<template v-if="provider === 'iec104'">
<j-form-item label="从机地址" :name="['configuration', 'host']" :rules="LeftTreeRules.host">
<j-input v-model:value="formData.configuration.host" autocomplete="off" placeholder="请输入" :disabled="false"/>
</j-form-item>
<j-form-item label="从机端口" :name="['configuration', 'port']" >
<j-input-number style="width: 100%" v-model:value="formData.configuration.port" :precision="0" autocomplete="off" placeholder="请输入从机端口"/>
</j-form-item>
<j-form-item label="分组地址" :name="['configuration', 'terminnalAddress']" :rules="LeftTreeRules.terminnalAddress">
<j-input-number style="width: 100%" :min="0" :max="65535" :precision="0" v-model:value="formData.configuration.terminnalAddress" autocomplete="off" placeholder="请输入分组地址"></j-input-number>
</j-form-item>
<j-form-item label="确认帧数量" :name="['configuration', 'frameAmountMax']" :rules="LeftTreeRules.frameAmountMax">
<j-input-number style="width: 100%" v-model:value="formData.configuration.frameAmountMax" placeholder="请输入确认帧数量" :min="1" :maxlength="16" :precision="0"></j-input-number>
</j-form-item>
</template>
<j-form-item <j-form-item
v-if="provider === 'COLLECTOR_GATEWAY'" v-if="provider === 'COLLECTOR_GATEWAY'"
label="通讯协议" label="通讯协议"

View File

@ -271,6 +271,8 @@ export const LeftTreeRules = {
timeout: [{ required: true, trigger: 'blur', validator: validator4 }], timeout: [{ required: true, trigger: 'blur', validator: validator4 }],
type: [{required: true, trigger: 'change', message: '请选择型号'}], type: [{required: true, trigger: 'change', message: '请选择型号'}],
serializable: [{required: true, trigger: 'change', message: '请选择型号'}], serializable: [{required: true, trigger: 'change', message: '请选择型号'}],
terminnalAddress: [{required: true, trigger: 'blur', message: '请输入分组地址'}],
frameAmountMax: [{required: true, message: '请输入确认帧数量', trigger: 'blur'}],
}; };
export const FormTableColumns = [ export const FormTableColumns = [

View File

@ -6,7 +6,7 @@
</div> </div>
<div class="right"> <div class="right">
<j-spin :spinning="spinning"> <j-spin :spinning="spinning">
<Point v-if="data || data === undefined" :data="data" /> <Point v-if="data" :data="data" />
<j-empty style="margin-top: 20%" v-else /> <j-empty style="margin-top: 20%" v-else />
</j-spin> </j-spin>
</div> </div>

View File

@ -113,10 +113,11 @@
<j-scrollbar> <j-scrollbar>
<div class="title">功能说明</div> <div class="title">功能说明</div>
<p> <p>
该功能用于将插件中的 该功能用于将插件/协议包中的
<b>物模型属性标识</b> <b>物模型属性标识</b>
<b>平台物模型属性标识</b <b>平台物模型属性标识</b
>进行映射,当两方属性标识不一致时可在当前页面直接修改映射管理系统将以映射后的物模型属性进行数据处理 >进行映射,当两方属性标识不一致时可在当前页面直接修改映射管理系统将以
<b>映射后</b><b>物模型属性</b>进行数据处理
</p> </p>
<p> <p>
未完成映射的属性标识目标属性列数据为空代表该属性值来源以在平台配置的来源为准 未完成映射的属性标识目标属性列数据为空代表该属性值来源以在平台配置的来源为准

View File

@ -315,7 +315,9 @@ const getProtocol = async () => {
]; ];
} }
} }
if (productStore.current?.accessProvider === 'plugin_gateway') { //
const protocol = res.result?.transports.find(item => item.id === productStore.current.transportProtocol);
if(protocol?.features.find(item => item.id === 'diffMetadataSameProduct')){
list.value.push({ key: 'MetadataMap', tab: '物模型映射'}) list.value.push({ key: 'MetadataMap', tab: '物模型映射'})
} }
} }

View File

@ -10,7 +10,7 @@
:label="item?.name" :label="item?.name"
v-for="(item, index) in variableDefinitions" v-for="(item, index) in variableDefinitions"
:key="item.id" :key="item.id"
:required="getType(item) !== 'file' ? true : false" :required="!['file', 'user', 'org', 'tag'].includes(getType(item)) ? true : false"
:rules="[ :rules="[
{ {
validator: (_rule, value) => checkValue(_rule, value, item), validator: (_rule, value) => checkValue(_rule, value, item),
@ -62,6 +62,7 @@ import Tag from './variableItem/Tag.vue';
import InputFile from './variableItem/InputFile.vue'; import InputFile from './variableItem/InputFile.vue';
import User from './variableItem/User.vue'; import User from './variableItem/User.vue';
import { PropType } from 'vue'; import { PropType } from 'vue';
import { onlyMessage } from '@/utils/comm';
const props = defineProps({ const props = defineProps({
variableDefinitions: { variableDefinitions: {
@ -209,6 +210,13 @@ const onChange = (val: any, type: any, index: number, options?: string) => {
const onSave = () => const onSave = () =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const pass = props.variableDefinitions.filter(item => ['user', 'org', 'tag'].includes(getType(item))).some(item => {
return modelRef[item.id]
})
if(!pass) {
onlyMessage('收信人,收信人部门,收信人标签至少填写一个', 'warning')
return reject(false)
}
formRef.value?.validate().then((_data: any) => { formRef.value?.validate().then((_data: any) => {
resolve(_data); resolve(_data);
}).catch(() => { }).catch(() => {