feat: 新增Bacnet数采;新增通道选择协议BacNet

* feat: 场景联动数组条件

* fix: bug#23054、22282

* fix: bug#23085

* feat: 新增Bacnet数采

* fix: bug#23160、23158

* fix: 网管子设备边缘端映射传值

* feat: 新增通道选择协议BacNet

* fix: 系统管理背景图上传bug

* fix: 校验条件设置为常量
This commit is contained in:
qiaochuLei 2024-04-01 16:22:02 +08:00 committed by GitHub
parent e40af1855b
commit f1aeeaed43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 3622 additions and 1167 deletions

View File

@ -69,3 +69,19 @@ export const getSnapTypes = () => server.get('/s7/client/s7codecs/list')
export const getArea = () => server.get('/s7/client/s7area/list')
export const exportTemplate = (provider: string, format: string) =>server.get(`/data-collect/point/${provider}/template.${format}`, {}, {responseType: 'blob'})
/**
* BACNet协议扫描对象
* @param channelId id
* @param instanceNumber
*/
export const getBacnetObjectList = (channelId: string, instanceNumber: string) => server.get(`/collect/bacnet/${channelId}/${instanceNumber}/objects`);
/**
* 使id
* @param data Id
*/
export const getBacnetPropertyIdNotUse = (data: any) => server.post(`/collect/bacnet/${data.collectorId}/unused/ids`, data)
/**查询bacnet值类型*/
export const getBacnetValueType = () => server.get(`/collect/bacnet/value/types`)

View File

