Merge pull request #59 from jetlinks/dev-hub

feat: 采集器 MODBUS、OPCUA表单,读写以及批量操作
feat: 采集器 OPCUA扫描全部功能
style: 调整TitleComponent组件样式
This commit is contained in:
胡彪 2023-03-12 15:53:05 +08:00 committed by GitHub
commit 39694d7b04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1708 additions and 363 deletions

View File

@ -21,3 +21,34 @@ export const _validateField = (id: string, data?: any) =>
server.get(`/data-collect/point/${id}/_validate`, data); server.get(`/data-collect/point/${id}/_validate`, data);
export const queryCodecProvider = () => server.get(`/things/collector/codecs`); export const queryCodecProvider = () => server.get(`/things/collector/codecs`);
export const updatePoint = (id: string, data: any) =>
server.put(`/data-collect/point/${id}`, data);
export const savePointBatch = (data: any) =>
server.patch(`/data-collect/point`, data);
export const savePoint = (data: any) =>
server.post(`/data-collect/point`, data);
export const batchDeletePoint = (data: any) =>
server.post(`/data-collect/point/batch/_delete`, data);
export const removePoint = (id: string) =>
server.remove(`/data-collect/point/${id}`);
export const readPoint = (collectorId: string, data: string[]) =>
server.post(`/data-collect/collector/${collectorId}/points/_read`, data);
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

