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

@ -68,4 +68,20 @@ export const getSnapTypes = () => server.get('/s7/client/s7codecs/list')
export const getArea = () => server.get('/s7/client/s7area/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'}) 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; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
gap: 24px;
.disabled { .disabled {
>div { >div {
color: rgba(0, 0, 0, 0.25); color: rgba(0, 0, 0, 0.25);
@ -105,7 +106,7 @@ const handleRadio = (item: any) => {
} }
&-item { &-item {
width: 49%; flex:1;
height: 70px; height: 70px;
padding: 10px 15px; padding: 10px 15px;
margin-bottom: 12px; margin-bottom: 12px;
@ -113,7 +114,7 @@ const handleRadio = (item: any) => {
border-radius: 2px; border-radius: 2px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 24px;
cursor: pointer; cursor: pointer;
.img { .img {
width: 32px; width: 32px;

View File

@ -40,7 +40,7 @@ const defaultOwnParams = [
column: "options" 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: 'MODBUS_TCP', value: 'MODBUS_TCP', alias: 'Modbus/TCP' },
{ label: 'COLLECTOR_GATEWAY', value: 'COLLECTOR_GATEWAY', alias: 'GATEWAY' }, { label: 'COLLECTOR_GATEWAY', value: 'COLLECTOR_GATEWAY', alias: 'GATEWAY' },
{ label: 'S7', value: 'snap7', alias: 'snap7' }, { label: 'S7', value: 'snap7', alias: 'snap7' },
{ label: 'IEC104', value: 'iec104', alias: 'IEC104' } { label: 'IEC104', value: 'iec104', alias: 'IEC104' },
{ label: 'BACNetIp', value: 'BACNetIp', alias: 'BACNet/IP' }
] ]

View File

@ -1,4 +1,4 @@
<template lang=""> <template>
<j-modal <j-modal
:title="data.id ? '编辑' : '新增'" :title="data.id ? '编辑' : '新增'"
:visible="true" :visible="true"
@ -94,14 +94,14 @@
</j-form-item> </j-form-item>
<j-form-item <j-form-item
v-if="formData.provider === 'COLLECTOR_GATEWAY'" v-if="formData.provider === 'COLLECTOR_GATEWAY'"
:name="['configuration','deviceId']" :name="['configuration', 'deviceId']"
:rules="[{ required: true, message: '请选择网关设备'}]" :rules="[{ required: true, message: '请选择网关设备' }]"
label="选择网关设备" label="选择网关设备"
> >
<GateWayFormItem <GateWayFormItem
v-model:name="formData.configuration.deviceName" v-model:name="formData.configuration.deviceName"
v-model:value="formData.configuration.deviceId" v-model:value="formData.configuration.deviceId"
/> />
</j-form-item> </j-form-item>
<j-form-item <j-form-item
v-if="formData.provider === 'OPC_UA'" v-if="formData.provider === 'OPC_UA'"
@ -171,6 +171,50 @@
v-model:value="formData.configuration.password" v-model:value="formData.configuration.password"
/> />
</j-form-item> </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 <!-- <j-form-item
v-if="formData.provider === 'snap7'" v-if="formData.provider === 'snap7'"
:name="['configuration', 'connect']" :name="['configuration', 'connect']"
@ -215,7 +259,7 @@ import type { FormInstance } from 'ant-design-vue';
import type { FormDataType } from '../type.d'; import type { FormDataType } from '../type.d';
import { cloneDeep, isArray } from 'lodash-es'; import { cloneDeep, isArray } from 'lodash-es';
import { protocolList } from '@/utils/consts'; 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({ const props = defineProps({
data: { data: {
@ -242,20 +286,20 @@ const formData = ref<FormDataType>(cloneDeep(FormState));
const handleOk = async () => { const handleOk = async () => {
const params: any = await formRef.value?.validate(); const params: any = await formRef.value?.validate();
if (params?.provider === 'COLLECTOR_GATEWAY') { if (params?.provider === 'COLLECTOR_GATEWAY') {
params.configuration.deviceName = formData.value.configuration.deviceName params.configuration.deviceName =
formData.value.configuration.deviceName;
} }
if (params?.provider === 'snap7') {
if(params?.provider === 'snap7'){ params.configuration = {
params.configuration={ connect: false,
connect : false };
}
} else if (params?.provider === 'iec104') { } else if (params?.provider === 'iec104') {
params.configuration = {} params.configuration = {};
} }
params.circuitBreaker = { params.circuitBreaker = {
type: 'Ignore' type: 'Ignore',
} };
loading.value = true; loading.value = true;
const response = !id const response = !id
@ -266,6 +310,18 @@ const handleOk = async () => {
formRef.value?.resetFields(); 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 = () => { const handleCancel = () => {
emit('change', false); emit('change', false);
formRef.value?.resetFields(); formRef.value?.resetFields();
@ -317,8 +373,15 @@ const getProvidersList = async () => {
const resp: any = await getProviders(); const resp: any = await getProviders();
if (resp.status === 200) { if (resp.status === 200) {
const arr = resp.result const arr = resp.result
.filter( .filter((item: any) =>
(item: any) => ['GATEWAY', 'Modbus/TCP', 'opc-ua','snap7', 'IEC104'].includes(item.name), [
'GATEWAY',
'Modbus/TCP',
'opc-ua',
'snap7',
'IEC104',
'BACNet/IP',
].includes(item.name),
) )
.map((it: any) => it.name); .map((it: any) => it.name);
const providers: any = protocolList.filter((item: any) => const providers: any = protocolList.filter((item: any) =>
@ -337,7 +400,7 @@ watch(
() => props.data, () => props.data,
(value) => { (value) => {
if (value.id) { if (value.id) {
formData.value = value as FormDataType; formData.value = value as FormDataType;
} }
}, },
{ immediate: true, deep: true }, { immediate: true, deep: true },

View File

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

View File

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

View File

@ -11,6 +11,11 @@ export interface ConfigurationType {
deviceId: string | undefined, deviceId: string | undefined,
deviceName: string | undefined, deviceName: string | undefined,
connect:boolean | undefined connect:boolean | undefined
instanceNumber?:number
overIp: {
localBindAddress: string,
port: number
}
} }
export interface FormDataType { 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"> <j-modal title="扫描" :visible="true" width="95%" @cancel="handleCancel">
<div class="content"> <div class="content">
<Tree <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"> <div class="sizeText">
将批量修改 将批量修改
{{ data.length }} 条数据的访问类型采集频率只推送变化的数据 {{ data.length }} 条数据的{{ labelName.join(',') }}
</div> </div>
<j-form <j-form
class="form" class="form"
@ -18,6 +18,15 @@
autocomplete="off" autocomplete="off"
ref="formRef" 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-form-item label="访问类型" name="accessModes">
<j-card-select <j-card-select
multiple multiple
@ -86,7 +95,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { FormInstance } from 'ant-design-vue'; 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 { cloneDeep, isObject } from 'lodash-es';
import { regOnlyNumber } from '../../../data'; import { regOnlyNumber } from '../../../data';
@ -95,6 +104,10 @@ const props = defineProps({
type: Array, type: Array,
default: () => [], default: () => [],
}, },
provider: {
type: String,
default: '',
}
}); });
const emit = defineEmits(['change']); const emit = defineEmits(['change']);
@ -105,16 +118,28 @@ const formData = ref({
accessModes: [], accessModes: [],
interval: undefined, interval: undefined,
features: [], 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 handleOk = async () => {
const data = cloneDeep(formData.value); const data = cloneDeep(formData.value);
const { accessModes, features, interval } = data; const { accessModes, features, interval, valueType } = data;
const ischange = const ischange =
accessModes.length !== 0 || accessModes.length !== 0 ||
features.length !== 0 || features.length !== 0 ||
Number(interval) === 0 || Number(interval) === 0 ||
!!interval; !!interval || !!valueType;
if (ischange) { if (ischange) {
const params = cloneDeep(props.data); const params = cloneDeep(props.data);
params.forEach((i: any) => { params.forEach((i: any) => {
@ -139,6 +164,12 @@ const handleOk = async () => {
interval: data.interval, interval: data.interval,
}; };
} }
if(data.valueType) {
i.configuration = {
...i.configuration,
valueType: data.valueType
}
}
}); });
loading.value = true; loading.value = true;
const response = await savePointBatch(params).catch(() => {}); const response = await savePointBatch(params).catch(() => {});
@ -152,6 +183,32 @@ const handleOk = async () => {
const handleCancel = () => { const handleCancel = () => {
emit('change', false); 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> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

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

View File

@ -1,4 +1,4 @@
<template lang=""> <template>
<j-modal <j-modal
:title="data.id ? '编辑' : '新增'" :title="data.id ? '编辑' : '新增'"
:visible="true" :visible="true"
@ -112,6 +112,32 @@
:max="255" :max="255"
/> />
</j-form-item> </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 <j-form-item
v-if="provider !== 'COLLECTOR_GATEWAY'" v-if="provider !== 'COLLECTOR_GATEWAY'"
:name="['configuration', 'inheritBreakerSpec', 'type']" :name="['configuration', 'inheritBreakerSpec', 'type']"
@ -222,6 +248,8 @@ import type { FormInstance } from 'ant-design-vue';
import {cloneDeep, omit} from "lodash-es"; import {cloneDeep, omit} from "lodash-es";
import {protocolList} from "@/utils/consts"; import {protocolList} from "@/utils/consts";
const route = useRoute()
const loading = ref(false); const loading = ref(false);
const visibleEndian = ref(false); const visibleEndian = ref(false);
const visibleUnitId = 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 visible
@ok="submitData" @ok="submitData"
@cancel="close" @cancel="close"
:confirmLoading="loading"
okText="确定" okText="确定"
cancelText="取消" cancelText="取消"
v-bind="layout" v-bind="layout"
@ -86,6 +87,7 @@ const arr = ref([]);
const updateObj = ref({}); const updateObj = ref({});
const addObj = ref({}); const addObj = ref({});
const addParams = ref({}); const addParams = ref({});
const loading = ref(false)
/** /**
* 表单数据 * 表单数据
*/ */
@ -118,6 +120,7 @@ const { resetFields, validate, validateInfos } = useForm(
*/ */
const submitData = async () => { const submitData = async () => {
formRef.value.validate().then(async () => { formRef.value.validate().then(async () => {
loading.value = true
addParams.value = {}; addParams.value = {};
if (props.isAdd === 0) { if (props.isAdd === 0) {
if (props.isChild === 1) { if (props.isChild === 1) {
@ -139,7 +142,9 @@ const submitData = async () => {
// sortIndex: arr.value[arr.value.length - 1].sortIndex + 1, // 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) { if (res.status === 200) {
onlyMessage('操作成功!'); onlyMessage('操作成功!');
visible.value = false; visible.value = false;
@ -155,7 +160,9 @@ const submitData = async () => {
key: updateObj.value.key, key: updateObj.value.key,
parentId: updateObj.value.parentId, parentId: updateObj.value.parentId,
}; };
const res = await updateTree(id, updateParams); const res = await updateTree(id, updateParams).finally(()=>{
loading.value = false
})
if (res.status === 200) { if (res.status === 200) {
onlyMessage('操作成功!'); onlyMessage('操作成功!');
visible.value = false; visible.value = false;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<template> <template>
<pro-search <pro-search
class="device-running-search" class="device-running-search"
type="simple"
:columns="columns" :columns="columns"
target="device-instance-running-events" target="device-instance-running-events"
@search="handleSearch" @search="handleSearch"
@ -24,7 +24,7 @@
</template> </template>
</JProTable> </JProTable>
<j-modal :width="600" v-model:visible="visible" title="详情" class="device-running-event-modal"> <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> <template #footer>
<j-button type="primary" @click="visible = false">关闭</j-button> <j-button type="primary" @click="visible = false">关闭</j-button>
</template> </template>

View File

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

View File

@ -193,8 +193,10 @@ const getStatus = (id: string) => {
{ {
deviceId: id, deviceId: id,
}, },
).subscribe(() => { ).subscribe((message:any) => {
instanceStore.refresh(id); 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, id: id,
configuration: { ...extra }, configuration: { ...extra },
storePolicy: storePolicy, storePolicy: storePolicy,
}); }).finally(()=>{
submitLoading.value = false submitLoading.value = false
})
if (resp.status === 200) { if (resp.status === 200) {
onlyMessage('操作成功!'); onlyMessage('操作成功!');
productStore.current!.storePolicy = storePolicy; productStore.current!.storePolicy = storePolicy;

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -94,6 +94,7 @@ const goProviders = (param: any) => {
showType.value = param.type; showType.value = param.type;
provider.value = param; provider.value = param;
type.value = false; type.value = false;
console.log(showType.value,provider.value)
}; };
const goBack = () => { const goBack = () => {
@ -105,6 +106,7 @@ const goBack = () => {
const TypeMap = new Map([ const TypeMap = new Map([
['fixed-media', 'media'], ['fixed-media', 'media'],
['gb28181-2016', 'media'], ['gb28181-2016', 'media'],
['onvif', 'media'],
['OneNet', 'cloud'], ['OneNet', 'cloud'],
['Ctwing', 'cloud'], ['Ctwing', 'cloud'],
['modbus-tcp', 'channel'], ['modbus-tcp', 'channel'],
@ -117,6 +119,7 @@ const TypeMap = new Map([
const DataMap = new Map(); const DataMap = new Map();
DataMap.set('fixed-media', { type: 'media', title: '视频类设备接入' }); DataMap.set('fixed-media', { type: 'media', title: '视频类设备接入' });
DataMap.set('gb28181-2016', { type: 'media', title: '视频类设备接入' }); DataMap.set('gb28181-2016', { type: 'media', title: '视频类设备接入' });
DataMap.set('onvif',{ type: 'media' , title:'视频类设备接入'});
DataMap.set('OneNet', { type: 'cloud', title: '云平台接入' }); DataMap.set('OneNet', { type: 'cloud', title: '云平台接入' });
DataMap.set('Ctwing', { type: 'cloud', title: '云平台接入' }); DataMap.set('Ctwing', { type: 'cloud', title: '云平台接入' });
DataMap.set('modbus-tcp', { type: 'channel', title: '通道类设备接入' }); DataMap.set('modbus-tcp', { type: 'channel', title: '通道类设备接入' });
@ -133,7 +136,7 @@ const getTypeList = (result: Record<string, any>) => {
const channel: any[] = []; const channel: any[] = [];
const edge: any[] = []; const edge: any[] = [];
result.map((item: 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'; item.type = 'media';
media.push(item); media.push(item);
} else if (item.id === 'OneNet' || item.id === 'Ctwing') { } else if (item.id === 'OneNet' || item.id === 'Ctwing') {
@ -190,6 +193,7 @@ const queryProviders = async () => {
if (resp.status === 200) { if (resp.status === 200) {
const _data = resp.result || []; const _data = resp.result || [];
dataSource.value = getTypeList(accessConfigTypeFilter(_data as any[])); dataSource.value = getTypeList(accessConfigTypeFilter(_data as any[]));
console.log(dataSource.value)
// dataSource.value = getTypeList(resp.result)[0].list.filter( // dataSource.value = getTypeList(resp.result)[0].list.filter(
// (item) => item.name !== '', // (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'"> <div v-else-if="channel === 'gb28181'">
<GB28181 :provider="props.provider" :data="props.data"></GB28181> <GB28181 :provider="props.provider" :data="props.data"></GB28181>
</div> </div>
<div v-else-if="channel === 'onvif'">
<Onvif :provider="props.provider" :data="props.data"></Onvif>
</div>
</div> </div>
</template> </template>
<script lang="ts" setup name="AccessMedia"> <script lang="ts" setup name="AccessMedia">
import { onlyMessage } from '@/utils/comm'; import { onlyMessage } from '@/utils/comm';
import GB28181 from './GB28181.vue'; import GB28181 from './GB28181.vue';
import Onvif from './Onvif.vue'
import { update, save } from '@/api/link/accessConfig'; import { update, save } from '@/api/link/accessConfig';
interface FormState { interface FormState {

View File

@ -198,6 +198,7 @@ import {
import { onlyMessage } from '@/utils/comm'; import { onlyMessage } from '@/utils/comm';
import { useMenuStore } from 'store/menu'; import { useMenuStore } from 'store/menu';
import { accessConfigTypeFilter } from '@/utils/setting'; import { accessConfigTypeFilter } from '@/utils/setting';
import { cloneDeep } from 'lodash-es';
const menuStory = useMenuStore(); const menuStory = useMenuStore();
const tableRef = ref<Record<string, any>>({}); const tableRef = ref<Record<string, any>>({});
@ -331,6 +332,9 @@ const getProvidersList = async () => {
const res: any = await getProviders(); const res: any = await getProviders();
providersList.value = res.result; providersList.value = res.result;
providersOptions.value = accessConfigTypeFilter(res.result || []); providersOptions.value = accessConfigTypeFilter(res.result || []);
providersOptions.value = providersOptions.value.filter((i:any)=>{
return i.id !== 'modbus-tcp' && i.id !== 'opc-ua'
})
}; };
getProvidersList(); getProvidersList();
@ -363,7 +367,23 @@ const getStatus = (slotProps: Record<string, any>) =>
* @param params * @param params
*/ */
const handleSearch = (e: any) => { 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> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,10 @@
border-radius: 2px; border-radius: 2px;
box-shadow: 0 3px 6px -4px #0000001f, 0 6px 16px #00000014, 0 9px 28px 8px #0000000d; 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 { .column {
color: #00a4fe; 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> <template>
<ParamsDropdown <ParamsDropdown
v-for="(i,index) in myValue" v-for="(i,index) in myValue"
v-model:value='myValue[index]' v-model:value='myValue[index]'
v-model:source='mySource' v-model:source='mySource'
:valueName='valueName' :valueName='valueName'
:labelName='labelName' :labelName='labelName'
:options='options' :options='options'
:icon='icon' :icon='icon'
:placeholder='placeholder' :placeholder='placeholder'
:tabs-options='tabsOptions' :tabs-options='tabsOptions'
:metricOptions='metricOptions' :metricOptions='metricOptions'
@select='(v, l) => onSelect(v, l, index)' @select='(v, l) => onSelect(v, l, index)'
@tabChange='tabChange' @tabChange='tabChange'
/> />
<!-- <ParamsDropdown <!-- <ParamsDropdown
v-model:value='myValue[1]' v-model:value='myValue[1]'
@ -26,13 +26,13 @@
@select='(v, l) => onSelect(v, l,1)' @select='(v, l) => onSelect(v, l,1)'
@tabChange='tabChange' @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> </template>
<script lang='ts' setup name='DoubleParamsDropdown'> <script lang='ts' setup name='DoubleParamsDropdown'>
import ParamsDropdown from './index.vue' import ParamsDropdown from './index.vue'
import { defaultSetting, ValueType } from './typings' import { defaultSetting, ValueType } from './typings'
import Array from './Array.vue'
type Emit = { type Emit = {
(e: 'update:value', data: ValueType): void (e: 'update:value', data: ValueType): void
@ -63,13 +63,6 @@ const onSelect = (v: any, _label: string, index: number) => {
emit('select', myValue.value, _label, label) emit('select', myValue.value, _label, label)
} }
const addDropdown = () =>{
myValue.value.push(undefined)
}
const deleteDropdown = () =>{
myValue.value.pop()
}
const tabChange = (e: string) => { const tabChange = (e: string) => {
emit('update:source', e) emit('update:source', e)
} }

View File

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

View File

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

View File

@ -1,76 +1,95 @@
<template> <template>
<div class='terms-params-item'> <div class="terms-params-item">
<div v-if='!isFirst' class='term-type-warp'> <div v-if="!isFirst" class="term-type-warp">
<DropdownButton <DropdownButton
:options='[ :options="[
{ label: "并且", value: "and" }, { label: '并且', value: 'and' },
{ label: "或者", value: "or" }, { label: '或者', value: 'or' },
]' ]"
type='type' type="type"
v-model:value='paramsValue.type' v-model:value="paramsValue.type"
@select='typeSelect' @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>
<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> </template>
<script setup lang="ts" name="ParamsItem"> <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 type { TermsType } from '@/views/rule-engine/Scene/typings';
import DropdownButton from '../DropdownButton'; import DropdownButton from '../DropdownButton';
import { getOption } from '../DropdownButton/util'; import { getOption } from '../DropdownButton/util';
import ParamsDropdown, { DoubleParamsDropdown } from '../ParamsDropdown'; import ParamsDropdown, {
DoubleParamsDropdown,
ArrayParamsDropdown,
} from '../ParamsDropdown';
import { inject, watch } from 'vue'; import { inject, watch } from 'vue';
import { ContextKey, arrayParamsKey, timeTypeKeys } from './util'; import {
ContextKey,
arrayParamsKey,
timeTypeKeys,
doubleParamsKey,
} from './util';
import { useSceneStore } from 'store/scene'; import { useSceneStore } from 'store/scene';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { Form } from 'jetlinks-ui-components'; import { Form } from 'jetlinks-ui-components';
@ -108,51 +135,47 @@ type TabsOption = {
}; };
const props = defineProps({ const props = defineProps({
isFirst: { isFirst: {
type: Boolean, type: Boolean,
default: true default: true,
}, },
isLast: { isLast: {
type: Boolean, type: Boolean,
default: true default: true,
}, },
showDeleteBtn: { showDeleteBtn: {
type: Boolean, type: Boolean,
default: true default: true,
}, },
name: { name: {
type: Number, type: Number,
default: 0 default: 0,
}, },
termsName: { termsName: {
type: Number, type: Number,
default: 0 default: 0,
}, },
branchName: { branchName: {
type: Number, type: Number,
default: 0 default: 0,
}, },
branches_Index: { whenName: {
type: Number, type: Number,
default: 0 default: 0,
}, },
whenName: { value: {
type: Number, type: Object as PropType<TermsType>,
default: 0 default: () => ({
}, column: '',
value: { type: '',
type: Object as PropType<TermsType>, termType: 'eq',
default: () => ({ value: {
column: '', source: 'manual',
type: '', value: undefined,
termType: 'eq', },
value: { }),
source: 'manual', },
value: undefined });
}
})
}
})
const emit = defineEmits<Emit>(); const emit = defineEmits<Emit>();
@ -280,7 +303,7 @@ watch(
const showDouble = computed(() => { const showDouble = computed(() => {
const isRange = paramsValue.termType const isRange = paramsValue.termType
? arrayParamsKey.includes(paramsValue.termType) ? doubleParamsKey.includes(paramsValue.termType)
: false; : false;
const isSourceMetric = paramsValue.value?.source === 'metric'; const isSourceMetric = paramsValue.value?.source === 'metric';
if (metricsCacheOption.value.length) { if (metricsCacheOption.value.length) {
@ -300,6 +323,32 @@ const showDouble = computed(() => {
return false; 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 = () => { const mouseover = () => {
if (props.showDeleteBtn) { if (props.showDeleteBtn) {
showDelete.value = true; showDelete.value = true;
@ -319,20 +368,32 @@ const columnSelect = (option: any) => {
// //
const termTypes = option.termTypes; const termTypes = option.termTypes;
if (!termTypes.some((item: {id: string}) => paramsValue.termType === item.id)) { // if (
termTypeChange = true !termTypes.some(
paramsValue.termType = termTypes?.length ? termTypes[0].id : 'eq' (item: { id: string }) => paramsValue.termType === item.id,
} )
) {
if (hasTypeChange || !tabsOptions.value.every(a => a.key === paramsValue.value.source )) { // //
paramsValue.termType = termTypes?.length ? termTypes[0].id : 'eq' termTypeChange = true;
paramsValue.value = { paramsValue.termType = termTypes?.length ? termTypes[0].id : 'eq';
source: tabsOptions.value[0].key,
value: undefined
} }
} else if (termTypeChange) { if (
const oldValue = isArray(paramsValue.value!.value) ? paramsValue.value!.value[0] : paramsValue.value!.value hasTypeChange ||
const value = arrayParamsKey.includes(paramsValue.termType as string) ? [ oldValue, undefined ] : oldValue !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 _source = paramsValue.value?.source || tabsOptions.value[0].key;
const newValue: any = { const newValue: any = {
@ -344,22 +405,21 @@ const columnSelect = (option: any) => {
newValue.metric = paramsValue.value?.metric; newValue.metric = paramsValue.value?.metric;
} }
paramsValue.value = newValue paramsValue.value = newValue;
} }
console.log(paramsValue, hasTypeChange);
handOptionByColumn(option) handOptionByColumn(option);
emit('update:value', { ...paramsValue }) emit('update:value', { ...paramsValue });
nextTick(() => { nextTick(() => {
formItemContext.onFieldChange() formItemContext.onFieldChange();
}) });
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
if (!formModel.value.options!.when[props.branches_Index]) { props.termsName
formModel.value.options!.when[props.branches_Index] = {terms:[{terms: [['', '', '', '并且']]}]} ][0] = option.name;
} formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
console.log([props.branchName, props.whenName, props.termsName]) props.termsName
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[props.termsName][0] = option.name ][1] = paramsValue.termType;
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[props.termsName][1] = paramsValue.termType };
}
const termsTypeSelect = (e: { key: string; name: string }) => { const termsTypeSelect = (e: { key: string; name: string }) => {
const oldValue = isArray(paramsValue.value!.value) const oldValue = isArray(paramsValue.value!.value)
@ -390,26 +450,27 @@ const termsTypeSelect = (e: { key: string; name: string }) => {
value: value, value: value,
}; };
if (_source === 'metric') { if (_source === 'metric') {
newValue.metric = paramsValue.value?.metric newValue.metric = paramsValue.value?.metric;
const isArray = isString(paramsValue.value!.value) ? paramsValue.value!.value?.includes?.(',') : false const isArray = isString(paramsValue.value!.value)
if (arrayParamsKey.includes(e.key) !== isArray) { // ? paramsValue.value!.value?.includes?.(',')
newValue.value = undefined : false;
if (arrayParamsKey.includes(e.key) !== isArray) {
//
newValue.value = undefined;
}
} }
} if (['isnull', 'notnull'].includes(e.key)) {
newValue.value = 1;
if( newValue.source = tabsOptions.value[0].key;
['isnull','notnull'].includes(e.key) }
){ paramsValue.value = newValue;
newValue.value = 1 emit('update:value', { ...paramsValue });
} formItemContext.onFieldChange();
paramsValue.value = newValue formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
props.termsName
emit('update:value', { ...paramsValue }) ][1] = e.name;
formItemContext.onFieldChange() };
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[props.termsName][1] = e.key
}
const valueSelect = ( const valueSelect = (
v: any, v: any,
@ -420,48 +481,51 @@ const valueSelect = (
if (paramsValue.value?.source === 'metric') { if (paramsValue.value?.source === 'metric') {
paramsValue.value.metric = option?.id; paramsValue.value.metric = option?.id;
} }
const newValues = { ...paramsValue }; const newValues = { ...paramsValue };
if (paramsValue.value?.source !== 'metric') {
if (paramsValue.value?.source !== 'metric') { delete newValues.value.metric;
delete newValues.value.metric }
} emit('update:value', { ...newValues });
emit('update:value', { ...newValues }) formItemContext.onFieldChange();
formItemContext.onFieldChange() formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
props.termsName
if (!formModel.value.options!.when[props.branches_Index]) { ][2] = labelObj;
formModel.value.options!.when[props.branches_Index] = {terms:[{terms: [['', '', '', '并且']]}]} };
}
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[props.termsName][2] = labelObj
}
const typeSelect = (e: any) => { const typeSelect = (e: any) => {
emit('update:value', { ...paramsValue }) emit('update:value', { ...paramsValue });
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[props.termsName][3] = e.label formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
} props.termsName
][3] = e.label;
};
const termAdd = () => { const termAdd = () => {
const terms = { const terms = {
column: undefined, column: undefined,
value: { value: {
source: 'manual', source: 'manual',
value: undefined value: undefined,
}, },
termType: undefined, termType: undefined,
type: 'and', type: 'and',
key: `params_${new Date().getTime()}` key: `params_${new Date().getTime()}`,
} };
if (!formModel.value.options!.when[props.branches_Index]) { formModel.value.branches?.[props.branchName]?.when?.[
formModel.value.options!.when[props.branches_Index] = {terms:[{terms: [['', '', '', '并且']]}]} props.whenName
} ]?.terms?.push(terms);
formModel.value.branches?.[props.branches_Index]?.when?.[props.whenName]?.terms?.push(terms) formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms.push(['', '', '', '并且']) props.termsName
} ].push(['', '', '', '并且']);
};
const onDelete = () => { const onDelete = () => {
formModel.value.branches?.[props.branches_Index]?.when?.[props.whenName]?.terms?.splice(props.termsName, 1) formModel.value.branches?.[props.branchName]?.when?.[
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms.splice(props.termsName, 1) props.whenName
} ]?.terms?.splice(props.termsName, 1);
formModel.value.options!.when[props.branchName].terms[
props.whenName
].terms.splice(props.termsName, 1);
};
nextTick(() => { nextTick(() => {
Object.assign( Object.assign(
@ -477,9 +541,7 @@ nextTick(() => {
'key', 'key',
]), ]),
); );
}) });
</script> </script>
<style scoped> <style scoped></style>
</style>

View File

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

View File

@ -1274,6 +1274,10 @@
required: true, required: true,
message: '请输入用户名前缀', message: '请输入用户名前缀',
}, },
{
validator: checkCh,
trigger: 'change'
}
]" ]"
> >
<j-input <j-input
@ -1548,7 +1552,14 @@ const form = reactive({
uploadLoading: false, 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 headerValid = ref(true);
const paramsValid = ref(true); const paramsValid = ref(true);
const headerValidator = () => { const headerValidator = () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -118,7 +118,7 @@
<div class="other-button"> <div class="other-button">
<div <div
class='other-button-item' class='other-button-item'
v-for="(item, index) in bindings" v-for="(item, index) in bindings.slice(0,4)"
:key="index" :key="index"
@click="handleClickOther(item)" @click="handleClickOther(item)"
> >
@ -133,6 +133,9 @@
/> />
</div> </div>
</div> </div>
<div class="more" v-if="bindings.length > 4" @click="moreVisible = true">
查看更多
</div>
</div> </div>
</div> </div>
</div> </div>
@ -158,6 +161,31 @@
</div> </div>
</j-spin> </j-spin>
</div> </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> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -189,7 +217,7 @@ const viewLogo = getImage('/view-logo.png');
const LoginWarpStyle = reactive({ const LoginWarpStyle = reactive({
backgroundImage: `url(${bgImage})`, backgroundImage: `url(${bgImage})`,
}); });
const moreVisible = ref(false)
const screenWidth = ref(document.body.clientWidth); const screenWidth = ref(document.body.clientWidth);
const screenHeight = ref(document.body.clientHeight); const screenHeight = ref(document.body.clientHeight);
@ -515,6 +543,10 @@ onMounted(()=>{
} }
} }
.more{
text-align: center;
cursor: pointer;
}
} }
.prefixIcon { .prefixIcon {
@ -601,4 +633,15 @@ onMounted(()=>{
margin-top: 12% !important; 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> </style>