Merge pull request #58 from jetlinks/dev-hub
feat: 采集器 左侧树全部功能,右侧卡片列表以及部分表单
This commit is contained in:
commit
d200ac9ace
|
@ -2,3 +2,22 @@ import server from '@/utils/request';
|
|||
|
||||
export const queryCollector = (data: any) =>
|
||||
server.post(`/data-collect/collector/_query/no-paging?paging=false`, data);
|
||||
|
||||
export const queryChannelNoPaging = () =>
|
||||
server.post(`/data-collect/channel/_query/no-paging?paging=false`, {});
|
||||
|
||||
export const save = (data: any) => server.post(`/data-collect/collector`, data);
|
||||
|
||||
export const update = (id: string, data: any) =>
|
||||
server.put(`/data-collect/collector/${id}`, data);
|
||||
|
||||
export const remove = (id: string) =>
|
||||
server.remove(`/data-collect/collector/${id}`);
|
||||
|
||||
export const queryPoint = (data: any) =>
|
||||
server.post(`/data-collect/point/_query`, data);
|
||||
|
||||
export const _validateField = (id: string, data?: any) =>
|
||||
server.get(`/data-collect/point/${id}/_validate`, data);
|
||||
|
||||
export const queryCodecProvider = () => server.get(`/things/collector/codecs`);
|
||||
|
|
|
@ -75,7 +75,8 @@ const iconKeys = [
|
|||
'LogoutOutlined',
|
||||
'ReadIconOutlined',
|
||||
'CloudDownloadOutlined',
|
||||
'PauseCircleOutlined',
|
||||
'PauseCircleOutlined',,
|
||||
'FormOutlined',
|
||||
]
|
||||
|
||||
const Icon = (props: {type: string}) => {
|
||||
|
|
|
@ -0,0 +1,472 @@
|
|||
<template lang="">
|
||||
<j-modal
|
||||
:title="data.id ? '编辑' : '新增'"
|
||||
:visible="true"
|
||||
width="700px"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<j-form
|
||||
class="form"
|
||||
layout="vertical"
|
||||
:model="formData"
|
||||
name="basic"
|
||||
autocomplete="off"
|
||||
>
|
||||
<j-form-item
|
||||
label="点位名称"
|
||||
name="name"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入点位名称',
|
||||
},
|
||||
{
|
||||
max: 64,
|
||||
message: '最多可输入64个字符',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<j-input
|
||||
placeholder="请输入点位名称"
|
||||
v-model:value="formData.name"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item
|
||||
label="功能码"
|
||||
:name="['configuration', 'function']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择功能码',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<j-select
|
||||
style="width: 100%"
|
||||
v-model:value="formData.configuration.function"
|
||||
:options="[
|
||||
{ label: '01线圈寄存器', value: 'Coils' },
|
||||
{ label: '03保存寄存器', value: 'HoldingRegisters' },
|
||||
{ label: '04输入寄存器', value: 'DiscreteInputs' },
|
||||
]"
|
||||
placeholder="请选择所功能码"
|
||||
allowClear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item
|
||||
label="地址"
|
||||
:name="['configuration', 'parameter', 'address']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入地址',
|
||||
},
|
||||
{
|
||||
pattern: regOnlyNumber,
|
||||
message: '请输入0-255之间的正整数',
|
||||
},
|
||||
{
|
||||
validator: checkAddress,
|
||||
trigger: 'blur',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<j-input-number
|
||||
style="width: 100%"
|
||||
placeholder="请输入地址"
|
||||
v-model:value="formData.configuration.parameter.address"
|
||||
:min="0"
|
||||
:max="255"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item
|
||||
label="寄存器数量"
|
||||
:name="['configuration', 'parameter', 'quantity']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入寄存器数量',
|
||||
},
|
||||
{
|
||||
pattern: regOnlyNumber,
|
||||
message: '请输入1-255之间的正整数',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<j-input-number
|
||||
style="width: 100%"
|
||||
placeholder="请输入寄存器数量"
|
||||
v-model:value="formData.configuration.parameter.quantity"
|
||||
:min="1"
|
||||
:max="255"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item
|
||||
label="数据类型"
|
||||
:name="['configuration', 'codec', 'provider']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择数据类型',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<j-select
|
||||
style="width: 100%"
|
||||
v-model:value="formData.configuration.codec.provider"
|
||||
:options="providerList"
|
||||
placeholder="请选择数据类型"
|
||||
allowClear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item
|
||||
label="缩放因子"
|
||||
:name="[
|
||||
'configuration',
|
||||
'codec',
|
||||
'configuration',
|
||||
'scaleFactor',
|
||||
]"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入缩放因子',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<j-input
|
||||
placeholder="请输入缩放因子"
|
||||
v-model:value="
|
||||
formData.configuration.codec.configuration.scaleFactor
|
||||
"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item
|
||||
label="访问类型"
|
||||
:name="['accessModes']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择访问类型',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<RadioCard
|
||||
layout="horizontal"
|
||||
:checkStyle="true"
|
||||
:options="[
|
||||
{ label: '读', value: 'read' },
|
||||
{ label: '写', value: 'write' },
|
||||
]"
|
||||
v-model="formData.accessModes"
|
||||
/>
|
||||
<!-- <j-card-select
|
||||
v-model:value="formData.accessModes"
|
||||
:options="[
|
||||
{
|
||||
label: '读',
|
||||
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 label="非标准协议写入配置" :name="['nspwc']"> -->
|
||||
<j-form-item :name="['nspwc']">
|
||||
<span>非标准协议写入配置</span>
|
||||
<j-switch v-model:checked="formData.nspwc" />
|
||||
</j-form-item>
|
||||
|
||||
<j-form-item
|
||||
label="是否写入数据区长度"
|
||||
:name="['configuration', 'parameter', 'writeByteCount']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择是否写入数据区长度',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<RadioCard
|
||||
layout="horizontal"
|
||||
:checkStyle="true"
|
||||
:options="[
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
]"
|
||||
v-model="formData.configuration.parameter.writeByteCount"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item
|
||||
label="自定义数据区长度(byte)"
|
||||
:name="['configuration', 'parameter', 'byteCount']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入自定义数据区长度(byte)',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<j-input
|
||||
placeholder="请输入自定义数据区长度(byte)"
|
||||
v-model:value="formData.configuration.parameter.byteCount"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item
|
||||
label="采集频率"
|
||||
:name="['configuration', 'interval']"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入采集频率',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<j-input-number
|
||||
style="width: 100%"
|
||||
placeholder="请输入采集频率"
|
||||
v-model:value="formData.configuration.interval"
|
||||
:min="1"
|
||||
/>
|
||||
</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 { Form } from 'ant-design-vue';
|
||||
import {
|
||||
save,
|
||||
update,
|
||||
_validateField,
|
||||
queryCodecProvider,
|
||||
} from '@/api/data-collect/collector';
|
||||
import { Store } from 'jetlinks-store';
|
||||
|
||||
const loading = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
const channelListAll = ref();
|
||||
const channelList = ref();
|
||||
const visibleEndian = ref(false);
|
||||
const visibleUnitId = ref(false);
|
||||
const providerList = ref([]);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const id = props.data.id;
|
||||
const treeId = props.data.treeId;
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
configuration: {
|
||||
function: undefined,
|
||||
interval: '',
|
||||
parameter: {
|
||||
address: '',
|
||||
quantity: '',
|
||||
writeByteCount: '',
|
||||
byteCount: '',
|
||||
},
|
||||
codec: {
|
||||
provider: '',
|
||||
configuration: {
|
||||
scaleFactor: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
accessModes: undefined,
|
||||
nspwc: '',
|
||||
byte: '',
|
||||
features: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
const regOnlyNumber = new RegExp(/^\d+$/);
|
||||
|
||||
const checkAddress = (_rule: Rule, value: string): Promise<any> =>
|
||||
new Promise(async (resolve, reject) => {
|
||||
if (value) {
|
||||
const res = await _validateField(props.data.treeId, {
|
||||
pointKey: value,
|
||||
});
|
||||
return res.result.passed ? resolve('') : reject(res.result.reason);
|
||||
}
|
||||
});
|
||||
|
||||
// const { resetFields, validate, validateInfos } = useForm(
|
||||
// formData,
|
||||
// reactive({
|
||||
// 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) =>
|
||||
value === 'LowerFrequency'
|
||||
? '连续20次异常,降低连接频率至原有频率的1/10(重试间隔不超过1分钟),故障处理后自动恢复至设定连接频率'
|
||||
: value === 'Break'
|
||||
? '连续10分钟异常,停止采集数据进入熔断状态,设备重新启用后恢复采集状态'
|
||||
: '忽略异常,保持原采集频率超时时间为5s';
|
||||
|
||||
const handleOk = () => {
|
||||
onSubmit();
|
||||
};
|
||||
const handleCancel = () => {
|
||||
emit('change', false);
|
||||
};
|
||||
|
||||
// const getChannelNoPaging = async () => {
|
||||
// channelListAll.value = Store.get('channelListAll');
|
||||
// channelList.value = channelListAll.value.map((item) => ({
|
||||
// value: item.id,
|
||||
// label: item.name,
|
||||
// }));
|
||||
// };
|
||||
// getChannelNoPaging();
|
||||
|
||||
const getProviderList = async () => {
|
||||
const res = await queryCodecProvider();
|
||||
console.log(222, res.result);
|
||||
|
||||
providerList.value = res.result
|
||||
.filter((i) => i.id !== 'property')
|
||||
.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
}));
|
||||
};
|
||||
getProviderList();
|
||||
|
||||
// watch(
|
||||
// () => formData.value.channelId,
|
||||
// (value) => {
|
||||
// const dt = channelListAll.value.find((item) => item.id === value);
|
||||
// visibleUnitId.value = visibleEndian.value =
|
||||
// dt?.provider && dt?.provider === 'MODBUS_TCP';
|
||||
// },
|
||||
// { deep: true },
|
||||
// );
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(value) => {
|
||||
if (value.id) formData.value = 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>
|
|
@ -0,0 +1,415 @@
|
|||
<template>
|
||||
<div class="card">
|
||||
<div
|
||||
class="card-warp"
|
||||
:class="{ active: active ? 'active' : '' }"
|
||||
@click="handleClick"
|
||||
>
|
||||
<div class="card-content">
|
||||
<div class="card-header">
|
||||
<div class="card-header-left">
|
||||
<slot name="title"> </slot>
|
||||
</div>
|
||||
<div class="card-header-right">
|
||||
<slot name="action"> </slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex">
|
||||
<!-- 图片 -->
|
||||
<div class="card-item-avatar">
|
||||
<slot name="img"> </slot>
|
||||
</div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="card-item-body">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 勾选 -->
|
||||
<div v-if="active" class="checked-icon">
|
||||
<div>
|
||||
<AIcon type="CheckOutlined" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态 -->
|
||||
<div
|
||||
v-if="showStatus"
|
||||
class="card-state"
|
||||
:class="statusNames ? statusNames[status] : ''"
|
||||
>
|
||||
<div class="card-state-content">
|
||||
<BadgeStatus
|
||||
:status="status"
|
||||
:text="statusText"
|
||||
:statusNames="statusNames"
|
||||
></BadgeStatus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-mask" v-if="props.hasMark">
|
||||
<div class="mask-content">
|
||||
<slot name="mark" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<slot name="bottom-tool">
|
||||
<div
|
||||
v-if="showTool && actions && actions.length"
|
||||
class="card-tools"
|
||||
>
|
||||
<div
|
||||
v-for="item in actions"
|
||||
:key="item.key"
|
||||
class="card-button"
|
||||
:class="{
|
||||
delete: item.key === 'delete',
|
||||
}"
|
||||
>
|
||||
<slot name="actions" v-bind="item"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BadgeStatus from '@/components/BadgeStatus/index.vue';
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
type EmitProps = {
|
||||
// (e: 'update:modelValue', data: Record<string, any>): void;
|
||||
(e: 'click', data: Record<string, any>): void;
|
||||
};
|
||||
|
||||
type TableActionsType = Partial<ActionsType>;
|
||||
|
||||
const emit = defineEmits<EmitProps>();
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => {},
|
||||
},
|
||||
showStatus: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showTool: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
statusText: {
|
||||
type: String,
|
||||
default: '正常',
|
||||
},
|
||||
status: {
|
||||
type: [String, Number],
|
||||
default: 'default',
|
||||
},
|
||||
statusNames: {
|
||||
type: Object,
|
||||
},
|
||||
actions: {
|
||||
type: Array as PropType<TableActionsType[]>,
|
||||
default: () => [],
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hasMark: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
emit('click', props.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.card {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
.checked-icon {
|
||||
position: absolute;
|
||||
right: -22px;
|
||||
bottom: -22px;
|
||||
z-index: 2;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
color: #fff;
|
||||
background-color: red;
|
||||
background-color: #2f54eb;
|
||||
transform: rotate(-45deg);
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
transform: rotate(45deg);
|
||||
|
||||
> span {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-warp {
|
||||
position: relative;
|
||||
border: 1px solid #e6e6e6;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 24px rgba(#000, 0.1);
|
||||
|
||||
.card-mask {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
position: relative;
|
||||
border: 1px solid #2f54eb;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
position: relative;
|
||||
padding: 40px 12px 16px 30px;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 30px + 10px;
|
||||
display: block;
|
||||
width: 15%;
|
||||
min-width: 64px;
|
||||
height: 2px;
|
||||
// background-image: url('/images/rectangle.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
.card-header {
|
||||
// display: flex;
|
||||
position: absolute;
|
||||
left: 133px;
|
||||
top: 20px;
|
||||
// background: #5995f5;
|
||||
// width: calc(100% - 100px);
|
||||
width: calc(100% - 150px);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-item-avatar {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.card-item-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.card-state {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: -12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
padding: 2px 0;
|
||||
background-color: rgba(#5995f5, 0.15);
|
||||
transform: skewX(-45deg);
|
||||
|
||||
&.success {
|
||||
background-color: @success-color-deprecated-bg;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: rgba(#ff9000, 0.1);
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: rgba(#e50012, 0.1);
|
||||
}
|
||||
|
||||
.card-state-content {
|
||||
transform: skewX(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.card-item-content-title) {
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: @primary-color;
|
||||
width: calc(100% - 100px);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
:deep(.card-item-heard-name) {
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(.card-item-content-text) {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
background-color: rgba(#000, 0.5);
|
||||
visibility: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
.mask-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.item-active {
|
||||
position: relative;
|
||||
color: #2f54eb;
|
||||
|
||||
.checked-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-warp {
|
||||
border: 1px solid #2f54eb;
|
||||
}
|
||||
}
|
||||
|
||||
.card-tools {
|
||||
display: flex;
|
||||
margin-top: 8px;
|
||||
|
||||
.card-button {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
& > :deep(span, button) {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:deep(button) {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
background: #f6f6f6;
|
||||
border: 1px solid #e6e6e6;
|
||||
color: #2f54eb;
|
||||
|
||||
&:hover {
|
||||
background-color: @primary-color-hover;
|
||||
border-color: @primary-color-hover;
|
||||
|
||||
span {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: @primary-color-active;
|
||||
border-color: @primary-color-active;
|
||||
|
||||
span {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&.delete {
|
||||
flex-basis: 60px;
|
||||
flex-grow: 0;
|
||||
|
||||
:deep(button) {
|
||||
background: @error-color-deprecated-bg;
|
||||
border: 1px solid @error-color-outline;
|
||||
|
||||
span {
|
||||
color: @error-color !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: @error-color-hover;
|
||||
|
||||
span {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: @error-color-active;
|
||||
|
||||
span {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(button[disabled]) {
|
||||
background: @disabled-bg;
|
||||
border-color: @disabled-color;
|
||||
|
||||
span {
|
||||
color: @disabled-color !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: @disabled-active-bg;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: @disabled-active-bg;
|
||||
}
|
||||
}
|
||||
|
||||
// :deep(.ant-tooltip-disabled-compatible-wrapper) {
|
||||
// width: 100%;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,484 @@
|
|||
<template>
|
||||
<div>
|
||||
<j-advanced-search
|
||||
:columns="columns"
|
||||
target="search"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<j-pro-table
|
||||
ref="tableRef"
|
||||
model="CARD"
|
||||
:columns="columns"
|
||||
:gridColumn="2"
|
||||
:gridColumns="[1, 2]"
|
||||
:request="queryPoint"
|
||||
:defaultParams="{
|
||||
sorts: [{ name: 'id', order: 'desc' }],
|
||||
}"
|
||||
:params="params"
|
||||
:rowSelection="{
|
||||
selectedRowKeys: _selectedRowKeys,
|
||||
onChange: onSelectChange,
|
||||
}"
|
||||
@cancelSelect="cancelSelect"
|
||||
>
|
||||
<template #headerTitle>
|
||||
<j-space>
|
||||
<PermissionButton
|
||||
type="primary"
|
||||
@click="handlAdd"
|
||||
hasPermission="DataCollect/Collector:add"
|
||||
>
|
||||
<template #icon><AIcon type="PlusOutlined" /></template>
|
||||
{{ data?.provider === 'OPC_UA' ? '扫描' : '新增点位' }}
|
||||
</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"
|
||||
>
|
||||
<template #icon
|
||||
><AIcon type="FormOutlined"
|
||||
/></template>
|
||||
编辑
|
||||
</PermissionButton>
|
||||
</j-menu-item>
|
||||
<j-menu-item>
|
||||
<PermissionButton
|
||||
hasPermission="DataCollect/Collector:delete"
|
||||
>
|
||||
<template #icon
|
||||
><AIcon type="EditOutlined"
|
||||
/></template>
|
||||
删除
|
||||
</PermissionButton>
|
||||
</j-menu-item>
|
||||
</j-menu>
|
||||
</template>
|
||||
</j-dropdown>
|
||||
</j-space>
|
||||
</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())"
|
||||
>
|
||||
<!-- <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>
|
||||
<slot name="title">
|
||||
<div class="card-box-title">
|
||||
{{ slotProps.name }}
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
<template #action>
|
||||
<div class="card-box-action">
|
||||
<a><AIcon type="DeleteOutlined" /></a>
|
||||
<a><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">
|
||||
<span>--</span>
|
||||
<a><AIcon type="EditOutlined" /></a>
|
||||
<a><AIcon type="RedoOutlined" /></a>
|
||||
</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>
|
||||
<SaveModBus
|
||||
v-if="visibleSaveModBus"
|
||||
:data="current"
|
||||
@change="saveChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup name="PointPage">
|
||||
import type { ActionsType } from '@/components/Table/index.vue';
|
||||
import { getImage } from '@/utils/comm';
|
||||
import { queryPoint } from '@/api/data-collect/collector';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useMenuStore } from 'store/menu';
|
||||
import PointCardBox from './components/PointCardBox/index.vue';
|
||||
import { colorMap, getState } from '../data.ts';
|
||||
import SaveModBus from './Save/SaveModBus.vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
});
|
||||
|
||||
const menuStory = useMenuStore();
|
||||
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 visibleSaveModBus = ref(false);
|
||||
const current = ref({});
|
||||
const accessModesOption = ref();
|
||||
const _selectedRowKeys = ref<string[]>([]);
|
||||
|
||||
const accessModesMODBUS_TCP = [
|
||||
{
|
||||
label: '读',
|
||||
value: 'read',
|
||||
},
|
||||
{
|
||||
label: '写',
|
||||
value: 'write',
|
||||
},
|
||||
];
|
||||
|
||||
const accessModesOPC_UA = accessModesMODBUS_TCP.concat({
|
||||
label: '订阅',
|
||||
value: 'subscribe',
|
||||
});
|
||||
|
||||
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 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 = () => {
|
||||
visibleSaveModBus.value = true;
|
||||
current.value = {
|
||||
treeId: props.data.id,
|
||||
};
|
||||
};
|
||||
const handlEdit = (id: string) => {
|
||||
// menuStory.jumpPage(`media/Stream/Detail`, { id }, { view: false });
|
||||
};
|
||||
const handlEye = (id: string) => {
|
||||
// menuStory.jumpPage(`media/Stream/Detail`, { id }, { view: true });
|
||||
};
|
||||
|
||||
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) => i?.text).join(',');
|
||||
};
|
||||
const getInterval = (item: Partial<Record<string, any>>) => {
|
||||
const { interval } = item.configuration || '';
|
||||
return !!interval ? '采集频率' + interval + 'ms' : '';
|
||||
};
|
||||
|
||||
const getaccessModesOption = () => {
|
||||
return props.data?.provider !== 'MODBUS_TCP'
|
||||
? accessModesMODBUS_TCP.concat({
|
||||
label: '订阅',
|
||||
value: 'subscribe',
|
||||
})
|
||||
: accessModesMODBUS_TCP;
|
||||
};
|
||||
|
||||
const saveChange = (value: object) => {
|
||||
visibleSaveModBus.value = false;
|
||||
current.value = {};
|
||||
if (value) {
|
||||
handleSearch(params.value);
|
||||
// message.success('操作成功');
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectChange = (keys: string[]) => {
|
||||
_selectedRowKeys.value = [...keys];
|
||||
};
|
||||
|
||||
const cancelSelect = () => {
|
||||
_selectedRowKeys.value = [];
|
||||
};
|
||||
|
||||
const handleClick = (dt: any) => {
|
||||
if (_selectedRowKeys.value.includes(dt.id)) {
|
||||
const _index = _selectedRowKeys.value.findIndex((i) => i === dt.id);
|
||||
_selectedRowKeys.value.splice(_index, 1);
|
||||
} else {
|
||||
_selectedRowKeys.value = [..._selectedRowKeys.value, dt.id];
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(value) => {
|
||||
if (!!value) {
|
||||
accessModesOption.value =
|
||||
value.provider === 'MODBUS_TCP'
|
||||
? accessModesMODBUS_TCP
|
||||
: accessModesMODBUS_TCP.concat({
|
||||
label: '订阅',
|
||||
value: 'subscribe',
|
||||
});
|
||||
|
||||
params.value = {
|
||||
terms: [
|
||||
{
|
||||
terms: [
|
||||
{
|
||||
column: 'collectorId',
|
||||
value: value.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* @param params
|
||||
*/
|
||||
const handleSearch = (e: any) => {
|
||||
params.value = e;
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.card-box {
|
||||
min-width: 480px;
|
||||
a {
|
||||
color: #474747;
|
||||
}
|
||||
a:hover {
|
||||
color: #315efb;
|
||||
}
|
||||
.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: 10px;
|
||||
display: flex;
|
||||
.card-box-content-left {
|
||||
flex: 0.2;
|
||||
border-right: 1px solid #e0e4e8;
|
||||
height: 68px;
|
||||
padding-right: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.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>
|
|
@ -0,0 +1,278 @@
|
|||
<template lang="">
|
||||
<j-modal
|
||||
:title="data.id ? '编辑' : '新增'"
|
||||
:visible="true"
|
||||
width="700px"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<j-form
|
||||
class="form"
|
||||
layout="vertical"
|
||||
:model="formData"
|
||||
name="basic"
|
||||
autocomplete="off"
|
||||
>
|
||||
<j-form-item label="所属通道" v-bind="validateInfos.channelId">
|
||||
<j-select
|
||||
style="width: 100%"
|
||||
v-model:value="formData.channelId"
|
||||
:options="channelList"
|
||||
placeholder="请选择所属通道"
|
||||
allowClear
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
:disabled="!!id"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item label="采集器名称" v-bind="validateInfos.name">
|
||||
<j-input
|
||||
placeholder="请输入采集器名称"
|
||||
v-model:value="formData.name"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item
|
||||
label="从机地址"
|
||||
v-bind="validateInfos['configuration.unitId']"
|
||||
v-if="visibleUnitId"
|
||||
>
|
||||
<j-input-number
|
||||
style="width: 100%"
|
||||
placeholder="请输入从机地址"
|
||||
v-model:value="formData.configuration.unitId"
|
||||
:min="0"
|
||||
:max="255"
|
||||
/>
|
||||
</j-form-item>
|
||||
|
||||
<j-form-item v-bind="validateInfos['circuitBreaker.type']">
|
||||
<template #label>
|
||||
<span>
|
||||
故障处理
|
||||
<j-tooltip
|
||||
:title="
|
||||
getTypeTooltip(formData.circuitBreaker.type)
|
||||
"
|
||||
>
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="margin-left: 2px"
|
||||
/>
|
||||
</j-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<RadioCard
|
||||
layout="horizontal"
|
||||
:checkStyle="true"
|
||||
:options="[
|
||||
{ label: '降频', value: 'LowerFrequency' },
|
||||
{ label: '熔断', value: 'Break' },
|
||||
{ label: '忽略', value: 'Ignore' },
|
||||
]"
|
||||
v-model="formData.circuitBreaker.type"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item
|
||||
v-bind="validateInfos['configuration.endian']"
|
||||
v-if="visibleEndian"
|
||||
>
|
||||
<template #label>
|
||||
<span>
|
||||
高低位切换
|
||||
<j-tooltip title="统一配置所有点位的高低位切换">
|
||||
<AIcon
|
||||
type="QuestionCircleOutlined"
|
||||
style="margin-left: 2px"
|
||||
/>
|
||||
</j-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<RadioCard
|
||||
layout="horizontal"
|
||||
:checkStyle="true"
|
||||
:options="[
|
||||
{ label: 'AB', value: 'BIG' },
|
||||
{ label: 'BA', value: 'LITTLE' },
|
||||
]"
|
||||
v-model="formData.configuration.endian"
|
||||
/>
|
||||
</j-form-item>
|
||||
<j-form-item label="说明" v-bind="validateInfos.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 { Form } from 'ant-design-vue';
|
||||
import { save, update } from '@/api/data-collect/collector';
|
||||
import { Store } from 'jetlinks-store';
|
||||
|
||||
const loading = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
const channelListAll = ref();
|
||||
const channelList = ref();
|
||||
const visibleEndian = ref(false);
|
||||
const visibleUnitId = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const id = props.data.id;
|
||||
|
||||
const formData = ref({
|
||||
channelId: undefined,
|
||||
name: '',
|
||||
configuration: {
|
||||
unitId: '',
|
||||
type: 'LowerFrequency',
|
||||
endian: 'BIG',
|
||||
},
|
||||
circuitBreaker: {
|
||||
type: 'LowerFrequency',
|
||||
},
|
||||
description: '',
|
||||
});
|
||||
|
||||
const regOnlyNumber = new RegExp(/^\d+$/);
|
||||
|
||||
const { resetFields, validate, validateInfos } = useForm(
|
||||
formData,
|
||||
reactive({
|
||||
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) =>
|
||||
value === 'LowerFrequency'
|
||||
? '连续20次异常,降低连接频率至原有频率的1/10(重试间隔不超过1分钟),故障处理后自动恢复至设定连接频率'
|
||||
: value === 'Break'
|
||||
? '连续10分钟异常,停止采集数据进入熔断状态,设备重新启用后恢复采集状态'
|
||||
: '忽略异常,保持原采集频率超时时间为5s';
|
||||
|
||||
const handleOk = () => {
|
||||
onSubmit();
|
||||
};
|
||||
const handleCancel = () => {
|
||||
emit('change', false);
|
||||
};
|
||||
|
||||
const getChannelNoPaging = async () => {
|
||||
channelListAll.value = Store.get('channelListAll');
|
||||
channelList.value = channelListAll.value.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
}));
|
||||
};
|
||||
getChannelNoPaging();
|
||||
|
||||
watch(
|
||||
() => formData.value.channelId,
|
||||
(value) => {
|
||||
const dt = channelListAll.value.find((item) => item.id === value);
|
||||
visibleUnitId.value = visibleEndian.value =
|
||||
dt?.provider && dt?.provider === 'MODBUS_TCP';
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(value) => {
|
||||
if (value.id) formData.value = 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>
|
|
@ -10,10 +10,12 @@
|
|||
|
||||
<div class="add-btn">
|
||||
<PermissionButton
|
||||
type="primary"
|
||||
class="add-btn"
|
||||
@click="openDialog()"
|
||||
type="primary"
|
||||
@click="handlAdd()"
|
||||
hasPermission="DataCollect/Collector:add"
|
||||
>
|
||||
<template #icon><AIcon type="PlusOutlined" /></template>
|
||||
新增采集器
|
||||
</PermissionButton>
|
||||
</div>
|
||||
|
@ -21,18 +23,10 @@
|
|||
<a-tree
|
||||
:tree-data="defualtDataSource"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
:fieldNames="{ key: 'name' }"
|
||||
:fieldNames="{ key: 'id' }"
|
||||
v-if="defualtDataSource[0].children.length !== 0"
|
||||
@check="checkTree"
|
||||
>
|
||||
<!-- <a-tree
|
||||
:tree-data="defualtDataSource"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
:fieldNames="{ key: 'name' }"
|
||||
:height="600"
|
||||
v-if="defualtDataSource[0].children.length !== 0"
|
||||
@check="checkTree"
|
||||
> -->
|
||||
>
|
||||
<template #title="{ name, data }">
|
||||
<Ellipsis class="tree-left-title">
|
||||
{{ name }}
|
||||
|
@ -53,42 +47,43 @@
|
|||
:tooltip="{
|
||||
title: '编辑',
|
||||
}"
|
||||
@click="openDialog(data)"
|
||||
@click="handlEdit(data)"
|
||||
hasPermission="DataCollect/Collector:update"
|
||||
>
|
||||
<AIcon type="EditOutlined" />
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
v-if="data?.state?.value === 'disabled'"
|
||||
type="link"
|
||||
:tooltip="{
|
||||
title: '启用',
|
||||
title:
|
||||
data?.state?.value === 'disabled'
|
||||
? '启用'
|
||||
: '禁用',
|
||||
}"
|
||||
@click="openDialog(data)"
|
||||
hasPermission="DataCollect/Collector:update"
|
||||
@click="handlUpdate(data)"
|
||||
>
|
||||
<AIcon type="CheckCircleOutlined" />
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
v-if="data?.state?.value !== 'disabled'"
|
||||
type="link"
|
||||
:tooltip="{
|
||||
title: '禁用',
|
||||
}"
|
||||
@click="
|
||||
openDialog({
|
||||
...data,
|
||||
id: '',
|
||||
parentId: data.id,
|
||||
})
|
||||
"
|
||||
>
|
||||
<AIcon type="StopOutlined" />
|
||||
<AIcon
|
||||
:type="
|
||||
data?.state?.value === 'disabled'
|
||||
? 'CheckCircleOutlined'
|
||||
: 'StopOutlined'
|
||||
"
|
||||
/>
|
||||
</PermissionButton>
|
||||
<PermissionButton
|
||||
type="link"
|
||||
:tooltip="{ title: '删除' }"
|
||||
:disabled="data?.state?.value !== 'disabled'"
|
||||
:tooltip="{
|
||||
title:
|
||||
data?.state?.value !== 'disabled'
|
||||
? '正常的采集器不能删除'
|
||||
: '删除',
|
||||
}"
|
||||
hasPermission="DataCollect/Collector:delete"
|
||||
:popConfirm="{
|
||||
title: `确定要删除吗`,
|
||||
onConfirm: () => openDialog(data.id),
|
||||
title: `该操作将会删除下属点位,确定删除?`,
|
||||
onConfirm: () => handlDelete(data.id),
|
||||
}"
|
||||
>
|
||||
<AIcon type="DeleteOutlined" />
|
||||
|
@ -98,13 +93,22 @@
|
|||
</a-tree>
|
||||
<j-empty v-else description="暂无数据" />
|
||||
</a-spin>
|
||||
<Save v-if="visible" :data="current" @change="saveChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="TreePage">
|
||||
import type { TreeProps } from 'ant-design-vue';
|
||||
import { treeFilter } from '@/utils/comm';
|
||||
import { queryCollector } from '@/api/data-collect/collector';
|
||||
import {
|
||||
queryCollector,
|
||||
queryChannelNoPaging,
|
||||
update,
|
||||
remove,
|
||||
} from '@/api/data-collect/collector';
|
||||
import Save from './Save/index.vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { Store } from 'jetlinks-store';
|
||||
import _ from 'lodash';
|
||||
import { colorMap, getState } from '../data.ts';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
|
@ -116,16 +120,12 @@ const emits = defineEmits(['change']);
|
|||
|
||||
const route = useRoute();
|
||||
const channelId = route.query?.channelId;
|
||||
|
||||
const colorMap = new Map();
|
||||
colorMap.set('running', 'success');
|
||||
colorMap.set('partialError', 'warning');
|
||||
colorMap.set('failed', 'error');
|
||||
colorMap.set('stopped', 'default');
|
||||
|
||||
const spinning = ref(false);
|
||||
const selectedKeys = ref();
|
||||
const selectedKeys = ref([]);
|
||||
const searchValue = ref();
|
||||
const visible = ref(false);
|
||||
const current = ref({});
|
||||
const collectorAll = ref();
|
||||
|
||||
const defualtDataSource = ref([
|
||||
{
|
||||
|
@ -154,17 +154,49 @@ const defualtParams = {
|
|||
};
|
||||
const params = ref();
|
||||
|
||||
const openDialog = (row: any = {}) => {
|
||||
console.log(row);
|
||||
const handlAdd = () => {
|
||||
current.value = {};
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const checkTree = (value: any) => {
|
||||
console.log(22, value);
|
||||
const handlEdit = (data: object) => {
|
||||
current.value = _.cloneDeep(data);
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const handlUpdate = async (data: object) => {
|
||||
const state = data?.state?.value;
|
||||
const resp = await update(data?.id, {
|
||||
state: state !== 'disabled' ? 'disabled' : 'enabled',
|
||||
runningState: state !== 'disabled' ? 'stopped' : 'running',
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
handleSearch(params.value);
|
||||
message.success('操作成功');
|
||||
}
|
||||
};
|
||||
const handlDelete = async (id: string) => {
|
||||
const resp = await remove(id);
|
||||
if (resp.status === 200) {
|
||||
handleSearch(params.value);
|
||||
message.success('操作成功');
|
||||
}
|
||||
};
|
||||
|
||||
const saveChange = (value: object) => {
|
||||
visible.value = false;
|
||||
current.value = {};
|
||||
if (value) {
|
||||
handleSearch(params.value);
|
||||
message.success('操作成功');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (value: string) => {
|
||||
if (!!searchValue.value) {
|
||||
params.value = { ...defualtParams };
|
||||
if (!searchValue.value && !value) {
|
||||
params.value = _.cloneDeep(defualtParams);
|
||||
} else if (!!searchValue.value) {
|
||||
params.value = { ..._.cloneDeep(defualtParams) };
|
||||
params.value.terms[1] = {
|
||||
terms: [
|
||||
{
|
||||
|
@ -181,55 +213,37 @@ const handleSearch = async (value: string) => {
|
|||
const res = await queryCollector(params.value);
|
||||
if (res.status === 200) {
|
||||
defualtDataSource.value[0].children = res.result;
|
||||
collectorAll.value = res.result;
|
||||
if (selectedKeys.value.length === 0) {
|
||||
selectedKeys.value = [res?.result[0]?.id] || [];
|
||||
emits('change', res?.result[0]);
|
||||
}
|
||||
}
|
||||
spinning.value = false;
|
||||
};
|
||||
|
||||
const getState = (record: any) => {
|
||||
const enabled = record?.state?.value === 'enabled';
|
||||
if (record) {
|
||||
return enabled
|
||||
? {
|
||||
value: record?.runningState?.value,
|
||||
text: record?.runningState?.text,
|
||||
}
|
||||
: {
|
||||
value: 'processing',
|
||||
text: '禁用',
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
const getChannelNoPaging = async () => {
|
||||
const res = await queryChannelNoPaging();
|
||||
Store.set('channelListAll', res.result);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
handleSearch(defualtParams);
|
||||
handleSearch(_.cloneDeep(defualtParams));
|
||||
getChannelNoPaging();
|
||||
});
|
||||
|
||||
watch(selectedKeys, (n) => {
|
||||
emits('change', n[0]);
|
||||
const key = _.isArray(n) ? n[0] : n;
|
||||
const row = collectorAll.value.find((i) => i.id === key);
|
||||
emits('change', row);
|
||||
});
|
||||
// watch(
|
||||
// () => route.query,
|
||||
// (value) => {
|
||||
// if (value?.channelId) {
|
||||
// params.value = {
|
||||
// ...defualtParams,
|
||||
// terms: [
|
||||
// {
|
||||
// column: 'channelId',
|
||||
// value: value?.channelId,
|
||||
// },
|
||||
// ],
|
||||
// };
|
||||
|
||||
// handleSearch(params.value);
|
||||
// } else {
|
||||
// handleSearch(defualtParams);
|
||||
// }
|
||||
// },
|
||||
// { immediate: true, deep: true },
|
||||
// );
|
||||
watch(
|
||||
() => searchValue.value,
|
||||
(value) => {
|
||||
!value && handleSearch(value);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
@ -253,16 +267,13 @@ watch(selectedKeys, (n) => {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
.tree-left-title {
|
||||
width: 80px;
|
||||
// margin-left: -5px;
|
||||
}
|
||||
.tree-left-tag {
|
||||
width: 70px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.func-btns {
|
||||
// display: none;
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
export const colorMap = new Map();
|
||||
colorMap.set('running', 'success');
|
||||
colorMap.set('partialError', 'warning');
|
||||
colorMap.set('failed', 'error');
|
||||
colorMap.set('stopped', 'default');
|
||||
colorMap.set('processing', '#cccccc');
|
||||
|
||||
export const getState = (record: any) => {
|
||||
const enabled = record?.state?.value === 'enabled';
|
||||
if (record) {
|
||||
return {
|
||||
value: enabled ? record?.runningState?.value : 'processing',
|
||||
text: enabled ? record?.runningState?.text : '禁用',
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
};
|
|
@ -4,16 +4,21 @@
|
|||
<div class="left">
|
||||
<Tree @change="changeTree" />
|
||||
</div>
|
||||
<div class="right">right</div>
|
||||
<div class="right">
|
||||
<Point :data="data"></Point>
|
||||
</div>
|
||||
</div>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="CollectorPage">
|
||||
import Tree from './Tree/index.vue';
|
||||
import Point from './Point/index.vue';
|
||||
|
||||
const changeTree = (value: any) => {
|
||||
console.log(32, value);
|
||||
const data = ref();
|
||||
|
||||
const changeTree = (row: any) => {
|
||||
data.value = row;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -31,6 +36,7 @@ const changeTree = (value: any) => {
|
|||
margin: 10px;
|
||||
}
|
||||
.right {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,6 @@ import { useMenuStore } from 'store/menu';
|
|||
|
||||
const menuStory = useMenuStore();
|
||||
const tableRef = ref<Record<string, any>>({});
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const params = ref<Record<string, any>>({});
|
||||
|
||||
|
|
Loading…
Reference in New Issue