@ -1,6 +1,5 @@
<template> <template>
<div class="title" :style='style'> <div class="title" :style='style'>
<div class="title-before"></div>
<span>{{ data }}</span> <span>{{ data }}</span>
<slot name="extra"></slot> <slot name="extra"></slot>
</div> </div>
@ -23,7 +22,6 @@ const props = defineProps({
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.title { .title {
position: relative; position: relative;
width: 100%; width: 100%;
@ -32,14 +30,14 @@ const props = defineProps({
color: rgba(0, 0, 0, 0.8); color: rgba(0, 0, 0, 0.8);
font-weight: 600; font-weight: 600;
font-size: 16px; font-size: 16px;
} &::before {
.title-before {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 4px; width: 4px;
height: calc(100% - 2px); height: 100%;
background-color: @primary-color; background-color: @primary-color;
border-radius: 0 3px 3px 0; content: '';
}
} }
</style> </style>

View File

@ -11,21 +11,10 @@
:model="formData" :model="formData"
name="basic" name="basic"
autocomplete="off" autocomplete="off"
:rules="ModBusRules"
ref="formRef"
> >
<j-form-item <j-form-item label="点位名称" name="name">
label="点位名称"
name="name"
:rules="[
{
required: true,
message: '请输入点位名称',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<j-input <j-input
placeholder="请输入点位名称" placeholder="请输入点位名称"
v-model:value="formData.name" v-model:value="formData.name"
@ -34,12 +23,7 @@
<j-form-item <j-form-item
label="功能码" label="功能码"
:name="['configuration', 'function']" :name="['configuration', 'function']"
:rules="[ :rules="ModBusRules.function"
{
required: true,
message: '请选择功能码',
},
]"
> >
<j-select <j-select
style="width: 100%" style="width: 100%"
@ -57,18 +41,11 @@
</j-form-item> </j-form-item>
<j-form-item <j-form-item
label="地址" label="地址"
:name="['configuration', 'parameter', 'address']" :name="['pointKey']"
:rules="[ :rules="[
...ModBusRules.pointKey,
{ {
required: true, validator: checkPointKey,
message: '请输入地址',
},
{
pattern: regOnlyNumber,
message: '请输入0-255之间的正整数',
},
{
validator: checkAddress,
trigger: 'blur', trigger: 'blur',
}, },
]" ]"
@ -76,7 +53,7 @@
<j-input-number <j-input-number
style="width: 100%" style="width: 100%"
placeholder="请输入地址" placeholder="请输入地址"
v-model:value="formData.configuration.parameter.address" v-model:value="formData.pointKey"
:min="0" :min="0"
:max="255" :max="255"
/> />
@ -84,16 +61,7 @@
<j-form-item <j-form-item
label="寄存器数量" label="寄存器数量"
:name="['configuration', 'parameter', 'quantity']" :name="['configuration', 'parameter', 'quantity']"
:rules="[ :rules="ModBusRules.quantity"
{
required: true,
message: '请输入寄存器数量',
},
{
pattern: regOnlyNumber,
message: '请输入1-255之间的正整数',
},
]"
> >
<j-input-number <j-input-number
style="width: 100%" style="width: 100%"
@ -104,12 +72,14 @@
/> />
</j-form-item> </j-form-item>
<j-form-item <j-form-item
v-if="formData.configuration.function === 'HoldingRegisters'"
label="数据类型" label="数据类型"
:name="['configuration', 'codec', 'provider']" :name="['configuration', 'codec', 'provider']"
:rules="[ :rules="[
...ModBusRules.provider,
{ {
required: true, validator: checkProvider,
message: '请选择数据类型', trigger: 'change',
}, },
]" ]"
> >
@ -131,31 +101,18 @@
'configuration', 'configuration',
'scaleFactor', 'scaleFactor',
]" ]"
:rules="[ :rules="ModBusRules.scaleFactor"
{
required: true,
message: '请输入缩放因子',
},
]"
> >
<j-input <j-input-number
style="width: 100%"
placeholder="请输入缩放因子" placeholder="请输入缩放因子"
v-model:value=" v-model:value="
formData.configuration.codec.configuration.scaleFactor formData.configuration.codec.configuration.scaleFactor
" "
/> />
</j-form-item> </j-form-item>
<j-form-item <j-form-item label="访问类型" name="accessModes">
label="访问类型" <!-- <RadioCard
:name="['accessModes']"
:rules="[
{
required: true,
message: '请选择访问类型',
},
]"
>
<RadioCard
layout="horizontal" layout="horizontal"
:checkStyle="true" :checkStyle="true"
:options="[ :options="[
@ -163,42 +120,36 @@
{ label: '写', value: 'write' }, { label: '写', value: 'write' },
]" ]"
v-model="formData.accessModes" v-model="formData.accessModes"
/> /> -->
<!-- <j-card-select <j-checkbox-group
v-model:value="formData.accessModes" v-model:value="formData.accessModes"
:options="[ :options="[
{ { label: '读', value: 'read' },
label: '读', { label: '写', value: 'write' },
value: 'read',
iconUrl:
'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
},
{
label: '写',
value: 'write',
iconUrl:
'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
},
]" ]"
multiple />
/> -->
</j-form-item> </j-form-item>
<!-- <j-form-item label="非标准协议写入配置" :name="['nspwc']"> --> <j-form-item
<j-form-item :name="['nspwc']"> :name="['nspwc']"
<span>非标准协议写入配置</span> v-if="
formData.accessModes?.includes('write') &&
formData.configuration.function === 'HoldingRegisters'
"
>
<span style="margin-right: 10px">非标准协议写入配置</span>
<j-switch v-model:checked="formData.nspwc" /> <j-switch v-model:checked="formData.nspwc" />
</j-form-item> </j-form-item>
<j-form-item <j-form-item
v-if="
!!formData.nspwc &&
formData.accessModes?.includes('write') &&
formData.configuration.function === 'HoldingRegisters'
"
label="是否写入数据区长度" label="是否写入数据区长度"
:name="['configuration', 'parameter', 'writeByteCount']" :name="['configuration', 'parameter', 'writeByteCount']"
:rules="[ :rules="ModBusRules.writeByteCount"
{
required: true,
message: '请选择是否写入数据区长度',
},
]"
> >
<RadioCard <RadioCard
layout="horizontal" layout="horizontal"
@ -211,14 +162,14 @@
/> />
</j-form-item> </j-form-item>
<j-form-item <j-form-item
v-if="
!!formData.nspwc &&
formData.accessModes?.includes('write') &&
formData.configuration.function === 'HoldingRegisters'
"
label="自定义数据区长度byte" label="自定义数据区长度byte"
:name="['configuration', 'parameter', 'byteCount']" :name="['configuration', 'parameter', 'byteCount']"
:rules="[ :rules="ModBusRules.byteCount"
{
required: true,
message: '请输入自定义数据区长度byte',
},
]"
> >
<j-input <j-input
placeholder="请输入自定义数据区长度byte" placeholder="请输入自定义数据区长度byte"
@ -229,9 +180,10 @@
label="采集频率" label="采集频率"
:name="['configuration', 'interval']" :name="['configuration', 'interval']"
:rules="[ :rules="[
...ModBusRules.interval,
{ {
required: true, validator: checkLength,
message: '请输入采集频率', trigger: 'change',
}, },
]" ]"
> >
@ -240,6 +192,7 @@
placeholder="请输入采集频率" placeholder="请输入采集频率"
v-model:value="formData.configuration.interval" v-model:value="formData.configuration.interval"
:min="1" :min="1"
addon-after="ms"
/> />
</j-form-item> </j-form-item>
@ -279,22 +232,16 @@
</j-modal> </j-modal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Form } from 'ant-design-vue';
import { import {
save, savePointBatch,
update, updatePoint,
_validateField, _validateField,
queryCodecProvider, queryCodecProvider,
} from '@/api/data-collect/collector'; } from '@/api/data-collect/collector';
import { Store } from 'jetlinks-store'; import { ModBusRules, checkProviderData } from '../../data.ts';
import type { FormInstance } from 'ant-design-vue';
const loading = ref(false); import { Rule } from 'ant-design-vue/lib/form';
const useForm = Form.useForm; import { cloneDeep, isArray } from 'lodash-es';
const channelListAll = ref();
const channelList = ref();
const visibleEndian = ref(false);
const visibleUnitId = ref(false);
const providerList = ref([]);
const props = defineProps({ const props = defineProps({
data: { data: {
@ -304,104 +251,71 @@ const props = defineProps({
}); });
const emit = defineEmits(['change']); const emit = defineEmits(['change']);
const loading = ref(false);
const providerList = ref([]);
const formRef = ref<FormInstance>();
const id = props.data.id; const id = props.data.id;
const treeId = props.data.treeId; const collectorId = props.data.collectorId;
const provider = props.data.provider;
const oldPointKey = props.data.pointKey;
const formData = ref({ const formData = ref({
name: '', name: '',
configuration: { configuration: {
function: undefined, function: undefined,
interval: '', interval: 3000,
parameter: { parameter: {
address: '', quantity: 1,
quantity: '',
writeByteCount: '', writeByteCount: '',
byteCount: '', byteCount: 2,
address: '',
}, },
codec: { codec: {
provider: '', provider: undefined,
configuration: { configuration: {
scaleFactor: '', scaleFactor: 1,
}, },
}, },
}, },
accessModes: undefined, pointKey: '',
nspwc: '', accessModes: [],
byte: '', nspwc: false,
features: '', features: [],
description: '', description: '',
}); });
const regOnlyNumber = new RegExp(/^\d+$/); const onSubmit = async () => {
const data = await formRef.value?.validate();
const checkAddress = (_rule: Rule, value: string): Promise<any> => delete data?.nspwc;
new Promise(async (resolve, reject) => { const { codec } = data?.configuration;
if (value) {
const res = await _validateField(props.data.treeId, { if (data?.configuration.function !== 'HoldingRegisters') {
pointKey: value, codec.provider = 'int8';
});
return res.result.passed ? resolve('') : reject(res.result.reason);
} }
}); const params = {
...props.data,
// const { resetFields, validate, validateInfos } = useForm( ...data,
// formData, provider,
// reactive({ collectorId,
// channelId: [
// { required: true, message: '', trigger: 'blur' },
// ],
// name: [
// { required: true, message: '', trigger: 'blur' },
// { max: 64, message: '64' },
// ],
// 'configuration.unitId': [
// { required: true, message: '', trigger: 'blur' },
// {
// pattern: regOnlyNumber,
// message: '0-255',
// },
// ],
// 'circuitBreaker.type': [
// { required: true, message: '', trigger: 'blur' },
// ],
// 'configuration.endian': [
// { required: true, message: '', trigger: 'blur' },
// ],
// description: [{ max: 200, message: '200' }],
// }),
// );
const onSubmit = () => {
// validate()
// .then(async (res) => {
// const { provider, name } = channelListAll.value.find(
// (item) => item.id === formData.value.channelId,
// );
// const params = {
// ...toRaw(formData.value),
// provider,
// channelName: name,
// };
// loading.value = true;
// const response = !id
// ? await save(params)
// : await update(id, { ...props.data, ...params });
// if (response.status === 200) {
// emit('change', true);
// }
// loading.value = false;
// })
// .catch((err) => {
// loading.value = false;
// });
}; };
const getTypeTooltip = (value: string) => // addressreact使
value === 'LowerFrequency' params.configuration.parameter = {
? '连续20次异常降低连接频率至原有频率的1/10重试间隔不超过1分钟故障处理后自动恢复至设定连接频率' ...params.configuration.parameter,
: value === 'Break' address: data?.pointKey,
? '连续10分钟异常停止采集数据进入熔断状态设备重新启用后恢复采集状态' };
: '忽略异常保持原采集频率超时时间为5s';
loading.value = true;
const response = !id
? await savePointBatch(params)
: await updatePoint(id, { ...props.data, ...params });
if (response.status === 200) {
emit('change', true);
}
loading.value = false;
};
const handleOk = () => { const handleOk = () => {
onSubmit(); onSubmit();
@ -410,19 +324,42 @@ const handleCancel = () => {
emit('change', false); emit('change', false);
}; };
// const getChannelNoPaging = async () => { const checkLength = (_rule: Rule, value: string): Promise<any> =>
// channelListAll.value = Store.get('channelListAll'); new Promise(async (resolve, reject) => {
// channelList.value = channelListAll.value.map((item) => ({ if (value) {
// value: item.id, return String(value).length > 64
// label: item.name, ? reject('最多可输入64个字符')
// })); : resolve('');
// }; }
// getChannelNoPaging(); });
const checkProvider = (_rule: Rule, value: string): Promise<any> =>
new Promise(async (resolve, reject) => {
if (value) {
const { quantity } = formData.value.configuration.parameter;
return checkProviderData[value] > Number(quantity) * 2
? reject('数据类型长度需 <= 寄存器数量 * 2')
: resolve('');
}
});
const checkPointKey = (_rule: Rule, value: string): Promise<any> =>
new Promise(async (resolve, reject) => {
if (value) {
if (Number(oldPointKey) === Number(value)) return resolve('');
const res = await _validateField(collectorId, {
pointKey: value,
});
return res.result.passed ? resolve('') : reject(res.result.reason);
}
});
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const getProviderList = async () => { const getProviderList = async () => {
const res = await queryCodecProvider(); const res = await queryCodecProvider();
console.log(222, res.result);
providerList.value = res.result providerList.value = res.result
.filter((i) => i.id !== 'property') .filter((i) => i.id !== 'property')
.map((item) => ({ .map((item) => ({
@ -432,20 +369,31 @@ const getProviderList = async () => {
}; };
getProviderList(); getProviderList();
// watch( watch(
// () => formData.value.channelId, () => formData.value.configuration.parameter.quantity,
// (value) => { (value) => {
// const dt = channelListAll.value.find((item) => item.id === value); formData.value.configuration.parameter.byteCount = Number(value) * 2;
// visibleUnitId.value = visibleEndian.value = },
// dt?.provider && dt?.provider === 'MODBUS_TCP'; { immediate: true, deep: true },
// }, );
// { deep: true },
// );
watch( watch(
() => props.data, () => props.data,
(value) => { (value) => {
if (value.id) formData.value = value; if (value.id && value.provider === 'MODBUS_TCP') {
const _value = cloneDeep(value);
const { writeByteCount, byteCount } =
_value.configuration.parameter;
formData.value = _value;
if (!!_value.accessModes[0]?.value) {
formData.value.accessModes = value.accessModes.map(
(i) => i.value,
);
}
if (!!_value.features[0]?.value) {
formData.value.features = value.features.map((i) => i.value);
}
formData.value.nspwc = !!writeByteCount || !!byteCount;
}
}, },
{ immediate: true, deep: true }, { immediate: true, deep: true },
); );

View File

@ -0,0 +1,217 @@
<template lang="">
<j-modal title="编辑" :visible="true" width="700px" @cancel="handleCancel">
<j-form
class="form"
layout="vertical"
:model="formData"
name="basic"
autocomplete="off"
:rules="OPCUARules"
ref="formRef"
>
<j-form-item label="点位名称" name="name">
<j-input
placeholder="请输入点位名称"
v-model:value="formData.name"
/>
</j-form-item>
<j-form-item label="数据类型" :name="['configuration', 'type']">
<j-select
style="width: 100%"
v-model:value="formData.configuration.type"
:options="[
{ value: 'Number', label: '数值类型' },
{ value: 'DateTime', label: '时间类型' },
{ value: 'Array', label: '数组类型' },
{ value: 'String', label: '文本类型' },
{ value: 'Boolean', label: '布尔' },
]"
placeholder="请选择数据类型"
allowClear
show-search
:filter-option="filterOption"
/>
</j-form-item>
<j-form-item label="访问类型" name="accessModes">
<j-checkbox-group
v-model:value="formData.accessModes"
:options="[
{ label: '读', value: 'read' },
{ label: '写', value: 'write' },
{ label: '订阅', value: 'subscribe' },
]"
/>
</j-form-item>
<j-form-item
label="采集频率"
:name="['configuration', 'interval']"
:rules="[
...OPCUARules.interval,
{
validator: checkLength,
trigger: 'change',
},
]"
>
<j-input-number
style="width: 100%"
placeholder="请输入采集频率"
v-model:value="formData.configuration.interval"
:min="1"
addon-after="ms"
/>
</j-form-item>
<a-form-item label="" :name="['features']">
<a-checkbox-group v-model:value="formData.features">
<a-checkbox value="changedOnly" name="type"
>只推送变化的数据</a-checkbox
>
</a-checkbox-group>
</a-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 key="back" @click="handleCancel">取消</j-button>
<PermissionButton
key="submit"
type="primary"
:loading="loading"
@click="handleOk"
style="margin-left: 8px"
:hasPermission="`DataCollect/Collector:${
id ? 'update' : 'add'
}`"
>
确认
</PermissionButton>
</template>
</j-modal>
</template>
<script lang="ts" setup>
import {
savePoint,
updatePoint,
_validateField,
} from '@/api/data-collect/collector';
import { OPCUARules, checkProviderData } from '../../data.ts';
import type { FormInstance } from 'ant-design-vue';
import { Rule } from 'ant-design-vue/lib/form';
import { cloneDeep } from 'lodash-es';
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(['change']);
const loading = ref(false);
const providerList = ref([]);
const formRef = ref<FormInstance>();
const id = props.data.id;
const collectorId = props.data.collectorId;
const provider = props.data.provider;
const formData = ref({
name: '',
configuration: {
type: undefined,
interval: 3000,
},
accessModes: [],
features: [],
description: '',
});
const onSubmit = async () => {
const data = await formRef.value?.validate();
const params = {
...props.data,
...data,
provider,
collectorId,
};
loading.value = true;
const response = !id
? await savePoint(params)
: await updatePoint(id, { ...props.data, ...params });
if (response.status === 200) {
emit('change', true);
}
loading.value = false;
};
const handleOk = () => {
onSubmit();
};
const handleCancel = () => {
emit('change', false);
};
const checkLength = (_rule: Rule, value: string): Promise<any> =>
new Promise(async (resolve, reject) => {
if (value) {
return String(value).length > 64
? reject('最多可输入64个字符')
: resolve('');
}
});
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
watch(
() => props.data,
(value) => {
if (value.id && value.provider === 'OPC_UA') {
const _value = cloneDeep(value);
formData.value = _value;
if (!!_value.accessModes[0]?.value) {
formData.value.accessModes = value.accessModes.map(
(i) => i.value,
);
}
if (!!_value.features[0]?.value) {
formData.value.features = value.features.map((i) => i.value);
}
}
},
{ immediate: true, deep: true },
);
</script>
<style lang="less" scoped>
.form {
.form-radio-button {
width: 148px;
height: 80px;
padding: 0;
img {
width: 100%;
height: 100%;
}
}
.form-upload-button {
margin-top: 10px;
}
.form-submit {
background-color: @primary-color !important;
}
}
</style>

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

@ -0,0 +1,152 @@
<template lang="">
<j-modal
title="批量编辑"
:visible="true"
width="700px"
@cancel="handleCancel"
>
<div class="sizeText">
将批量修改{{
data.length
}}条数据的访问类型采集频率只推送变化的数据
</div>
<j-form
class="form"
layout="vertical"
:model="formData"
name="basic"
autocomplete="off"
ref="formRef"
>
<j-form-item label="访问类型" name="accessModes">
<j-checkbox-group
v-model:value="formData.accessModes"
:options="[
{ label: '读', value: 'read' },
{ label: '写', value: 'write' },
{ label: '订阅', value: 'subscribe' },
]"
/>
</j-form-item>
<j-form-item :name="['interval']">
<template #label>
<span>
采集频率
<j-tooltip title="采集频率为0时不执行轮询任务">
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
/>
</j-tooltip>
</span>
</template>
<j-input-number
style="width: 100%"
placeholder="请输入采集频率"
v-model:value="formData.interval"
:min="1"
addon-after="ms"
/>
</j-form-item>
<a-form-item label="" :name="['features']">
<a-checkbox-group v-model:value="formData.features">
<a-checkbox value="changedOnly" name="type"
>只推送变化的数据</a-checkbox
>
</a-checkbox-group>
</a-form-item>
</j-form>
<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';
const props = defineProps({
data: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['change']);
const loading = ref(false);
const formRef = ref<FormInstance>();
const formData = ref({
accessModes: [],
interval: '',
features: [],
});
const checkLength = (_rule: Rule, value: string): Promise<any> =>
new Promise(async (resolve, reject) => {
if (value) {
return String(value).length > 64
? reject('最多可输入64个字符')
: resolve('');
}
});
const onSubmit = async () => {
const data = await formRef.value?.validate();
const { accessModes, features, interval } = data;
const ischange =
accessModes.length !== 0 || features.length !== 0 || !!interval;
if (ischange) {
const params = cloneDeep(props.data);
params.forEach((i: any) => {
accessModes.length !== 0 && (i.accessModes = data.accessModes);
features.length !== 0 && (i.features = data.features);
if (!!interval) {
i.interval = data.interval;
i.configuration = {
...i.configuration,
interval: data.interval,
};
}
});
loading.value = true;
const response = await savePointBatch(params);
if (response.status === 200) {
emit('change', true);
}
loading.value = false;
} else {
emit('change', true);
}
};
const handleOk = () => {
onSubmit();
};
const handleCancel = () => {
emit('change', false);
};
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
</script>
<style lang="less" scoped>
.sizeText {
margin-bottom: 20px;
}
</style>

View File

@ -1,10 +1,6 @@
<template> <template>
<div class="card"> <div class="card">
<div <div class="card-warp" :class="{ active: active ? 'active' : '' }">
class="card-warp"
:class="{ active: active ? 'active' : '' }"
@click="handleClick"
>
<div class="card-content"> <div class="card-content">
<div class="card-header"> <div class="card-header">
<div class="card-header-left"> <div class="card-header-left">
@ -15,7 +11,11 @@
</div> </div>
</div> </div>
<div style="display: flex"> <div
class="card-item"
style="display: flex"
@click="handleClick"
>
<!-- 图片 --> <!-- 图片 -->
<div class="card-item-avatar"> <div class="card-item-avatar">
<slot name="img"> </slot> <slot name="img"> </slot>
@ -169,7 +169,7 @@ const handleClick = () => {
border: 1px solid #e6e6e6; border: 1px solid #e6e6e6;
&:hover { &:hover {
cursor: pointer; // cursor: pointer;
box-shadow: 0 0 24px rgba(#000, 0.1); box-shadow: 0 0 24px rgba(#000, 0.1);
.card-mask { .card-mask {
@ -214,6 +214,9 @@ const handleClick = () => {
align-items: center; align-items: center;
} }
.card-item {
cursor: pointer;
}
.card-item-avatar { .card-item-avatar {
margin-right: 16px; margin-right: 16px;
} }

View File

@ -0,0 +1,169 @@
<template lang="">
<j-modal title="写入" :visible="true" width="500px" @cancel="handleCancel">
<j-form
class="form"
layout="vertical"
:model="formData"
name="basic"
autocomplete="off"
ref="formRef"
>
<j-form-item
:label="data.name"
name="value"
:rules="[
{
required: true,
message: `请输入${data.name}`,
},
]"
v-if="
data.provider === 'MODBUS_TCP' &&
data?.configuration.function === 'Coils'
"
>
<j-textarea
placeholder="请输入"
v-model:value="formData.value"
:maxlength="200"
:rows="3"
showCount
/>
</j-form-item>
<j-form-item
:label="data.name"
name="value"
:rules="[
{
required: true,
message: `请输入${data.name}`,
},
]"
v-else
>
<j-input-number
v-if="valueTypeArray.includes(valueType)"
style="width: 100%"
placeholder="请输入"
v-model:value="formData.value"
/>
<j-select
v-else-if="['boolean'].includes(valueType)"
style="width: 100%"
v-model:value="formData.value"
:options="[
{
label: '是',
value: true,
},
{
label: '否',
value: false,
},
]"
placeholder="请选择"
allowClear
show-search
:filter-option="filterOption"
/>
<j-date-picker
v-else-if="['datetime'].includes(valueType)"
style="width: 100%"
format="YYYY-MM-DD HH:mm:ss"
show-time
placeholder="请选择"
@change="onChange"
/>
<j-input
v-else
placeholder="请输入"
v-model:value="formData.value"
/>
</j-form-item>
</j-form>
<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 type { Dayjs } from 'dayjs';
import { writePoint } from '@/api/data-collect/collector';
const props = defineProps({
data: {
type: Object,
default: () => {},
},
});
const valueType: string = (
props.data?.provider === 'OPC_UA'
? props?.data?.configuration?.type || 'Number'
: props.data?.configuration?.codec?.provider || 'int8'
).toLocaleLowerCase();
const valueTypeArray = [
'int8',
'int16',
'int32',
'ieee754_float',
'ieee754_double',
'hex,',
'number',
];
const emit = defineEmits(['change']);
const loading = ref(false);
const formRef = ref<FormInstance>();
const collectorId = props.data.collectorId;
const pointId = props.data.id;
const formData = ref({
value: '',
});
const onChange = (value: Dayjs, dateString: string) => {
formData.value.value = dateString;
};
const onSubmit = async () => {
const data = await formRef.value?.validate();
const params = {
...data,
pointId,
};
loading.value = true;
const response = await writePoint(collectorId, [params]);
if (response.status === 200) {
emit('change', true);
}
loading.value = false;
};
const handleOk = () => {
onSubmit();
};
const handleCancel = () => {
emit('change', false);
};
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
</script>
<style lang="less" scoped></style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <j-spin :spinning="spinning">
<j-advanced-search <j-advanced-search
:columns="columns" :columns="columns"
target="search" target="search"
@ -13,9 +13,7 @@
:gridColumn="2" :gridColumn="2"
:gridColumns="[1, 2]" :gridColumns="[1, 2]"
:request="queryPoint" :request="queryPoint"
:defaultParams="{ :defaultParams="defaultParams"
sorts: [{ name: 'id', order: 'desc' }],
}"
:params="params" :params="params"
:rowSelection="{ :rowSelection="{
selectedRowKeys: _selectedRowKeys, selectedRowKeys: _selectedRowKeys,
@ -26,12 +24,23 @@
<template #headerTitle> <template #headerTitle>
<j-space> <j-space>
<PermissionButton <PermissionButton
v-if="data?.provider !== 'OPC_UA'"
type="primary" type="primary"
@click="handlAdd" @click="handlAdd"
hasPermission="DataCollect/Collector:add" hasPermission="DataCollect/Collector:add"
> >
<template #icon><AIcon type="PlusOutlined" /></template> <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> </PermissionButton>
<j-dropdown v-if="data?.provider === 'OPC_UA'"> <j-dropdown v-if="data?.provider === 'OPC_UA'">
<j-button <j-button
@ -42,6 +51,7 @@
<j-menu-item> <j-menu-item>
<PermissionButton <PermissionButton
hasPermission="DataCollect/Collector:update" hasPermission="DataCollect/Collector:update"
@click="handlBatchUpdate()"
> >
<template #icon <template #icon
><AIcon type="FormOutlined" ><AIcon type="FormOutlined"
@ -52,6 +62,10 @@
<j-menu-item> <j-menu-item>
<PermissionButton <PermissionButton
hasPermission="DataCollect/Collector:delete" hasPermission="DataCollect/Collector:delete"
:popConfirm="{
title: `确定删除?`,
onConfirm: () => handlDelete(),
}"
> >
<template #icon <template #icon
><AIcon type="EditOutlined" ><AIcon type="EditOutlined"
@ -75,17 +89,6 @@
:statusText="getState(slotProps)?.text" :statusText="getState(slotProps)?.text"
:statusNames="Object.fromEntries(colorMap.entries())" :statusNames="Object.fromEntries(colorMap.entries())"
> >
<!-- <PointCardBox
:showStatus="true"
:value="slotProps"
:actions="getActions(slotProps)"
:active="_selectedRowKeys.includes(slotProps.id)"
v-bind="slotProps"
class="card-box"
:status="getState(slotProps).value"
:statusText="slotProps.runningState?.text"
:statusNames="Object.fromEntries(colorMap.entries())"
> -->
<template #title> <template #title>
<slot name="title"> <slot name="title">
<div class="card-box-title"> <div class="card-box-title">
@ -95,8 +98,19 @@
</template> </template>
<template #action> <template #action>
<div class="card-box-action"> <div class="card-box-action">
<a><AIcon type="DeleteOutlined" /></a> <a>
<a><AIcon type="FormOutlined" /></a> <j-popconfirm
title="确定删除?"
@confirm="handlDelete(slotProps.id)"
>
<AIcon type="DeleteOutlined" />
</j-popconfirm>
</a>
<a
><AIcon
@click="handlEdit(slotProps)"
type="FormOutlined"
/></a>
</div> </div>
</template> </template>
<template #img> <template #img>
@ -112,8 +126,26 @@
<div class="card-box-content"> <div class="card-box-content">
<div class="card-box-content-left"> <div class="card-box-content-left">
<span>--</span> <span>--</span>
<a><AIcon type="EditOutlined" /></a> <a
<a><AIcon type="RedoOutlined" /></a> v-if="
getAccessModes(slotProps).includes(
'write',
)
"
><AIcon
@click.stop="clickEdit(slotProps)"
type="EditOutlined"
/></a>
<a
v-if="
getAccessModes(slotProps).includes(
'read',
)
"
><AIcon
@click.stop="clickRedo(slotProps)"
type="RedoOutlined"
/></a>
</div> </div>
<div class="card-box-content-right"> <div class="card-box-content-right">
<div <div
@ -135,21 +167,46 @@
</template> </template>
</j-pro-table> </j-pro-table>
<SaveModBus <SaveModBus
v-if="visibleSaveModBus" v-if="visible.saveModBus"
:data="current" :data="current"
@change="saveChange" @change="saveChange"
/> />
</div> <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> </template>
<script lang="ts" setup name="PointPage"> <script lang="ts" setup name="PointPage">
import type { ActionsType } from '@/components/Table/index.vue';
import { getImage } from '@/utils/comm'; import { getImage } from '@/utils/comm';
import { queryPoint } from '@/api/data-collect/collector'; import {
queryPoint,
batchDeletePoint,
removePoint,
readPoint,
} from '@/api/data-collect/collector';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useMenuStore } from 'store/menu'; import { useMenuStore } from 'store/menu';
import PointCardBox from './components/PointCardBox/index.vue'; import PointCardBox from './components/PointCardBox/index.vue';
import { colorMap, getState } from '../data.ts'; import WritePoint from './components/WritePoint/index.vue';
import BatchUpdate from './components/BatchUpdate/index.vue';
import SaveModBus from './Save/SaveModBus.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';
const props = defineProps({ const props = defineProps({
data: { data: {
@ -163,11 +220,35 @@ const tableRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({}); const params = ref<Record<string, any>>({});
const opcImage = getImage('/DataCollect/device-opcua.png'); const opcImage = getImage('/DataCollect/device-opcua.png');
const modbusImage = getImage('/DataCollect/device-modbus.png'); const modbusImage = getImage('/DataCollect/device-modbus.png');
const visibleSaveModBus = ref(false); const visible = reactive({
saveModBus: false,
saveOPCUA: false,
writePoint: false,
batchUpdate: false,
scan: false,
});
const current = ref({}); const current = ref({});
const accessModesOption = ref(); const accessModesOption = ref();
const _selectedRowKeys = ref<string[]>([]); 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 = [ const accessModesMODBUS_TCP = [
{ {
label: '读', label: '读',
@ -179,11 +260,6 @@ const accessModesMODBUS_TCP = [
}, },
]; ];
const accessModesOPC_UA = accessModesMODBUS_TCP.concat({
label: '订阅',
value: 'subscribe',
});
const columns = [ const columns = [
{ {
title: '点位名称', title: '点位名称',
@ -256,81 +332,57 @@ const columns = [
}, },
]; ];
// const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
// if (!data) return [];
// const state = data.state.value;
// const stateText = state === 'enabled' ? '' : '';
// const actions = [
// {
// key: 'update',
// text: '',
// tooltip: {
// title: '',
// },
// icon: 'EditOutlined',
// onClick: () => {
// handlEdit(data.id);
// },
// },
// {
// key: 'action',
// text: stateText,
// tooltip: {
// title: stateText,
// },
// icon: state === 'enabled' ? 'StopOutlined' : 'CheckCircleOutlined',
// popConfirm: {
// title: `${stateText}?`,
// onConfirm: async () => {
// let res =
// state === 'enabled'
// ? await disable(data.id)
// : await enalbe(data.id);
// if (res.success) {
// message.success('');
// tableRef.value?.reload();
// } else {
// message.error('');
// }
// },
// },
// },
// {
// key: 'delete',
// text: '',
// disabled: state === 'enabled',
// tooltip: {
// title: state === 'enabled' ? '' : '',
// },
// popConfirm: {
// title: '?',
// onConfirm: async () => {
// const res = await remove(data.id);
// if (res.success) {
// message.success('');
// tableRef.value.reload();
// } else {
// message.error('');
// }
// },
// },
// icon: 'DeleteOutlined',
// },
// ];
// return actions;
// };
const handlAdd = () => { const handlAdd = () => {
visibleSaveModBus.value = true; visible.saveModBus = true;
current.value = { current.value = {
treeId: props.data.id, collectorId: collectorId.value,
provider: props.data?.provider || 'MODBUS_TCP',
}; };
}; };
const handlEdit = (id: string) => { const handlEdit = (data: Object) => {
// menuStory.jumpPage(`media/Stream/Detail`, { id }, { view: false }); if (data?.provider === 'OPC_UA') {
visible.saveOPCUA = true;
} else {
visible.saveModBus = true;
}
current.value = cloneDeep(data);
}; };
const handlEye = (id: string) => { const handlDelete = async (data: string | undefined = undefined) => {
// menuStory.jumpPage(`media/Stream/Detail`, { id }, { view: true }); spinning.value = true;
const res = !data
? await batchDeletePoint(_selectedRowKeys.value)
: await removePoint(data as string);
if (res.status === 200) {
cancelSelect();
tableRef.value?.reload();
message.success('操作成功');
}
spinning.value = false;
};
const handlBatchUpdate = () => {
const dataSet = new Set(_selectedRowKeys.value);
const dataMap = new Map();
tableRef?.value?._dataSource.forEach((i) => {
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: object) => {
const res = await readPoint(data?.collectorId, [data?.id]);
if (res.status === 200) {
cancelSelect();
tableRef.value?.reload();
message.success('操作成功');
}
}; };
const getQuantity = (item: Partial<Record<string, any>>) => { const getQuantity = (item: Partial<Record<string, any>>) => {
@ -345,11 +397,9 @@ const getScaleFactor = (item: Partial<Record<string, any>>) => {
const { scaleFactor } = item.configuration?.codec?.configuration || ''; const { scaleFactor } = item.configuration?.codec?.configuration || '';
return !!scaleFactor ? scaleFactor + '(缩放因子)' : ''; return !!scaleFactor ? scaleFactor + '(缩放因子)' : '';
}; };
const getRight1 = (item: Partial<Record<string, any>>) => { const getRight1 = (item: Partial<Record<string, any>>) => {
return !!getQuantity(item) || getAddress(item) || getScaleFactor(item); return !!getQuantity(item) || getAddress(item) || getScaleFactor(item);
}; };
const getText = (item: Partial<Record<string, any>>) => { const getText = (item: Partial<Record<string, any>>) => {
return (item?.accessModes || []).map((i) => i?.text).join(','); return (item?.accessModes || []).map((i) => i?.text).join(',');
}; };
@ -357,22 +407,18 @@ const getInterval = (item: Partial<Record<string, any>>) => {
const { interval } = item.configuration || ''; const { interval } = item.configuration || '';
return !!interval ? '采集频率' + interval + 'ms' : ''; return !!interval ? '采集频率' + interval + 'ms' : '';
}; };
const getAccessModes = (item: Partial<Record<string, any>>) => {
const getaccessModesOption = () => { return item?.accessModes?.map((i) => i?.value);
return props.data?.provider !== 'MODBUS_TCP'
? accessModesMODBUS_TCP.concat({
label: '订阅',
value: 'subscribe',
})
: accessModesMODBUS_TCP;
}; };
const saveChange = (value: object) => { const saveChange = (value: object) => {
visibleSaveModBus.value = false; for (let key in visible) {
visible[key] = false;
}
current.value = {}; current.value = {};
if (value) { if (value) {
handleSearch(params.value); tableRef.value?.reload();
// message.success(''); message.success('操作成功');
} }
}; };
@ -404,22 +450,12 @@ watch(
label: '订阅', label: '订阅',
value: 'subscribe', value: 'subscribe',
}); });
defaultParams.value.terms[0].terms[0].value = value.id;
params.value = { // defaultParams.value.terms[0].terms[0].value = '1610517928766550016'; //
terms: [ tableRef?.value?.reload && tableRef?.value?.reload();
{
terms: [
{
column: 'collectorId',
value: value.id,
},
],
},
],
};
} }
}, },
{ deep: true, immediate: true }, { immediate: true, deep: true },
); );
/** /**
@ -435,9 +471,11 @@ const handleSearch = (e: any) => {
min-width: 480px; min-width: 480px;
a { a {
color: #474747; color: #474747;
z-index: 1;
} }
a:hover { a:hover {
color: #315efb; color: #315efb;
z-index: 1;
} }
.card-box-title { .card-box-title {
font-size: 18px; font-size: 18px;
@ -459,7 +497,10 @@ const handleSearch = (e: any) => {
height: 68px; height: 68px;
padding-right: 20px; padding-right: 20px;
display: flex; display: flex;
justify-content: space-between; justify-content: flex-start;
a {
margin-left: 10px;
}
} }
.card-box-content-right { .card-box-content-right {
flex: 0.8; flex: 0.8;

View File

@ -201,7 +201,6 @@ const onSubmit = () => {
}; };
loading.value = true; loading.value = true;
const response = !id const response = !id
? await save(params) ? await save(params)
: await update(id, { ...props.data, ...params }); : await update(id, { ...props.data, ...params });
@ -238,6 +237,10 @@ const getChannelNoPaging = async () => {
}; };
getChannelNoPaging(); getChannelNoPaging();
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
watch( watch(
() => formData.value.channelId, () => formData.value.channelId,
(value) => { (value) => {

View File

@ -19,7 +19,7 @@
新增采集器 新增采集器
</PermissionButton> </PermissionButton>
</div> </div>
<a-spin :spinning="spinning"> <j-spin :spinning="spinning">
<a-tree <a-tree
:tree-data="defualtDataSource" :tree-data="defualtDataSource"
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
@ -92,7 +92,7 @@
</template> </template>
</a-tree> </a-tree>
<j-empty v-else description="暂无数据" /> <j-empty v-else description="暂无数据" />
</a-spin> </j-spin>
<Save v-if="visible" :data="current" @change="saveChange" /> <Save v-if="visible" :data="current" @change="saveChange" />
</div> </div>
</template> </template>

View File

@ -16,3 +16,166 @@ export const getState = (record: any) => {
return {}; return {};
} }
}; };
const regOnlyNumber = new RegExp(/^\d+$/);
export const checkProviderData = {
int8: 1,
int16: 2,
int32: 4,
int64: 8,
ieee754_float: 4,
ieee754_double: 8,
hex: 1,
};
export const ModBusRules = {
name: [
{
required: true,
message: '请输入点位名称',
},
{
max: 64,
message: '最多可输入64个字符',
},
],
function: [
{
required: true,
message: '请选择功能码',
},
],
pointKey: [
{
required: true,
message: '请输入地址',
},
{
pattern: regOnlyNumber,
message: '请输入0-255之间的正整数',
},
],
quantity: [
{
required: true,
message: '请输入寄存器数量',
},
{
pattern: regOnlyNumber,
message: '请输入1-255之间的正整数',
},
],
provider: [
{
required: true,
message: '请选择数据类型',
},
],
scaleFactor: [
{
required: true,
message: '请输入缩放因子',
},
],
accessModes: [
{
required: true,
message: '请选择访问类型',
},
],
writeByteCount: [
{
required: true,
message: '请选择是否写入数据区长度',
},
],
byteCount: [
{
required: true,
message: '请输入自定义数据区长度byte',
},
],
interval: [
{
required: true,
message: '请输入采集频率',
},
],
description: [{ max: 200, message: '最多可输入200个字符' }],
};
export const OPCUARules = {
name: [
{
required: true,
message: '请输入点位名称',
},
{
max: 64,
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,
},
];

View File

@ -5,7 +5,7 @@
<Tree @change="changeTree" /> <Tree @change="changeTree" />
</div> </div>
<div class="right"> <div class="right">
<Point :data="data"></Point> <Point v-if="!!data" :data="data" />
</div> </div>
</div> </div>
</page-container> </page-container>
@ -32,7 +32,6 @@ const changeTree = (row: any) => {
.left { .left {
width: 300px; width: 300px;
border-right: 1px #eeeeee solid; border-right: 1px #eeeeee solid;
// padding: 10px;
margin: 10px; margin: 10px;
} }
.right { .right {