@ -96,6 +96,7 @@ const handleRadio = (item: any) => {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 24px;
.disabled {
>div {
color: rgba(0, 0, 0, 0.25);
@ -105,7 +106,7 @@ const handleRadio = (item: any) => {
}
&-item {
width: 49%;
flex:1;
height: 70px;
padding: 10px 15px;
margin-bottom: 12px;
@ -113,7 +114,7 @@ const handleRadio = (item: any) => {
border-radius: 2px;
display: flex;
align-items: center;
gap: 24px;
cursor: pointer;
.img {
width: 32px;

View File

@ -40,7 +40,7 @@ const defaultOwnParams = [
column: "options"
}
],
type:'or'
type:'and'
}
]
}

View File

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

View File

@ -1,4 +1,4 @@
<template lang="">
<template>
<j-modal
:title="data.id ? '编辑' : '新增'"
:visible="true"
@ -94,14 +94,14 @@
</j-form-item>
<j-form-item
v-if="formData.provider === 'COLLECTOR_GATEWAY'"
:name="['configuration','deviceId']"
:rules="[{ required: true, message: '请选择网关设备'}]"
:name="['configuration', 'deviceId']"
:rules="[{ required: true, message: '请选择网关设备' }]"
label="选择网关设备"
>
<GateWayFormItem
v-model:name="formData.configuration.deviceName"
v-model:value="formData.configuration.deviceId"
/>
<GateWayFormItem
v-model:name="formData.configuration.deviceName"
v-model:value="formData.configuration.deviceId"
/>
</j-form-item>
<j-form-item
v-if="formData.provider === 'OPC_UA'"
@ -171,6 +171,50 @@
v-model:value="formData.configuration.password"
/>
</j-form-item>
<template v-if="formData.provider === 'BACNetIp'">
<j-form-item label="BACNet实例号" :name="['configuration', 'instanceNumber']" :rules="{
required: true,
trigger:'blur',
validator: validate,
}">
<j-input-number
style="width: 100%"
v-model:value="formData.configuration.instanceNumber"
:min="0"
:precision="0"
:max="999999999999"
placeholder="请输入BACNet实例号"
></j-input-number>
</j-form-item>
<j-form-item
label="网卡"
:name="['configuration', 'overIp', 'localBindAddress']"
:rules="{
required: true,
trigger:'blur',
message: '请选择网卡'
}"
>
<j-input
v-model:value="formData.configuration.overIp.localBindAddress"
>
</j-input>
</j-form-item>
<j-form-item label="广播端口" :name="['configuration', 'overIp', 'port']" :rules="{
required: true,
trigger: 'blur',
message: '请输入广播端口'
}">
<j-input-number
v-model:value="formData.configuration.overIp.port"
style="width: 100%"
:min="0"
:max="65535"
:precision="0"
placeholder="请输入广播端口"
></j-input-number>
</j-form-item>
</template>
<!-- <j-form-item
v-if="formData.provider === 'snap7'"
:name="['configuration', 'connect']"
@ -215,7 +259,7 @@ import type { FormInstance } from 'ant-design-vue';
import type { FormDataType } from '../type.d';
import { cloneDeep, isArray } from 'lodash-es';
import { protocolList } from '@/utils/consts';
import GateWayFormItem from "@/views/DataCollect/Channel/Save/GateWayFormItem.vue";
import GateWayFormItem from '@/views/DataCollect/Channel/Save/GateWayFormItem.vue';
const props = defineProps({
data: {
@ -242,20 +286,20 @@ const formData = ref<FormDataType>(cloneDeep(FormState));
const handleOk = async () => {
const params: any = await formRef.value?.validate();
if (params?.provider === 'COLLECTOR_GATEWAY') {
params.configuration.deviceName = formData.value.configuration.deviceName
params.configuration.deviceName =
formData.value.configuration.deviceName;
}
if(params?.provider === 'snap7'){
params.configuration={
connect : false
}
if (params?.provider === 'snap7') {
params.configuration = {
connect: false,
};
} else if (params?.provider === 'iec104') {
params.configuration = {}
params.configuration = {};
}
params.circuitBreaker = {
type: 'Ignore'
}
type: 'Ignore',
};
loading.value = true;
const response = !id
@ -266,6 +310,18 @@ const handleOk = async () => {
formRef.value?.resetFields();
};
const validate = async (_rule: any, value: string) => {
if (!value) {
return Promise.reject('请输入BACnet实例号');
} else {
const reg = new RegExp(/^[0-9]*$/)
if(!reg.test(value) || parseInt(value) < 0) {
return Promise.reject('请输入正确的BACnet实例号');
}
return Promise.resolve()
}
}
const handleCancel = () => {
emit('change', false);
formRef.value?.resetFields();
@ -317,8 +373,15 @@ const getProvidersList = async () => {
const resp: any = await getProviders();
if (resp.status === 200) {
const arr = resp.result
.filter(
(item: any) => ['GATEWAY', 'Modbus/TCP', 'opc-ua','snap7', 'IEC104'].includes(item.name),
.filter((item: any) =>
[
'GATEWAY',
'Modbus/TCP',
'opc-ua',
'snap7',
'IEC104',
'BACNet/IP',
].includes(item.name),
)
.map((it: any) => it.name);
const providers: any = protocolList.filter((item: any) =>
@ -337,7 +400,7 @@ watch(
() => props.data,
(value) => {
if (value.id) {
formData.value = value as FormDataType;
formData.value = value as FormDataType;
}
},
{ immediate: true, deep: true },

View File

@ -18,6 +18,11 @@ export const FormState: FormDataType = {
deviceId: undefined,
deviceName: undefined,
connect:false,
instanceNumber: undefined,
overIp: {
localBindAddress: '0.0.0.0',
port: 47808
}
},
description: '',
};

View File

@ -195,7 +195,7 @@ const columns = [
if (resp.status === 200) {
const arr = resp.result
.filter(
(item: any) => ['GATEWAY', 'Modbus/TCP', 'opc-ua'].includes(item.name),
(item: any) => ['GATEWAY', 'Modbus/TCP', 'opc-ua','snap7', 'IEC104','BACNetIp'].includes(item.name),
)
.map((it: any) => it.name);
const providers: any = protocolList.filter((item: any) =>

View File

@ -11,6 +11,11 @@ export interface ConfigurationType {
deviceId: string | undefined,
deviceName: string | undefined,
connect:boolean | undefined
instanceNumber?:number
overIp: {
localBindAddress: string,
port: number
}
}
export interface FormDataType {

View File

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

View File

@ -1,4 +1,4 @@
<template lang="">
<template>
<j-modal title="扫描" :visible="true" width="95%" @cancel="handleCancel">
<div class="content">
<Tree

View File

@ -0,0 +1,80 @@
<template>
<div>
<j-select v-model:value="propertyId" show-search style="width: 80%" placeholder="请选择">
<j-select-option
v-for="item in propertyIdListInUse"
:key="item"
:value="item"
>{{ item }}</j-select-option
>
</j-select>
</div>
</template>
<script setup lang="ts" name="propertyId">
import { getBacnetPropertyIdNotUse } from '@/api/data-collect/collector';
const props = defineProps({
value: {
type: String,
default: '',
},
collectorId: {
type: String,
default: '',
},
objectId: {
type: Object,
default: () => {},
},
checkedList: {
type: Array,
default: () => [],
},
index: {
type: Number,
default: 0,
},
valueTypeList: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update:value']);
const propertyId = computed({
get: () => {
return props.value;
},
set: (val) => {
emit('update:value', val);
}
})
const propertyIdList = ref<Record<string, any>[]>([]);
const getBacnetProperty = async () => {
const params = {
collectorId: props.collectorId,
objectId: props.objectId,
};
const res: any = await getBacnetPropertyIdNotUse(params);
if (res.success) {
propertyIdList.value = res.result;
}
};
getBacnetProperty();
const propertyIdListInUse = computed(() => {
const result = props.checkedList
.filter((item: any, index: number) => {
return (
item.nodeId ==
`${props.objectId.type}:${props.objectId.instanceNumber}` &&
props.index != index
);
})
.map((item: any) => item.propertyId);
return propertyIdList.value.filter((item: any) => {
return !result.includes(item);
});
});
</script>

View File

@ -0,0 +1,394 @@
<template>
<j-form class="table" ref="formTableRef" :model="modelRef">
<j-table
v-if="modelRef.dataSource.length !== 0"
:dataSource="modelRef.dataSource"
:columns="BacnetFormTableColumns"
:scroll="{ y: 580 }"
:pagination="false"
>
<template #headerCell="{ column }">
<template
v-if="column.key === 'nodeId' || column.key === 'action'"
>
<span> {{ column.title }} </span>
</template>
<template v-else>
<span> {{ column.title }} </span>
<span style="margin-left: 5px; color: red">*</span>
</template>
</template>
<template #bodyCell="{ column: { dataIndex }, record, index }">
<template v-if="dataIndex === 'name'">
<j-form-item
:name="['dataSource', index, 'name']"
:rules="[
{
required: true,
message: '请输入',
},
{
validator: checkLength,
trigger: 'change',
},
]"
>
<j-input
v-model:value="record[dataIndex]"
placeholder="请输入"
style="width: 80%"
allowClear
></j-input>
</j-form-item>
</template>
<template v-if="dataIndex === 'type'">
<j-form-item>
{{ record.objectId?.type }}
</j-form-item>
</template>
<template v-if="dataIndex === 'instanceNumber'">
<j-form-item>
{{ record.objectId?.instanceNumber }}
</j-form-item>
</template>
<template v-if="dataIndex === 'propertyId'">
<j-form-item
:name="['dataSource', index, 'propertyId']"
:rules="{
required: true,
message: '请选择',
}"
>
<PropertyId
v-model:value="record[dataIndex]"
:valueTypeList="valueTypeList"
:collectorId="collectorData.collectorId"
:objectId="record.objectId"
:checkedList="modelRef.dataSource"
:index="index"
></PropertyId>
</j-form-item>
</template>
<template v-if="dataIndex === 'valueType'">
<j-form-item
:name="['dataSource', index, 'valueType']"
:rules="{
required: true,
message: '请选择',
}"
>
<j-select
v-model:value="record[dataIndex]"
style="width: 80%"
placeholder="请选择"
>
<j-select-option
v-for="item in valueTypeList"
:key="item"
:value="item"
>{{ item }}</j-select-option
>
</j-select>
</j-form-item>
</template>
<template v-if="dataIndex === 'accessModes'">
<j-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
>
</j-form-item>
</template>
<template v-if="dataIndex === 'interval'">
<j-form-item
class="form-item"
:name="[
'dataSource',
index,
'configuration',
'interval',
'value',
]"
:rules="[
{
required: true,
message: '请输入',
},
{
pattern: regOnlyNumber,
message: '请输入0或者正整数',
},
]"
>
<j-input-number
style="width: 60%"
v-model:value="
record.configuration[dataIndex].value
"
placeholder="请输入"
allowClear
addon-after="ms"
:max="2147483647"
:min="0"
:disabled="
index !== 0 &&
record.configuration[dataIndex].check
"
@blur="changeValue(index, dataIndex)"
></j-input-number>
<j-checkbox
style="margin-left: 5px; margin-top: 5px"
v-show="index !== 0"
v-model:checked="
record.configuration[dataIndex].check
"
@click="changeCheckbox(index, dataIndex)"
>同上</j-checkbox
>
</j-form-item>
</template>
<template v-if="dataIndex === 'features'">
<j-form-item
class="form-item"
:name="['dataSource', index, 'features', 'value']"
:rules="[
{
required: true,
message: '请选择',
},
]"
>
<j-select
style="width: 40%"
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
>
</j-form-item>
</template>
<template v-if="dataIndex === 'action'">
<j-tooltip title="删除">
<j-popconfirm
title="确认删除"
@confirm="clickDelete(record.nodeId, index)"
>
<a style="color: red"
><AIcon type="DeleteOutlined"
/></a>
</j-popconfirm>
</j-tooltip>
</template>
</template>
</j-table>
<j-empty v-else style="margin-top: 10%" />
</j-form>
</template>
<script lang="ts" setup>
import { getBacnetValueType } from '@/api/data-collect/collector';
import { BacnetFormTableColumns, regOnlyNumber } from '../../data';
import { Rule } from 'ant-design-vue/lib/form';
import PropertyId from './PropertyId.vue';
const props = defineProps({
data: {
type: Array,
default: () => [],
},
collectorData: {
type: Object,
default: () => {},
},
});
const emits = defineEmits(['change']);
const formTableRef = ref();
const defaultType = ['accessModes', 'interval', 'features'];
const modelRef: any = reactive({
dataSource: [],
});
const checkLength = (_rule: Rule, value: string): Promise<any> =>
new Promise(async (resolve, reject) => {
if (value) {
return String(value).length > 64
? reject('最多可输入64个字符')
: resolve('');
} else {
resolve('');
}
});
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
};
const clickDelete = (value: string, index: number) => {
emits('change', value, index);
// todo
};
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 = async (index: number, type: string) => {
//Dom setTimeout await nextTick()
setTimeout(() => {
let startIndex = 0;
const { dataSource } = modelRef;
const currentCheck = getTargetData(index, type).check;
if (!currentCheck) return;
for (let i = index; i >= 0; i--) {
const preDataCheck = getTargetData(i, type).check;
if (!preDataCheck) {
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 valueTypeList = ref([]);
const getValueTypeData = async () => {
const res: any = await getBacnetValueType();
if (res.success) {
valueTypeList.value = res.result;
}
};
getValueTypeData();
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>
.table {
width: 100%;
min-width: 600px;
:deep(.ant-table-header) {
.ant-table-cell {
padding: 16px 5px;
}
}
:deep(.ant-table-tbody) {
.ant-table-cell {
padding: 24px 0 0 0;
}
.ant-table-cell-fix-right-first {
padding: 0 0 0 10px;
}
}
:deep(.ant-pagination) {
display: none;
}
}
.form-item {
display: flex;
}
</style>

View File

@ -0,0 +1,244 @@
<template>
<div class="tree-content">
<div class="tree-header">
<div>数据源</div>
<!-- <j-checkbox v-model:checked="isSelected">隐藏已有节点</j-checkbox> -->
</div>
{{ unSelectKeys }}
<j-spin :spinning="spinning">
<j-tree
v-if="!!treeData"
:tree-data="treeData"
v-model:checkedKeys="checkedKeys"
:selectable="false"
@check="onCheck"
:height="600"
>
<template #title="{ data, name, key }">
<j-space>
<j-ellipsis>
<span
:class="[
selectKeys.includes(key)
? 'tree-selected'
: 'tree-title',
]"
>
{{ name }}
</span>
</j-ellipsis>
<j-button type="link" @click="onCheck(data)">
<AIcon type="ArrowRightOutlined"/>
</j-button>
</j-space>
</template>
</j-tree>
<j-empty v-else style="margin-top: 22%" />
</j-spin>
</div>
</template>
<script lang="ts" setup>
import type { TreeProps } from 'ant-design-vue';
import {
scanOpcUAList,
queryPointNoPaging,
getBacnetObjectList,
} from '@/api/data-collect/collector';
import { cloneDeep } from 'lodash-es';
const props = defineProps({
data: {
type: Object,
default: () => {},
},
unSelectKeys: {
type: String,
default: '',
},
});
const emits = defineEmits(['change']);
const channelId = props.data?.channelId;
const instanceNumber = props.data?.configuration?.instanceNumber;
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 = async () => {
spinning.value = true
const resp: any = await getBacnetObjectList(channelId, instanceNumber).finally(() => spinning.value = false);
if (resp.status === 200) {
treeData.value = resp.result.map((item: any) => {
return {
...item,
key: item.id,
title: item.name,
};
});
}
};
const handleData = (arr: any): any[] => {
const data = arr.filter((item: any) => {
return (
(isSelected && !selectKeys.value.includes(item.id)) || !isSelected
);
});
return data.map((item: any) => {
if (item.children && item.children?.length) {
return {
...item,
children: handleData(item.children),
};
} else {
return item;
}
});
};
const onCheck = ( info: any) => {
const one: any = { ...info };
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,
},
nodeId: `${one?.objectId.type}:${one?.objectId.instanceNumber}` || '',
objectId: one?.objectId || {},
name: one?.name || '',
propertyId: one?.propertyId || '',
valueType: one?.valueType || '',
accessModes: {
value: last ? last?.accessModes?.value : one?.accessModes || [],
check: true,
},
type: one?.type,
configuration: {
...one?.configuration,
interval: {
value: last
? last?.configuration?.interval?.value
: one?.configuration?.interval || 3000,
check: true,
},
nodeId: one?.id,
},
};
emits('change', item);
};
const updateTreeData = (list: any, key: string, children: any[]): any[] => {
const arr = list.map((node: any) => {
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 () => {
const res: any = await queryPointNoPaging({
paging: false,
terms: [
{
terms: [
{
column: 'collectorId',
value: props.data?.id,
},
],
},
],
});
if (res.status === 200) {
selectKeys.value = res.result.map((item: any) => item.pointKey);
}
// getScanOpcUAList();
};
onMounted(() => {
getPoint();
onLoadData();
});
const getScanOpcUAList = async () => {
spinning.value = true;
const res: any = await getBacnetObjectList(channelId, instanceNumber);
treeAllData.value = res.result.map((item: any) => ({
...item,
key: item.id,
title: item.name,
disabled: item?.folder || false,
}));
spinning.value = false;
};
// getScanOpcUAList();
watch(
() => isSelected.value,
(value) => {
treeData.value = value
? handleData(treeAllData.value)
: treeAllData.value;
},
{ deep: true },
);
watch(
() => treeAllData.value,
(value) => {
treeData.value = isSelected.value ? handleData(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;
min-width: 180px;
.tree-header {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
}
.tree-selected {
padding: 2px 5px;
background-color: #d6e4ff;
border-radius: 2px;
}
.tree-title {
color: black;
}
}
</style>

View File

@ -0,0 +1,105 @@
<template>
<j-modal title="扫描" :visible="true" width="95%" @cancel="handleCancel">
<div class="content">
<Tree
:data="treeData"
class="tree"
@change="changeTree"
:unSelectKeys="unSelectKeys"
></Tree>
<Table
:data="tableData"
:collectorData="treeData"
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 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<any[]>([]);
const tableDataMap = new Map();
const unSelectKeys = ref();
const handleOk = async () => {
const data: any = await formTableRef.value?.validate().catch(() => {});
if (!data) return;
const list: any = data.map((item: any) => {
return {
name: item.name,
provider: 'BACNetIp',
collectorId: props.data?.id,
collectorName: props.data?.name,
pointKey: item.id,
configuration: {
interval: item.configuration?.interval?.value,
valueType: item.valueType,
propertyId: item.propertyId,
objectId: item.objectId,
},
pointKey: `${item.objectId.type}:${item.objectId.instanceNumber}:${item.propertyId}`,
features: !item.features?.value ? [] : ['changedOnly'],
accessModes: item.accessModes?.value || [],
};
});
loading.value = true;
const resp = await savePointBatch([...list]).catch(() => {});
emit('change', resp?.status === 200);
loading.value = false;
};
const handleCancel = () => {
emit('change', false);
};
const changeTree = (row: any) => {
tableData.value.push(row)
};
const changeTable = (value: string, index: number) => {
tableData.value.splice(index, 1)
};
</script>
<style lang="less" scoped>
.content {
display: flex;
min-height: 600px;
.tree {
width: 300px;
}
.table {
flex: 1;
}
}
</style>

View File

@ -8,7 +8,7 @@
>
<div class="sizeText">
将批量修改
{{ data.length }} 条数据的访问类型采集频率只推送变化的数据
{{ data.length }} 条数据的{{ labelName.join(',') }}
</div>
<j-form
class="form"
@ -18,6 +18,15 @@
autocomplete="off"
ref="formRef"
>
<j-form-item label="值类型" v-if="provider === 'BACNetIp'">
<j-select
v-model:value="formData.valueType"
allowClear
placeholder="请选择值类型"
>
<j-select-option v-for="item in bacnetValueType" :key="item" :value="item">{{ item }}</j-select-option>
</j-select>
</j-form-item>
<j-form-item label="访问类型" name="accessModes">
<j-card-select
multiple
@ -86,7 +95,7 @@
</template>
<script lang="ts" setup>
import type { FormInstance } from 'ant-design-vue';
import { savePointBatch } from '@/api/data-collect/collector';
import { savePointBatch, getBacnetValueType } from '@/api/data-collect/collector';
import { cloneDeep, isObject } from 'lodash-es';
import { regOnlyNumber } from '../../../data';
@ -95,6 +104,10 @@ const props = defineProps({
type: Array,
default: () => [],
},
provider: {
type: String,
default: '',
}
});
const emit = defineEmits(['change']);
@ -105,16 +118,28 @@ const formData = ref({
accessModes: [],
interval: undefined,
features: [],
valueType: undefined
});
const bacnetValueType = ref<string[]>([])
const getIdAndType = async () => {
const resp: any = await getBacnetValueType()
if(resp.success) {
bacnetValueType.value = resp.result
}
}
const handleOk = async () => {
const data = cloneDeep(formData.value);
const { accessModes, features, interval } = data;
const { accessModes, features, interval, valueType } = data;
const ischange =
accessModes.length !== 0 ||
features.length !== 0 ||
Number(interval) === 0 ||
!!interval;
!!interval || !!valueType;
if (ischange) {
const params = cloneDeep(props.data);
params.forEach((i: any) => {
@ -139,6 +164,12 @@ const handleOk = async () => {
interval: data.interval,
};
}
if(data.valueType) {
i.configuration = {
...i.configuration,
valueType: data.valueType
}
}
});
loading.value = true;
const response = await savePointBatch(params).catch(() => {});
@ -152,6 +183,32 @@ const handleOk = async () => {
const handleCancel = () => {
emit('change', false);
};
watch(() => props.provider, () => {
if(props.provider === 'BACNetIp') {
getIdAndType()
}
}, { immediate: true });
const labelName = computed(() => {
const arr = [];
if (formData.value.accessModes.length) {
arr.push('访问类型');
}
if (!!formData.value.interval) {
arr.push('采集频率');
}
if (formData.value.features.length) {
arr.push('只推送变化的数据');
}
if (formData.value.type) {
arr.push('数据类型');
}
if (formData.value.valueType) {
arr.push('值类型');
}
return arr;
});
</script>
<style lang="less" scoped>

View File

@ -42,7 +42,7 @@
批量导入
</PermissionButton>
<PermissionButton
v-if="data?.provider === 'OPC_UA'"
v-if="data?.provider === 'OPC_UA' || data?.provider === 'BACNetIp'"
type="primary"
@click="handlScan"
hasPermission="DataCollect/Collector:add"
@ -53,8 +53,7 @@
扫描
</PermissionButton>
<j-dropdown
v-if="data?.provider === 'OPC_UA'"
:trigger="['click']"
v-if="data?.provider === 'OPC_UA' || data?.provider === 'BACNetIp'"
>
<j-button @click.prevent="clickBatch"
>批量操作 <AIcon type="DownOutlined"
@ -92,7 +91,7 @@
</j-dropdown>
</j-space>
<div
v-if="data?.provider === 'OPC_UA'"
v-if="data?.provider === 'OPC_UA' || data?.provider === 'BACNetIp'"
style="margin-top: 15px"
>
<j-checkbox
@ -321,11 +320,14 @@
<BatchUpdate
v-if="visible.batchUpdate"
:data="current"
:provider="data.provider"
@change="saveChange"
/>
<SaveS7 v-if="visible.saveS7" :data="current" @change="saveChange"/>
<SaveIEC104 v-if="visible.saveIEC104" :data="current" @change="saveChange"/>
<SaveBACNet v-if="visible.saveBACNet" :data="current" @change="saveChange"/>
<Scan v-if="visible.scan" :data="current" @change="saveChange" />
<ScanBacnet v-if="visible.scanBacnet" :data="current" @change="saveChange" />
<Import v-if="visible.import" :data="current" @close-import="closeImport"/>
</j-spin>
</template>
@ -346,6 +348,8 @@ import BatchUpdate from './components/BatchUpdate/index.vue';
import SaveModBus from './Save/SaveModBus.vue';
import SaveOPCUA from './Save/SaveOPCUA.vue';
import Scan from './Scan/index.vue';
import ScanBacnet from './ScanBacnet/index.vue';
import SaveBACNet from './Save/SaveBACNet.vue';
import { colorMap } from '../data';
import { cloneDeep, isBoolean, isNumber, throttle } from 'lodash-es';
import { getWebSocket } from '@/utils/websocket';
@ -385,7 +389,9 @@ const visible = reactive({
scan: false,
saveS7:false,
import:false,
saveIEC104: false
saveIEC104: false,
scanBacnet: false,
saveBACNet: false,
});
const current: any = ref({});
const accessModesOption = ref();
@ -527,12 +533,14 @@ const handlEdit = (data: any) => {
visible.saveS7 = true
} else if(data?.provider === 'iec104') {
visible.saveIEC104 = true
} else if (data?.provider === 'BACNetIp') {
visible.saveBACNet = true
} else {
visible.saveModBus = true;
}
current.value = cloneDeep({
...data,
deviceType:props.data?.configuration.type,
deviceType:props.data?.configuration?.type || props.data?.configuration?.valueType,
});
};
@ -563,7 +571,11 @@ const handlBatchUpdate = () => {
visible.batchUpdate = true;
};
const handlScan = () => {
visible.scan = true;
if(props.data?.provider === 'OPC_UA'){
visible.scan = true;
} else if(props.data?.provider === 'BACNetIp'){
visible.scanBacnet = true
}
current.value = cloneDeep(props.data);
};
const handleImport = () =>{
@ -657,7 +669,7 @@ const cancelSelect = () => {
};
const handleClick = (dt: any) => {
if (props.data?.provider !== 'OPC_UA') return;
if (props.data?.provider !== 'OPC_UA' && props.data?.provider !== 'BACNetIp') return;
if (_selectedRowKeys.value.includes(dt.id)) {
const _index = _selectedRowKeys.value.findIndex((i) => i === dt.id);
_selectedRowKeys.value.splice(_index, 1);

View File

@ -1,4 +1,4 @@
<template lang="">
<template>
<j-modal
:title="data.id ? '编辑' : '新增'"
:visible="true"
@ -112,6 +112,32 @@
:max="255"
/>
</j-form-item>
<template v-if="provider === 'BACNetIp'">
<j-form-item
label="设备实例号"
:name="['configuration', 'instanceNumber']"
:rules="[{ required: true, trigger: 'change' }]"
>
<j-input
type="number"
style="width: 100%"
v-model:value="formData.configuration.instanceNumber"
placeholder="请输入设备实例号"
:maxlength="64"
:disabled="route.query.id ? true : false"
/>
</j-form-item>
<j-form-item label="地址" :name="['configuration', 'address']">
<j-input
style="width: 100%"
v-model:value="formData.configuration.address"
:maxlength="64"
type="tel"
placeholder="请输入地址"
>
</j-input>
</j-form-item>
</template>
<j-form-item
v-if="provider !== 'COLLECTOR_GATEWAY'"
:name="['configuration', 'inheritBreakerSpec', 'type']"
@ -222,6 +248,8 @@ import type { FormInstance } from 'ant-design-vue';
import {cloneDeep, omit} from "lodash-es";
import {protocolList} from "@/utils/consts";
const route = useRoute()
const loading = ref(false);
const visibleEndian = ref(false);
const visibleUnitId = ref(false);

View File

@ -317,4 +317,59 @@ export const FormTableColumns = [
},
];
export const BacnetFormTableColumns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 140,
fixed: 'left',
},
{
title: '对象类型',
dataIndex: 'type',
key: 'type',
},
{
title: '对象号',
dataIndex: 'instanceNumber',
key: 'instanceNumber',
},
{
title: '属性ID',
dataIndex: 'propertyId',
key: 'propertyId',
width: 200,
},
{
title: '值类型',
dataIndex: 'valueType',
key: 'valueType',
width: 200,
},
{
title: '访问类型',
dataIndex: 'accessModes',
key: 'accessModes',
width: 260,
},
{
title: '采集频率',
key: 'interval',
dataIndex: 'interval',
width: 200,
},
{
title: '只推送变化的数据',
key: 'features',
dataIndex: 'features',
width: 140,
},
{
title: '操作',
key: 'action',
dataIndex: 'action',
fixed: 'right',
width: 50,
},
]

View File

@ -8,6 +8,7 @@
visible
@ok="submitData"
@cancel="close"
:confirmLoading="loading"
okText="确定"
cancelText="取消"
v-bind="layout"
@ -86,6 +87,7 @@ const arr = ref([]);
const updateObj = ref({});
const addObj = ref({});
const addParams = ref({});
const loading = ref(false)
/**
* 表单数据
*/
@ -118,6 +120,7 @@ const { resetFields, validate, validateInfos } = useForm(
*/
const submitData = async () => {
formRef.value.validate().then(async () => {
loading.value = true
addParams.value = {};
if (props.isAdd === 0) {
if (props.isChild === 1) {
@ -139,7 +142,9 @@ const submitData = async () => {
// sortIndex: arr.value[arr.value.length - 1].sortIndex + 1,
};
}
const res = await saveTree(addParams.value);
const res = await saveTree(addParams.value).finally(() => {
loading.value = false
})
if (res.status === 200) {
onlyMessage('操作成功!');
visible.value = false;
@ -155,7 +160,9 @@ const submitData = async () => {
key: updateObj.value.key,
parentId: updateObj.value.parentId,
};
const res = await updateTree(id, updateParams);
const res = await updateTree(id, updateParams).finally(()=>{
loading.value = false
})
if (res.status === 200) {
onlyMessage('操作成功!');
visible.value = false;

View File

@ -90,6 +90,7 @@ const title = ref('');
const isAdd = ref(0);
const isChild = ref(0);
const tableLoading = ref(false);
const addSortId = ref()
//
const query = reactive({
columns: [
@ -182,6 +183,7 @@ const getActions = (
}
nextTick(() => {
modifyRef.value.show(data);
addSortId.value = data.id
});
},
},
@ -257,6 +259,10 @@ const table = reactive({
* 刷新表格数据
*/
refresh: () => {
if(isAdd.value === 0 && isChild.value !==3){
expandedRowKeys.value.push(addSortId.value);
}
console.log(expandedRowKeys.value)
tableRef.value.reload();
},
});

View File

@ -23,8 +23,12 @@
</template>
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'metadataName'">
<span v-if="record.metadataName">{{ record.metadataName }}</span>
<span v-else style="color: red;">{{ record.metadataId }}</span>
<span v-if="record.metadataName">{{
record.metadataName
}}</span>
<span v-else style="color: red">{{
record.metadataId
}}</span>
</template>
<template v-if="column.dataIndex === 'channelId'">
<j-form-item
@ -96,11 +100,7 @@
status="success"
text="启用"
/>
<j-badge
v-else
status="warning"
text="禁用"
/>
<j-badge v-else status="warning" text="禁用" />
</template>
<j-badge v-else status="error" text="未绑定" />
</template>
@ -251,7 +251,7 @@ const columns = [
const permissionStore = usePermissionStore();
const data:any = ref([])
const data: any = ref([]);
const isPermission = permissionStore.hasPermission('device/Instance:update');
const current = ref<number>(1);
@ -292,19 +292,18 @@ const getChannel = async () => {
const handleSearch = async () => {
loading.value = true;
const _metadataMap = new Map ()
const _metadataMap = new Map();
const _metadata: any[] = metadata.properties.map((item: any) => {
const value = {
metadataId: item.id,
metadataName: `${item.name}(${item.id})`,
metadataType: 'property',
name: item.name,
}
_metadataMap.set(item.id,value)
return value
};
_metadataMap.set(item.id, value);
return value;
});
if (_metadata && _metadata.length) {
const resp: any = await getEdgeMap(
instanceStore.current?.parentId || '',
@ -331,11 +330,19 @@ const handleSearch = async () => {
// }
// })
resp.result?.[0].forEach((item:any)=>{
_metadataMap.has(item.metadataId) ? _metadataMap.set(item.metadataId,Object.assign(_metadataMap.get(item.metadataId),item)) : _metadataMap.set(item.metadataId,item)
})
data.value = [..._metadataMap.values()]
onPageChange()
resp.result?.[0]?.forEach((item: any) => {
_metadataMap.has(item.metadataId)
? _metadataMap.set(
item.metadataId,
Object.assign(
_metadataMap.get(item.metadataId),
item,
),
)
: _metadataMap.set(item.metadataId, item);
});
data.value = [..._metadataMap.values()];
onPageChange();
}
}
loading.value = false;
@ -372,25 +379,21 @@ const onChannelChange = (_index: number, type: 'collector' | 'channel') => {
}
};
onMounted(() => {
getChannel();
handleSearch();
});
const onPageChange = () => {
const _cur = current.value >= 1 ? current.value : 1;
const _pageSize = pageSize.value
const array = data.value.slice((_cur - 1) * _pageSize, _cur * _pageSize) || [];
modelRef.dataSource = array;
formRef.value?.validate().then(() => {
const _cur = current.value >= 1 ? current.value : 1;
const _pageSize = pageSize.value;
const array =
data.value.slice((_cur - 1) * _pageSize, _cur * _pageSize) || [];
modelRef.dataSource = array;
});
};
const onSave = () => {
formRef.value
.validate()
.then(async (_data: any) => {
const arr = toRaw(modelRef).dataSource.filter(
(i: any) => i.channelId,
);
const arr = toRaw(data.value).filter((i: any) => i.channelId);
if (arr && arr.length !== 0) {
const submitData = {
deviceId: instanceStore.current.id,
@ -469,6 +472,26 @@ const onRefresh = async () => {
}
loading.value = false;
};
watch(
() => modelRef.dataSource,
(val) => {
const dataMap = new Map();
val.forEach((item: any) => {
dataMap.set(item.metadataId, item);
});
data.value.forEach((item: any, index: number) => {
dataMap.has(item.metadataId) ? (data.value[index] = item) : '';
});
},
{
deep: true,
},
);
onMounted(() => {
getChannel();
handleSearch();
});
</script>
<style lang="less" scoped>

View File

@ -143,15 +143,15 @@ const instanceStore = useInstanceStore();
const visible = ref<boolean>(false);
const config = ref<ConfigMetadata[]>([]);
watchEffect(() => {
if (instanceStore.current.id) {
getConfigMetadata(instanceStore.current.id).then((resp) => {
watch(()=>instanceStore.current.id,(val) => {
if (val) {
getConfigMetadata(val).then((resp) => {
if (resp.status === 200) {
config.value = resp?.result as ConfigMetadata[];
}
});
}
});
},{ immediate: true });
const isExit = (property: string) => {
return (

View File

@ -3,7 +3,6 @@
:columns="columns"
target="device-instance-log"
@search="handleSearch"
type="simple"
class="device-log-search"
/>
<JProTable
@ -13,7 +12,7 @@
model="TABLE"
:defaultParams="{ sorts: [{ name: 'timestamp', order: 'desc' }] }"
:params="params"
:bodyStyle="{ padding: 0 }"
:bodyStyle="{ padding: 0 , minHeight: 'auto' }"
>
<template #type="slotProps">
{{ slotProps?.type?.text }}
@ -144,7 +143,7 @@ const handleSearch = (_params: any) => {
};
</script>
<style lang="less">
<style lang="less" scoped>
.device-log-search {
padding: 0;
}

View File

@ -1,7 +1,7 @@
<template>
<pro-search
class="device-running-search"
type="simple"
:columns="columns"
target="device-instance-running-events"
@search="handleSearch"
@ -24,7 +24,7 @@
</template>
</JProTable>
<j-modal :width="600" v-model:visible="visible" title="详情" class="device-running-event-modal">
<JsonViewer :value="info" />
<JsonViewer :value="info" style="max-height: calc(100vh - 400px);overflow: auto;"/>
<template #footer>
<j-button type="primary" @click="visible = false">关闭</j-button>
</template>

View File

@ -60,9 +60,9 @@ const instanceStore = useInstanceStore();
const options = ref({});
const _type = computed(() => {
const flag = list.includes(prop.data?.valueType?.type || '')
cycle.value = flag ? '*' : '1m'
return flag
const flag = list.includes(prop.data?.valueType?.type || '');
cycle.value = flag ? '*' : '1m';
return flag;
});
const queryChartsAggList = async () => {
@ -81,11 +81,9 @@ const queryChartsAggList = async () => {
from: prop.time[0],
to: prop.time[1],
},
}).finally(
()=>{
}).finally(() => {
loading.value = false;
}
)
});
if (resp.status === 200) {
const dataList: any[] = [
{
@ -130,19 +128,11 @@ const queryChartsList = async () => {
],
sorts: [{ name: 'timestamp', order: 'asc' }],
},
).finally(
()=>{
).finally(() => {
loading.value = false;
}
)
});
if (resp.status === 200) {
const dataList: any[] = [
{
year: prop.time[0],
value: undefined,
type: prop.data?.name || '',
},
];
const dataList: any[] = [];
(resp.result as any)?.forEach((i: any) => {
dataList.push({
...i,
@ -151,12 +141,27 @@ const queryChartsList = async () => {
type: prop.data?.name || '',
});
});
dataList.push({
year: prop.time[1],
value: undefined,
type: prop.data?.name || '',
const beginTimeExist = dataList.find((i: any) => {
return i.year === prop.time[0];
});
chartsList.value = dataList || [];
const endTimeExist = dataList.find((i: any) => {
return i.year === prop.time[1];
});
if (!beginTimeExist) {
dataList.unshift({
year: prop.time[0],
value: undefined,
type: prop.data?.name || '',
});
}
if (!endTimeExist) {
dataList.push({
year: prop.time[1],
value: undefined,
type: prop.data?.name || '',
});
chartsList.value = dataList || [];
}
}
};
@ -191,7 +196,7 @@ const getOptions = (arr: any[]) => {
tooltip: {
trigger: 'axis',
position: function (pt: any) {
const left = pt[0] - 80
const left = pt[0] - 80;
return [left, '10%'];
},
},

View File

@ -193,8 +193,10 @@ const getStatus = (id: string) => {
{
deviceId: id,
},
).subscribe(() => {
instanceStore.refresh(id);
).subscribe((message:any) => {
if(message.payload?.value?.type !== instanceStore.current?.state.value){
instanceStore.refresh(id);
}
});
};

View File

@ -826,8 +826,9 @@ const updateAccessData = async (id: string, values: any) => {
id: id,
configuration: { ...extra },
storePolicy: storePolicy,
});
submitLoading.value = false
}).finally(()=>{
submitLoading.value = false
})
if (resp.status === 200) {
onlyMessage('操作成功!');
productStore.current!.storePolicy = storePolicy;

View File

@ -33,7 +33,7 @@
</j-form-item>
</j-col>
<j-col flex="auto">
<j-form-item name="id">
<j-form-item name="id" :validateFirst="true">
<template #label>
<span>ID</span>
<j-tooltip
@ -148,10 +148,10 @@ const photoValue = ref('/images/device-product.png');
const imageTypes = reactive([
'image/jpeg',
'image/png',
'image/jpg',
// 'image/jpg',
'image/jfif',
'image/pjp',
'image/pjpeg',
// 'image/pjpeg',
]);
const deviceList = ref([
{
@ -188,7 +188,6 @@ const form = reactive({
*/
const validateInput = async (_rule: Rule, value: string) => {
if (value) {
console.log(value.split('').length);
if (!isInput(value)) {
return Promise.reject('请输入英文或者数字或者-或者_');
} else {

View File

@ -70,7 +70,11 @@
</slot>
</template>
<template #content>
<Ellipsis style="width: calc(100% - 100px); margin-bottom: 18px;"
<Ellipsis
style="
width: calc(100% - 100px);
margin-bottom: 18px;
"
><span
style="font-weight: 600; font-size: 16px"
>
@ -191,7 +195,7 @@ import {
updateDevice,
} from '@/api/device/product';
import { isNoCommunity, downloadObject } from '@/utils/utils';
import { omit , cloneDeep } from 'lodash-es';
import { omit, cloneDeep } from 'lodash-es';
import { typeOptions } from '@/components/Search/util';
import Save from './Save/index.vue';
import { useMenuStore } from 'store/menu';
@ -260,9 +264,7 @@ const columns = [
ellipsis: true,
},
];
const permission = usePermissionStore().hasPermission(
`device/Product:import`,
);
const permission = usePermissionStore().hasPermission(`device/Product:import`);
const _selectedRowKeys = ref<string[]>([]);
const currentForm = ref({});
@ -318,7 +320,7 @@ const getActions = (
'accessProvider',
'messageProtocol',
]);
downloadObject(extra, data.name+'产品');
downloadObject(extra, data.name + '产品');
},
},
{
@ -397,31 +399,31 @@ const beforeUpload = (file: any) => {
onlyMessage('请上传json格式文件', 'error');
return false;
}
if(!text){
onlyMessage('文件内容不能为空','error')
if (!text) {
onlyMessage('文件内容不能为空', 'error');
return false;
}
const data = JSON.parse(text);
//
data.state = 0;
if (Array.isArray(data)) {
onlyMessage('请上传正确格式文件', 'error');
return false;
}
delete data.state;
if(!data?.name){
data.name = "产品" + Date.now();
}
if(!data?.deviceType || JSON.stringify(data?.deviceType) === '{}' ){
onlyMessage('缺少deviceType字段或对应的值','error')
return false
}
const res = await updateDevice(data);
if (res.status === 200) {
onlyMessage('操作成功');
tableRef.value?.reload();
}
return true;
const data = JSON.parse(text);
//
data.state = 0;
if (Array.isArray(data)) {
onlyMessage('请上传正确格式文件', 'error');
return false;
}
delete data.state;
if (!data?.name) {
data.name = '产品' + Date.now();
}
if (!data?.deviceType || JSON.stringify(data?.deviceType) === '{}') {
onlyMessage('缺少deviceType字段或对应的值', 'error');
return false;
}
const res = await updateDevice(data);
if (res.status === 200) {
onlyMessage('操作成功');
tableRef.value?.reload();
}
return true;
};
return false;
};
@ -471,7 +473,13 @@ const query = reactive({
return new Promise((resolve) => {
getProviders().then((resp: any) => {
const data = resp.result || [];
resolve(accessConfigTypeFilter(data));
resolve(accessConfigTypeFilter(data).filter((i: any) => {
return (
i.id !== 'modbus-tcp' &&
i.id !== 'opc-ua'
);
}));
});
});
},
@ -548,7 +556,7 @@ const query = reactive({
},
},
{
title: '分类',
title: '产品分类',
key: 'classified',
dataIndex: 'classifiedId',
search: {
@ -571,6 +579,7 @@ const query = reactive({
search: {
first: true,
type: 'treeSelect',
termOptions:['eq'],
options: async () => {
return new Promise((res) => {
queryOrgThree({ paging: false }).then((resp: any) => {
@ -614,33 +623,40 @@ const query = reactive({
});
const saveRef = ref();
const handleSearch = (e: any) => {
const newTerms = cloneDeep(e);
if (newTerms.terms?.length) {
newTerms.terms.forEach((a: any) => {
a.terms = a.terms.map((b: any) => {
if (b.column === 'id$dim-assets') {
const value = b.value;
b = {
column: 'id',
termType: 'dim-assets',
value: {
assetType: 'product',
targets: [
{
type: 'org',
id: value,
},
],
},
};
}
if(b.column === 'accessProvider'){
if(b.value === 'collector-gateway'){
b.termType = b.termType === 'eq' ? 'in' : 'nin';
b.value = ['opc-ua','modbus-tcp','collector-gateway'];
}else if(Array.isArray(b.value) && b.value.includes('collector-gateway')){
b.value = ['opc-ua','modbus-tcp',...b.value];
}
}
return b;
});
});
}
const newTerms = cloneDeep(e)
if (newTerms.terms?.length) {
newTerms.terms.forEach((a : any) => {
a.terms = a.terms.map((b: any) => {
if (b.column === 'id$dim-assets') {
const value = b.value
b = {
column: 'id',
termType: 'dim-assets',
value: {
assetType: 'product',
targets: [
{
type: 'org',
id: value,
},
],
},
}
}
return b
})
})
}
params.value = newTerms;
params.value = newTerms;
};
const routerParams = useRouterParams();
onMounted(() => {

View File

@ -63,8 +63,12 @@ export const validatorConfig = (value: any, _isObject: boolean = false) => {
export const handleTypeValue = (type:string, value: any = {}) => {
let obj: any = {}
switch (type) {
//bug#22609
case 'array':
obj.elementType = value
obj.elementType = {
type: 'object',
properties: []
}
break;
case 'object':
obj.properties = (value || []).map((item: any) => {

File diff suppressed because it is too large Load Diff

View File

@ -154,9 +154,9 @@
</template>
<template #registryTime="slotProps">
<span>{{
dayjs(slotProps.registryTime).format(
slotProps.registryTime ? dayjs(slotProps.registryTime).format(
'YYYY-MM-DD HH:mm:ss',
)
) : ''
}}</span>
</template>
<template #action="slotProps">
@ -269,6 +269,7 @@ const columns = [
type: 'string',
defaultTermType: 'eq',
},
ellipsis:true
},
{
title: '设备名称',
@ -278,6 +279,7 @@ const columns = [
type: 'string',
first: true,
},
ellipsis:true
},
{
title: '产品名称',
@ -375,6 +377,7 @@ const columns = [
title: '说明',
dataIndex: 'describe',
key: 'describe',
ellipsis:true,
search: {
type: 'string',
},

View File

@ -45,7 +45,7 @@ const props = defineProps({
width: 100%;
height: calc(100% - 50px);
margin-top: 40px;
background-size: 95%;
background-size: 85%;
background-position: center;
background-repeat: no-repeat;
}

View File

@ -94,6 +94,7 @@ const goProviders = (param: any) => {
showType.value = param.type;
provider.value = param;
type.value = false;
console.log(showType.value,provider.value)
};
const goBack = () => {
@ -105,6 +106,7 @@ const goBack = () => {
const TypeMap = new Map([
['fixed-media', 'media'],
['gb28181-2016', 'media'],
['onvif', 'media'],
['OneNet', 'cloud'],
['Ctwing', 'cloud'],
['modbus-tcp', 'channel'],
@ -117,6 +119,7 @@ const TypeMap = new Map([
const DataMap = new Map();
DataMap.set('fixed-media', { type: 'media', title: '视频类设备接入' });
DataMap.set('gb28181-2016', { type: 'media', title: '视频类设备接入' });
DataMap.set('onvif',{ type: 'media' , title:'视频类设备接入'});
DataMap.set('OneNet', { type: 'cloud', title: '云平台接入' });
DataMap.set('Ctwing', { type: 'cloud', title: '云平台接入' });
DataMap.set('modbus-tcp', { type: 'channel', title: '通道类设备接入' });
@ -133,7 +136,7 @@ const getTypeList = (result: Record<string, any>) => {
const channel: any[] = [];
const edge: any[] = [];
result.map((item: any) => {
if (item.id === 'fixed-media' || item.id === 'gb28181-2016') {
if (item.id === 'fixed-media' || item.id === 'gb28181-2016' || item.id ==='onvif') {
item.type = 'media';
media.push(item);
} else if (item.id === 'OneNet' || item.id === 'Ctwing') {
@ -190,6 +193,7 @@ const queryProviders = async () => {
if (resp.status === 200) {
const _data = resp.result || [];
dataSource.value = getTypeList(accessConfigTypeFilter(_data as any[]));
console.log(dataSource.value)
// dataSource.value = getTypeList(resp.result)[0].list.filter(
// (item) => item.name !== '',
// );

View File

@ -0,0 +1,147 @@
<template>
<div class="card-last">
<j-row :gutter="[24, 24]">
<j-col :span="12">
<title-component data="基本信息" />
<div>
<j-form
:model="formState"
name="basic"
autocomplete="off"
layout="vertical"
@finish="onFinish"
>
<j-form-item
label="名称"
name="name"
:rules="[
{
required: true,
message: '请输入名称',
trigger: 'blur',
},
{
max: 64,
message: '最多可输入64个字符',
trigger: 'blur',
},
]"
>
<j-input
placeholder="请输入名称"
v-model:value="formState.name"
/>
</j-form-item>
<j-form-item label="说明" name="description">
<j-textarea
placeholder="请输入说明"
:rows="4"
v-model:value="formState.description"
show-count
:maxlength="200"
/>
</j-form-item>
<j-form-item>
<PermissionButton
v-if="view === 'false'"
type="primary"
html-type="submit"
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
:loading="loading"
>
保存
</PermissionButton>
</j-form-item>
</j-form>
</div>
</j-col>
<j-col :span="12">
<div class="doc">
<h1>接入方式</h1>
<p>
{{ provider.name }}
</p>
<p>
{{ provider.description }}
</p>
<h1>消息协议</h1>
<p>
{{ provider.id === 'fixed-media' ? 'URL' : 'SIP' }}
</p>
</div>
</j-col>
</j-row>
</div>
</template>
<script lang="ts" setup>
import { onlyMessage } from '@/utils/comm';
import { update, save } from '@/api/link/accessConfig';
interface FormState {
name: string;
description: string;
}
const route = useRoute();
const view = route.query.view as string;
const id = route.params.id as string;
const props = defineProps({
provider: {
type: Object,
default: () => {},
},
data: {
type: Object,
default: () => {},
},
});
const loading = ref(false);
const channel = ref(props.provider.channel);
const formState = ref<FormState>({
name: '',
description: '',
});
const onFinish = async (values: any) => {
loading.value = true;
const params = {
...values,
provider: 'onvif',
transport: 'ONVIF',
channel: 'onvif',
};
const resp =
id === ':id' ? await save(params) : await update({ ...params, id });
if (resp.status === 200) {
onlyMessage('操作成功', 'success');
if (route.query.save) {
// @ts-ignore
if ((window as any).onTabSaveSuccess) {
(window as any).onTabSaveSuccess(resp.result);
setTimeout(() => window.close(), 300);
}
} else {
history.back();
}
}
loading.value = false;
};
onMounted(() => {
if (id !== ':id') {
formState.value = {
name: props.data.name,
description: props.data?.description || '',
};
}
});
</script>
<style lang="less" scoped>
.card-last {
padding-right: 5px;
overflow-y: auto;
overflow-x: hidden;
}
</style>

View File

@ -78,12 +78,16 @@
<div v-else-if="channel === 'gb28181'">
<GB28181 :provider="props.provider" :data="props.data"></GB28181>
</div>
<div v-else-if="channel === 'onvif'">
<Onvif :provider="props.provider" :data="props.data"></Onvif>
</div>
</div>
</template>
<script lang="ts" setup name="AccessMedia">
import { onlyMessage } from '@/utils/comm';
import GB28181 from './GB28181.vue';
import Onvif from './Onvif.vue'
import { update, save } from '@/api/link/accessConfig';
interface FormState {

View File

@ -198,6 +198,7 @@ import {
import { onlyMessage } from '@/utils/comm';
import { useMenuStore } from 'store/menu';
import { accessConfigTypeFilter } from '@/utils/setting';
import { cloneDeep } from 'lodash-es';
const menuStory = useMenuStore();
const tableRef = ref<Record<string, any>>({});
@ -331,6 +332,9 @@ const getProvidersList = async () => {
const res: any = await getProviders();
providersList.value = res.result;
providersOptions.value = accessConfigTypeFilter(res.result || []);
providersOptions.value = providersOptions.value.filter((i:any)=>{
return i.id !== 'modbus-tcp' && i.id !== 'opc-ua'
})
};
getProvidersList();
@ -363,7 +367,23 @@ const getStatus = (slotProps: Record<string, any>) =>
* @param params
*/
const handleSearch = (e: any) => {
params.value = e;
const newTerms = cloneDeep(e);
if (newTerms.terms?.length) {
newTerms.terms.forEach((a: any) => {
a.terms = a.terms.map((b: any) => {
if(b.column === 'provider'){
if(b.value === 'collector-gateway'){
b.termType = b.termType === 'eq' ? 'in' : 'nin';
b.value = ['opc-ua','modbus-tcp','collector-gateway'];
}else if(Array.isArray(b.value) && b.value.includes('collector-gateway')){
b.value = ['opc-ua','modbus-tcp',...b.value];
}
}
return b;
});
});
}
params.value = newTerms;
};
</script>
<style lang="less" scoped>

View File

@ -99,7 +99,6 @@ const pickerTimeChange = () => {
};
const echartsOptions = computed(() => {
console.log(serverActive.value,'---')
const series = serverActive.value.length
? serverActive.value.map((key) => setOptions(serverData.data, key))
: typeDataLine

View File

@ -231,8 +231,12 @@ const handleClick = async (e: any) => {
e.protocol,
e.transport,
);
extendFormItem.value = result.properties.map((item: any) => ({
if(e.protocol === 'onvif' && !result.scopes.find((i)=>{
return i === 'product'
})){
return ''
}
extendFormItem.value = result?.properties?.map((item: any) => ({
name: ['configuration', item.property],
label: item.name,
type: item.type?.type,

View File

@ -135,8 +135,69 @@
placeholder="请输入接入密码"
/>
</j-form-item>
<template v-if="formData.channel === 'onvif'">
<j-form-item
label="接入地址"
:name="['others', 'onvifUrl']"
:rules="[
{
required: true,
message: '请输入接入密码',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<j-input
v-model:value="formData.others.onvifUrl"
></j-input>
</j-form-item>
<j-form-item
label="接入账户"
:name="['others', 'onvifUsername']"
:rules="[
{
required: true,
message: '请输入接入密码',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<j-input
v-model:value="
formData.others.onvifUsername
"
></j-input>
</j-form-item>
<j-form-item
label="接入密码"
:name="['others', 'onvifPassword']"
:rules="[
{
required: true,
message: '请输入接入密码',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<j-input-password
v-model:value="
formData.others.onvifPassword
"
></j-input-password>
</j-form-item>
</template>
<template v-if="!!route.query.id">
<j-form-item
v-if="formData.channel === 'gb28181-2016'"
label="流传输模式"
name="streamMode"
:rules="{
@ -156,26 +217,49 @@
</j-radio-button>
</j-radio-group>
</j-form-item>
<j-form-item label="设备厂商"
<j-form-item
label="设备厂商"
name="manufacturer"
:rules="[{ max: 64, message: '最多可输入64位字符', trigger: 'change' }]">
:rules="[
{
max: 64,
message: '最多可输入64位字符',
trigger: 'change',
},
]"
>
<j-input
v-model:value="formData.manufacturer"
placeholder="请输入设备厂商"
/>
</j-form-item>
<j-form-item label="设备型号"
<j-form-item
label="设备型号"
name="model"
:rules="[{ max: 64, message: '最多可输入64位字符', trigger: 'change' }]">
:rules="[
{
max: 64,
message: '最多可输入64位字符',
trigger: 'change',
},
]"
>
<j-input
v-model:value="formData.model"
placeholder="请输入设备型号"
/>
</j-form-item>
<j-form-item label="固件版本"
<j-form-item
label="固件版本"
name="firmware"
:rules="[{ max: 64, message: '最多可输入64位字符', trigger: 'change' }]">
:rules="[
{
max: 64,
message: '最多可输入64位字符',
trigger: 'change',
},
]"
>
<j-input
v-model:value="formData.firmware"
placeholder="请输入固件版本"
@ -311,6 +395,7 @@ import { PROVIDER_OPTIONS } from '@/views/media/Device/const';
import type { ProductType } from '@/views/media/Device/typings';
import SaveProduct from './SaveProduct.vue';
import { notification } from 'jetlinks-ui-components';
import { omit } from 'lodash-es';
const router = useRouter();
const route = useRoute();
@ -325,6 +410,9 @@ const formData = ref({
description: '',
others: {
access_pwd: '',
onvifUrl: '',
onvifPassword: '',
onvifUsername: '',
},
//
streamMode: 'UDP',
@ -375,6 +463,7 @@ const getDetail = async () => {
const res = await DeviceApi.detail(route.query.id as string);
Object.assign(formData.value, res.result);
formData.value.channel = res.result.provider;
console.log(formData.value,'formData')
};
onMounted(() => {
@ -387,7 +476,7 @@ onMounted(() => {
const btnLoading = ref<boolean>(false);
const formRef = ref();
const handleSubmit = () => {
const {
let {
others,
id,
streamMode,
@ -402,48 +491,58 @@ const handleSubmit = () => {
params = !id
? extraParams
: { id, streamMode, manufacturer, model, firmware, ...extraParams };
} else {
} else if (formData.value.channel === 'gb28181-2016') {
//
const getParmas = () =>{
if(others?.stream_mode){
others.stream_mode = streamMode
}
return{
others,
id,
streamMode,
manufacturer,
model,
firmware,
...extraParams,
};
others = omit(others, [
'onvifUrl',
'onvifPassword',
'onvifUsername',
]);
const getParmas = () => {
if (others?.stream_mode) {
others.stream_mode = streamMode;
}
return {
others,
id,
streamMode,
manufacturer,
model,
firmware,
...extraParams,
};
};
params = !id ? { others, id, ...extraParams } : getParmas();
} else {
others = omit(others, ['access_pwd']);
params = !id
? { others, id, ...extraParams }
: getParmas()
? {others,...extraParams}
: { id, streamMode, manufacturer, model, firmware,others, ...extraParams };
}
formRef.value
?.validate()
.then(async () => {
btnLoading.value = true;
let res;
if(!route.query.id){
const resp:any = await DeviceApi.validateId(id)
if(resp.status === 200 && resp?.result?.passed){
res = await DeviceApi.save(params)
}else{
notification.error({ key: 'error', message: '设备ID已重复'})
}
}else{
res = await DeviceApi.update(params);
}
if (res?.success) {
onlyMessage('保存成功');
history.back();
btnLoading.value = true;
let res;
if (!route.query.id) {
const resp: any = await DeviceApi.validateId(id);
if (resp.status === 200 && resp?.result?.passed) {
res = await DeviceApi.save(params);
} else {
notification.error({
key: 'error',
message: '设备ID已重复',
});
}
} else {
res = await DeviceApi.update(params);
}
)
if (res?.success) {
onlyMessage('保存成功');
history.back();
}
})
.catch((err: any) => {
console.log('err: ', err);
})

View File

@ -1,6 +1,7 @@
export const PROVIDER_OPTIONS = [
{ label: 'GB/T28181', value: 'gb28181-2016' },
{ label: '固定地址', value: 'fixed-media' },
{ label: 'Onvif', value: 'onvif'}
]
export const streamMode = [
{ label: 'UDP', value: 'UDP' },
@ -10,4 +11,5 @@ export const streamMode = [
export const providerType = {
'gb28181-2016': 'GB/T28181',
'fixed-media': '固定地址',
'onvif': 'Onvif'
};

View File

@ -248,6 +248,7 @@ const columns = [
messageProtocol$in: [
'gb28181-2016',
'fixed-media',
'onvif',
],
},
}),

View File

@ -31,6 +31,7 @@
started: 'processing',
disable: 'error',
}"
@click="handleView(slotProps.id, slotProps.triggerType)"
>
<template #type>
<span
@ -108,6 +109,8 @@ import { getImage, onlyMessage } from '@/utils/comm';
import Save from './Save/index.vue';
import { useAlarmConfigurationStore } from '@/store/alarm';
import { storeToRefs } from 'pinia';
import { useMenuStore } from 'store/menu';
const menuStory = useMenuStore();
const route = useRoute();
const id = route.query?.id;
@ -191,6 +194,18 @@ const saveSuccess = () => {
visible.value = false;
actionRef.value.reload();
};
/**
* 查看
* @param id
* @param triggerType 触发类型
*/
const handleView = (id: string, triggerType: string) => {
menuStory.jumpPage(
'rule-engine/Scene/Save',
{},
{ triggerType: triggerType, id, type: 'view' },
);
};
</script>
<style lang="less" scoped>
.subTitle {

View File

@ -13,7 +13,6 @@
:params="params"
:gridColumns="[1, 1, 2]"
:gridColumn="2"
model="CARD"
ref="tableRef"
>
<template #card="slotProps">
@ -42,7 +41,7 @@
/>
</template>
<template #content>
<Ellipsis style="width: calc(100% - 100px);">
<Ellipsis style="width: calc(100% - 100px)">
<span style="font-weight: 500">
{{ slotProps.alarmName }}
</span>
@ -116,6 +115,60 @@
</template>
</CardBox>
</template>
<template #targetType="slotProps">
{{ titleMap.get(slotProps.targetType) }}
</template>
<template #alarmTime="slotProps">
{{ dayjs(slotProps.alarmTime).format('YYYY-MM-DD HH:mm:ss')}}
</template>
<template #state="slotProps">
<BadgeStatus
:status="slotProps.state.value"
:statusName="{
warning: 'warning',
normal: 'default',
}"
>
</BadgeStatus
><span
:style="
slotProps.state.value === 'warning'
? 'color: #E50012'
: 'color:black'
"
>
{{ slotProps.state.text }}
</span>
</template>
<template #actions="slotProps">
<j-space>
<template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
>
<PermissionButton
type="link"
:disabled="
i.key === 'solve' &&
slotProps.state.value === 'normal'
"
:tooltip="{
...i.tooltip,
}"
@click="i.onClick"
:hasPermission="
i.key == 'solve'
? 'rule-engine/Alarm/Log:action'
: 'rule-engine/Alarm/Log:view'
"
>
<template #icon>
<AIcon :type="i.icon" />
</template>
</PermissionButton>
</template>
</j-space>
</template>
</JProTable>
</FullPage>
<SolveComponent
@ -128,21 +181,13 @@
<script lang="ts" setup>
import { getImage } from '@/utils/comm';
import {
getProductList,
getDeviceList,
getOrgList,
query,
getAlarmProduct
} from '@/api/rule-engine/log';
import { getOrgList, query, getAlarmProduct } from '@/api/rule-engine/log';
import { queryLevel } from '@/api/rule-engine/config';
import Search from '@/components/Search';
import { useAlarmStore } from '@/store/alarm';
import { storeToRefs } from 'pinia';
import dayjs from 'dayjs';
import type { ActionsType } from '@/components/Table';
import SolveComponent from '../SolveComponent/index.vue';
import SolveLog from '../SolveLog/index.vue';
import { useMenuStore } from '@/store/menu';
import { usePermissionStore } from '@/store/permission';
const menuStory = useMenuStore();
@ -150,14 +195,14 @@ const tableRef = ref();
const alarmStore = useAlarmStore();
const { data } = storeToRefs(alarmStore);
const getDefaulitLevel = () => {
const getDefaultLevel = () => {
queryLevel().then((res) => {
if (res.status === 200) {
data.value.defaultLevel = res.result?.levels || [];
}
});
};
getDefaulitLevel();
getDefaultLevel();
const props = defineProps<{
type: string;
id?: string;
@ -175,24 +220,34 @@ titleMap.set('device', '设备');
titleMap.set('other', '其他');
titleMap.set('org', '组织');
const columns = [
{
title:'配置名称',
dataIndex:'alarmName',
key:'alarmName',
},{
title:'类型',
dataIndex:'targetType',
key:'targetType',
scopedSlots:true
},
{
title:'关联场景联动',
dataIndex:'sourceName',
key:'sourceName',
},
{
title: '告警级别',
dataIndex: 'level',
key: 'level',
search: {
type: 'select',
options: async () => {
const res = await queryLevel()
if (res.success && res.result?.levels) {
return (res.result.levels as any[]).map((item: any) => {
return {
options: data.value.defaultLevel.map((item: any) => {
return {
label: item.title,
value: item.level
}
})
}
return []
}
value: item.level,
};
}),
},
},
{
@ -202,6 +257,7 @@ const columns = [
search: {
type: 'date',
},
scopedSlots: true,
},
{
title: '状态',
@ -220,131 +276,133 @@ const columns = [
},
],
},
scopedSlots: true,
},
{
title: '操作',
dateIndex: 'actions',
key: 'actions',
scopedSlots: true,
width: 200,
},
];
const newColumns = computed(() => {
const otherColumns = {
title: '产品名称',
dataIndex: 'targetId',
key: 'targetId',
search: {
type: 'select',
options: async () => {
const termType = [
{
column: 'targetType',
termType: 'eq',
type: 'and',
value: props.type,
},
];
const otherColumns = {
title: '产品名称',
dataIndex: 'targetId',
key: 'targetId',
search: {
type: 'select',
options: async () => {
const termType = [
{
column: "targetType",
termType: "eq",
type: "and",
value: props.type,
}
]
if (props.id) {
termType.push({
termType: 'eq',
column: 'alarmConfigId',
value: props.id,
type: 'and',
});
}
if (props.id) {
termType.push({
termType: 'eq',
column: 'alarmConfigId',
value: props.id,
type: 'and',
},)
}
const resp: any = await handleSearch({
sorts: [{ name: 'alarmTime', order: 'desc' }],
terms: termType,
});
const listMap: Map<string, any> = new Map();
const resp: any = await handleSearch({
sorts: [{ name: 'alarmTime', order: 'desc' }],
terms: termType
});
const listMap: Map<string, any> = new Map()
if (resp.status === 200) {
resp.result.data.forEach((item) => {
if (item.targetId) {
listMap.set(item.targetId, {
label: item.targetName,
value: item.targetId,
});
}
});
if (resp.status === 200) {
resp.result.data.forEach(item => {
if (item.targetId) {
listMap.set(item.targetId, {
label: item.targetName,
value: item.targetId,
})
}
return [...listMap.values()];
}
return [];
},
},
};
})
switch (props.type) {
case 'device':
otherColumns.title = '设备名称';
break;
case 'org':
otherColumns.title = '组织名称';
break;
case 'other':
otherColumns.title = '场景名称';
break;
}
if (props.type === 'device') {
const productColumns = {
title: '产品名称',
dataIndex: 'product_id',
key: 'product_id',
search: {
type: 'select',
options: async () => {
const termType = [
{
column: 'id$alarm-record',
value: [
{
column: 'targetType',
termType: 'eq',
value: 'device',
},
],
},
];
const resp: any = await getAlarmProduct({
sorts: [{ name: 'alarmTime', order: 'desc' }],
terms: termType,
});
const listMap: Map<string, any> = new Map();
return [...listMap.values()]
}
return [];
},
},
}
switch(props.type) {
case 'device':
otherColumns.title = '设备名称'
break;
case 'org':
otherColumns.title = '组织名称'
break;
case 'other':
otherColumns.title = '场景名称'
break;
}
if(props.type === 'device'){
const productColumns = {
title: '产品名称',
dataIndex: 'product_id',
key: 'product_id',
search: {
type: 'select',
options: async () => {
const termType = [
{
column:"id$alarm-record",
value:[
{
column: "targetType",
termType: "eq",
value: "device",
}
]
}
]
const resp: any = await getAlarmProduct({
sorts: [{ name: 'alarmTime', order: 'desc' }],
terms: termType
});
const listMap: Map<string, any> = new Map()
if (resp.status === 200) {
resp.result.data.forEach(item => {
if (item.productId) {
listMap.set(item.productId, {
label: item.productName,
value: item.productId,
})
}
})
return [...listMap.values()]
}
return [];
},
},
}
return [otherColumns,productColumns,...columns]
}
return ['all', 'detail'].includes(props.type) ? columns : [
otherColumns,
...columns,
]
})
if (resp.status === 200) {
resp.result.data.forEach((item) => {
if (item.productId) {
listMap.set(item.productId, {
label: item.productName,
value: item.productId,
});
}
});
return [...listMap.values()];
}
return [];
},
},
};
return [otherColumns, productColumns, ...columns];
}
return ['all', 'detail'].includes(props.type)
? columns
: [otherColumns, ...columns];
});
let params: any = ref({
sorts: [{ name: 'alarmTime', order: 'desc' }],
terms: [],
});
const handleSearch = async (params: any) => {
const resp:any = await query(params);
const resp: any = await query(params);
if (resp.status === 200) {
const res:any = await getOrgList();
const res: any = await getOrgList();
if (res.status === 200) {
resp.result.data.map((item: any) => {
if (item.targetType === 'org') {
@ -363,31 +421,6 @@ const handleSearch = async (params: any) => {
}
}
};
onMounted(() => {
if (props.type !== 'all' && !props.id) {
params.value.terms = [
{
termType: 'eq',
column: 'targetType',
value: props.type,
type: 'and',
},
];
}
if (props.id) {
params.value.terms = [
{
termType: 'eq',
column: 'alarmConfigId',
value: props.id,
type: 'and',
},
];
}
if (props.type === 'all') {
params.value.terms = [];
}
});
const search = (data: any) => {
params.value.terms = [...data?.terms];
@ -399,13 +432,16 @@ const search = (data: any) => {
type: 'and',
});
}
if(props.type === 'device' && data?.terms[0]?.terms[0]?.column === 'product_id'){
params.value.terms = [{
column:"targetId$dev-instance",
value:[
data?.terms[0]?.terms[0]
]
}]
if (
props.type === 'device' &&
data?.terms[0]?.terms[0]?.column === 'product_id'
) {
params.value.terms = [
{
column: 'targetId$dev-instance',
value: [data?.terms[0]?.terms[0]],
},
];
}
if (props.id) {
params.value.terms.push({
@ -419,7 +455,7 @@ const search = (data: any) => {
const getActions = (
currentData: Partial<Record<string, any>>,
type: 'card',
type: 'card' | 'table',
): ActionsType[] => {
if (!currentData) return [];
const actions = [
@ -485,12 +521,31 @@ const closeSolve = () => {
data.value.solveVisible = false;
tableRef.value.reload(params.value);
};
/**
* 关闭处理记录
*/
const closeLog = () => {
data.value.logVisible = false;
};
onMounted(() => {
if (props.type !== 'all' && !props.id) {
params.value.terms = [
{
termType: 'eq',
column: 'targetType',
value: props.type,
type: 'and',
},
];
}
if (props.id) {
params.value.terms = [
{
termType: 'eq',
column: 'alarmConfigId',
value: props.id,
type: 'and',
},
];
}
if (props.type === 'all') {
params.value.terms = [];
}
});
</script>
<style lang="less" scoped>
.content-left {

View File

@ -8,7 +8,6 @@
import { isNoCommunity } from '@/utils/utils';
import { useAlarmStore } from '@/store/alarm';
import { storeToRefs } from 'pinia';
import { queryLevel } from '@/api/rule-engine/config';
import TableComponents from './TabComponent/index.vue';
const list = [
{
@ -52,14 +51,6 @@ const noList = [
];
const alarmStore = useAlarmStore();
const { data } = storeToRefs(alarmStore);
const getDefaulitLevel = () => {
queryLevel().then((res)=>{
if(res.status === 200 ){
data.value.defaultLevel = res.result?.levels || [];
}
})
}
getDefaulitLevel();
const onTabChange = (key:string) =>{
data.value.tab = key;
}

View File

@ -15,6 +15,10 @@
border-radius: 2px;
box-shadow: 0 3px 6px -4px #0000001f, 0 6px 16px #00000014, 0 9px 28px 8px #0000000d;
}
.scene-select-item{
border-bottom: 1px;
margin-bottom: 20px;
}
.column {
color: #00a4fe;

View File

@ -0,0 +1,135 @@
<template>
<j-dropdown
class="scene-select-value"
trigger="click"
v-model:visible="visible"
:overlayStyle="{
maxWidth: '300px',
}"
@visibleChange="visibleChange"
>
<div @click.prevent="visible = true">
<slot :label="label">
<div class="dropdown-button value">
<AIcon v-if="!!icon" :type="icon" />
<Ellipsis style="max-width: 220px">
{{ label }}
</Ellipsis>
</div>
</slot>
</div>
<template #overlay>
<div class="scene-select-content">
<j-tabs @change="tabsChange" v-model:activeKey="mySource">
<j-tab-pane
v-for="item in tabsOptions"
:tab="item.label"
:key="item.key"
>
<div v-for="(i, index) in myValue" class="scene-select-item">
<ArrayItem
v-model:value="myValue[index]"
:valueName="valueName"
:labelName="labelName"
:options="options"
:component="item.component"
@select="(v, l) => onSelect(v, l, index)"
/>
</div>
<j-button @click="addItem">+</j-button>
<j-button @click="deleteItem">-</j-button>
</j-tab-pane>
</j-tabs>
</div>
</template>
</j-dropdown>
</template>
<script lang="ts" setup name="ArrayParamsDropdown">
import type { ValueType } from './typings';
import { defaultSetting } from './typings';
import ArrayItem from './ArrayItem.vue';
import { cloneDeep } from 'lodash-es';
import { getOption } from '../DropdownButton/util';
type Emit = {
(e: 'update:value', data: ValueType): void;
(e: 'update:source', data: string): void;
(
e: 'select',
data: any,
label?: string,
labelObj?: Record<number, any>,
option?: any,
): void;
(e: 'tabChange', data: any): void;
};
const props = defineProps({
...defaultSetting,
});
const emit = defineEmits<Emit>();
const myValue = ref<ValueType>(cloneDeep(props.value) || [undefined, undefined] as any);
const mySource = ref<string>(props.source);
const label = ref<any>(props.placeholder);
const visible = ref(false);
const tabsChange = (e: string) => {
mySource.value = e;
emit('update:source', mySource.value);
};
const onSelect = (v: any, _label: string, index: number) => {
emit('update:value', myValue.value);
label.value[index] = _label;
emit('select', myValue.value, _label, label.value);
};
const visibleChange = (v: boolean) => {
visible.value = v;
};
const addItem = () => {
myValue.value?.push(null);
emit('update:value', myValue.value);
};
const deleteItem = () => {
myValue.value?.pop();
emit('update:value', myValue.value);
};
watchEffect(() => {
const _options = props.options;
const _value = props.value;
const _valueName = props.valueName;
if (Array.isArray(_value) && _value.length) {
label.value = []
_value?.forEach((i: any, index: number) => {
const option = getOption(_options, i as string, _valueName);
if (option) {
label.value.push(option[props.labelName] || option.name);
} else {
label.value.push(i);
}
});
}
});
watch(()=>props.value,()=>{
myValue.value = cloneDeep(props.value);
})
</script>
<style scoped lang="less">
@import '../DropdownButton/index.less';
.manual-time-picker {
position: absolute;
top: -2px;
left: 0;
border: none;
visibility: hidden;
:deep(.ant-picker-input) {
display: none;
}
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<div class="select-box-content">
<DropdownTimePicker
v-if="['time', 'date'].includes(props.component)"
type="time"
v-model:value="myValue"
@change="timeChange"
/>
<template
v-else-if="['select', 'enum', 'boolean'].includes(props.component)"
>
<DropdownMenus
v-if="options.length"
:options="options"
:value="myValue"
:valueName="valueName"
@click="onSelect"
/>
<div class="scene-select-empty" v-else>
<j-empty />
</div>
</template>
<ValueItem
v-else
v-model:modelValue="myValue"
:itemType="props.component"
:options="options"
@change="valueItemChange"
/>
</div>
</template>
<script lang="ts" setup>
import ValueItem from '@/components/ValueItem/index.vue';
import type { ValueType } from './typings';
import { DropdownMenus, DropdownTimePicker } from '../DropdownButton';
import { getOption } from '../DropdownButton/util';
import { defaultSetting } from './typings';
const props = defineProps({
...defaultSetting,
component: {
type: String,
default: '',
},
});
type Emit = {
(e: 'update:value', data: ValueType): void;
(e: 'update:source', data: string): void;
(
e: 'select',
data: any,
label?: string,
labelObj?: Record<number, any>,
option?: any,
): void;
(e: 'tabChange', data: any): void;
};
const emit = defineEmits<Emit>();
const label = ref();
const myValue = ref<ValueType>(props.value);
const valueItemChange = (e: string) => {
label.value = e;
emit('update:value', e);
emit('select', e, label.value, { 0: label.value });
};
const onSelect = (e: string, option: any, index: number) => {
label.value = option[props.labelName];
emit('update:value', e);
emit('select', e, label.value, { 0: label.value }, option);
};
const timeChange = (e: any) => {
label.value = e;
emit('update:value', e);
emit('select', e, label.value, { 0: label.value });
};
</script>
<style lang="less" scoped>
@import '../DropdownButton/index.less';
</style>

View File

@ -1,17 +1,17 @@
<template>
<ParamsDropdown
v-for="(i,index) in myValue"
v-model:value='myValue[index]'
v-model:source='mySource'
:valueName='valueName'
:labelName='labelName'
:options='options'
:icon='icon'
:placeholder='placeholder'
:tabs-options='tabsOptions'
:metricOptions='metricOptions'
@select='(v, l) => onSelect(v, l, index)'
@tabChange='tabChange'
<ParamsDropdown
v-for="(i,index) in myValue"
v-model:value='myValue[index]'
v-model:source='mySource'
:valueName='valueName'
:labelName='labelName'
:options='options'
:icon='icon'
:placeholder='placeholder'
:tabs-options='tabsOptions'
:metricOptions='metricOptions'
@select='(v, l) => onSelect(v, l, index)'
@tabChange='tabChange'
/>
<!-- <ParamsDropdown
v-model:value='myValue[1]'
@ -26,13 +26,13 @@
@select='(v, l) => onSelect(v, l,1)'
@tabChange='tabChange'
/> -->
<j-button @click="addDropdown" v-if="['contains_all', 'contains_any', 'not_contains'].includes(props.termType)" class="operation">+</j-button>
<j-button @click="deleteDropdown" v-if="['contains_all', 'contains_any', 'not_contains'].includes(props.termType) && myValue?.length > 2" class="operation">-</j-button>
</template>
<script lang='ts' setup name='DoubleParamsDropdown'>
import ParamsDropdown from './index.vue'
import { defaultSetting, ValueType } from './typings'
import Array from './Array.vue'
type Emit = {
(e: 'update:value', data: ValueType): void
@ -63,13 +63,6 @@ const onSelect = (v: any, _label: string, index: number) => {
emit('select', myValue.value, _label, label)
}
const addDropdown = () =>{
myValue.value.push(undefined)
}
const deleteDropdown = () =>{
myValue.value.pop()
}
const tabChange = (e: string) => {
emit('update:source', e)
}

View File

@ -1,8 +1,10 @@
import ParamsDropdown from './index.vue'
import DoubleParamsDropdown from './Double.vue'
import ArrayParamsDropdown from './Array.vue'
export default ParamsDropdown
export {
DoubleParamsDropdown
DoubleParamsDropdown,
ArrayParamsDropdown
}

View File

@ -167,7 +167,6 @@ watchEffect(() => {
const option = getOption(_options, _value as string, _valueName) // label
myValue.value = isMetric ? props.metric : props.value
mySource.value = props.source
console.log(option)
if (option) {
label.value = option[props.labelName] || option.name
treeOpenKeys.value = openKeysByTree(_options, props.value, props.valueName)

View File

@ -1,76 +1,95 @@
<template>
<div class='terms-params-item'>
<div v-if='!isFirst' class='term-type-warp'>
<DropdownButton
:options='[
{ label: "并且", value: "and" },
{ label: "或者", value: "or" },
]'
type='type'
v-model:value='paramsValue.type'
@select='typeSelect'
/>
<div class="terms-params-item">
<div v-if="!isFirst" class="term-type-warp">
<DropdownButton
:options="[
{ label: '并且', value: 'and' },
{ label: '或者', value: 'or' },
]"
type="type"
v-model:value="paramsValue.type"
@select="typeSelect"
/>
</div>
<div
class="params-item_button"
@mouseover="mouseover"
@mouseout="mouseout"
>
<DropdownButton
:options="columnOptions"
icon="icon-zhihangdongzuoxie-1"
type="column"
value-name="column"
label-name="fullName"
placeholder="请选择参数"
v-model:value="paramsValue.column"
component="treeSelect"
@select="columnSelect"
/>
<DropdownButton
:options="termTypeOptions"
type="termType"
value-name="id"
label-name="name"
placeholder="操作符"
v-model:value="paramsValue.termType"
@select="termsTypeSelect"
/>
<div v-if="!['notnull', 'isnull'].includes(paramsValue.termType)">
<DoubleParamsDropdown
v-if="showDouble"
icon="icon-canshu"
placeholder="参数值"
:options="valueOptions"
:metricOptions="metricOption"
:tabsOptions="tabsOptions"
v-model:value="paramsValue.value.value"
v-model:source="paramsValue.value.source"
@select="valueSelect"
/>
<ArrayParamsDropdown
v-else-if="
showArray
"
icon="icon-canshu"
placeholder="参数值"
:options="valueOptions"
:metricOptions="metricOption"
:tabsOptions="tabsOptions"
v-model:value="paramsValue.value.value"
v-model:source="paramsValue.value.source"
@select="valueSelect"
/>
<ParamsDropdown
v-else
icon="icon-canshu"
placeholder="参数值"
:options="valueOptions"
:metricOptions="metricOption"
:tabsOptions="tabsOptions"
:metric="paramsValue.value?.metric"
v-model:value="paramsValue.value.value"
v-model:source="paramsValue.value.source"
@select="valueSelect"
/>
</div>
<j-popconfirm
title="确认删除?"
@confirm="onDelete"
:overlayStyle="{ minWidth: '180px' }"
>
<div v-show="showDelete" class="button-delete">
<AIcon type="CloseOutlined" />
</div>
</j-popconfirm>
</div>
<div class="term-add" @click.stop="termAdd" v-if="isLast">
<div class="terms-content">
<AIcon type="PlusOutlined" style="font-size: 12px" />
</div>
</div>
</div>
<div
class='params-item_button'
@mouseover='mouseover'
@mouseout='mouseout'
>
<DropdownButton
:options='columnOptions'
icon='icon-zhihangdongzuoxie-1'
type='column'
value-name='column'
label-name='fullName'
placeholder='请选择参数'
v-model:value='paramsValue.column'
component='treeSelect'
@select='columnSelect'
/>
<DropdownButton
:options='termTypeOptions'
type="termType"
value-name='id'
label-name='name'
placeholder="操作符"
v-model:value='paramsValue.termType'
@select='termsTypeSelect'
/>
<div v-if="!['notnull','isnull'].includes(paramsValue.termType)">
<DoubleParamsDropdown
v-if='showDouble'
icon='icon-canshu'
placeholder='参数值'
:options='valueOptions'
:metricOptions='metricOption'
:tabsOptions='tabsOptions'
v-model:value='paramsValue.value.value'
v-model:source='paramsValue.value.source'
@select='valueSelect'
/>
<ParamsDropdown
v-else
icon='icon-canshu'
placeholder='参数值'
:options='valueOptions'
:metricOptions='metricOption'
:tabsOptions='tabsOptions'
:metric='paramsValue.value?.metric'
v-model:value='paramsValue.value.value'
v-model:source='paramsValue.value.source'
@select='valueSelect'
/>
</div>
<j-popconfirm title='确认删除?' @confirm='onDelete' :overlayStyle='{minWidth: "180px"}'>
<div v-show='showDelete' class='button-delete'> <AIcon type='CloseOutlined' /></div>
</j-popconfirm>
</div>
<div class='term-add' @click.stop='termAdd' v-if='isLast'>
<div class='terms-content'>
<AIcon type='PlusOutlined' style='font-size: 12px' />
</div>
</div>
</div>
</template>
<script setup lang="ts" name="ParamsItem">
@ -78,9 +97,17 @@ import type { PropType } from 'vue';
import type { TermsType } from '@/views/rule-engine/Scene/typings';
import DropdownButton from '../DropdownButton';
import { getOption } from '../DropdownButton/util';
import ParamsDropdown, { DoubleParamsDropdown } from '../ParamsDropdown';
import ParamsDropdown, {
DoubleParamsDropdown,
ArrayParamsDropdown,
} from '../ParamsDropdown';
import { inject, watch } from 'vue';
import { ContextKey, arrayParamsKey, timeTypeKeys } from './util';
import {
ContextKey,
arrayParamsKey,
timeTypeKeys,
doubleParamsKey,
} from './util';
import { useSceneStore } from 'store/scene';
import { storeToRefs } from 'pinia';
import { Form } from 'jetlinks-ui-components';
@ -108,51 +135,47 @@ type TabsOption = {
};
const props = defineProps({
isFirst: {
type: Boolean,
default: true
},
isLast: {
type: Boolean,
default: true
},
showDeleteBtn: {
type: Boolean,
default: true
},
name: {
type: Number,
default: 0
},
termsName: {
type: Number,
default: 0
},
branchName: {
type: Number,
default: 0
},
branches_Index: {
type: Number,
default: 0
},
whenName: {
type: Number,
default: 0
},
value: {
type: Object as PropType<TermsType>,
default: () => ({
column: '',
type: '',
termType: 'eq',
value: {
source: 'manual',
value: undefined
}
})
}
})
isFirst: {
type: Boolean,
default: true,
},
isLast: {
type: Boolean,
default: true,
},
showDeleteBtn: {
type: Boolean,
default: true,
},
name: {
type: Number,
default: 0,
},
termsName: {
type: Number,
default: 0,
},
branchName: {
type: Number,
default: 0,
},
whenName: {
type: Number,
default: 0,
},
value: {
type: Object as PropType<TermsType>,
default: () => ({
column: '',
type: '',
termType: 'eq',
value: {
source: 'manual',
value: undefined,
},
}),
},
});
const emit = defineEmits<Emit>();
@ -280,7 +303,7 @@ watch(
const showDouble = computed(() => {
const isRange = paramsValue.termType
? arrayParamsKey.includes(paramsValue.termType)
? doubleParamsKey.includes(paramsValue.termType)
: false;
const isSourceMetric = paramsValue.value?.source === 'metric';
if (metricsCacheOption.value.length) {
@ -300,6 +323,32 @@ const showDouble = computed(() => {
return false;
});
const showArray = computed(()=>{
const isRange = paramsValue.termType ? [
'in',
'nin',
'contains_all',
'contains_any',
'not_contains',
].includes(paramsValue.termType) : false;
const isSourceMetric = paramsValue.value?.source === 'metric';
if (metricsCacheOption.value.length) {
metricOption.value = metricsCacheOption.value.filter((item) =>
isRange ? item.range : !item.range,
);
} else {
metricOption.value = [];
}
if (isRange) {
if (isMetric.value) {
return !isSourceMetric;
}
return true;
}
return false;
})
const mouseover = () => {
if (props.showDeleteBtn) {
showDelete.value = true;
@ -319,20 +368,32 @@ const columnSelect = (option: any) => {
//
const termTypes = option.termTypes;
if (!termTypes.some((item: {id: string}) => paramsValue.termType === item.id)) { //
termTypeChange = true
paramsValue.termType = termTypes?.length ? termTypes[0].id : 'eq'
}
if (hasTypeChange || !tabsOptions.value.every(a => a.key === paramsValue.value.source )) { //
paramsValue.termType = termTypes?.length ? termTypes[0].id : 'eq'
paramsValue.value = {
source: tabsOptions.value[0].key,
value: undefined
if (
!termTypes.some(
(item: { id: string }) => paramsValue.termType === item.id,
)
) {
//
termTypeChange = true;
paramsValue.termType = termTypes?.length ? termTypes[0].id : 'eq';
}
} else if (termTypeChange) {
const oldValue = isArray(paramsValue.value!.value) ? paramsValue.value!.value[0] : paramsValue.value!.value
const value = arrayParamsKey.includes(paramsValue.termType as string) ? [ oldValue, undefined ] : oldValue
if (
hasTypeChange ||
!tabsOptions.value.every((a) => a.key === paramsValue.value.source)
) {
//
paramsValue.termType = termTypes?.length ? termTypes[0].id : 'eq';
paramsValue.value = {
source: tabsOptions.value[0].key,
value: undefined,
};
} else if (termTypeChange) {
const oldValue = isArray(paramsValue.value!.value)
? paramsValue.value!.value[0]
: paramsValue.value!.value;
const value = arrayParamsKey.includes(paramsValue.termType as string)
? [oldValue, undefined]
: oldValue;
const _source = paramsValue.value?.source || tabsOptions.value[0].key;
const newValue: any = {
@ -344,22 +405,21 @@ const columnSelect = (option: any) => {
newValue.metric = paramsValue.value?.metric;
}
paramsValue.value = newValue
}
handOptionByColumn(option)
emit('update:value', { ...paramsValue })
nextTick(() => {
formItemContext.onFieldChange()
})
if (!formModel.value.options!.when[props.branches_Index]) {
formModel.value.options!.when[props.branches_Index] = {terms:[{terms: [['', '', '', '并且']]}]}
}
console.log([props.branchName, props.whenName, props.termsName])
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[props.termsName][0] = option.name
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[props.termsName][1] = paramsValue.termType
}
paramsValue.value = newValue;
}
console.log(paramsValue, hasTypeChange);
handOptionByColumn(option);
emit('update:value', { ...paramsValue });
nextTick(() => {
formItemContext.onFieldChange();
});
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
props.termsName
][0] = option.name;
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
props.termsName
][1] = paramsValue.termType;
};
const termsTypeSelect = (e: { key: string; name: string }) => {
const oldValue = isArray(paramsValue.value!.value)
@ -390,26 +450,27 @@ const termsTypeSelect = (e: { key: string; name: string }) => {
value: value,
};
if (_source === 'metric') {
newValue.metric = paramsValue.value?.metric
const isArray = isString(paramsValue.value!.value) ? paramsValue.value!.value?.includes?.(',') : false
if (arrayParamsKey.includes(e.key) !== isArray) { //
newValue.value = undefined
if (_source === 'metric') {
newValue.metric = paramsValue.value?.metric;
const isArray = isString(paramsValue.value!.value)
? paramsValue.value!.value?.includes?.(',')
: false;
if (arrayParamsKey.includes(e.key) !== isArray) {
//
newValue.value = undefined;
}
}
}
if(
['isnull','notnull'].includes(e.key)
){
newValue.value = 1
}
paramsValue.value = newValue
emit('update:value', { ...paramsValue })
formItemContext.onFieldChange()
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[props.termsName][1] = e.key
}
if (['isnull', 'notnull'].includes(e.key)) {
newValue.value = 1;
newValue.source = tabsOptions.value[0].key;
}
paramsValue.value = newValue;
emit('update:value', { ...paramsValue });
formItemContext.onFieldChange();
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
props.termsName
][1] = e.name;
};
const valueSelect = (
v: any,
@ -420,48 +481,51 @@ const valueSelect = (
if (paramsValue.value?.source === 'metric') {
paramsValue.value.metric = option?.id;
}
const newValues = { ...paramsValue };
if (paramsValue.value?.source !== 'metric') {
delete newValues.value.metric
}
emit('update:value', { ...newValues })
formItemContext.onFieldChange()
if (!formModel.value.options!.when[props.branches_Index]) {
formModel.value.options!.when[props.branches_Index] = {terms:[{terms: [['', '', '', '并且']]}]}
}
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[props.termsName][2] = labelObj
}
if (paramsValue.value?.source !== 'metric') {
delete newValues.value.metric;
}
emit('update:value', { ...newValues });
formItemContext.onFieldChange();
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
props.termsName
][2] = labelObj;
};
const typeSelect = (e: any) => {
emit('update:value', { ...paramsValue })
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[props.termsName][3] = e.label
}
emit('update:value', { ...paramsValue });
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
props.termsName
][3] = e.label;
};
const termAdd = () => {
const terms = {
column: undefined,
value: {
source: 'manual',
value: undefined
},
termType: undefined,
type: 'and',
key: `params_${new Date().getTime()}`
}
if (!formModel.value.options!.when[props.branches_Index]) {
formModel.value.options!.when[props.branches_Index] = {terms:[{terms: [['', '', '', '并且']]}]}
}
formModel.value.branches?.[props.branches_Index]?.when?.[props.whenName]?.terms?.push(terms)
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms.push(['', '', '', '并且'])
}
const terms = {
column: undefined,
value: {
source: 'manual',
value: undefined,
},
termType: undefined,
type: 'and',
key: `params_${new Date().getTime()}`,
};
formModel.value.branches?.[props.branchName]?.when?.[
props.whenName
]?.terms?.push(terms);
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
props.termsName
].push(['', '', '', '并且']);
};
const onDelete = () => {
formModel.value.branches?.[props.branches_Index]?.when?.[props.whenName]?.terms?.splice(props.termsName, 1)
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms.splice(props.termsName, 1)
}
formModel.value.branches?.[props.branchName]?.when?.[
props.whenName
]?.terms?.splice(props.termsName, 1);
formModel.value.options!.when[props.branchName].terms[
props.whenName
].terms.splice(props.termsName, 1);
};
nextTick(() => {
Object.assign(
@ -477,9 +541,7 @@ nextTick(() => {
'key',
]),
);
})
});
</script>
<style scoped>
</style>
<style scoped></style>

View File

@ -2,6 +2,7 @@ import { BranchesThen } from '@/views/rule-engine/Scene/typings'
export const ContextKey = 'columnOptions'
export const arrayParamsKey = ['nbtw', 'btw', 'in', 'nin', 'contains_all', 'contains_any', 'not_contains']
export const doubleParamsKey= ['nbtw','btw']
export const timeTypeKeys = ['time_gt_now', 'time_lt_now']

View File

@ -1274,6 +1274,10 @@
required: true,
message: '请输入用户名前缀',
},
{
validator: checkCh,
trigger: 'change'
}
]"
>
<j-input
@ -1548,7 +1552,14 @@ const form = reactive({
uploadLoading: false,
});
//
const checkCh = (_rule:Rule,value:string): Promise<any> =>
new Promise((resolve,reject) => {
if (/[\u4e00-\u9fa5]/.test(value)) return reject('用户名不能包含中文');
else return resolve('')
})
//
const headerValid = ref(true);
const paramsValid = ref(true);
const headerValidator = () => {

View File

@ -271,15 +271,15 @@
</div>
<div
class="upload-image"
v-if="formValue.backgroud"
v-if="formValue.background"
:style="
formValue.backgroud
? `background-image: url(${formValue.backgroud});`
formValue.background
? `background-image: url(${formValue.background});`
: ''
"
></div>
<div
v-if="formValue.backgroud"
v-if="formValue.background"
class="upload-image-mask"
>
点击修改
@ -345,7 +345,7 @@ const form = reactive<formType>({
'base-path': `${window.location.origin}/api`,
logo: '',
ico: '',
backgroud: '',
background: '',
},
rulesFrom: {
title: [
@ -387,8 +387,8 @@ const form = reactive<formType>({
headerTheme: configInfo.front?.headerTheme,
logo: configInfo.front?.logo || '/logo.png',
ico: configInfo.front?.ico || '/favicon.ico',
backgroud:
configInfo.front?.backgroud || '/images/login.png',
background:
configInfo.front?.background || '/images/login.png',
apiKey: configInfo.amap?.apiKey,
'base-path': configInfo.paths?.['base-path'],
};
@ -497,7 +497,7 @@ const uploader: uploaderType = {
} else if (info.file.status === 'done') {
info.file.url = info.file.response?.result;
form.backLoading = false;
form.formValue.backgroud = info.file.response?.result;
form.formValue.background = info.file.response?.result;
} else if (info.file.status === 'error') {
form.logoLoading = false;
onlyMessage('背景图上传失败,请稍后再试', 'error');

View File

@ -4,40 +4,65 @@
<h3>基本信息</h3>
<j-form ref="basicFormRef" :model="form.data" class="basic-form">
<div class="row" style="display: flex">
<j-form-item ref="uploadIcon" label="菜单图标" name="icon" :rules="[
{
required: true,
message: '请上传图标',
trigger: 'change',
},
]" style="flex: 0 0 186px">
<j-form-item
ref="uploadIcon"
label="菜单图标"
name="icon"
:rules="[
{
required: true,
message: '请上传图标',
trigger: 'change',
},
]"
style="flex: 0 0 186px"
>
<div class="icon-upload has-icon" v-if="form.data.icon">
<AIcon :type="form.data.icon" style="font-size: 90px" />
<span class="mark" @click="dialogVisible = true">点击修改</span>
<AIcon
:type="form.data.icon"
style="font-size: 90px"
/>
<span class="mark" @click="dialogVisible = true"
>点击修改</span
>
</div>
<div v-else @click="dialogVisible = true" class="icon-upload no-icon">
<div
v-else
@click="dialogVisible = true"
class="icon-upload no-icon"
>
<span>
<AIcon type="PlusOutlined" style="font-size: 30px" />
<AIcon
type="PlusOutlined"
style="font-size: 30px"
/>
<p>点击选择图标</p>
</span>
</div>
</j-form-item>
<j-row :gutter="24" style="flex: 1 1 auto">
<j-col :span="12">
<j-form-item label="名称" name="name" :rules="[
{
required: true,
message: '请输入名称',
trigger: 'change',
},
{
max: 64,
message: '最多可输入64个字符',
trigger: 'change',
},
]">
<j-input v-model:value="form.data.name" placeholder="请输入名称" />
<j-form-item
label="名称"
name="name"
:rules="[
{
required: true,
message: '请输入名称',
trigger: 'change',
},
{
max: 64,
message: '最多可输入64个字符',
trigger: 'change',
},
]"
>
<j-input
v-model:value="form.data.name"
placeholder="请输入名称"
/>
</j-form-item>
</j-col>
<j-col :span="12">
@ -46,81 +71,140 @@
name="code"
:validateFirst="true"
:rules="[
{
required: true,
message: '请输入编码',
trigger: 'change',
},
{
max: 64,
message: '最多可输入64个字符',
trigger: 'change',
},
{
validator: form.checkCode,
trigger: 'blur',
},
]">
<j-input v-model:value="form.data.code" placeholder="请输入编码" />
{
required: true,
message: '请输入编码',
trigger: 'change',
},
{
max: 64,
message: '最多可输入64个字符',
trigger: 'change',
},
{
validator: checkCh,
trigger: ['change', 'blur'],
},
{
validator: form.checkCode,
trigger: 'blur',
},
]"
>
<j-input
v-model:value="form.data.code"
placeholder="请输入编码"
/>
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item :rules="[
{
required: true,
message: '请输入页面地址',
},
{ max: 128, message: '最多可输入128个字符' },
{ pattern: /^\//, message: '请正确填写地址,以/开头' },
]" :validateFirst="true" label="页面地址" name="url">
<j-input v-model:value="form.data.url" placeholder="请输入页面地址" />
<j-form-item
:rules="[
{
required: true,
message: '请输入页面地址',
},
{
max: 128,
message: '最多可输入128个字符',
},
{
pattern: /^\//,
message: '请正确填写地址,以/开头',
},
]"
:validateFirst="true"
label="页面地址"
name="url"
>
<j-input
v-model:value="form.data.url"
placeholder="请输入页面地址"
/>
</j-form-item>
</j-col>
<j-col :span="12">
<j-form-item label="排序" name="sortIndex" :rules="[
{
pattern: /^[0-9]*[1-9][0-9]*$/,
message: '请输入大于0的整数',
},
]">
<j-input-number v-model:value="form.data.sortIndex" placeholder="请输入排序"
style="width: 100%" />
<j-form-item
label="排序"
name="sortIndex"
:rules="[
{
pattern: /^[0-9]*[1-9][0-9]*$/,
message: '请输入大于0的整数',
},
]"
>
<j-input-number
v-model:value="form.data.sortIndex"
placeholder="请输入排序"
style="width: 100%"
/>
</j-form-item>
</j-col>
</j-row>
</div>
<j-form-item label="说明" name="describe">
<j-textarea v-model:value="form.data.describe" :rows="4" show-count :maxlength="200"
placeholder="请输入说明" />
<j-textarea
v-model:value="form.data.describe"
:rows="4"
show-count
:maxlength="200"
placeholder="请输入说明"
/>
</j-form-item>
</j-form>
</div>
<div class="card" v-if="!form.data.appId">
<h3>权限配置</h3>
<j-form ref="permissFormRef" :model="form.data" class="basic-form permiss-form">
<j-form
ref="permissFormRef"
:model="form.data"
class="basic-form permiss-form"
>
<j-form-item name="accessSupport" required v-if="isNoCommunity">
<template #label>
<span style="margin-right: 3px">数据权限控制</span>
<j-tooltip title="此菜单页面数据所对应的资产类型">
<AIcon type="QuestionCircleOutlined" class="img-style" style="color: #a6a6a6" />
<AIcon
type="QuestionCircleOutlined"
class="img-style"
style="color: #a6a6a6"
/>
</j-tooltip>
</template>
<j-radio-group v-model:value="form.data.accessSupport" name="radioGroup">
<j-radio-group
v-model:value="form.data.accessSupport"
name="radioGroup"
>
<j-radio value="unsupported">不支持</j-radio>
<j-radio value="support">支持</j-radio>
<j-radio value="indirect">
<span style="margin-right: 3px">间接控制</span>
<j-tooltip title="此菜单内的数据基于其他菜单的数据权限控制">
<AIcon type="QuestionCircleFilled" class="img-style" />
<j-tooltip
title="此菜单内的数据基于其他菜单的数据权限控制"
>
<AIcon
type="QuestionCircleFilled"
class="img-style"
/>
</j-tooltip>
</j-radio>
</j-radio-group>
<j-form-item name="assetType" v-if="form.data.accessSupport === 'support'"
:rules="[{ required: true, message: '请选择资产类型' }]" style="margin-top: 24px; margin-bottom: 0">
<j-select v-model:value="form.data.assetType" style="width: 500px" placeholder="请选择资产类型" show-search
:options="form.assetsType">
<j-form-item
name="assetType"
v-if="form.data.accessSupport === 'support'"
:rules="[{ required: true, message: '请选择资产类型' }]"
style="margin-top: 24px; margin-bottom: 0"
>
<j-select
v-model:value="form.data.assetType"
style="width: 500px"
placeholder="请选择资产类型"
show-search
:options="form.assetsType"
>
<!-- <j-select-option
v-for="item in form.assetsType"
:value="item.value"
@ -129,32 +213,60 @@
</j-select>
</j-form-item>
<j-form-item name="indirectMenus" v-if="form.data.accessSupport === 'indirect'"
:rules="[{ required: true, message: '请选择关联菜单' }]" style="margin-top: 24px; margin-bottom: 0">
<j-tree-select v-model:value="form.data.indirectMenus" style="width: 400px" :dropdown-style="{
maxHeight: '400px',
overflow: 'auto',
}" placeholder="请选择关联菜单" multiple show-search :tree-data="form.treeData" :field-names="{
children: 'children',
label: 'name',
value: 'id',
}">
<j-form-item
name="indirectMenus"
v-if="form.data.accessSupport === 'indirect'"
:rules="[{ required: true, message: '请选择关联菜单' }]"
style="margin-top: 24px; margin-bottom: 0"
>
<j-tree-select
v-model:value="form.data.indirectMenus"
style="width: 400px"
:dropdown-style="{
maxHeight: '400px',
overflow: 'auto',
}"
placeholder="请选择关联菜单"
multiple
show-search
:tree-data="form.treeData"
:field-names="{
children: 'children',
label: 'name',
value: 'id',
}"
>
</j-tree-select>
</j-form-item>
</j-form-item>
<j-form-item label="权限">
<PermissChoose :first-width="3" max-height="350px" v-model:value="form.data.permissions"
:key="form.data.id || ''" />
<PermissChoose
:first-width="3"
max-height="350px"
v-model:value="form.data.permissions"
:key="form.data.id || ''"
/>
</j-form-item>
</j-form>
</div>
<PermissionButton type="primary" :hasPermission="`${permission}:${route.params.id === ':id' ? 'add' : 'update'
}`" @click="form.clickSave" :loading='form.saveLoading' class="saveBtn">
保存
<PermissionButton
type="primary"
:hasPermission="`${permission}:${
route.params.id === ':id' ? 'add' : 'update'
}`"
@click="form.clickSave"
:loading="form.saveLoading"
class="saveBtn"
>
保存
</PermissionButton>
<!-- 弹窗 -->
<ChooseIconDialog v-if="dialogVisible" v-model:visible="dialogVisible" :icon="form.data.icon"
@confirm="(typeStr: string) => choseIcon(typeStr)" />
<ChooseIconDialog
v-if="dialogVisible"
v-model:visible="dialogVisible"
:icon="form.data.icon"
@confirm="(typeStr: string) => choseIcon(typeStr)"
/>
</div>
</template>
@ -175,7 +287,6 @@ import { Rule } from 'ant-design-vue/lib/form';
import { isNoCommunity } from '@/utils/utils';
import { onlyMessage } from '@/utils/comm';
const permission = 'system/Menu';
//
const route = useRoute();
@ -191,7 +302,7 @@ const basicFormRef = ref<FormInstance>();
const permissFormRef = ref<FormInstance>();
const uploadIcon = ref<FormInstance>();
//
const appOptions = ref<any>([])
const appOptions = ref<any>([]);
const form = reactive({
data: {
name: '',
@ -216,7 +327,9 @@ const form = reactive({
getMenuInfo_api(routeParams.id).then((resp: any) => {
form.data = {
...(resp.result as formType),
permissions: resp.result?.permissions ? resp.result.permissions : [],
permissions: resp.result?.permissions
? resp.result.permissions
: [],
accessSupport:
resp.result?.accessSupport?.value || 'unsupported',
};
@ -224,25 +337,34 @@ const form = reactive({
});
if (isNoCommunity) {
//
getMenuTree_api({ paging: false,terms:[{terms:[{
terms:[
{
value:"%show\":true%",
termType:"like",
column:"options"
}
]
}]}]}).then((resp: any) => {
form.treeData = resp.result;
});
//
getAssetsType_api().then((resp: any) => {
form.assetsType = resp.result.map((item: any) => ({
label: item.name,
value: item.id,
}));
});
//
getMenuTree_api({
paging: false,
terms: [
{
terms: [
{
terms: [
{
value: '%show":true%',
termType: 'like',
column: 'options',
},
],
},
],
},
],
}).then((resp: any) => {
form.treeData = resp.result;
});
//
getAssetsType_api().then((resp: any) => {
form.assetsType = resp.result.map((item: any) => ({
label: item.name,
value: item.id,
}));
});
}
},
checkCode: async (_rule: Rule, value: string): Promise<any> => {
@ -270,7 +392,7 @@ const form = reactive({
const api = routeParams.id ? saveMenuInfo_api : addMenuInfo_api;
form.saveLoading = true;
const accessSupportValue = form.data.accessSupport;
const params:any = {
const params: any = {
...form.data,
owner: 'iot',
options: { show: true },
@ -280,8 +402,8 @@ const form = reactive({
accessSupportValue === 'unsupported'
? '不支持'
: accessSupportValue === 'support'
? '支持'
: '间接控制',
? '支持'
: '间接控制',
},
};
api(params)
@ -302,15 +424,20 @@ const form = reactive({
})
.finally(() => (form.saveLoading = false));
})
.catch((err) => { });
.catch((err) => {});
},
});
form.init();
const checkCh = async (_rule: Rule, value: string) => {
if (/[\u4e00-\u9fa5]/.test(value))
return Promise.reject('编码不能包含中文');
else return Promise.resolve('');
};
const choseIcon = (typeStr: string) => {
form.data.icon = typeStr;
uploadIcon.value?.clearValidate();
}
};
//
const dialogVisible = ref(false);

View File

@ -55,6 +55,7 @@
:menu-info="menuInfo"
:mode="dialogTitle"
:data="selectItem"
:menuData="table.tableData"
@confirm="table.getList"
/>
</div>
@ -84,8 +85,6 @@ const openDialog = (mode: '查看' | '新增' | '编辑', row: object) => {
if (!routeParams.id && !paramsId.value) {
return onlyMessage('请先新增菜单基本信息', 'warning');
}
console.log(3);
selectItem.value = { ...row };
dialogTitle.value = mode;
dialogVisible.value = true;

View File

@ -15,6 +15,7 @@
:rules="[
{ required: true, message: '请输入编码' },
{ max: 64, message: '最多可输入64个字符' },
{ validator: validateIdRepeat, trigger: 'blur' },
]"
>
<j-auto-complete
@ -59,12 +60,12 @@
max-height="350px"
v-model:value="form.data.permissions"
:disabled="props.mode === '查看'"
:btnId="form.data.id "
:btnId="form.data.id"
/>
</j-form-item>
<j-form-item label="说明" name="describe">
<j-form-item label="说明" name="description">
<j-textarea
v-model:value="form.data.describe"
v-model:value="form.data.description"
:rows="4"
placeholder="请输入说明"
:disabled="props.mode === '查看'"
@ -90,6 +91,10 @@ const props = defineProps<{
visible: boolean;
mode: '查看' | '新增' | '编辑';
data: formType;
menuData: {
type: Array<any>;
default: [];
};
}>();
const loading = ref(false);
@ -126,7 +131,7 @@ const initForm = {
name: '',
id: '',
permissions: [],
describe: '',
description: '',
} as formType;
const formRef = ref<FormInstance>();
const form = reactive({
@ -141,12 +146,24 @@ const codeOptions = [
{ label: 'delete', value: 'delete', message: '删除' },
{ label: 'update', value: 'update', message: '更新' },
];
const validateIdRepeat = (rule: any, val: any) => {
if (props.mode === '编辑') {
return Promise.resolve('');
}
const isRepeat = props.menuData.find((i: any) => {
return i.id === val;
});
if (isRepeat) {
return Promise.reject('编码重复');
} else {
return Promise.resolve('');
}
};
type formType = {
name: string;
id: string;
permissions: any[];
describe: string;
description: string;
};
</script>

View File

@ -25,7 +25,7 @@
v-model:checked="rowItem.checkAll"
:indeterminate="rowItem.indeterminate"
@change="() => permission.selectAllOpions(rowItem)"
:disabled="props.disabled"
:disabled="props.disabled || rowItem.disabled"
>
{{ rowItem.name }}
</j-checkbox>
@ -35,7 +35,7 @@
v-model:value="rowItem.checkedList"
:options="rowItem.options"
@change="((checkValue:string[])=>permission.selectOption(rowItem, checkValue))"
:disabled="props.disabled"
:disabled="props.disabled || rowItem.disabled"
/>
</j-col>
</j-row>
@ -177,6 +177,7 @@ const permission = reactive({
checked.actions.length < item.actions.length) ||
false,
options,
disabled: item.status === 0,
};
}) as permissionType[];
@ -192,6 +193,7 @@ type permissionType = {
checkAll: boolean;
indeterminate: boolean;
options: any[];
disabled: boolean;
};
type paramsType = {
paging: boolean;

View File

@ -108,7 +108,7 @@
</j-button>
</div>
<j-monaco-editor
v-if="refStr"
v-if=" method !=='get' && method !=='patch'"
ref="editorRef"
language="json"
style="height: 100% ; min-height: 200px;"
@ -143,6 +143,7 @@ const props = defineProps<{
const responsesContent = ref({});
const editorRef = ref();
const formRef = ref<FormInstance>();
const method = ref()
const requestBody = reactive({
tableColumns: [
{
@ -199,9 +200,10 @@ let schema: any = {};
const refStr = ref('');
const init = () => {
if (!props.selectApi.requestBody) return;
schema = props.selectApi.requestBody.content['application/json'].schema;
refStr.value = schema.$ref || schema?.items?.$ref;
method.value = props.selectApi.method
// if (!props.selectApi.requestBody) return;
// schema = props.selectApi.requestBody.content['application/json'].schema;
// refStr.value = schema.$ref || schema?.items?.$ref;
};
init();

View File

@ -142,14 +142,16 @@
<j-form-item
name="username"
label="用户名"
:validateFirst="true"
:rules="[
{ required: true, message: '' },
{ required: true, message: '请输入用户名' },
{
validator: checkCh,
trigger: ['change', 'blur'],
},
{
validator: form.rules.checkUserName,
trigger: 'blur',
},{
validator: form.rules.checkCh,
trigger: 'change'
}
]"
>
@ -267,15 +269,9 @@ const form = reactive({
data: {} as formType,
rules: {
checkCh: (_rule:Rule,value:string): Promise<any> =>
new Promise((resolve,reject) => {
if (/[\u4e00-\u9fa5]/.test(value)) return reject('用户名不能包含中文');
else return resolve('')
}),
checkUserName: (_rule: Rule, value: string): Promise<any> =>
new Promise((resolve, reject) => {
if (props.type === 'edit') return resolve('');
if (!value) return reject('请输入用户名');
else if (value.length > 64) return reject('最多可输入64个字符');
validateField_api('username', value).then((resp: any): any => {
resp.result.passed
@ -397,7 +393,10 @@ const form = reactive({
};
},
});
const checkCh = async(_rule:Rule,value:string) => {
if (/[\u4e00-\u9fa5]/.test(value)) return Promise.reject('用户名不能包含中文');
else return Promise.resolve('')
}
const dealRoleList = (data:any) =>{
return data.map((item:any)=>{
return {

View File

@ -118,7 +118,7 @@
<div class="other-button">
<div
class='other-button-item'
v-for="(item, index) in bindings"
v-for="(item, index) in bindings.slice(0,4)"
:key="index"
@click="handleClickOther(item)"
>
@ -133,6 +133,9 @@
/>
</div>
</div>
<div class="more" v-if="bindings.length > 4" @click="moreVisible = true">
查看更多
</div>
</div>
</div>
</div>
@ -158,6 +161,31 @@
</div>
</j-spin>
</div>
<j-modal
title="更多登录"
:visible="moreVisible"
@cancel="() => (moreVisible = false)"
:footer="null"
:width="800"
>
<div class="more-button">
<div
class="more-button-item"
v-for="(item, index) in bindings"
:key="index"
@click="handleClickOther(item)"
>
<img
style="width: 100px; height: 100px"
:alt="item.name"
:src="item.logoUrl"
/>
<Ellipsis style="margin-top: 5px; width:calc(100% - 40px); margin: 0 auto">
{{ item.name }}
</Ellipsis>
</div>
</div>
</j-modal>
</template>
<script setup lang="ts">
@ -189,7 +217,7 @@ const viewLogo = getImage('/view-logo.png');
const LoginWarpStyle = reactive({
backgroundImage: `url(${bgImage})`,
});
const moreVisible = ref(false)
const screenWidth = ref(document.body.clientWidth);
const screenHeight = ref(document.body.clientHeight);
@ -515,6 +543,10 @@ onMounted(()=>{
}
}
.more{
text-align: center;
cursor: pointer;
}
}
.prefixIcon {
@ -601,4 +633,15 @@ onMounted(()=>{
margin-top: 12% !important;
}
}
.more-button {
display: flex;
flex-wrap: wrap;
cursor: pointer;
.more-button-item {
width: 18%;
margin-left: 2%;
text-align: center;
margin-bottom: 20px
}
}
</style>