merge: merge dev

merge: merge dev
This commit is contained in:
XieYongHong 2024-08-02 15:52:51 +08:00 committed by GitHub
parent 33d1aff0be
commit 951215955a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 6208 additions and 16300 deletions

View File

@ -1,3 +1,3 @@
#!/usr/bin/env bash
docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.3.0-SNAPSHOT .
docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.3.0-SNAPSHOT
docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.2.0-TEST .
docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.2.0-TEST

14892
package-lock.json generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

File diff suppressed because one or more lines are too long

View File

@ -22,7 +22,7 @@ import '@vuemap/vue-amap/dist/style.css';
import { getAMapUiPromise } from './utils';
import { useSystem } from '@/store/system';
const emit = defineEmits(['init'])
const emit = defineEmits(['init']);
const system = useSystem();
interface AMapProps {
@ -31,11 +31,12 @@ interface AMapProps {
AMapUI?: string | boolean;
}
const amapKey = system.$state.configInfo.amap?.apiKey;
const secretKey = system.$state.configInfo.amap?.secretKey;
initAMapApiLoader({
key: amapKey || 'c86c1b22c6b223e3ed08815532676445',
securityJsCode: 'b0efcf1ce14cbf2d56d3cde630cd19cf',
plugins: ['AMap.DistrictSearch'],
key: amapKey,
securityJsCode: secretKey,
plugins: ['AMap.DistrictSearch', 'AMap.GeoJSON'],
});
const props = defineProps({
@ -49,7 +50,7 @@ const mapRef = ref();
const uiLoading = ref<boolean>(false);
const map = ref<any>(null);
let mapInstance:any = null;
const isOpenUi = computed(() => {
return 'AMapUI' in props || props.AMapUI;
@ -65,13 +66,27 @@ const getAMapUI = () => {
const marker = ref<any[]>([]);
const initMap = (e: any) => {
map.value = e;
mapInstance = e;
if (isOpenUi.value) {
getAMapUI();
}
emit('init', e)
emit('init', e);
};
const setBounds = (bounds: any) => {
console.log(bounds)
if (mapInstance) {
mapInstance.setBounds(bounds)
}
}
onMounted(()=>{
console.log(secretKey,'secretKey')
})
defineExpose({
setBounds
})
</script>
<style lang="less" scoped>
</style>
<style lang="less" scoped></style>

View File

@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
import type { FormModelType } from '@/views/rule-engine/Scene/typings'
import { detail } from '@/api/rule-engine/scene'
import {cloneDeep, isArray, isObject} from 'lodash-es'
import { randomString } from '@/utils/utils'
import {randomNumber, randomString} from '@/utils/utils'
type DataType = {
data: FormModelType | any
@ -12,7 +12,6 @@ type DataType = {
const assignmentKey = (data: any[]): any[] => {
const onlyKey = ['when', 'then', 'terms', 'actions'];
if (!data) return [];
return data.map((item: any) => {
if (item) {
item.key = randomString();
@ -25,7 +24,7 @@ const assignmentKey = (data: any[]): any[] => {
return item;
});
};
const defaultBranchId = randomNumber()
export const defaultBranches = [
{
when: [
@ -55,8 +54,8 @@ export const defaultBranches = [
},
then: [],
executeAnyway: true,
branchId: Math.floor(Math.random() * 100000000),
branchName:''
branchId: defaultBranchId,
branchName:'条件'
},
];
@ -69,6 +68,10 @@ const defaultOptions = {
terms: [['','eq','','and']],
},
],
branchName:'条件',
key: 'branches_1',
executeAnyway: true,
groupIndex: 1
},
],
};
@ -156,7 +159,22 @@ export const useSceneStore = defineStore('scene', () => {
// branches.push(null);
// }
}
console.log('result.options',branches)
branches = branches.map(item => {
if (item?.then) {
item?.then.forEach(thenItem => {
if (thenItem.actions) {
thenItem.actions.forEach(actionItem => {
if (!actionItem.actionId) {
actionItem.actionId = randomNumber()
}
})
}
})
}
return item
})
data.value = {
...result,
trigger: result.trigger || {},

View File

@ -1,8 +1,6 @@
<template>
<page-container>
<FullPage>
<j-card>
<div class="box">
<!-- <j-card> -->
<div class="box" v-if="!noData">
<div class="left">
<div class="left-content">
<TitleComponent data="基本信息" />
@ -84,9 +82,7 @@
>
<AIcon
type="QuestionCircleOutlined"
style="
margin-left: 2px;
"
style="margin-left: 2px"
/>
</j-tooltip>
</span>
@ -94,8 +90,7 @@
<j-select
placeholder="请选择服务地址"
v-model:value="
modelRef.accessConfig
.regionId
modelRef.accessConfig.regionId
"
show-search
@change="regionChange"
@ -106,19 +101,14 @@
:key="item.id"
:value="item.id"
:label="item.name"
>{{
item.name
}}</j-select-option
>{{ item.name }}</j-select-option
>
</j-select>
</j-form-item>
</j-col>
<j-col :span="24">
<j-form-item
:name="[
'accessConfig',
'instanceId',
]"
:name="['accessConfig', 'instanceId']"
>
<template #label>
<span>
@ -128,9 +118,7 @@
>
<AIcon
type="QuestionCircleOutlined"
style="
margin-left: 2px;
"
style="margin-left: 2px"
/>
</j-tooltip>
</span>
@ -138,8 +126,7 @@
<j-input
placeholder="请输入实例ID"
v-model:value="
modelRef.accessConfig
.instanceId
modelRef.accessConfig.instanceId
"
@blur="productChange"
/>
@ -147,10 +134,7 @@
</j-col>
<j-col :span="24">
<j-form-item
:name="[
'accessConfig',
'accessKeyId',
]"
:name="['accessConfig', 'accessKeyId']"
:rules="[
{
required: true,
@ -170,9 +154,7 @@
>
<AIcon
type="QuestionCircleOutlined"
style="
margin-left: 2px;
"
style="margin-left: 2px"
/>
</j-tooltip>
</span>
@ -180,8 +162,7 @@
<j-input
placeholder="请输入accessKey"
v-model:value="
modelRef.accessConfig
.accessKeyId
modelRef.accessConfig.accessKeyId
"
@blur="productChange"
/>
@ -189,15 +170,11 @@
</j-col>
<j-col :span="24">
<j-form-item
:name="[
'accessConfig',
'accessSecret',
]"
:name="['accessConfig', 'accessSecret']"
:rules="[
{
required: true,
message:
'请输入accessSecret',
message: '请输入accessSecret',
},
{
max: 64,
@ -213,9 +190,7 @@
>
<AIcon
type="QuestionCircleOutlined"
style="
margin-left: 2px;
"
style="margin-left: 2px"
/>
</j-tooltip>
</span>
@ -223,8 +198,7 @@
<j-input
placeholder="请输入accessSecret"
v-model:value="
modelRef.accessConfig
.accessSecret
modelRef.accessConfig.accessSecret
"
@blur="productChange"
/>
@ -246,9 +220,7 @@
>
<AIcon
type="QuestionCircleOutlined"
style="
margin-left: 2px;
"
style="margin-left: 2px"
/>
</j-tooltip>
</span>
@ -328,9 +300,7 @@
item?.productKey ||
'',
)"
:key="
i.productKey
"
:key="i.productKey"
:value="
i.productKey
"
@ -361,35 +331,10 @@
{
validator:
_validator,
trigger:
'change',
trigger: 'change',
},
]"
>
<!-- <j-select
placeholder="请选择平台产品"
v-model:value="
item.productId
"
show-search
>
<j-select-option
v-for="i in getPlatProduct(
item.productId ||
'',
)"
:key="i.id"
:value="
i?.id
"
:label="
i.name
"
>{{
i.name
}}</j-select-option
>
</j-select> -->
<MSelect
v-model:value="
item.productId
@ -400,9 +345,7 @@
'',
)
"
@error="
onPlatError
"
@error="onPlatError"
/>
</j-form-item>
</j-col>
@ -416,10 +359,7 @@
<j-col :span="24">
<j-button
type="dashed"
style="
width: 100%;
margin-top: 10px;
"
style="width: 100%; margin-top: 10px"
@click="addItem"
>
<AIcon
@ -438,9 +378,7 @@
}"
>
<j-textarea
v-model:value="
modelRef.description
"
v-model:value="modelRef.description"
placeholder="请输入说明"
showCount
:maxlength="200"
@ -449,7 +387,53 @@
</j-col>
</j-row>
</j-form>
<div v-if="type === 'edit'">
</div>
</div>
<div class="right">
<Doc />
</div>
<div class="control">
<a-space>
<PermissionButton
v-if="data?.id"
hasPermission="Northbound/AliCloud:delete"
danger
:disabled="data?.state?.value !== 'disabled'"
:tooltip="{
title:
data?.state?.value !== 'disabled'
? '请先禁用该数据,再删除。'
: '删除',
}"
:popConfirm="{
title: `确认删除`,
onConfirm: deleteData,
}"
>删除
</PermissionButton>
<PermissionButton
v-if="data?.id"
type="primary"
ghost
hasPermission="Northbound/AliCloud:action"
:tooltip="{
title:
data?.state?.value !== 'disabled'
? '禁用'
: '启用',
}"
:popConfirm="{
title: `确认${
data?.state?.value !== 'disabled'
? '禁用'
: '启用'
}?`,
onConfirm: actionAliCloud,
}"
>{{
data?.state?.value !== 'disabled' ? '禁用' : '启用'
}}
</PermissionButton>
<PermissionButton
type="primary"
:loading="loading"
@ -461,16 +445,14 @@
>
保存
</PermissionButton>
</a-space>
</div>
</div>
</div>
<div class="right">
<Doc />
</div>
</div>
</j-card>
</FullPage>
</page-container>
<j-empty
v-else
style="height: calc(100vh - 300px); padding-top: 200px"
></j-empty>
<!-- </j-card> -->
</template>
<script lang="ts" setup>
@ -481,18 +463,23 @@ import {
getRegionsList,
getAliyunProductsList,
queryProductList,
_delete,
_deploy,
_undeploy,
} from '@/api/northbound/alicloud';
import _ from 'lodash-es';
import { onlyMessage } from '@/utils/comm';
import MSelect from '../../components/MSelect/index.vue';
import { _deploy } from '@/api/device/product';
const router = useRouter();
const route = useRoute();
import { _deploy as deploy } from '@/api/device/product';
const props = defineProps({
data: {
type: Object,
},
});
const emit = defineEmits(['refreshList']);
const formRef = ref();
const _errorSet = ref<Set<string>>(new Set());
const noData = ref(false);
const modelRef = reactive<any>({
id: undefined,
name: undefined,
@ -530,7 +517,6 @@ const productList = ref<Record<string, any>[]>([]);
const regionsList = ref<Record<string, any>[]>([]);
const aliyunProductList = ref<Record<string, any>[]>([]);
const loading = ref<boolean>(false);
const type = ref<'edit' | 'view'>('edit');
const activeKey = ref<string[]>(['0']);
const queryRegionsList = async () => {
@ -601,7 +587,7 @@ const regionChange = (val: any) => {
};
const onActiveProduct = async () => {
[..._errorSet.value.values()].forEach(async (i: any) => {
const resp = await _deploy(i).catch((error) => {
const resp = await deploy(i).catch((error) => {
onlyMessage('操作失败', 'error');
});
if (resp?.status === 200) {
@ -634,6 +620,7 @@ const saveBtn = () => {
formRef.value
.validate()
.then(async (data: any) => {
console.log(data, 'data');
const product = (aliyunProductList.value || []).find(
(item: any) => item?.productKey === data?.bridgeProductKey,
);
@ -648,8 +635,11 @@ const saveBtn = () => {
});
if (resp.status === 200) {
onlyMessage('操作成功!');
formRef.value.resetFields();
router.push('/iot/northbound/AliCloud');
if (props.data?.id) {
emit('refreshList', true);
} else {
emit('refreshList');
}
}
})
.catch((err: any) => {
@ -661,14 +651,47 @@ const saveBtn = () => {
});
});
};
const actionAliCloud = () => {
let response = undefined;
if (props.data?.state?.value !== 'disabled') {
response = _undeploy(props.data?.id);
} else {
response = _deploy(props.data?.id);
}
response.then((res) => {
if (res && res.status === 200) {
onlyMessage('操作成功!');
emit('refreshList', true);
} else {
onlyMessage('操作失败!', 'error');
}
});
return response;
};
const deleteData = () => {
const response = _delete(props.data?.id);
response.then((resp) => {
if (resp.status === 200) {
onlyMessage('操作成功!');
emit('refreshList');
} else {
onlyMessage('操作失败!', 'error');
}
});
return response;
};
watch(
() => route.params?.id,
async (newId) => {
if (newId) {
() => props.data,
async () => {
_errorSet.value?.clear();
noData.value = false;
formRef.value?.resetFields();
if (props.data?.id) {
queryRegionsList();
await getProduct();
if (newId === ':id' || !newId) return;
const resp = await detail(newId as string);
const resp = await detail(props.data?.id as string);
const _data: any = resp.result;
if (_data) {
getAliyunProduct(_data?.accessConfig);
@ -677,16 +700,29 @@ watch(
activeKey.value = (_data?.mappings || []).map(
(_: any, index: number) => index,
);
}
} else {
if (props.data?.type === 'add') {
modelRef.id = undefined;
modelRef.name = undefined;
modelRef.accessConfig = {
regionId: undefined,
instanceId: undefined,
accessKeyId: undefined,
accessSecret: undefined,
apiEndpoint: undefined,
};
modelRef.bridgeProductKey = undefined;
modelRef.bridgeProductName = undefined;
modelRef.mappings = [
{
productKey: undefined,
productId: undefined,
},
{ immediate: true, deep: true },
);
watch(
() => route.query.type,
(newVal) => {
if (newVal) {
type.value = newVal as 'edit' | 'view';
];
modelRef.description = undefined;
} else if (props.data?.type === 'noData') {
noData.value = true;
}
}
},
{ immediate: true, deep: true },
@ -696,18 +732,35 @@ watch(
<style scoped lang="less">
.box {
position: relative;
margin: 20px;
.left {
.left-content {
width: 66%;
padding: 0 20px;
height: calc(100vh - 300px);
overflow-y: auto;
&::-webkit-scrollbar {
width: 5px; /* 滚动条宽度 */
background-color: #edf5ff; /* 滚动条背景色 */
}
&::-webkit-scrollbar-thumb {
background-color: #d0d0d0; /* 滚动条拖动部分颜色 */
border-radius: 4px; /* 滚动条拖动部分圆角 */
}
}
}
.right {
width: 33%;
position: absolute;
right: 0;
top: 0;
top: 40px;
overflow-y: auto;
height: 100%;
height: 95%;
}
.control {
position: absolute;
right: 0;
top: 0;
}
}
</style>

View File

@ -6,6 +6,7 @@
:visible="true"
width="700px"
:confirm-loading="loading"
:maskClosable="false"
@cancel="handleCancel"
@ok="handleOk"
>
@ -40,7 +41,10 @@
/> </j-form-item
></j-col>
<j-col :span="12"
><j-form-item label="版本号" v-bind="validateInfos.version">
><j-form-item
label="版本号"
v-bind="validateInfos.version"
>
<j-input
placeholder="请输入版本号"
v-model:value="formData.version" /></j-form-item
@ -82,7 +86,9 @@
><j-form-item v-bind="validateInfos.sign">
<template #label>
签名
<j-tooltip title="请输入本地文件进行签名加密后的值">
<j-tooltip
title="请输入本地文件进行签名加密后的值"
>
<AIcon
type="QuestionCircleOutlined"
style="margin-left: 2px"
@ -94,7 +100,10 @@
v-model:value="formData.sign" /></j-form-item
></j-col>
<j-col :span="24">
<j-form-item label="固件上传" v-bind="validateInfos.url">
<j-form-item
label="固件上传"
v-bind="validateInfos.url"
>
<FileUpload
v-model:modelValue="formData.url"
v-model:extraValue="extraValue"
@ -107,8 +116,8 @@
>
<j-form
:class="
dynamicValidateForm.properties.length !== 0 &&
'formRef'
dynamicValidateForm.properties.length !==
0 && 'formRef'
"
ref="formRef"
name="dynamic_form_nest_item"
@ -154,18 +163,24 @@
class="formRef-form-item"
style="width: 10%"
>
<j-popconfirm
title="确认删除吗?"
ok-text="确认"
cancel-text="取消"
@confirm="removeList(propertie)"
<PermissionButton
type="text"
:popConfirm="{
title: '确认删除吗?',
onConfirm: () =>
removeList(propertie),
}"
>
<AIcon type="DeleteOutlined" />
</j-popconfirm>
<AIcon type="DeleteOutlined"
/></PermissionButton>
</j-form-item>
</div>
<j-form-item class="formRef-form-item-add">
<j-button type="dashed" block @click="addList">
<j-button
type="dashed"
block
@click="addList"
>
<AIcon type="PlusOutlined" />
添加
</j-button>
@ -226,13 +241,16 @@ const addList = () => {
const loading = ref(false);
const _loading = ref(false);
const useForm = Form.useForm;
const productOptions = ref([]);
const props = defineProps({
data: {
type: Object,
default: () => {},
},
productOptions: {
type: Array,
default: [],
},
});
const emit = defineEmits(['change']);
@ -288,27 +306,25 @@ const validatorProductExist = async (_: Record<string, any>, value: string) => {
if (!value) {
return Promise.resolve();
} else {
const dt = productOptions.value.find((i: any) => i.value === value)
if(dt){
const dt = props.productOptions.find((i: any) => i.value === value);
if (dt) {
return Promise.resolve();
} else {
return Promise.reject('当前产品不存在,请选择产品')
return Promise.reject('当前产品不存在,请选择产品');
}
}
};
const validatorVersionValue = async (_rule:any,value:any) => {
return new Promise (async(resolve,reject)=>{
const validatorVersionValue = async (_rule: any, value: any) => {
return new Promise(async (resolve, reject) => {
const posReg = /^[1-9]\d*$/;
if(posReg.test(value.toString())){
if (posReg.test(value.toString())) {
return resolve('');
}else {
} else {
return reject('请输入1~99999之间的正整数');
}
}
)
}
});
};
const { resetFields, validate, validateInfos } = useForm(
formData,
reactive({
@ -318,7 +334,7 @@ const { resetFields, validate, validateInfos } = useForm(
],
productId: [
{ required: true, message: '请选择所属产品' },
{ validator: validatorProductExist, trigger: 'blur' }
{ validator: validatorProductExist, trigger: 'blur' },
],
version: [
{ required: true, message: '请输入版本号' },
@ -327,7 +343,7 @@ const { resetFields, validate, validateInfos } = useForm(
versionOrder: [
{ required: true, message: '请输入版本序号' },
{ validator: validatorVersionOrder, trigger: 'blur' },
{ validator: validatorVersionValue, trigger: 'change'}
{ validator: validatorVersionValue, trigger: 'change' },
],
signMethod: [{ required: true, message: '请选择签名方式' }],
sign: [
@ -348,7 +364,7 @@ const handleOk = async () => {
validate()
.then(async (res: any) => {
const product: any = productOptions.value.find(
const product: any = props.productOptions.find(
(item: any) => item?.value === res.productId,
);
const productName = product?.label || props.data?.url;
@ -385,20 +401,6 @@ const changeSignMethod = () => {
formData.value.url = '';
};
onMounted(() => {
_loading.value = true
queryProduct({
paging: false,
terms: [{ column: 'state', value: 1 }],
sorts: [{ name: 'createTime', order: 'desc' }],
}).then((resp: any) => {
_loading.value = false
productOptions.value = resp.result.map((item: any) => ({
value: item.id,
label: item.name,
}));
});
});
watch(
() => props.data,
(value) => {

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,13 @@
import { ColumnProps } from "ant-design-vue/es/table";
import { DataType, Source, InputParams, OtherSetting, OutputParams, ConfigParams, TagsType } from './components'
import SelectColumn from './components/Events/SelectColumn.vue';
import AsyncSelect from './components/Function/AsyncSelect.vue';
import { EventLevel } from "@/views/device/data";
import {MetadataType} from "@/views/device/Product/typings";
import { DataType } from './components'
import {MetadataItem, MetadataType} from "@/views/device/Product/typings";
import { getUnit } from '@/api/device/instance';
import {Ref} from "vue";
import {omit, pick, isObject, cloneDeep} from "lodash-es";
import { message } from 'jetlinks-ui-components'
import {omit, isObject, groupBy} from "lodash-es";
import { onlyMessage } from "@/utils/comm";
import {getSourceMap} from "@/views/device/components/Metadata/Base/utils";
import {getTypeMap} from "components/Metadata/Table/components/Type/data";
import {getEventLevelMap} from "@/views/device/data";
interface DataTableColumnProps extends ColumnProps {
type?: string,
components?: {
@ -21,7 +20,10 @@ interface DataTableColumnProps extends ColumnProps {
},
options?: any[]
doubleClick?: (record: any, index: number, dataIndex: string) => boolean
control?: (newValue: any, oldValue: any) => Boolean
control?: (newValue: any, oldValue: any) => boolean
filter?: boolean
sort?: Record<string, any>
}
const SourceMap = {
@ -36,6 +38,8 @@ const type = {
report: '上报',
};
const METADATA_UNIT = 'metadata-unit'
export const validatorConfig = (value: any, _isObject: boolean = false) => {
if (!value) {
@ -53,6 +57,10 @@ export const validatorConfig = (value: any, _isObject: boolean = false) => {
return Promise.reject('请添加参数')
}
if (value.type === 'date' && !value.format) {
return Promise.reject('请选择时间格式')
}
if (value.type === 'file' && (!value.bodyType || (isObject(value.bodyType) && !Object.keys(value.bodyType).length))) {
return Promise.reject('请选择文件类型')
}
@ -93,9 +101,8 @@ export const handleTypeValue = (type:string, value: any = {}) => {
obj.format = value
break;
case 'string':
obj.maxLength = JSON.stringify(value) === '{}' ? undefined : value
case 'password':
obj.maxLength = JSON.stringify(value) === '{}' ? undefined : value
obj.expands.maxLength = JSON.stringify(value) === '{}' ? undefined : value
break;
default:
obj = value
@ -133,7 +140,7 @@ export const typeSelectChange = (type: string) => {
break;
case 'string':
case 'password':
obj.maxLength = undefined
obj.expands.maxLength = undefined
break;
case 'boolean':
obj.trueText = '是'
@ -155,27 +162,28 @@ const isExtendsProduct = (id: string, productKeys: string, type: string) => {
return false
}
export const useColumns = (type?: MetadataType, target?: 'device' | 'product', noEdit?: Ref<any>, productNoEdit?: Ref<any>) => {
export const useColumns = (dataSource: Ref<MetadataItem[]>, type?: MetadataType, target?: 'device' | 'product', noEdit?: Ref<any>, productNoEdit?: Ref<any>) => {
const BaseColumns: DataTableColumnProps[] = [
{
title: '标识',
dataIndex: 'id',
type: 'text',
form: {
required: true,
rules: [{
callback(rule:any,value: any, dataSource: any[]) {
asyncValidator(rule:any,value: any, ...setting: any) {
if (value) {
const field = rule.field.split('.')
const fieldIndex = Number(field[1])
const hasId = dataSource.some((item, index) => item.id === value && fieldIndex !== index)
if (hasId) {
const option = setting[2]
const isSome = dataSource.value.some((item) => {
return item.__dataIndex !== option.index && item.id && item.id === value
})
if (isSome) {
return Promise.reject('该标识已存在')
}
return Promise.resolve()
}
return Promise.reject('请输入标识')
return Promise.resolve()
// return Promise.reject('请输入标识')
},
},
{ max: 64, message: '最多可输入64个字符' },
@ -185,273 +193,41 @@ export const useColumns = (type?: MetadataType, target?: 'device' | 'product', n
},
]
},
doubleClick(record) {
if (isExtendsProduct(record.id, productNoEdit?.value, 'id')) {
return false
}
const ids = (noEdit?.value?.id || []) as any[]
return !ids.includes(record.id)
}
filter: true
},
{
title: '名称',
dataIndex: 'name',
width: 300,
type: 'text',
form: {
required: true,
rules: [
{
required: true,
message: '请输入名称'
},
{ max: 64, message: '最多可输入64个字符' },
]
// rules:[{
// callback(rule:any,value: any, dataSource: any[]) {
// console.log(rule,value,dataSource,123)
// return value
// }
// }]
},
doubleClick(record) {
if (isExtendsProduct(record.id, productNoEdit?.value, 'name')) {
return false
}
return true
}
},
];
asyncValidator(_: any, value: any) {
const EventColumns: DataTableColumnProps[] = BaseColumns.concat([
{
title: '事件级别',
dataIndex: 'expands',
type: 'components',
components: {
name: SelectColumn,
props: {
options: EventLevel
}
},
doubleClick(record) {
if (isExtendsProduct(record.id, productNoEdit?.value, 'expands')) {
return false
}
return true
},
control(newValue, oldValue) {
return newValue.expands.level !== oldValue?.expands?.level
},
},
{
title: '输出参数',
dataIndex: 'outInput',
},
{
title: '配置参数',
dataIndex: 'properties',
width: 100,
form: {
required: true,
rules: [{
callback(rule: any, value: any, dataSource: any[]) {
const field = rule.field.split('.')
const fieldIndex = Number(field[1])
const record = dataSource.find((item, index) => index === fieldIndex)
if (!record.valueType.properties.length) {
return Promise.reject('请添加配置参数')
if (!value) {
return Promise.reject('请输入名称')
} else if (value.length > 64) {
return Promise.reject('最多可输入64个字符')
}
return Promise.resolve()
}
}]
},
control(newValue, oldValue) {
if (newValue && !oldValue) {
return true
} else if (newValue && oldValue) {
return JSON.stringify(newValue.valueType) !== JSON.stringify(oldValue.valueType)
}
return false
},
},
{
title: '其它配置',
dataIndex: 'other',
width: 100,
control(newValue, oldValue) {
if (!oldValue) {
return true
} else if (newValue && oldValue) {
// 仅留下存储和指标值
const keys = ['source', 'type', 'virtualRule', 'required']
const newObj = omit(cloneDeep(newValue.expands), keys)
const oldObj = omit(cloneDeep(oldValue.expands), keys)
return JSON.stringify(newObj) !== JSON.stringify(oldObj)
}
return false
},
},
{
title: '说明',
dataIndex: 'description',
type: 'text',
form: {
rules: [
{ max: 20, message: '最多可输入20个字符' },
]},
doubleClick(record) {
if (isExtendsProduct(record.id, productNoEdit?.value, 'description')) {
return false
}
return true
}
},
{
title: '操作',
dataIndex: 'action',
width: 120
}
]);
const FunctionColumns: DataTableColumnProps[] = BaseColumns.concat([
{
title: '是否异步',
dataIndex: 'async',
type: 'components',
components: {
name: AsyncSelect,
props: {
options: [
{ label: '是', value: true },
{ label: '否', value: false }
]
}
},
doubleClick(record) {
return !isExtendsProduct(record.id, productNoEdit?.value, 'async');
filter: true
},
control(newValue, oldValue) {
return newValue.async !== oldValue?.async
},
},
{
title: '输入参数',
dataIndex: 'inputs',
width: 120,
// form: {
// required: true,
// rules: [{
// callback(rule:any,value: any, dataSource: any[]) {
// const field = rule.field.split('.')
// const fieldIndex = Number(field[1])
//
// const values = dataSource.find((item, index) => index === fieldIndex)
//
// return validatorConfig({
// type: 'object',
// properties: values.inputs
// }, true)
// }
// }]
// },
control(newValue, oldValue) {
if (newValue && !oldValue) {
return true
} else if (newValue && oldValue) {
return JSON.stringify(newValue.inputs) !== JSON.stringify(oldValue.inputs)
}
return false
},
},
{
title: '输出参数',
dataIndex: 'output',
type: 'components',
components: {
name: OutputParams
},
form: {
rules: [{
callback(rule:any,value: any, dataSource: any[]) {
const field = rule.field.split('.')
const fieldIndex = Number(field[1])
const values = dataSource.find((item, index) => index === fieldIndex)
return validatorConfig(values?.output)
}
}]
},
doubleClick(record) {
return !isExtendsProduct(record.id, productNoEdit?.value, 'output');
},
control(newValue, oldValue) {
if (newValue && !oldValue) {
return true
} else if (newValue && oldValue) {
return JSON.stringify(newValue.output) !== JSON.stringify(oldValue.output)
}
return false
},
},
{
title: '其它配置',
dataIndex: 'other',
width: 100,
control(newValue, oldValue) {
if (!oldValue) {
return true
} else if (newValue && oldValue) {
// 仅留下存储和指标值
const keys = ['source', 'type', 'virtualRule', 'required']
const newObj = omit(cloneDeep(newValue.expands), keys)
const oldObj = omit(cloneDeep(oldValue.expands), keys)
return JSON.stringify(newObj) !== JSON.stringify(oldObj)
}
return false
},
},
{
title: '说明',
dataIndex: 'description',
type: 'text',
form: {
rules: [
{ max: 20, message: '最多可输入20个字符' },
]},
doubleClick(record) {
if (isExtendsProduct(record.id, productNoEdit?.value, 'description')) {
return false
}
return true
}
},
{
title: '操作',
dataIndex: 'action',
width: 120
}
// {
// title: '读写类型',
// dataIndex: 'expands',
// render: (text: any) => (text?.type || []).map((item: string | number) => <Tag>{type[item]}</Tag>),
// },
]);
];
const PropertyColumns: DataTableColumnProps[] = BaseColumns.concat([
{
title: '数据类型',
dataIndex: 'valueType',
type: 'components',
components: {
name: DataType
},
form: {
required: true,
rules: [{
validator(_: any, value: any) {
asyncValidator(_: any, value: any) {
if (!value?.type) {
return Promise.reject('请选择数据类型')
}
@ -459,49 +235,35 @@ export const useColumns = (type?: MetadataType, target?: 'device' | 'product', n
}
}]
},
width: 230,
doubleClick(record) {
if (isExtendsProduct(record.id, productNoEdit?.value, 'valueType')) {
return false
}
return true
}
// sort: {
// sortKey: ['valueType', 'type'],
// dataSource: () => {
// const group = groupBy(dataSource.value.filter(item => item.id && item.valueType.type), (e) => e.valueType.type)
// const typeMap = getTypeMap()
// return Object.keys(group).map((key, index) => {
// return {
// name: typeMap[key],
// key: key,
// total: group[key].length
// }
// })
// }
// },
width: 260,
},
{
title: '属性来源',
dataIndex: 'expands',
type: 'components',
components: {
name: Source,
props: {
noEdit: noEdit?.value?.source || [],
target: target,
productNoEdit: productNoEdit?.value
}
},
doubleClick(record){
if (record.expands.source === 'rule') {
return true
}
return !isExtendsProduct(record.id, productNoEdit?.value, 'expands')
},
form: {
required: true,
rules: [
{
callback: async (rule: any, value: any, dataSource: any[]) => {
const field = rule.field.split('.')
const fieldIndex = Number(field[1])
asyncValidator: async (rule: any, value: any) => {
const values = dataSource.find((item, index) => index === fieldIndex)
const virtualRule = values.expands?.virtualRule
const source = value.source
console.log(source, value)
if (source) {
if (source === 'device' && !value.type?.length) {
return Promise.reject('请选择读写类型');
} else if( source === 'rule' && !virtualRule){
return Promise.reject('请配置规则');
}
return Promise.resolve()
@ -512,55 +274,176 @@ export const useColumns = (type?: MetadataType, target?: 'device' | 'product', n
},
]
},
control(newValue, oldValue) {
if (newValue && !oldValue) {
return true
} else if (newValue && oldValue) {
const keys = ['source', 'type', 'virtualRule']
const newObj = pick(cloneDeep(newValue.expands), keys)
const oldObj = pick(cloneDeep(oldValue.expands), keys)
return JSON.stringify(newObj) !== JSON.stringify(oldObj)
}
return false
},
width: 150
// sort: {
// sortKey: ['expands', 'source'],
// dataSource: () => {
// const group = groupBy(dataSource.value.filter(item => item.id), (e) => e.expands.source)
// const sourceMap = getSourceMap()
//
// return Object.keys(group).map(key => {
// return {
// name: sourceMap[key],
// key: key,
// total: group[key].length
// }
// })
// }
// },
width: 220
},
// {
// title: '属性分组',
// dataIndex: 'group',
// width: 140,
// },
{
title: '其它配置',
dataIndex: 'other',
width: 100,
control(newValue, oldValue) {
if (!oldValue) {
return true
} else if (newValue && oldValue) {
// 仅留下存储和指标值
const keys = ['source', 'type', 'virtualRule', 'required']
const newObj = omit(cloneDeep(newValue.expands), keys)
const oldObj = omit(cloneDeep(oldValue.expands), keys)
return JSON.stringify(newObj) !== JSON.stringify(oldObj)
}
return false
width: 110,
},
]);
const FunctionColumns: DataTableColumnProps[] = BaseColumns.concat([
{
title: '是否异步',
dataIndex: 'async',
width: 120,
// sort: {
// sortKey: ['async'],
// dataSource: () => {
// const group = groupBy(dataSource.value.filter(item => item.id), (e) => e.async)
//
// return Object.keys(group).map(key => {
// return {
// name: key ? '是' : '否',
// key: key,
// total: group[key].length
// }
// })
// }
// },
},
{
title: '操作',
dataIndex: 'action',
width: 120
title: '输入参数',
dataIndex: 'inputs',
width: 110,
},
{
title: '输出参数',
dataIndex: 'output',
width: 240,
form: {
rules: [{
asyncValidator: async (rule: any, value: any) => {
return validatorConfig(value)
}
}]
},
// sort: {
// sortKey: ['output', 'type'],
// dataSource: () => {
// const group = groupBy(dataSource.value.filter(item => item.id && item.output.type), (e) => e.output.type)
// const typeMap = getTypeMap()
// return Object.keys(group).map(key => {
// return {
// name: typeMap[key],
// key: key,
// total: group[key].length
// }
// })
// }
// }
},
// {
// title: '属性分组',
// dataIndex: 'group',
// width: 140,
// },
{
title: '其它配置',
dataIndex: 'other',
width: 120,
},
{
title: '说明',
dataIndex: 'description',
width: 220,
form: {
rules: [
{ max: 20, message: '最多可输入20个字符' },
]},
},
]);
const EventColumns: DataTableColumnProps[] = BaseColumns.concat([
{
title: '事件级别',
dataIndex: 'expands',
width: 150,
// sort: {
// sortKey: ['expands', 'level'],
// dataSource: () => {
// const group = groupBy(dataSource.value.filter(item => item.id), (e) => e.expands.level)
// const typeMap = getEventLevelMap()
// return Object.keys(group).map(key => {
// return {
// name: typeMap[key],
// key: key,
// total: group[key].length
// }
// })
// }
// }
},
{
title: '输出参数',
dataIndex: 'valueType',
width: 110,
form: {
required: true,
rules: [{
asyncValidator: async (rule: any, value: any) => {
if (!value.properties?.length) {
return Promise.reject('请添加配置参数')
}
return Promise.resolve()
}
}]
},
},
// {
// title: '属性分组',
// dataIndex: 'group',
// width: 140,
// },
{
title: '其它配置',
dataIndex: 'other',
width: 120,
},
{
title: '说明',
dataIndex: 'description',
width: 220,
form: {
rules: [
{ max: 20, message: '最多可输入20个字符' },
]},
},
]);
const TagColumns: DataTableColumnProps[] = BaseColumns.concat([
{
title: '数据类型',
dataIndex: 'valueType',
type: 'components',
components: {
name: DataType,
},
width: 240,
form: {
required: true,
rules: [{
validator(_: any, value: any) {
asyncValidator: async (rule: any, value: any) => {
if (!value?.type) {
return Promise.reject('请选择数据类型')
}
@ -568,78 +451,56 @@ export const useColumns = (type?: MetadataType, target?: 'device' | 'product', n
}
}]
},
doubleClick(record) {
if (isExtendsProduct(record.id, productNoEdit?.value, 'valueType')) {
return false
}
return true
},
control(newValue, oldValue) {
if (newValue && !oldValue) {
return true
} else if (newValue && oldValue) {
return JSON.stringify(newValue.valueType) !== JSON.stringify(oldValue.valueType)
}
return false
},
// sort: {
// sortKey: ['valueType', 'type'],
// dataSource: () => {
// const group = groupBy(dataSource.value.filter(item => item.id && item.valueType.type), (e) => e.valueType.type)
// const typeMap = getTypeMap()
// return Object.keys(group).map(key => {
// return {
// name: typeMap[key],
// key: key,
// total: group[key].length
// }
// })
// }
// }
},
// {
// title: '读写类型',
// dataIndex: 'expands',
// width: 190,
// form: {
// rules: [
// {
// asyncValidator: async (rule: any, value: any) => {
// if (!value?.type?.length) {
// return Promise.reject('请选择读写类型')
// }
// return Promise.resolve()
// }
// }]
// },
// },
// {
// title: '属性分组',
// dataIndex: 'group',
// width: 140,
// },
{
title: '读写类型',
dataIndex: 'readType',
type: 'components',
components: {
name: TagsType
},
doubleClick(record) {
if (isExtendsProduct(record.id, productNoEdit?.value, 'readType')) {
return false
}
return true
},
form: {
required: true,
rules: [
{
callback(rule:any,value: any, dataSource: any[]) {
const field = rule.field.split('.')
const fieldIndex = Number(field[1])
const values = dataSource.find((item, index) => index === fieldIndex)
if (!values?.expands?.type?.length) {
return Promise.reject('请选择读写类型')
}
return Promise.resolve()
}
}]
},
control(newValue, oldValue) {
if (newValue && !oldValue) {
return true
} else if (newValue && oldValue) {
return JSON.stringify(newValue.expands) !== JSON.stringify(oldValue.expands)
}
return false
},
title: '其它配置',
dataIndex: 'other',
width: 110,
},
{
title: '说明',
dataIndex: 'description',
type: 'text',
width: 250,
form: {
rules: [
{ max: 20, message: '最多可输入20个字符' },
]},
doubleClick(record) {
if (isExtendsProduct(record.id, productNoEdit?.value, 'description')) {
return false
}
return true
}
},
{
title: '操作',
dataIndex: 'action',
width: 120
}
]);
const columns = ref<any[]>([])
@ -680,10 +541,30 @@ export const useUnit = (type: Ref<string>) => {
}
}, { immediate: true })
return { unitOptions }
}
export const useSaveUnit = () => {
const unitOptions = ref<Array<{ label: string, value: any }>>([])
provide(METADATA_UNIT, unitOptions)
getUnit().then((res) => {
if (res.success) {
unitOptions.value = res.result.map((item: any) => ({
label: item.description,
value: item.id,
}));
}
});
return {
unitOptions
}
}
export const useGetUnit = () => inject(METADATA_UNIT)
export const TypeStringMap = {
int: 'int(整数型)',

View File

@ -1,22 +1,33 @@
<template>
<j-popconfirm-modal
<PopoverModal
v-if="!disabled"
v-model:visible="modalVisible"
body-style="padding-top:4px;width:600px;"
placement="bottomRight"
:disabled="disabled"
:get-popup-container="(node) => fullRef || node"
@confirm="confirm"
@ok="confirm"
@cancel="cancel"
@visibleChange="visibleChange"
>
<template v-if="visible" #content>
<j-scrollbar height="350" v-if="showMetrics || config.length > 0">
<j-collapse v-model:activeKey="activeKey">
<template #content>
<j-scrollbar height="350" v-if="showContent">
<j-collapse v-model:activeKey="activeKey" v-if="visible">
<j-collapse-panel
v-if="!(props.isProduct && target === 'device')"
v-for="(item, index) in config"
:key="'store_' + index"
:header="item.name"
>
<template #header>
{{ item.name }}
<j-tooltip
v-if="item.description"
:title="item.description"
>
<AIcon
type="ExclamationCircleOutlined"
style="padding-left: 12px; padding-top: 4px"
/>
</j-tooltip>
</template>
<j-table
:columns="columns"
:data-source="item.properties"
@ -35,18 +46,37 @@
:itemType="
item.properties[index].type?.type
"
:options="(item.properties[index].type?.elements || []).map((a:any) => ({
:extra="{
dropdownStyle: {
zIndex: 1071,
},
popupStyle: {
zIndex: 1071,
},
}"
:getPopupContainer="
(node) => tableWrapperRef || node
"
:options="
(
item.properties[index].type
?.elements || []
).map((a) => ({
label: a.text,
value: a.value,
}))"
:get-popup-container="
(node) => fullRef || node
}))
"
/>
</template>
</j-table>
</j-collapse-panel>
<j-collapse-panel key="metrics" v-if="showMetrics">
<j-collapse-panel
key="metrics"
v-if="
showMetrics &&
!(props.isProduct && target === 'device')
"
>
<template #header>
指标配置
<j-tooltip
@ -65,6 +95,136 @@
:value="myValue.metrics"
/>
</j-collapse-panel>
<j-collapse-panel key="extra" v-if="showExtra">
<template #header>
<a-space>
<div>阈值限制</div>
<template
v-if="
props.isProduct && target === 'device'
"
>
<a-button
type="link"
style="padding: 0 4px; height: 22px"
@click="(e) => resetThreshold(e)"
>
<template #icon>
<AIcon type="RedoOutlined" />
</template>
重置
</a-button>
<a-tooltip>
<template #title
>重置后阀值限制继承于产品</template
>
<AIcon type="QuestionCircleOutlined" />
</a-tooltip>
</template>
<a-button
v-else
type="link"
style="padding: 0 4px; height: 22px"
@click="(e) => resetThreshold(e)"
>
<template #icon>
<AIcon type="RedoOutlined" />
</template>
清空
</a-button>
</a-space>
</template>
<a-form
:model="extraForm"
layout="vertical"
ref="ThresholdRef"
>
<a-form-item>
<div class="extra-limit extra-check-group">
<CardSelect
v-model:value="extraForm.type"
:options="[
{
label: '上下限',
value: 'number-range',
},
]"
:showImage="false"
@select="limitSelect"
/>
</div>
</a-form-item>
<a-form-item
v-if="extraForm.type"
name="limit"
:rules="[
{
required: true,
message: '请输入上下限',
},
{
validator: validateLimit,
trigger: 'change',
},
]"
>
<template #label
><div class="extra-title">
阈值
</div></template
>
<a-space
v-if="extraForm.type === 'number-range'"
>
<a-input-number
v-model:value="extraForm.limit.lower"
style="width: 178px"
placeholder="请输入下限"
/>
<span>~</span>
<a-input-number
v-model:value="extraForm.limit.upper"
style="width: 178px"
:min="extraForm.limit.lower"
placeholder="请输入上限"
/>
</a-space>
</a-form-item>
<a-form-item
v-if="extraForm.type"
name="mode"
:rules="[
{
required: true,
message: '请选择处理方式',
},
]"
>
<template #label>
<div class="extra-title">
超出阈值数据处理方式
</div></template
>
<div class="extra-handle extra-check-group">
<CardSelect
v-model:value="extraForm.mode"
:options="[
{ label: '忽略', value: 'ignore' },
{ label: '记录', value: 'record' },
{
label: '告警',
value: 'device-alarm',
},
]"
:showImage="false"
/>
<div style="margin: 8px 0">
{{ handleTip }}
</div>
</div></a-form-item
>
</a-form>
</j-collapse-panel>
</j-collapse>
</j-scrollbar>
<div v-else style="padding-top: 24px">
@ -76,23 +236,21 @@
:disabled="disabled"
:has-permission="hasPermission"
:tooltip="tooltip"
style="padding-left: 0"
type="link"
type="primary"
>
<AIcon type="SettingOutlined" />
<AIcon type="EditOutlined" />
配置
</PermissionButton>
</j-popconfirm-modal>
</PopoverModal>
<PermissionButton
v-else
key="setting"
:disabled="disabled"
:has-permission="hasPermission"
:tooltip="tooltip"
style="padding-left: 0"
type="link"
type="primary"
>
<AIcon type="SettingOutlined" />
<AIcon type="EditOutlined" />
配置
</PermissionButton>
</template>
@ -106,9 +264,10 @@ import {
getMetadataConfig,
getMetadataDeviceConfig,
} from '@/api/device/product';
import ModelButton from '@/views/device/components/Metadata/Base/components/ModelButton.vue';
import { omit, cloneDeep } from 'lodash-es';
import { FULL_CODE } from 'jetlinks-ui-components/es/DataTable';
import { PopoverModal } from '@/components/Metadata/Table';
import { useTableWrapper } from '@/components/Metadata/Table/context';
import { useThreshold } from './hooks';
const props = defineProps({
value: {
@ -127,37 +286,87 @@ const props = defineProps({
type: String,
default: undefined,
},
name: {
type: String,
default: undefined,
},
record: {
type: Object,
default: () => ({}),
},
medataType: {
type: String,
default: undefined
},
hasPermission: String,
tooltip: Object,
metadataType: {
type: String,
default: 'properties',
},
target: String,
isProduct: {
type: Boolean,
default: false,
},
});
const fullRef = inject(FULL_CODE);
const type = inject('_metadataType');
const productStore = useProductStore();
const deviceStore = useInstanceStore();
const tableWrapperRef = useTableWrapper();
const emit = defineEmits(['update:value']);
const {
thresholdUpdate,
thresholdDetail,
thresholdDetailQuery,
thresholdDelete,
} = useThreshold(props);
const emit = defineEmits(['update:value', 'change']);
const ThresholdRef = ref();
const activeKey = ref();
const storageRef = ref();
const metricsRef = ref();
const myValue = ref(props.value);
const visible = ref(false);
const modalVisible = ref(false);
const config = ref<any>([]);
const configValue = ref(props.value?.expands);
const extraForm = reactive({
limit: {
upper: 0,
lower: 0,
},
mode: 'ignore',
type: '',
});
const typeMap = {
properties: 'property',
functions: 'function',
events: 'event',
tags: 'tag',
};
const handleTip = computed(() => {
if (extraForm.mode === 'ignore') {
return '平台将忽略超出阈值的数据,无法查看上报记录';
} else if (extraForm.mode === 'record') {
return '您可以在预处理数据-无效数据页面查看超出阈值的数据上报记录';
}
return '您可以在预处理数据-告警数据页面查看告警情况';
});
const showContent = computed(() => {
if (props.isProduct && props.target === 'device') {
//
return showExtra.value;
}
return showMetrics.value || config.value.length > 0;
});
const showMetrics = computed(() => {
return [
'int',
@ -170,6 +379,13 @@ const showMetrics = computed(() => {
].includes(props.type as any);
});
const showExtra = computed(() => {
return (
['int', 'long', 'float', 'double'].includes(props.type as any) &&
props.metadataType === 'properties'
);
});
const booleanOptions = ref([
{ label: '否', value: 'false' },
{ label: '是', value: 'true' },
@ -193,16 +409,41 @@ const columns = ref([
},
]);
const getType = () => {
const _typeMap = {
'propertys': 'property',
'functions': 'function',
'events': 'event',
'tags': 'tag',
const limitSelect = (keys: string[], key: string, isSelected: boolean) => {
if (!isSelected) {
//
if (key === 'number-range') {
extraForm.limit.lower = 0;
extraForm.limit.upper = 0;
}
}
return _typeMap[props.type] || 'property'
}
if (keys.length === 0) {
extraForm.mode = 'ignore';
}
};
const resetValue = () => {
extraForm.mode = 'ignore';
extraForm.type = '';
extraForm.limit.lower = 0;
extraForm.limit.upper = 0;
};
const resetThreshold = async (e: any) => {
e.stopPropagation();
await thresholdDelete();
resetValue();
thresholdDetailQuery();
};
const validateLimit = (_: any, value: any) => {
if (value.lower !== null && value.upper !== null) {
return value.upper < value.lower
? Promise.reject('上限必须大于下限')
: Promise.resolve();
} else {
return Promise.reject('请输入上下限');
}
};
const getConfig = async () => {
const id =
@ -226,7 +467,7 @@ const getConfig = async () => {
deviceId: id,
metadata: {
id: props.id,
type: getType(),
type: typeMap[props.metadataType],
dataType: props.type,
},
};
@ -235,14 +476,16 @@ const getConfig = async () => {
type === 'product'
? await getMetadataConfig(params)
: await getMetadataDeviceConfig(params);
if (resp.status === 200) {
if (resp.success) {
config.value = resp.result;
if (resp.result.length) {
activeKey.value = ['store_0'];
} else if (showMetrics.value) {
activeKey.value = ['metrics'];
}
if(showExtra.value){
activeKey.value = ['extra']
}
if (resp.result.length && !configValue.value) {
resp.result.forEach((a) => {
if (a.properties) {
@ -253,6 +496,7 @@ const getConfig = async () => {
});
}
}
visible.value = true;
};
const confirm = () => {
@ -268,21 +512,38 @@ const confirm = () => {
if (metrics) {
expands.metrics = metrics;
}
if (showExtra.value && extraForm.type) {
ThresholdRef.value?.validate().then(async () => {
await thresholdUpdate(extraForm);
expands.otherEdit = true;
emit('update:value', {
...props.value,
...expands,
});
emit('change');
modalVisible.value = false;
resolve(true);
});
} else {
expands.otherEdit = true;
emit('update:value', {
...props.value,
...expands,
});
emit('change');
modalVisible.value = false;
resolve(true);
}
} catch (err) {
reject(false);
}
});
};
const visibleChange = (e: boolean) => {
visible.value = e;
console.log('visibleChange',e)
if (e) {
watch(
() => modalVisible.value,
async () => {
if (modalVisible.value) {
configValue.value = omit(props.value, [
'source',
'type',
@ -290,21 +551,56 @@ const visibleChange = (e: boolean) => {
'required',
]);
getConfig();
if (showExtra.value) {
thresholdDetailQuery();
}
};
}
},
{ immediate: true },
);
const cancel = () => {
myValue.value = cloneDeep(props.value);
};
watch(
() => props.value,
() => thresholdDetail,
() => {
console.log(props.value);
myValue.value = cloneDeep(props.value);
if (
thresholdDetail.value &&
JSON.stringify(thresholdDetail.value) !== '{}'
) {
extraForm.mode = thresholdDetail.value?.mode;
extraForm.type = thresholdDetail.value?.type || '';
extraForm.limit = thresholdDetail.value?.limit;
}
},
{ immediate: true, deep: true },
);
watch(
() => JSON.stringify(props.value),
() => {
myValue.value = cloneDeep(props.value);
},
{ immediate: true },
);
</script>
<style scoped></style>
<style scoped lang="less">
.extra-tip {
padding: 8px;
background-color: rgba(#000, 0.05);
}
.extra-title {
font-size: 16px;
margin: 12px 0;
}
.extra-check-group {
:deep(.j-card-item) {
padding: 12px 14px !important;
}
}
</style>

View File

@ -4,28 +4,28 @@ export const testProperties = (data:any) =>{
}
export const testType = (data:any,index:number,isArray?:boolean,isObject?:boolean)=>{
if(data.type === 'boolean'){
if(!data?.trueText || !data?.trueValue || !data?.falseText || !data?.falseValue){
onlyMessage(`方法定义inputs第${index+1}个数组ValueType中缺失必填属性`,'error')
return true
}
}
if(data.type === 'enum' && !isObject){
if(data?.elements?.length > 0){
data.elements.forEach((a:any,b:number)=>{
if(!a.value || !a.text){
onlyMessage(`方法定义inputs第${index+1}个数组ValueType中elements缺失必填属性`,'error')
return true
}
})
}else{
onlyMessage(`方法定义inputs第${index+1}个数组ValueType中缺失elements属性`,'error')
return true
}
}
// if(data.type === 'boolean'){
// if(!data?.trueText || !data?.trueValue || !data?.falseText || !data?.falseValue){
// onlyMessage(`方法定义inputs第${index+1}个数组ValueType中缺失必填属性`,'error')
// return true
// }
// }
// if(data.type === 'enum' && !isObject){
// if(data?.elements?.length > 0){
// data.elements.forEach((a:any,b:number)=>{
// if(!a.value || !a.text){
// onlyMessage(`方法定义inputs第${index+1}个数组ValueType中elements缺失必填属性`,'error')
// return true
// }
// })
// }else{
// onlyMessage(`方法定义inputs第${index+1}个数组ValueType中缺失elements属性`,'error')
// return true
// }
// }
if(data.type === 'array' && !isArray && !isObject){
if(data?.elementType){
testType(data.elementType,index,true)
return testType(data.elementType,index,true)
}else{
onlyMessage(`方法定义inputs第${index+1}个数组ValueType中缺失elementType属性`,'error')
return true

View File

@ -1,7 +1,8 @@
<!-- 物联卡查看 -->
<template>
<page-container>
<page-container v-if="type === 'card'">
<!-- 新增编辑 -->
<div>
<Save
v-if="visible"
:type="saveType"
@ -43,13 +44,20 @@
detail.deviceName
}}</j-descriptions-item>
<j-descriptions-item label="平台类型">{{
detail.operatorPlatformType?.text
platformTypeList.find(
(item) =>
item.value ===
detail.operatorPlatformType?.text,
)?.label || detail.operatorPlatformType?.text
}}</j-descriptions-item>
<j-descriptions-item label="平台名称">{{
detail.platformConfigName
}}</j-descriptions-item>
<j-descriptions-item label="运营商">{{
detail.operatorName
OperatorList.find(
(item) =>
item.value === detail.operatorName,
)?.label || detail.operatorName
}}</j-descriptions-item>
<j-descriptions-item label="类型">{{
detail.cardType?.text
@ -83,18 +91,25 @@
? detail.residualFlow.toFixed(2) + ' M'
: '0 M'
}}</j-descriptions-item>
<j-descriptions-item label="状态">
{{
detail?.cardState?.text
}}
<span v-if="deactivateData.show" style="padding-left: 8px;">
<a-tooltip
:title="deactivateData.tip"
<j-descriptions-item label="运营商状态">
{{ detail?.cardState?.text }}
<span
v-if="deactivateData.show"
style="padding-left: 8px"
>
<AIcon type="ExclamationCircleOutlined" style="color: var(--ant-error-color);"/>
<a-tooltip :title="deactivateData.tip">
<AIcon
type="ExclamationCircleOutlined"
style="
color: var(--ant-error-color);
"
/>
</a-tooltip>
</span>
</j-descriptions-item>
<j-descriptions-item label="平台状态">
{{ detail?.cardStateType?.text }}
</j-descriptions-item>
<j-descriptions-item label="说明">{{
detail?.describe
}}</j-descriptions-item>
@ -126,13 +141,23 @@
<j-col :span="8">
<div class="card">
<Guide title="数据统计" />
<div class="static-info" style="min-height: 490px">
<div
class="static-info"
style="min-height: 490px"
>
<div class="data-statistics-item">
<div class="flow-info" style="width: 100%">
<div class="label">昨日流量消耗</div>
<div
class="flow-info"
style="width: 100%"
>
<div class="label">
昨日流量消耗
</div>
<j-tooltip placement="bottomLeft">
<template #title>
<span>{{ dayTotal }} M</span>
<span
>{{ dayTotal }} M</span
>
</template>
<div class="value">
{{ dayTotal }}
@ -146,11 +171,21 @@
/>
</div>
<div class="data-statistics-item">
<div class="flow-info" style="width: 100%">
<div class="label">当月流量消耗</div>
<div
class="flow-info"
style="width: 100%"
>
<div class="label">
当月流量消耗
</div>
<j-tooltip placement="bottomLeft">
<template #title>
<span>{{ monthTotal }} M</span>
<span
>{{
monthTotal
}}
M</span
>
</template>
<div class="value">
{{ monthTotal }}
@ -161,11 +196,18 @@
<LineChart :chartData="monthOptions" />
</div>
<div class="data-statistics-item">
<div class="flow-info" style="width: 100%">
<div class="label">本年流量消耗</div>
<div
class="flow-info"
style="width: 100%"
>
<div class="label">
本年流量消耗
</div>
<j-tooltip placement="bottomLeft">
<template #title>
<span>{{ yearTotal }} M</span>
<span
>{{ yearTotal }} M</span
>
</template>
<div class="value">
{{ yearTotal }}
@ -184,20 +226,262 @@
</j-row>
</j-col>
</j-row>
</div>
</page-container>
<div v-else>
<div v-if="cardId">
<Save
v-if="visible"
:type="saveType"
:data="current"
@change="saveChange"
/>
<j-row :gutter="[24, 24]">
<j-col :span="24">
<j-card>
<j-descriptions size="small" :column="3" bordered>
<template #title>
<Guide>
<template #title>
<span>基本信息</span>
<j-button
type="link"
@click="
() => {
visible = true;
current = detail;
saveType = 'edit';
}
"
>
<AIcon type="EditOutlined"></AIcon>
编辑
</j-button>
</template>
</Guide>
</template>
<j-descriptions-item label="卡号">{{
detail.id
}}</j-descriptions-item>
<j-descriptions-item label="ICCID">{{
detail.iccId
}}</j-descriptions-item>
<j-descriptions-item label="绑定设备">{{
detail.deviceName
}}</j-descriptions-item>
<j-descriptions-item label="平台类型">{{
platformTypeList.find(
(item) =>
item.value ===
detail.operatorPlatformType?.text,
)?.label || detail.operatorPlatformType?.text
}}</j-descriptions-item>
<j-descriptions-item label="平台名称">{{
detail.platformConfigName
}}</j-descriptions-item>
<j-descriptions-item label="运营商">{{
OperatorList.find(
(item) =>
item.value === detail.operatorName,
)?.label || detail.operatorName
}}</j-descriptions-item>
<j-descriptions-item label="类型">{{
detail.cardType?.text
}}</j-descriptions-item>
<j-descriptions-item label="激活日期">{{
detail.activationDate
? moment(detail.activationDate).format(
'YYYY-MM-DD HH:mm:ss',
)
: ''
}}</j-descriptions-item>
<j-descriptions-item label="更新时间">{{
detail.updateTime
? moment(detail.updateTime).format(
'YYYY-MM-DD HH:mm:ss',
)
: ''
}}</j-descriptions-item>
<j-descriptions-item label="总流量">{{
detail.totalFlow
? detail.totalFlow.toFixed(2) + ' M'
: '0 M'
}}</j-descriptions-item>
<j-descriptions-item label="使用流量">{{
detail.usedFlow
? detail.usedFlow.toFixed(2) + ' M'
: '0 M'
}}</j-descriptions-item>
<j-descriptions-item label="剩余流量">{{
detail.residualFlow
? detail.residualFlow.toFixed(2) + ' M'
: '0 M'
}}</j-descriptions-item>
<j-descriptions-item label="运营商状态">
{{ detail?.cardState?.text }}
<span
v-if="deactivateData.show"
style="padding-left: 8px"
>
<a-tooltip :title="deactivateData.tip">
<AIcon
type="ExclamationCircleOutlined"
style="
color: var(--ant-error-color);
"
/>
</a-tooltip>
</span>
</j-descriptions-item>
<j-descriptions-item label="平台状态">
{{ detail?.cardStateType?.text }}
</j-descriptions-item>
<j-descriptions-item label="说明">{{
detail?.describe
}}</j-descriptions-item>
</j-descriptions>
</j-card>
</j-col>
<j-col :span="24">
<!-- 流量统计 -->
<j-row :gutter="24">
<j-col :span="16">
<div class="card">
<Guide title="流量统计">
<template #extra>
<TimeSelect
:type="'week'"
:quickBtnList="quickBtnList"
@change="getEcharts"
/>
</template>
</Guide>
<LineChart
:showX="true"
:showY="true"
style="min-height: 490px"
:chartData="flowData"
/>
</div>
</j-col>
<j-col :span="8">
<div class="card">
<Guide title="数据统计" />
<div
class="static-info"
style="min-height: 490px"
>
<div class="data-statistics-item">
<div
class="flow-info"
style="width: 100%"
>
<div class="label">
昨日流量消耗
</div>
<j-tooltip placement="bottomLeft">
<template #title>
<span
>{{ dayTotal }} M</span
>
</template>
<div class="value">
{{ dayTotal }}
<span class="unit">M</span>
</div>
</j-tooltip>
</div>
<LineChart
color="#FBA500"
:chartData="dayOptions"
/>
</div>
<div class="data-statistics-item">
<div
class="flow-info"
style="width: 100%"
>
<div class="label">
当月流量消耗
</div>
<j-tooltip placement="bottomLeft">
<template #title>
<span
>{{
monthTotal
}}
M</span
>
</template>
<div class="value">
{{ monthTotal }}
<span class="unit">M</span>
</div>
</j-tooltip>
</div>
<LineChart :chartData="monthOptions" />
</div>
<div class="data-statistics-item">
<div
class="flow-info"
style="width: 100%"
>
<div class="label">
本年流量消耗
</div>
<j-tooltip placement="bottomLeft">
<template #title>
<span
>{{ yearTotal }} M</span
>
</template>
<div class="value">
{{ yearTotal }}
<span class="unit">M</span>
</div>
</j-tooltip>
</div>
<LineChart
color="#58E1D3"
:chartData="yearOptions"
/>
</div>
</div>
</div>
</j-col>
</j-row>
</j-col>
</j-row>
</div>
<JEmpty v-else></JEmpty>
</div>
</template>
<script setup lang="ts" name="CardDetail">
import moment from 'moment';
import type { CardManagement } from '../typing';
import {queryDeactivate, queryDetail} from '@/api/iot-card/cardManagement';
import {
queryDeactivate,
queryDetail,
query,
} from '@/api/iot-card/cardManagement';
import Save from '../Save.vue';
import Guide from '@/views/iot-card/components/Guide.vue';
import LineChart from '@/views/iot-card/components/LineChart.vue';
import { queryFlow } from '@/api/iot-card/home';
import TimeSelect from '@/views/iot-card/components/TimeSelect.vue';
import { OperatorList, platformTypeList } from '@/views/iot-card/data';
const props = defineProps({
type: {
type: String,
default: 'card',
},
});
const route = useRoute();
const cardId = ref();
const visible = ref<boolean>(false);
const current = ref<Partial<CardManagement>>({});
@ -214,8 +498,8 @@ const yearOptions = ref<any[]>([]);
const deactivateData = reactive({
show: false,
tip: ''
})
tip: '',
});
const quickBtnList = [
{ label: '昨日', value: 'yesterday' },
@ -225,18 +509,18 @@ const quickBtnList = [
];
const getDetail = () => {
queryDetail(route.params.id).then((resp: any) => {
queryDetail(cardId.value).then((resp: any) => {
if (resp.success) {
detail.value = resp.result;
if (resp.result.cardStateType?.value === 'deactivate') {
deactivateData.show = true
if (resp.result.cardStateType?.value === 'deactivate' && detail.value.operatorName === 'onelink') {
deactivateData.show = true;
//
queryDeactivate(route.params.id as string).then((deacResp: any) => {
queryDeactivate(cardId.value).then((deacResp: any) => {
if (deacResp.success && deacResp.result?.message) {
deactivateData.tip = deacResp.result.message.toString()
deactivateData.tip = deacResp.result.message.toString();
}
})
});
}
}
});
@ -254,18 +538,17 @@ const saveChange = (val: any) => {
}
};
const getData = (
start: number,
end: number,
): Promise<{ sortArray: any[]}> => {
const getData = (start: number, end: number): Promise<{ sortArray: any[] }> => {
return new Promise((resolve) => {
queryFlow(start, end, {
orderBy: 'date',
terms: [{
column : "cardId",
termType: "eq",
value: route.params.id
}]
terms: [
{
column: 'cardId',
termType: 'eq',
value: cardId.value,
},
],
}).then((resp: any) => {
if (resp.status === 200) {
const sortArray = resp.result.sort(
@ -278,7 +561,6 @@ const getData = (
}
});
});
};
/**
@ -308,7 +590,7 @@ const getDataTotal = () => {
.reduce((r, n) => r + Number(n.value), 0)
.toFixed(2);
monthOptions.value = resp.sortArray;
})
});
getData(yTime[0], yTime[1]).then((resp) => {
yearTotal.value = resp.sortArray
.reduce((r, n) => r + Number(n.value), 0)
@ -317,7 +599,6 @@ const getDataTotal = () => {
});
};
/**
* 流量统计
* @param data
@ -334,8 +615,35 @@ const getEcharts = (data: any) => {
});
};
getDetail();
getDataTotal();
/**
* 获取绑定设备的物联卡的信息
*/
const queryCard = async () => {
const res: any = await query({
terms: [
{
column: 'deviceId',
termType: 'eq',
value: route.params.id,
},
],
});
if (res.success && res.result?.data) {
cardId.value = res.result?.data?.[0]?.id;
}
};
onMounted(async () => {
if (props.type === 'device') {
await queryCard();
} else {
cardId.value = route.params.id;
}
if (cardId.value) {
getDetail();
getDataTotal();
}
});
</script>
<style scoped lang="less">
.card {

View File

@ -40,101 +40,6 @@
:actions="batchActions"
@change="onCheckChange"
/>
<!-- <j-dropdown>
<j-button>
批量操作
<AIcon type="DownOutlined" />
</j-button>
<template #overlay>
<j-menu>
<j-menu-item>
<PermissionButton
@click="exportVisible = true"
:hasPermission="'iot-card/CardManagement:export'"
>
<AIcon type="ExportOutlined" />
批量导出
</PermissionButton>
</j-menu-item>
<j-menu-item>
<PermissionButton
@click="importVisible = true"
:hasPermission="'iot-card/CardManagement:import'"
>
<AIcon type="ImportOutlined" />批量导入
</PermissionButton>
</j-menu-item>
<j-menu-item>
<PermissionButton
:popConfirm="{
title: '确认激活吗?',
onConfirm: handleActive,
}"
:hasPermission="'iot-card/CardManagement:active'"
>
<AIcon type="CheckCircleOutlined" />
批量激活
</PermissionButton>
</j-menu-item>
<j-menu-item>
<PermissionButton
:popConfirm="{
title: '确认停用吗?',
onConfirm: handleStop,
}"
ghost
type="primary"
:hasPermission="'iot-card/CardManagement:action'"
>
<AIcon type="StopOutlined" />
批量停用
</PermissionButton>
</j-menu-item>
<j-menu-item>
<PermissionButton
:popConfirm="{
title: '确认复机吗?',
onConfirm: handleResumption,
}"
ghost
type="primary"
:hasPermission="'iot-card/CardManagement:action'"
>
<AIcon type="PoweroffOutlined" />
批量复机
</PermissionButton>
</j-menu-item>
<j-menu-item>
<PermissionButton
:popConfirm="{
title: '确认同步状态吗?',
onConfirm: handleSync,
}"
ghost
type="primary"
:hasPermission="'iot-card/CardManagement:sync'"
>
<AIcon type="SwapOutlined" />
同步状态
</PermissionButton>
</j-menu-item>
<j-menu-item v-if="_selectedRowKeys.length > 0">
<PermissionButton
:popConfirm="{
title: '确认删除吗?',
onConfirm: handelRemove,
}"
ghost
type="primary"
:hasPermission="'iot-card/CardManagement:delete'"
>
<AIcon type="SwapOutlined" />
批量删除
</PermissionButton>
</j-menu-item>
</j-menu>
</template>
</j-dropdown> -->
</j-space>
</template>
<template #card="slotProps">
@ -166,7 +71,7 @@
</Ellipsis>
</span>
<j-row style="margin-top: 20px">
<j-col :span="8">
<j-col :span="6">
<div class="card-item-content-text">
平台对接
</div>
@ -176,6 +81,20 @@
</div>
</Ellipsis>
</j-col>
<j-col :span="6">
<div class="card-item-content-text">
运营商状态
</div>
<BadgeStatus
:status="slotProps.cardState?.value"
:text="slotProps.cardState?.text"
:statusNames="{
using: 'processing',
toBeActivated: 'default',
deactivate: 'error',
}"
/>
</j-col>
<j-col :span="6">
<div class="card-item-content-text">
类型
@ -296,6 +215,17 @@
}"
/>
</template>
<template #cardState="slotProps">
<BadgeStatus
:status="slotProps.cardState?.value"
:text="slotProps.cardState?.text"
:statusNames="{
using: 'processing',
toBeActivated: 'default',
deactivate: 'error',
}"
/>
</template>
<template #activationDate="slotProps">
{{
slotProps.activationDate
@ -371,11 +301,7 @@
@change="saveChange"
/>
<!-- 批量同步 -->
<SyncModal
v-if="syncVisible"
:params="params"
@close="syncClose"
/>
<SyncModal v-if="syncVisible" :params="params" @close="syncClose" />
</page-container>
</template>
@ -409,7 +335,8 @@ import { BatchActionsType } from '@/components/BatchDropdown/types';
import { usePermissionStore } from 'store/permission';
import { useRouterParams } from '@/utils/hooks/useParams';
import { OperatorMap } from '@/views/iot-card/data';
import SyncModal from './Sync.vue'
import SyncModal from './Sync.vue';
import { OperatorList } from '../data';
const router = useRouter();
const menuStory = useMenuStore();
@ -425,7 +352,7 @@ const cardId = ref<any>();
const current = ref<Partial<CardManagement>>({});
const saveType = ref<string>('');
const isCheck = ref<boolean>(false);
const syncVisible = ref(false)
const syncVisible = ref(false);
const columns = [
{
@ -447,7 +374,6 @@ const columns = [
width: 200,
search: {
type: 'string',
termOptions: ['eq'],
},
},
{
@ -494,20 +420,7 @@ const columns = [
search: {
type: 'select',
options: async () => {
return [
{
label: '移动',
value: '移动',
},
{
label: '电信',
value: '电信',
},
{
label: '联通',
value: '联通',
},
];
return OperatorList;
},
},
},
@ -567,7 +480,7 @@ const columns = [
},
},
{
title: '状态',
title: '平台状态',
dataIndex: 'cardStateType',
key: 'cardStateType',
width: 180,
@ -584,6 +497,45 @@ const columns = [
],
},
},
{
title: '运营商状态',
dataIndex: 'cardState',
key: 'cardState',
width: 180,
scopedSlots: true,
},
{
title: '运营商状态',
dataIndex: 'operatorState',
key: 'operatorState',
hidden: true,
search: {
type: 'select',
options: [
{ label: '激活(正常)', value: 'using' },
{ label: '测试激活', value: 'testActivation' },
{ label: '拆机', value: 'disassemble' },
{ label: '停用(已停用)', value: 'deactivate' },
{ label: '运营商管理状态', value: 'operatorManagement' },
{ label: '可激活(电信)', value: 'beActivated' },
{ label: '待激活', value: 'toBeActivated' },
{ label: '测试去激活', value: 'testToActivation' },
{ label: '可测试', value: 'testable' },
{ label: '库存(移动)', value: 'inStock' },
{ label: '预销户', value: 'preSeller' },
{ label: '单向停机', value: 'oneWayShutdown' },
{ label: '预销号', value: 'preSale' },
{ label: '过户', value: 'transfer' },
{ label: '休眠', value: 'dormant' },
{ label: '可激活(联通)', value: 'activatable' },
{ label: '已失效', value: 'expired' },
{ label: '已清除', value: 'cleared' },
{ label: '已更换', value: 'replaced' },
{ label: '库存(联通)', value: 'stock' },
{ label: '开始', value: 'start' },
],
},
},
{
title: '操作',
key: 'action',
@ -632,13 +584,15 @@ const getActions = (
title: '确认解绑设备?',
okText: '确定',
cancelText: '取消',
onConfirm: async () => {
unbind(data.id).then((resp: any) => {
onConfirm: () => {
const response = unbind(data.id);
response.then((resp: any) => {
if (resp.status === 200) {
onlyMessage('操作成功');
cardManageRef.value?.reload();
}
});
return response;
},
}
: undefined,
@ -683,29 +637,34 @@ const getActions = (
: '确认停用?',
okText: '确定',
cancelText: '取消',
onConfirm: async () => {
onConfirm: () => {
let response;
if (data.cardStateType?.value === 'toBeActivated') {
changeDeploy(data.id).then((resp) => {
response = changeDeploy(data.id);
response.then((resp) => {
if (resp.status === 200) {
onlyMessage('操作成功');
cardManageRef.value?.reload();
}
});
} else if (data.cardStateType?.value === 'deactivate') {
resumption(data.id).then((resp) => {
response = resumption(data.id);
response.then((resp) => {
if (resp.status === 200) {
onlyMessage('操作成功');
cardManageRef.value?.reload();
}
});
} else {
unDeploy(data.id).then((resp) => {
response = unDeploy(data.id);
response.then((resp) => {
if (resp.status === 200) {
onlyMessage('操作成功');
cardManageRef.value?.reload();
}
});
}
return response;
},
},
},
@ -719,8 +678,9 @@ const getActions = (
title: '确认删除?',
okText: '确定',
cancelText: '取消',
onConfirm: async () => {
const resp: any = await del(data.id);
onConfirm: () => {
const response: any = del(data.id);
response.then((resp: any) => {
if (resp.status === 200) {
onlyMessage('操作成功');
const index = _selectedRowKeys.value.findIndex(
@ -733,6 +693,8 @@ const getActions = (
} else {
onlyMessage('操作失败!', 'error');
}
});
return response;
},
},
icon: 'DeleteOutlined',
@ -860,11 +822,13 @@ const handleStop = () => {
_selectedRowKeys.value.length >= 10 &&
_selectedRowKeys.value.length <= 100
) {
unDeployBatch(_selectedRowKeys.value).then((res: any) => {
const response = unDeployBatch(_selectedRowKeys.value);
response.then((res: any) => {
if (res.status === 200) {
onlyMessage('操作成功');
}
});
return response;
} else {
onlyMessage(
'仅支持同一个运营商下且最少10条数据,最多100条数据',
@ -881,11 +845,13 @@ const handleResumption = () => {
_selectedRowKeys.value.length >= 10 &&
_selectedRowKeys.value.length <= 100
) {
resumptionBatch(_selectedRowKeys.value).then((res: any) => {
const response = resumptionBatch(_selectedRowKeys.value);
response.then((res: any) => {
if (res.status === 200) {
onlyMessage('操作成功');
}
});
return response;
} else {
onlyMessage(
'仅支持同一个运营商下且最少10条数据,最多100条数据',
@ -897,8 +863,8 @@ const handleResumption = () => {
/**
* 同步状态
*/
const handleSync = async() => {
syncVisible.value = true
const handleSync = async () => {
syncVisible.value = true;
// if (!_selectedRowKeys.value.length) {
// onlyMessage('', 'error');
// return;
@ -925,20 +891,23 @@ const handleResumption = () => {
/**
* 批量删除
*/
const handelRemove = async () => {
const handelRemove = () => {
if (!_selectedRowKeys.value.length) {
onlyMessage('请选择数据', 'error');
return;
}
const resp = await removeCards(
const response = removeCards(
_selectedRowKeys.value.map((v) => ({ id: v })),
);
response.then((resp) => {
if (resp.status === 200) {
onlyMessage('操作成功');
_selectedRowKeys.value = [];
// _selectedRow.value = [];
cardManageRef.value?.reload();
}
});
return response;
};
const batchActions: BatchActionsType[] = [
{
@ -959,18 +928,6 @@ const batchActions: BatchActionsType[] = [
importVisible.value = true;
},
},
// {
// key: 'active',
// text: '',
// permission: 'iot-card/CardManagement:active',
// icon: 'CheckCircleOutlined',
// selected: {
// popConfirm: {
// title: '',
// onConfirm: handleActive,
// },
// },
// },
{
key: 'stop',
text: '批量停用',
@ -1004,7 +961,7 @@ const batchActions: BatchActionsType[] = [
type: 'primary',
permission: 'iot-card/CardManagement:sync',
icon: 'SwapOutlined',
onClick: handleSync
onClick: handleSync,
},
{
key: 'delete',
@ -1022,9 +979,9 @@ const batchActions: BatchActionsType[] = [
];
const syncClose = () => {
syncVisible.value = false
syncVisible.value = false;
cardManageRef.value?.reload();
}
};
onMounted(() => {
if (routerParams.params.value.type === 'add' && paltformPermission) {

View File

@ -142,7 +142,7 @@
:rules="[
{
required: true,
message: '请输入接入密码',
message: '请输入接入地址',
},
{
max: 64,
@ -152,6 +152,7 @@
>
<j-input
v-model:value="formData.others.onvifUrl"
placeholder="请输入接入地址"
></j-input>
</j-form-item>
<j-form-item
@ -160,7 +161,7 @@
:rules="[
{
required: true,
message: '请输入接入密码',
message: '请输入接入账户',
},
{
max: 64,
@ -172,6 +173,7 @@
v-model:value="
formData.others.onvifUsername
"
placeholder="请输入接入账户"
></j-input>
</j-form-item>
<j-form-item
@ -192,9 +194,71 @@
v-model:value="
formData.others.onvifPassword
"
placeholder="请输入接入密码"
></j-input-password>
</j-form-item>
</template>
<template v-if="formData.channel === 'media-plugin'">
<j-form-item
:name="['others', item.property]"
v-for="item in metadata?.properties || []"
:key="item"
:label="item.name"
:rules="[
{
required:
!!item?.type?.expands?.required,
message: `${
item.type.type === 'enum' ||
'boolean'
? '请选择'
: '请输入'
}${item.name}`,
},
]"
>
<j-input
placeholder="请输入"
v-if="item.type.type === 'string'"
v-model:value="
formData.others[item.property]
"
></j-input>
<j-input-password
placeholder="请输入"
v-if="item.type.type === 'password'"
v-model:value="
formData.others[item.property]
"
></j-input-password>
<j-select
placeholder="请选择"
v-if="
item.type.type === 'enum' ||
item.type.type === 'boolean'
"
v-model:value="
formData.others[item.property]
"
:options="getOptions(item)"
>
</j-select>
<j-input-number
v-if="
[
'int',
'float',
'double',
'long',
].includes(item.type.type)
"
v-model:value="
formData.others[item.property]
"
placeholder="请输入"
></j-input-number>
</j-form-item>
</template>
<template v-if="!!route.query.id">
<j-form-item
v-if="formData.channel === 'gb28181-2016'"
@ -266,7 +330,6 @@
/>
</j-form-item>
</template>
<j-form-item label="说明">
<j-textarea
v-model:value="formData.description"
@ -288,7 +351,11 @@
</j-form>
</j-col>
<j-col :span="12">
<div v-if="1" class="doc" style="height: 800">
<div
v-if="formData.channel === 'gb28181-2016'"
class="doc"
style="height: 800"
>
<h1>1.概述</h1>
<div>
视频设备通过GB/T28181接入平台整体分为2部分包括平台端配置和设备端配置不同的设备端配置的路径或页面存在差异但配置项基本大同小异
@ -354,7 +421,7 @@
<div>不影响设备接入平台可保持设备初始化值</div>
</div>
<div v-else class="doc" style="height: 600">
<div v-else-if="formData.channel === 'fixed-media'" class="doc" style="height: 600">
<h1>1.概述</h1>
<div>
视频设备通过RTSPRTMP固定地址接入平台分为2步
@ -374,6 +441,55 @@
只能选择接入方式为固定地址的产品若当前无对应产品可点击右侧快速添加按钮填写产品名称和选择固定地址类型的网关完成产品创建
</div>
</div>
<div v-else-if="formData.channel === 'onvif'" class="doc" style="height: 600">
<h1>1.概述</h1>
<div>
JetLinks平台支持通过Onvif方式接入视频设备分为两个部分包括平台端配置和设备端配置本文通过海康摄像头为例将onvif视频接入到平台播放
</div>
<h1>2.配置说明</h1>
<div>
设备端配置
</div>
<div>1.本文以海康监控为例演示登录海康监控设备后台进入配置>网络>高级配置>集成协议用户自定义输入用户名和密码完成用户添加</div>
<div class="image">
<j-image
width="100%"
:src="getImage('/media/doc5.png')"
/>
</div>
<div>平台端配置</div>
<div>ID设备唯一标识若不填写系统将自动生成唯一标识</div>
<div>设备名称用户自定义输入小于或等于64位字符</div>
<div>所属产品选择接入方式为Onvif的产品若当前无对应产品可点击右侧快速添加按钮填写产品名称和选择Onvif类型的网关完成产品创建</div>
<div>接入地址不同平台的摄像头接入地址组合方式不一致请参考对应品牌接入Onvif的地址设置如海康http://ip/onvif/device_serviceIP>>>TCP/IP</div>
<div class="image">
<j-image
width="100%"
:src="getImage('/media/doc6.png')"
/>
</div>
<div>接入账户输入设备端配置时添加的用户名</div>
<div>接入密码输入设备端配置时添加的密码</div>
<div class="image">
<j-image
width="100%"
:src="getImage('/media/doc7.png')"
/>
</div>
<h1>3.所有配置项填写完成点击保存</h1>
</div>
<div v-else-if="formData.channel === 'media-plugin'" class="doc" style="height: 600">
<h1>1.概述</h1>
<div>
JetLinks平台支持通过调用SDK或API请求将第三方系统视频设备数据接入到平台
</div>
<h1>2.配置说明</h1>
<div>2.1平台端配置</div>
<div>ID设备唯一标识若不填写系统将自动生成唯一标识</div>
<div>设备名称用户自定义输入小于或等于64位字符</div>
<div>所属产品选择接入方式为插件视频接入的产品若当前无对应产品可点击右侧快速添加按钮填写产品名称和选择插件类型的网关完成产品创建</div>
<h1>3.所有配置项填写完成点击保存</h1>
</div>
</j-col>
</j-row>
</j-card>
@ -396,12 +512,12 @@ import type { ProductType } from '@/views/media/Device/typings';
import SaveProduct from './SaveProduct.vue';
import { notification } from 'jetlinks-ui-components';
import { omit } from 'lodash-es';
import { queryDeviceConfig } from '@/api/device/instance';
const router = useRouter();
const route = useRoute();
//
const formData = ref({
const formData = ref<any>({
id: '',
name: '',
channel: 'gb28181-2016',
@ -421,6 +537,9 @@ const formData = ref({
firmware: '',
});
const metadata = ref<any>({
properties: [],
});
const handleChannelChange = () => {
formData.value.productId = undefined;
getProductList();
@ -441,16 +560,46 @@ const getProductList = async () => {
};
const { result } = await DeviceApi.queryProductList(params);
productList.value = result;
if(result.length && !route.query.id){
formData.value.productId = result[0]?.id
formData.value.others.access_pwd = result[0]?.configuration?.access_pwd
formData.value.streamMode = result[0]?.configuration?.stream_mode
}
};
const handleProductChange = () => {
formData.value.others.access_pwd =
productList.value.find((f: any) => f.id === formData.value.productId)
?.configuration.access_pwd || '';
formData.value.streamMode = productList.value.find((f: any) => f.id === formData.value.productId)
formData.value.streamMode =
productList.value.find((f: any) => f.id === formData.value.productId)
?.configuration.stream_mode || '';
};
//
const getOptions = (i: any) => {
if (i.type.type === 'enum') {
return (i.type?.elements || []).map((item) => {
return {
label: item?.text,
value: item?.value,
};
});
} else if (i.type.type === 'boolean') {
return [
{
label: i.type?.falseText,
value: i.type?.falseValue,
},
{
label: i.type?.trueText,
value: i.type?.trueValue,
},
];
}
return undefined;
};
/**
* 新增产品
*/
@ -460,15 +609,15 @@ const saveProductVis = ref(false);
* 获取详情
*/
const getDetail = async () => {
if (!route.query.id) return;
const res = await DeviceApi.detail(route.query.id as string);
Object.assign(formData.value, res.result);
formData.value.channel = res.result.provider;
await getProductList();
if (formData.value.productId) {
const productData = productList.value.find((i: any) => {
return i.id === formData.value.productId;
});
if (productData) {
if (productData && formData.value.channel !== 'media-plugin') {
formData.value.others.access_pwd = formData.value.others.access_pwd
? formData.value.others.access_pwd
: productData?.configuration?.access_pwd;
@ -476,12 +625,26 @@ const getDetail = async () => {
? formData.value.streamMode
: productData?.configuration?.stream_mode;
}
if (productData && formData.value.channel === 'media-plugin') {
if(!res.result.others || JSON.stringify(res.result?.others) === "{}"){
formData.value.others = productData?.configuration;
}
const resp: any = await queryDeviceConfig(formData.value.id);
if (resp.success) {
metadata.value = resp?.result[0] || {
properties: [],
};
}
}
}
};
onMounted(() => {
getDetail();
onMounted(async () => {
if (!route.query.id) {
getProductList();
} else {
getDetail();
}
});
/**
@ -508,7 +671,7 @@ const handleSubmit = () => {
} else if (formData.value.channel === 'gb28181-2016') {
//
others = omit(others, ['onvifUrl', 'onvifPassword', 'onvifUsername']);
const getParmas = () => {
const getParams = () => {
if (others?.stream_mode) {
others.stream_mode = streamMode;
}
@ -522,8 +685,8 @@ const handleSubmit = () => {
...extraParams,
};
};
params = !id ? { others, id, ...extraParams } : getParmas();
} else {
params = !id ? { others, id, ...extraParams } : getParams();
} else if (formData.value.channel === 'onvif') {
others = omit(others, ['access_pwd']);
params = !id
? { others, ...extraParams }
@ -536,6 +699,18 @@ const handleSubmit = () => {
others,
...extraParams,
};
} else if (formData.value.channel === 'media-plugin') {
params = !id
? extraParams
: {
id,
streamMode,
manufacturer,
model,
firmware,
others,
...extraParams,
};
}
formRef.value

View File

@ -152,7 +152,7 @@ const handleTypeChange = (record: IVariable) => {
<style lang="less" scoped>
.table-wrapper {
background-color: #1d39c4;
background-color: @primary-color;
.has-error {
border-color: rgba(255, 77, 79);
&:focus {

View File

@ -4,27 +4,18 @@
title="新增"
okText="确定"
cancelText="取消"
:maskClosable="false"
:width="1000"
:loading="loading"
@cancel="closeModal"
@ok="saveCorrelation"
:maskClosable="false"
>
<!-- <div v-if="type !== 'other'" style="padding: 0 24px">
<j-steps :current="current" >
<j-step title="选择场景"></j-step>
<j-step title="选择条件"></j-step>
</j-steps>
</div> -->
<!-- <template v-if="current === 0 || type === 'other' "> -->
<pro-search :columns="columns" type="simple" @search="handleSearch"/>
<div style="height: 500px; overflow-y: auto">
<JProTable
model="CARD"
ref="tableRef"
:request="query"
:rowSelection="{
selectedRowKeys: _selectedRowKeys,
onChange: onSelectChange
}"
:gridColumns="[1, 1, 1]"
:defaultParams="{
sorts: [
@ -33,113 +24,54 @@
order: 'desc',
},
],
terms,
terms: [{
column: 'triggerType',
value: props.type === 'other' ? null : 'device'
},
{
terms: [
{
column: 'features',
termType: 'in',
value: [ 'alarmTrigger', 'alarmReliever']
},
{
column: 'features',
termType: 'isnull',
value: 1,
type: 'or'
}
],
type: 'and'
}
]
}"
:params="params"
>
<template #card="slotProps">
<CardBox
<SceneCardBox
:value="slotProps"
:status="slotProps.state?.value"
:statusText="slotProps.state?.text"
:active="_selectedRowKeys.includes(slotProps.id)"
@click="handleClick"
:statusNames="{
started: 'processing',
disable: 'error',
}"
>
<template #type>
<span
><img
:height="16"
:src="
typeMap.get(slotProps.triggerType)?.icon
"
style="margin-right: 5px"
/>{{
typeMap.get(slotProps.triggerType)?.text
}}</span
>
</template>
<template #img>
<img
:src="typeMap.get(slotProps.triggerType)?.img"
:alarmId="id"
:activeKeys="activeKeys[slotProps.id]"
:selectedKeys="selectedKeysMap[slotProps.id]"
:showMask="true"
@change="(key, selected) => onAlarmChange(key, selected, slotProps)"
@reload="reload"
/>
</template>
<template #content>
<Ellipsis style="width: calc(100% - 100px)">
<span style="font-size: 16px; font-weight: 600">
{{ slotProps.name }}
</span>
</Ellipsis>
<Ellipsis :lineClamp="2">
<div class="subTitle">
说明{{
slotProps?.description ||
typeMap.get(slotProps.triggerType)?.tip
}}
</div>
</Ellipsis>
</template>
</CardBox>
</template>
</JProTable>
</div>
<!-- </template> -->
<!-- <template v-if="current === 1">
<div class="branch-terms-items">
<j-tree
v-if="branchGroup.length"
defaultExpandAll
checkable
:treeData="branchGroup"
@check="branchCheck"
>
</j-tree> -->
<!-- <CardBox-->
<!-- v-for="(item, index) in branchGroup.branches"-->
<!-- :showStatus="false"-->
<!-- :active="branchActiveKey.includes(item.id)"-->
<!-- @click="() => branchClick(item.id)"-->
<!-- >-->
<!-- <template #content>-->
<!-- <div class="condition-name">-->
<!-- 条件 {{ index + 1 }}-->
<!-- </div>-->
<!-- <div style="height: 80px">-->
<!-- <j-scrollbar >-->
<!-- <div v-for="(b, bIndex) in item">-->
<!-- <div style="font-weight: bold">-->
<!-- <span v-if="bIndex === 0"></span>-->
<!-- <span v-else>否则</span>-->
<!-- <span>{{ b.condition }}</span>-->
<!-- </div>-->
<!-- <div style="padding-left: 16px" v-for="action in b.actions">-->
<!-- 执行 {{ action }}-->
<!-- </div>-->
<!-- </div>-->
<!-- </j-scrollbar>-->
<!-- </div>-->
<!-- </template>-->
<!-- </CardBox>-->
<!-- </div>
</template>
<template #footer>
<j-button v-if="current === 0" @click="closeModal">取消</j-button>
<j-button v-if="current === 0" type="primary" @click="next">下一步</j-button>
<j-button v-if="current === 1" @click="prev">上一步</j-button>
<j-button v-if="current === 1" type="primary" @click="saveCorrelation">完成</j-button>
</template> -->
</j-modal>
</template>
<script lang="ts" setup>
<script lang="ts" setup name="SceneSave">
import {query} from '@/api/rule-engine/scene';
import {bindScene} from '@/api/rule-engine/configuration';
import {getImage, onlyMessage} from '@/utils/comm';
import {handleSceneBranches} from './utils'
import {bindScene, queryBindScene} from '@/api/rule-engine/configuration';
import { onlyMessage} from '@/utils/comm';
import SceneCardBox from './CardBox.vue'
import { useRequest } from '@/hook'
const columns = [
{
@ -148,39 +80,6 @@ const columns = [
key: 'name',
search: {
type: 'string',
// rename: "id",
// options: async () => {
// const res = await query(
// {
// sorts: [
// {
// name: 'createTime',
// order: 'desc',
// },
// ],
// terms: [
// {
// column: 'id',
// termType: 'alarm-bind-rule$not',
// value: props.id,
// type: 'and',
// },
// {
// column: 'triggerType',
// termType: 'eq',
// value: props.type === 'other' ? undefined : 'device',
// },
// ]
// }
// );
// if (res.status === 200) {
// return res.result.data.map((item: any) => ({
// label: item.name,
// value: item.id,
// }));
// }
// return []
// }
},
},
{
@ -232,30 +131,6 @@ const props = defineProps({
type: String,
},
});
//
// const current = ref(0)
// const branchGroup = ref<any[]>([])
// const branchActiveKey = ref([])
// const branchCheckKeys = ref([])
// const terms = [
// {
// terms: [
// {
// column: 'id',
// termType: 'alarm-bind-rule$not',
// value: props.id,
// type: 'and',
// },
// {
// column: 'triggerType',
// termType: 'eq',
// value: props.type === 'other' ? undefined : 'device',
// },
// ],
// type: 'and',
// },
// ];
const terms = [
{
@ -275,117 +150,54 @@ const terms = [
type: 'and',
},
];
const params = ref();
const typeMap = new Map();
typeMap.set('manual', {
text: '手动触发',
img: getImage('/scene/scene-hand.png'),
icon: getImage('/scene/trigger-type-icon/manual.png'),
tip: '适用于第三方平台向物联网平台下发指令控制设备',
});
typeMap.set('timer', {
text: '定时触发',
img: getImage('/scene/scene-timer.png'),
icon: getImage('/scene/trigger-type-icon/timing.png'),
tip: '适用于定期执行固定任务',
});
typeMap.set('device', {
text: '设备触发',
img: getImage('/scene/scene-device.png'),
icon: getImage('/scene/trigger-type-icon/device.png'),
tip: '适用于设备数据或行为满足触发条件时,执行指定的动作',
});
const _selectedRowKeys = ref<string[]>([]);
const handleClick = (dt: any) => {
if (_selectedRowKeys.value.includes(dt.id)) {
const _index = _selectedRowKeys.value.findIndex((i) => i === dt.id);
_selectedRowKeys.value.splice(_index, 1);
const tableRef = ref();
const selectedKeysMap = reactive({})
const loading = ref(false)
const { data: activeKeys } = useRequest(queryBindScene, {
defaultParams: { terms: [{ column: 'alarmId', value: props.id}]},
onSuccess(res) {
const activeMap = res.result.data.reduce((prev, next) => {
if (prev[next.ruleId]) {
prev[next.ruleId].push(next.branchIndex)
} else {
_selectedRowKeys.value = [..._selectedRowKeys.value, dt.id];
prev[next.ruleId] = [next.branchIndex]
}
// _selectedRowKeys.value = [dt.id]
};
/**
* 取消选择事件
*/
const onSelectChange = (arr: any[]) => {
_selectedRowKeys.value = arr
};
// const branchClick = (id: string) => {
// const keys = new Set(branchActiveKey.value)
// }
return prev
}, {})
return activeMap || {}
},
defaultValue: {}
})
const handleSearch = (e: any) => {
params.value = e;
};
const emit = defineEmits(['closeSave', 'saveScene']);
// const next = () => {
// if (_selectedRowKeys.value.length) {
// query({
// pageSize: 99,
// terms: [{column: 'id', termType: 'in', value: _selectedRowKeys.value.join(',')}]
// }).then(res => {
// if (res.success) {
// branchGroup.value = handleSceneBranches(res.result.data) || []
// console.log(branchGroup.value)
// }
// })
// current.value += 1
// } else {
// onlyMessage('', 'warning')
// }
// }
// const branchCheck = (checkedKeys: string[], { checkedNodes }) => {
// branchCheckKeys.value = checkedNodes.filter(item => item.branchId).map(item => ({
// branchIndex: item.branchId,
// ruleId: item.sceneId,
// alarmId: props.id,
// }))
// }
// const prev = () => {
// current.value -= 1
// }
/**
* 保存选中关联场景
*/
// const saveCorrelation = async () => {
// if (_selectedRowKeys.value.length === 0 && branchCheckKeys.value.length === 0) {
// onlyMessage('', 'error')
// return
// }
// const list = props.type === 'other' ? _selectedRowKeys.value.map((item: any) => {
// return {
// alarmId: props.id,
// ruleId: item,
// };
// }) : branchCheckKeys.value
// const res = await bindScene([...list]);
// if (res.status === 200) {
// onlyMessage('');
// emit('saveScene');
// }
// };
const saveCorrelation = async () => {
if (_selectedRowKeys.value.length > 0) {
const list = _selectedRowKeys.value.map((item: any) => {
if (Object.keys(selectedKeysMap).length > 0) {
const list = Object.keys(selectedKeysMap).reduce((prev, next) => {
const branches = selectedKeysMap[next].map(key => {
return {
alarmId: props.id,
ruleId: item,
};
ruleId: next,
branchIndex: key
}
})
prev.push(...branches)
return prev
}, [])
loading.value = true
const res = await bindScene(list).finally(() =>{
loading.value = false
});
const res = await bindScene([...list]);
if (res.status === 200) {
if (res.success) {
onlyMessage('操作成功');
emit('saveScene');
}
@ -396,6 +208,28 @@ const saveCorrelation = async () => {
const closeModal = () => {
emit('closeSave');
};
const reload = () => {
tableRef.value?.reload()
}
const onAlarmChange = (key: string, selected: boolean, record: Record<string, any>) => {
const keys = selectedKeysMap[record.id]
const keySet = new Set(keys)
if (selected) {
keySet.add(key)
} else {
keySet.delete(key)
}
if (keySet.size === 0) {
delete selectedKeysMap[record.id]
}
selectedKeysMap[record.id] = [...keySet.values()]
}
</script>
<style lang="less" scoped>
.subTitle {

View File

@ -20,8 +20,15 @@
<template #alarmTime="slotProps">{{
dayjs(slotProps.alarmTime).format('YYYY-MM-DD HH:mm:ss')
}}</template>
<template #sourceId="slotProps"
>设备ID<a-button
type="link"
@click="() => gotoDevice(slotProps.sourceId)"
>{{ slotProps.sourceId }}</a-button
></template
>
<template #action="slotProps">
<j-space
<j-space :size="16"
><template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
@ -46,29 +53,41 @@
</JProTable>
</FullPage>
<Info
v-if="visible"
v-if="visible && alarmType !== 'device'"
:data="current"
@close="close"
/>
<LogDetail
v-if="visible && alarmType === 'device'"
:data="current"
@close="close"
:description="description"
/>
</page-container>
</template>
<script lang="ts" setup>
import {detail, queryHistoryLogList} from '@/api/rule-engine/log';
import { detail, queryLogList } from '@/api/rule-engine/log';
import { detail as configurationDetail } from '@/api/rule-engine/configuration';
import { useRoute } from 'vue-router';
import dayjs from 'dayjs';
import { useAlarmStore } from '@/store/alarm';
import Info from './info.vue';
import { useRouterParams } from '@/utils/hooks/useParams';
import { useMenuStore } from 'store/menu';
import LogDetail from '../TabComponent/components/LogDetail.vue';
const route = useRoute();
const id = route.params?.id;
const menuStory = useMenuStore();
const { params: routerParams } = useRouterParams();
let visible = ref(false);
let description = ref<string>();
const tableRef = ref()
const columns = [
const visible = ref(false);
const tableRef = ref();
const params = ref({});
const alarmStore = useAlarmStore();
const { data } = alarmStore;
const current = ref(); //
const details = ref(); //
const alarmType = ref();
const columns = ref([
{
title: '告警时间',
dataIndex: 'alarmTime',
@ -94,7 +113,7 @@ const columns = [
key: 'action',
scopedSlots: true,
},
];
]);
const getActions = (
data: Partial<Record<string, any>>,
type?: 'table',
@ -109,7 +128,7 @@ const getActions = (
tooltip: {
title: '查看',
},
icon: 'SearchOutlined',
icon: 'EyeOutlined',
onClick: () => {
current.value = data;
visible.value = true;
@ -126,21 +145,16 @@ const terms = [
type: 'and',
},
];
let params = ref({});
const alarmStore = useAlarmStore();
const { data } = alarmStore;
let current = ref(); //
let details = ref(); //
/**
* 获取详情列表
*/
const queryList = async (params: any) => {
if(data.current?.alarmConfigId){
const res = await queryHistoryLogList(data.current?.alarmConfigId,{
if (data.current?.alarmConfigId) {
const res: any = await queryLogList(data.current?.alarmConfigId, {
...params,
// sorts: [{ name: 'alarmTime', order: 'desc' }],
});
if (res.status === 200) {
if (res.status === 200 && res.result?.data) {
details.value = res.result.data[0];
return {
code: res.message,
@ -166,32 +180,70 @@ const queryList = async (params: any) => {
};
}
};
const gotoDevice = (id) => {
menuStory.jumpPage('device/Instance/Detail', { id, tab: 'Running' });
};
/**
* 根据id初始化数据
*/
watch(() => id, async () => {
watch(
() => id,
async () => {
const res = await detail(id);
if (res.status === 200) {
data.current = res.result || {};
tableRef.value?.reload()
if (res.result?.targetType === 'device') {
columns.splice(2, 0, {
dataIndex: 'targetName',
title: '告警设备',
key: 'targetName',
});
tableRef.value?.reload();
alarmType.value = res.result?.targetType;
if (alarmType.value === 'device') {
columns.value = [
{
title: '告警时间',
dataIndex: 'alarmTime',
key: 'alarmTime',
scopedSlots: true,
search: {
type: 'date',
},
},
{
title: '触发条件',
dataIndex: 'triggerDesc',
key: 'triggerDesc',
},
{
title: '告警源',
dataIndex: 'sourceId',
key: 'sourceId',
scopedSlots: true,
search: {
type: 'string',
},
},
{
title: '告警原因',
dataIndex: 'actualDesc',
key: 'actualDesc',
scopedSlots: true,
search: {
type: 'string',
},
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
scopedSlots: true,
},
];
}
configurationDetail(res.result?.alarmConfigId).then((res: any) => {
if (res.status === 200) {
description.value = res.result?.description;
}
});
}
}, {
},
{
deep: true,
immediate: true
})
immediate: true,
},
);
const handleSearch = (_params: any) => {
params.value = _params;
};
@ -210,5 +262,4 @@ watchEffect(() => {
}
});
</script>
<style lang="less" scoped>
</style>
<style lang="less" scoped></style>

View File

@ -11,8 +11,9 @@
:columns="columns"
:request="handleSearch"
:params="params"
:gridColumns="[1, 1, 2]"
:gridColumn="2"
:gridColumns="[1, 1, 1]"
:gridColumn="1"
model="CARD"
ref="tableRef"
>
<template #card="slotProps">
@ -20,20 +21,13 @@
:value="slotProps"
v-bind="slotProps"
:actions="getActions(slotProps, 'card')"
:statusText="
data.defaultLevel.find(
(i) => i.level === slotProps.level,
)?.title || slotProps.level
"
:status="slotProps.level"
:status="slotProps.state.value"
:statusNames="{
1: 'level1',
2: 'level2',
3: 'level3',
4: 'level4',
5: 'level5',
warning: 'error',
normal: 'default',
}"
:customBadge="true"
:statusText="slotProps.state.text"
@click="() => showDrawer(slotProps)"
>
<template #img>
<img
@ -42,55 +36,120 @@
/>
</template>
<template #content>
<Ellipsis style="width: calc(100% - 100px)">
<span style="font-weight: 500">
<div class="alarmTitle">
<div class="alarmName">
<Ellipsis style="width: 100%">
<span
style="
font-weight: 500;
font-size: 16px;
"
>
{{ slotProps.alarmName }}
</span>
</Ellipsis>
<j-row :gutter="24">
<j-col :span="8" class="content-left">
<div class="content-left-title">
{{ titleMap.get(slotProps.targetType) }}
</div>
<!-- <div
class="alarmLevel"
:style="{
backgroundColor: levelColorMap.get(
'level' + slotProps.level,
),
}"
>
<Ellipsis>
<span>
{{
levelMap?.[slotProps.level] ||
slotProps.level
}}
</span>
</Ellipsis>
</div> -->
<div style="display: flex;max-width: 50%;">
<LevelIcon :level="slotProps.level" ></LevelIcon>
<Ellipsis>
{{ levelMap[slotProps.level] }}
</Ellipsis>
</div>
</div>
<j-row :gutter="24">
<j-col
:span="
props.type === 'device' ||
slotProps.targetType === 'device'
? 6
: 8
"
class="content-left"
>
<div class="content-title">告警维度</div>
<Ellipsis
><div>
{{ slotProps?.targetName }}
</div></Ellipsis
>
</j-col>
<j-col :span="8">
<div class="content-right-title">
<j-col
:span="
props.type === 'device' ||
slotProps.targetType === 'device'
? 6
: 8
"
>
<div class="content-title">
最近告警时间
</div>
<Ellipsis
><div>
<Ellipsis>
<div>
{{
dayjs(
slotProps?.alarmTime,
).format('YYYY-MM-DD HH:mm:ss')
).format(
'YYYY-MM-DD HH:mm:ss',
) +
'至' +
(slotProps?.state?.value ===
'warning'
? '当前时间'
: dayjs(
slotProps?.handleTime,
).format(
'YYYY-MM-DD HH:mm:ss',
))
}}
</div></Ellipsis
>
</div>
</Ellipsis>
</j-col>
<j-col :span="8">
<div class="content-right-title">状态</div>
<BadgeStatus
:status="slotProps.state.value"
:statusName="{
warning: 'warning',
normal: 'default',
}"
>
</BadgeStatus
><span
:style="
slotProps.state.value === 'warning'
? 'color: #E50012'
: 'color:black'
<j-col
:span="
props.type === 'device' ||
slotProps.targetType === 'device'
? 6
: 8
"
>
{{ slotProps.state.text }}
</span>
<div class="content-title">
告警持续时长
</div>
<Ellipsis
><Duration :data="slotProps"></Duration
></Ellipsis>
</j-col>
<j-col
:span="6"
v-if="
props.type === 'device' ||
slotProps.targetType === 'device'
"
>
<div class="content-title">告警原因</div>
<Ellipsis
><div>
{{ slotProps?.actualDesc || '--' }}
</div></Ellipsis
>
</j-col>
</j-row>
</template>
@ -116,77 +175,21 @@
</template>
</CardBox>
</template>
<template #targetType="slotProps">
{{ titleMap.get(slotProps.targetType) }}
</template>
<template #alarmTime="slotProps">
{{
dayjs(slotProps.alarmTime).format('YYYY-MM-DD HH:mm:ss')
}}
</template>
<template #level="slotProps">
<Ellipsis style="width: calc(100% - 20px)">
{{
data.defaultLevel.find((i) => {
return i.level === slotProps.level;
}).title
}}
</Ellipsis>
</template>
<template #state="slotProps">
<BadgeStatus
:status="slotProps.state.value"
:statusName="{
warning: 'warning',
normal: 'default',
}"
>
</BadgeStatus
><span
:style="
slotProps.state.value === 'warning'
? 'color: #E50012'
: 'color:black'
"
>
{{ slotProps.state.text }}
</span>
</template>
<template #actions="slotProps">
<j-space>
<template
v-for="i in getActions(slotProps, 'table')"
:key="i.key"
>
<PermissionButton
type="link"
:disabled="
i.key === 'solve' &&
slotProps.state.value === 'normal'
"
:tooltip="{
...i.tooltip,
}"
@click="i.onClick"
:hasPermission="
i.key == 'solve'
? 'rule-engine/Alarm/Log:action'
: 'rule-engine/Alarm/Log:view'
"
>
<template #icon>
<AIcon :type="i.icon" />
</template>
</PermissionButton>
</template>
</j-space>
</template>
</JProTable>
</FullPage>
<SolveComponent
:data="data"
:data="data.current"
v-if="data.solveVisible"
@closeSolve="closeSolve"
@refresh="refresh"
/>
<LogDrawer
v-if="visibleDrawer"
:logData="drawerData"
:typeMap="titleMap"
:levelMap="levelMap"
@closeDrawer="visibleDrawer = false"
@refreshTable="refreshTable"
/>
</div>
</template>
@ -194,27 +197,22 @@
<script lang="ts" setup>
import { getImage } from '@/utils/comm';
import { getOrgList, query, getAlarmProduct } from '@/api/rule-engine/log';
import { queryLevel } from '@/api/rule-engine/config';
import { useAlarmStore } from '@/store/alarm';
import { storeToRefs } from 'pinia';
import dayjs from 'dayjs';
import type { ActionsType } from '@/components/Table';
import SolveComponent from '../SolveComponent/index.vue';
import { useMenuStore } from '@/store/menu';
import { usePermissionStore } from '@/store/permission';
import LogDrawer from './components/DetailDrawer.vue';
import Duration from '../components/Duration.vue';
import { useAlarmLevel } from '@/hook';
const menuStory = useMenuStore();
const tableRef = ref();
const { levelMap, levelList } = useAlarmLevel();
const alarmStore = useAlarmStore();
const { data } = storeToRefs(alarmStore);
const getDefaultLevel = () => {
queryLevel().then((res) => {
if (res.status === 200) {
data.value.defaultLevel = res.result?.levels || [];
}
});
};
getDefaultLevel();
const drawerData = ref();
const visibleDrawer = ref(false);
const props = defineProps<{
type: string;
id?: string;
@ -231,6 +229,7 @@ titleMap.set('product', '产品');
titleMap.set('device', '设备');
titleMap.set('other', '其他');
titleMap.set('org', '组织');
const columns = [
{
title: '配置名称',
@ -255,12 +254,9 @@ const columns = [
width: 200,
search: {
type: 'select',
options: data.value.defaultLevel.map((item: any) => {
return {
label: item.title,
value: item.level,
};
}),
options: async () => {
return levelList.value;
},
},
scopedSlots: true,
},
@ -306,48 +302,6 @@ const newColumns = computed(() => {
title: '产品名称',
dataIndex: 'targetName',
key: 'targetName',
// search: {
// type: 'select',
// options: async () => {
// const termType = [
// {
// column: 'targetType',
// termType: 'eq',
// type: 'and',
// value: props.type,
// },
// ];
// if (props.id) {
// termType.push({
// termType: 'eq',
// column: 'alarmConfigId',
// value: props.id,
// type: 'and',
// });
// }
// const resp: any = await handleSearch({
// sorts: [{ name: 'alarmTime', order: 'desc' }],
// terms: termType,
// });
// const listMap: Map<string, any> = new Map();
// if (resp.status === 200) {
// resp.result.data.forEach((item) => {
// if (item.targetId) {
// listMap.set(item.targetId, {
// label: item.targetName,
// value: item.targetId,
// });
// }
// });
// return [...listMap.values()];
// }
// return [];
// },
// },
search: {
type: 'string',
},
@ -449,16 +403,17 @@ const search = (data: any) => {
type: 'and',
});
}
if (
props.type === 'device' &&
data?.terms[0]?.terms[0]?.column === 'product_id'
) {
params.value.terms = [
{
if (props.type === 'device') {
data?.terms.forEach((i: any, _index: number) => {
i.terms.forEach((item: any, index: number) => {
if (item.column === 'product_id') {
params.value.terms[_index].terms[index] = {
column: 'targetId$dev-instance',
value: [data?.terms[0]?.terms[0]],
},
];
};
}
});
});
}
if (props.id) {
params.value.terms.push({
@ -490,15 +445,6 @@ const getActions = (
data.value.current = currentData;
data.value.solveVisible = true;
},
// popConfirm: {
// title: !usePermissionStore().hasPermission(
// 'rule-engine/Alarm/Log:action',
// )
// ? ''
// : data.state?.value === 'normal'
// ? ''
// : '',
// },
},
{
key: 'log',
@ -536,8 +482,21 @@ const getActions = (
*/
const closeSolve = () => {
data.value.solveVisible = false;
};
const refresh = () => {
data.value.solveVisible = false;
tableRef.value.reload(params.value);
};
const refreshTable = () => {
tableRef.value.reload(params.value);
};
const showDrawer = (data: any) => {
if (data.targetType === 'device') {
drawerData.value = data;
visibleDrawer.value = true;
}
};
onMounted(() => {
if (props.type !== 'all' && !props.id) {
params.value.terms = [
@ -565,14 +524,23 @@ onMounted(() => {
});
</script>
<style lang="less" scoped>
.content-left {
border-right: 0.2px solid rgba(0, 0, 0, 0.2);
}
.content-right-title {
.content-title {
color: #666;
font-size: 12px;
}
.content-left-title {
font-size: 18px;
.alarmTitle {
display: flex;
width: 60%;
.alarmLevel {
width: 30%;
text-align: center;
padding: 5px;
}
.alarmName {
max-width: 30%;
color: #1a1a1a;
margin-right: 10px;
}
}
</style>

View File

@ -66,6 +66,8 @@
<li
v-for="(item, i) in state.ranking"
:key="item.targetId"
style="cursor: pointer"
@click="jumpToDetail(item.targetId)"
>
<img
:src="
@ -103,7 +105,7 @@
<script lang="ts" setup>
import { Empty } from 'jetlinks-ui-components';
import { getImage } from '@/utils/comm';
import { getImage, onlyMessage } from '@/utils/comm';
import Charts from './components/Charts.vue';
import TopCard from './components/TopCard.vue';
import NewAlarm from './components/NewAlarm.vue';
@ -120,6 +122,10 @@ import {
getAlarmLevel,
} from '@/api/rule-engine/dashboard';
import dayjs from 'dayjs';
import { useMenuStore } from 'store/menu';
import { query } from '@/api/rule-engine/scene';
const menuStory = useMenuStore();
let currentMonAlarm = ref<Footer[]>([
{
title: '当月告警',
@ -232,12 +238,11 @@ const getDashBoard = () => {
dashboard([today, thisMonth, fifteen]).then((res) => {
if (res.status == 200) {
const _data = res.result as DashboardItem[];
state.today = _data.find(
(item) => item.group === 'today',
)?.data.value || 0;
state.thisMonth = _data.find(
(item) => item.group === 'thisMonth',
)?.data.value || 0;
state.today =
_data.find((item) => item.group === 'today')?.data.value || 0;
state.thisMonth =
_data.find((item) => item.group === 'thisMonth')?.data.value ||
0;
currentMonAlarm.value[0].value = state.thisMonth;
const fifteenData = _data
.filter((item) => item.group === '15day')
@ -369,11 +374,11 @@ const selectChange = () => {
const month = day * 30;
const year = 365 * day;
if (dt <= (hour + 10)) {
limit = 60
if (dt <= hour + 10) {
limit = 60;
format = 'HH:mm';
} else if (dt > hour && dt <= day) {
time = '1h'
time = '1h';
limit = 24;
} else if (dt > day && dt < year) {
limit = Math.abs(Math.ceil(dt / day)) + 1;
@ -438,17 +443,18 @@ const selectChange = () => {
res.result
.filter((item: any) => item.group === 'alarmTrend')
.forEach((item: any) => {
if(time === '1d'){
item.data.timeString = item.data.timeString.split(' ')[0]
if (time === '1d') {
item.data.timeString =
item.data.timeString.split(' ')[0];
}
xData.push(item.data.timeString);
sData.push(item.data.value);
});
const data:any = JSON.parse(JSON.stringify(sData))
if (data && data.length > 0 ) {
const maxY = data.sort((a,b)=>{
return b-a
})[0]
const data: any = JSON.parse(JSON.stringify(sData));
if (data && data.length > 0) {
const maxY = data.sort((a, b) => {
return b - a;
})[0];
alarmStatisticsOption.value = {
xAxis: {
type: 'category',
@ -500,8 +506,8 @@ const selectChange = () => {
},
],
};
}else{
console.log('data is empty ')
} else {
console.log('data is empty ');
}
state.ranking = res.result
?.filter(
@ -517,6 +523,40 @@ const selectChange = () => {
}
});
};
const jumpToDetail = (id: string) => {
switch (queryCodition.targetType) {
case 'device':
menuStory.jumpPage('device/Instance/Detail', { id });
break;
case 'product':
menuStory.jumpPage('device/Product/Detail', { id });
break;
case 'org':
menuStory.jumpPage('system/Department', {}, { id });
break;
case 'other':
query({
terms: [
{
column: 'id',
termType: 'eq',
value: id,
},
],
}).then((res:any) => {
if (res.success && res.result?.data) {
if(res.result?.total){
const scene = res.result.data[0]
menuStory.jumpPage('rule-engine/Scene/Save',{}, { triggerType: scene.trigger.type, id: id });
}else{
onlyMessage('数据已经删除','error')
}
}
});
break;
}
};
</script>
<style scoped lang="less">
.alarm-card {

View File

@ -36,7 +36,7 @@
v-model:value="paramsValue.termType"
@select="termsTypeSelect"
/>
<div v-if="!['notnull', 'isnull'].includes(paramsValue.termType)">
<div v-if="!['notnull', 'isnull'].includes(paramsValue.termType)" style="display: flex">
<DoubleParamsDropdown
v-if="showDouble"
icon="icon-canshu"
@ -64,7 +64,7 @@
<ParamsDropdown
v-else
icon="icon-canshu"
placeholder="参数值"
:placeholder="tabsOptions[0]?.component === 'array' ? '多个值以英文逗号隔开':'参数值'"
:options="valueOptions"
:metricOptions="metricOption"
:tabsOptions="tabsOptions"
@ -74,15 +74,9 @@
@select="valueSelect"
/>
</div>
<j-popconfirm
title="确认删除?"
@confirm="onDelete"
:overlayStyle="{ minWidth: '180px' }"
>
<div v-show="showDelete" class="button-delete">
<ConfirmModal title="确认删除?" :onConfirm="onDelete" className="button-delete" :show="showDelete">
<AIcon type="CloseOutlined" />
</div>
</j-popconfirm>
</ConfirmModal>
</div>
<div class="term-add" @click.stop="termAdd" v-if="isLast">
<div class="terms-content">
@ -308,7 +302,7 @@ watch(
const showDouble = computed(() => {
const isRange = paramsValue.termType
? arrayParamsKey.includes(paramsValue.termType)
? doubleParamsKey.includes(paramsValue.termType)
: false;
const isSourceMetric = paramsValue.value?.source === 'metric';
if (metricsCacheOption.value.length) {
@ -411,9 +405,10 @@ const columnSelect = (option: any) => {
nextTick(() => {
formItemContext.onFieldChange();
});
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[
props.termsName
][0] = option.name;
][0] = option.name || option.fullName;
formModel.value.options!.when[props.branches_Index].terms[props.whenName].terms[
props.termsName
][1] = paramsValue.termType;
@ -478,6 +473,7 @@ const valueSelect = (
labelObj: Record<number, any>,
option: any,
) => {
if (paramsValue.value?.source === 'metric') {
paramsValue.value.metric = option?.id;
}
@ -500,7 +496,7 @@ const typeSelect = (e: any) => {
};
const termAdd = () => {
const terms = {
const termsData = {
column: undefined,
value: {
source: 'manual',
@ -512,10 +508,9 @@ const termAdd = () => {
};
formModel.value.branches?.[props.branchName]?.when?.[
props.whenName
]?.terms?.push(terms);
formModel.value.options!.when[props.branchName].terms[props.whenName].terms[
props.termsName
].push(['', '', '', '并且']);
]?.terms?.push(termsData);
formModel.value.options!.when[props.branchName].terms[props.whenName].terms.push(['', '', '', '并且']);
};
const onDelete = () => {
@ -527,6 +522,26 @@ const onDelete = () => {
].terms.splice(props.termsName, 1);
};
watchEffect(() => {
const isRange = paramsValue.termType ? arrayParamsKey.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;
})
nextTick(() => {
Object.assign(
paramsValue,

View File

@ -1,6 +1,6 @@
<template>
<div class='actions-terms'>
<TitleComponent data='触发条件' style='font-size: 14px;' >
<TitleComponent data='执行动作' style='font-size: 14px;' >
<template #extra>
<j-switch
v-model:checked='open'
@ -11,15 +11,20 @@
/>
</template>
</TitleComponent>
<template v-if='open'>
<!-- <template v-if='open'>-->
<div>
<j-tabs type="editable-card" v-model:activeKey="activeKey" @edit="addGroup" @tabClick="showEditCondition">
<j-tab-pane
v-for="(b, i) in group"
:key="b.id"
:tab="b.branchName || `条件${i+1}`"
:closable="false"
:forceRender="true"
>
<template #tab>
<TermsTabPane :showClose="group.length > 1" @close="() => addGroup(b.id, 'close')">
{{ b.branchName || `条件${i+1}` }}
</TermsTabPane>
</template>
<template v-for='(item, index) in data.branches'>
<template v-if="index >= b.start && index < (b.start + b.len)">
<Branches
@ -29,12 +34,12 @@
:name='index'
:branches_Index='item.branches_Index'
:groupLen="b.start + b.len"
:groupIndex="i"
:groupIndex="i + 1"
:key='item.key'
:showGroupDelete="group.length !== 1"
@delete='branchesDelete'
@delete='branchesDelete(index)'
@deleteAll='branchesDeleteAll'
@deleteGroup="() => groupDelete(b, i)"
@add="branchesAdd"
/>
<div v-else class='actions-terms-warp' :style='{ marginTop: data.branches.length === 2 ? 0 : 24 }'>
<div class='actions-terms-title' style='padding: 0;margin-bottom: 24px;'>
@ -50,27 +55,26 @@
</j-tabs>
</div>
</template>
<div v-else class='actions-branches-item'>
<j-form-item
:name='["branches", 0, "then"]'
:rules='thenRules'
>
<Action
:name='0'
:openShakeLimit="true"
:thenOptions='data.branches[0]?.then'
<!-- </template>-->
<!-- <div v-else class='actions-branches-item'>-->
<!-- <j-form-item-->
<!-- :name='["branches", 0, "then"]'-->
<!-- :rules='thenRules'-->
<!-- >-->
<!-- <Action-->
<!-- :name='0'-->
<!-- :openShakeLimit="true"-->
<!-- :thenOptions='data.branches[0]?.then'-->
<!-- />-->
<!-- </j-form-item>-->
<!-- </div>-->
</div>
<BranchesNameEdit
v-if="editConditionVisible"
:name="conditionName"
@cancel="editConditionVisible = false"
@ok="changeBranchName"
/>
</j-form-item>
</div>
</div>
<j-modal v-if="editConditionVisible" title="编辑" visible @cancel="editConditionVisible = false" @ok="changeBranchName">
<j-form layout='vertical'>
<j-form-item label="条件名称:" :required="true">
<j-input v-model:value="conditionName"></j-input>
</j-form-item>
</j-form>
</j-modal>
</template>
<script setup lang='ts' name='Terms'>
@ -78,12 +82,15 @@ import { storeToRefs } from 'pinia';
import { useSceneStore } from '@/store/scene'
import { cloneDeep } from 'lodash-es'
import { provide } from 'vue'
import { ContextKey, handleParamsData, thenRules } from './util'
import { getParseTerm } from '@/api/rule-engine/scene'
import type { FormModelType } from '@/views/rule-engine/Scene/typings'
import { ContextKey, handleParamsData } from './util'
import {getParseTerm} from '@/api/rule-engine/scene'
import type { FormModelType} from '@/views/rule-engine/Scene/typings'
import Branches from './Branches.vue'
import Action from '../../action/index.vue'
import {randomString} from "@/utils/utils";
import {randomNumber, randomString} from "@/utils/utils";
import TermsTabPane from './TermsTabPane.vue'
import BranchesNameEdit from "./BranchesNameEdit.vue";
import {Modal} from "ant-design-vue";
import {queryBindScene, unBindAlarmMultiple} from "@/api/rule-engine/configuration";
const sceneStore = useSceneStore()
const { data } = storeToRefs(sceneStore)
@ -92,7 +99,7 @@ const columnOptions = ref<any>([])
const group = ref<Array<{ id: string, len: number}>>([])
const activeKey = ref('')
const editConditionVisible = ref(false);
const conditionName = ref<any>('')
const conditionName = ref<any>()
provide(ContextKey, columnOptions)
@ -129,14 +136,14 @@ const queryColumn = (dataModel: FormModelType) => {
const cloneDevice = cloneDeep(dataModel)
cloneDevice.branches = cloneDevice.branches?.filter(item => !!item)
getParseTerm(cloneDevice).then(res => {
columnOptions.value = handleParamsData(res.result as any[])
columnOptions.value = handleParamsData(res.result as any[], 'column', '0')
})
}
const addBranches = (len: number) => {
const branchesItem = {
when: [],
key: `branches_${new Date().getTime()}`,
key: randomNumber(),
shakeLimit: {
enabled: false,
time: 1,
@ -144,26 +151,28 @@ const addBranches = (len: number) => {
alarmFirst: false,
},
then: [],
branchId: Math.floor(Math.random() * 100000000)
branchId: randomNumber()
}
// const lastIndex = data.value.branches!.length - 1 || 0
data.value.branches?.splice(len - 1, 1, branchesItem)
data.value.options!.when.splice(len - 1, 1, {
terms: []
})
// data.value.options!.when = []
}
const branchesDelete = (index: number) => {
if (data.value.branches?.length === 2) {
data.value.branches?.splice(index, 1, null as any)
} else {
data.value.branches?.splice(index, 1)
}
data.value.options?.when?.splice(index, 1)
const branchesDelete = (index: any) => {
groupDelete({
start: index,
len: 1
}, -1)
}
const addGroup = () => {
const addGroup = (targetKey: string, action: string) => {
if (action === 'add') {
const lastGroup = group.value[group.value.length - 1]
const lastIndex = (lastGroup?.groupIndex || group.value.length) + 1
const key = randomNumber()
const branchesItem: any = {
when: [
{
@ -183,7 +192,7 @@ const addGroup = () => {
key: `terms_${randomString()}`,
},
],
key: `branches_${randomString()}`,
key: key,
shakeLimit: {
enabled: false,
time: 1,
@ -192,52 +201,175 @@ const addGroup = () => {
},
then: [],
executeAnyway: true,
branchId: Math.floor(Math.random() * 100000000),
branchName:''
branchId: key,
branchName: ''
}
data.value.branches?.push(branchesItem)
data.value.branches?.push(null as any)
activeKey.value = `group_${branchesItem.key}`
data.value.branches?.push(branchesItem, null)
// data.value.branches?.push(null as any)
activeKey.value = key
data.value.options!.when.push({
terms: [{
terms: [['','eq','','and']],
}]
}],
branchName: '',
key,
executeAnyway: true,
groupIndex: lastIndex
})
} else {
const index = group.value.findIndex(item => item.branchId === targetKey)
groupDelete(group.value[index], index)
}
}
const branchesDeleteAll = () => {
}
const groupDelete = (g: any, index: number) => {
let _index = index - 1
if (_index < 0) { //
_index = 0
const groupDelete = async (g: any, index: number) => {
//
let actionLen = 0
let alarmTerms: Array<Record<string, string>> = []
for (let i = g.start; i < g.start + g.len; i++) {
const item = data.value.branches[i]
if (item) {
item.then?.forEach(thenItem => {
actionLen += thenItem.actions.length
if (thenItem.actions) {
thenItem.actions.forEach((actionItem) => {
const _actionId = actionItem.actionId
if (actionItem.executor === 'alarm') {
alarmTerms.push({
column: 'branchIndex',
value: _actionId || item.branchId,
type: 'or'
})
}
})
}
})
}
}
if (actionLen) {
if (alarmTerms.length) {
const resp = await queryBindScene({
terms: alarmTerms
})
Modal.confirm({
title: `已关联 ${resp.result.total} 条告警,删除该条件会同步解除对应的关联告警,确认删除?`,
onOk() {
const _data = resp.result.data.map(item => {
return {
"alarmId": item.alarmId,
"ruleId": item.ruleId,
"branchIndex": item.branchIndex
}
})
unBindAlarmMultiple(_data)
removeBranchesData(g, index)
}
})
} else {
Modal.confirm({
title: '该条件下有执行动作,确认删除?',
onOk() {
removeBranchesData(g, index)
}
})
}
} else {
removeBranchesData(g, index)
}
}
const removeBranchesData = (g: any, index: number) => {
const removeBranches = data.value.branches.splice(g.start, g.len)
removeBranches.forEach(item => {
if (item) {
let _index = data.value.options!.when.findIndex(whenItem => whenItem.key === item.branchId)
if (_index !== -1) {
_index = item.branches_Index
}
data.value.options!.when.splice(_index, 1)
}
})
if (index >= 0) { //
group.value.splice(index, 1)
data.value.branches.splice(g.start, g.len)
data.value.options!.when.splice(g.start, g.len)
activeKey.value = group.value[_index].id
if (g.id === activeKey.value) { //
let _moveIndex = index - 1
if (_moveIndex < 0) { //
_moveIndex = 0
}
activeKey.value = group.value[_moveIndex].id
}
} else { //
const groupItem = group.value.find(item => item.id === activeKey.value) //
groupItem!.len -= 1
const branchesItem = data.value.branches[g.start]
if (branchesItem === undefined || branchesItem?.executeAnyway) { // undefined null
data.value.branches?.splice(g.start, 0, null)
}
}
}
const branchesAdd = () => {
// const groupItem = group.value.find(item => item.id === activeKey.value) //
// groupItem!.len += 1
}
const showEditCondition = (key:any) =>{
if(key === activeKey.value){
editConditionVisible.value = true;
conditionName.value = group.value.find((i:any)=>{
return i.id === key
return i.branchId === key
})?.branchName
}
}
const changeBranchName = () =>{
console.log(data.value)
const changeBranchName = (name: string) =>{
let _activeKey = activeKey.value
data.value.branches?.forEach((item:any)=>{
if(item?.key === activeKey.value.slice(6)){
item.branchName = conditionName.value
if(item?.branchId === _activeKey){
item.branchName = name
}
})
let optionsItem = data.value.options!.when.find(item => item.key === _activeKey)
if (!optionsItem) {
const _index = group.value.findIndex(item => item.branchId === _activeKey)
if (_index !== -1) {
data.value.options!.when[_index].branchName = name
}
} else {
optionsItem.branchName = name
}
editConditionVisible.value =false
}
const changePaneIndex = (index) => {
const _groupItem = group.value.find(item => {
return item.start >= index && index < (item.start + item.len)
})
if (_groupItem) {
activeKey.value = _groupItem.branchId
}
}
watchEffect(() => {
if (data.value.trigger?.device) {
queryColumn({ trigger: data.value.trigger })
@ -246,7 +378,6 @@ watchEffect(() => {
watchEffect(() => {
const branches = data.value.branches
if (data.value.branches?.filter(item => item).length) {
open.value = !!data.value.branches[0].when.length
} else {
@ -254,33 +385,43 @@ watchEffect(() => {
}
let _group = []
let _branchesIndex = 0
if (branches) {
branches.forEach((item, index) => {
// if (index === 0) {
// _group.push({
// id: `group_${item.key}`,
// len: 0,
// start: 0,
// })
// }
const lastIndex = _group.length - 1
let whenItem = data.value.options!.when.find(when => item?.branchId === when.key)
if (!whenItem) {
whenItem = data.value.options!.when[_branchesIndex]
}
if (index === 0 || item?.executeAnyway) {
_group[lastIndex + 1] = {
id: `group_${item.key}`,
id: item.branchId,
len: 1,
start: index,
branchName:item.branchName
branchKey: item.key,
branchId: item.branchId,
// branchName: item.branchName || whenItem?.branchName || ` ${_branchesIndex + 1}`,
branchName: item.branchName || whenItem?.branchName || `条件`,
groupIndex: _branchesIndex
}
} else {
_group[lastIndex].len += 1
}
if (item) {
item.branches_Index = _branchesIndex
_branchesIndex += 1
}
})
branches.filter(item => item).forEach((item, index) => {
item.branches_Index = index
})
// branches.filter(item => item).forEach((item, index) => {
// item.branches_Index = index
// })
group.value = _group
if (!activeKey.value) {
@ -289,8 +430,18 @@ watchEffect(() => {
}
})
defineExpose({
changePaneIndex
})
</script>
<style scoped lang='less'>
.actions-terms {
:deep(.ant-tabs-tab-active) {
.ant-tabs-tab-remove {
color: #fff;
}
}
}
</style>

View File

@ -260,7 +260,6 @@ const columns = [
ellipsis: true,
search: {
type: 'select',
options: typeOptions,
options: () =>
new Promise((resolve) => {
queryType().then((resp: any) => {
@ -320,8 +319,8 @@ const tableRef = ref();
const current = ref<any>({});
const table = {
refresh: () => {
// tableRef.value.reload(queryParams.value);
window.location.reload();
tableRef.value.reload(queryParams.value);
// window.location.reload();
},
toAdd: () => {
visible.value = true;
@ -332,20 +331,24 @@ const table = {
},
changeStatus: (row: any) => {
const state = row.state.value === 'enabled' ? 'disabled' : 'enabled';
changeApplyStatus_api(row.id, { state }).then((resp: any) => {
const response = changeApplyStatus_api(row.id, { state });
response.then((resp: any) => {
if (resp.status === 200) {
onlyMessage('操作成功');
table.refresh();
}
});
return response;
},
clickDel: (row: any) => {
delApply_api(row.id).then((resp: any) => {
const response = delApply_api(row.id);
response.then((resp: any) => {
if (resp.status === 200) {
onlyMessage('操作成功');
table.refresh();
}
});
return response;
},
getActions: (
data: Partial<Record<string, any>>,
@ -475,6 +478,17 @@ const table = {
const dialogVisible = ref(false);
const selectId = ref<string>('');
const selectProvider = ref<any>('');
onMounted(() => {
queryType().then((resp: any) => {
if (resp.status === 200) {
const arr = resp.result.map((item: any) => ({
label: item.name,
value: item.provider,
}));
typeOptions.value = arr;
}
});
});
</script>
<style lang="less" scoped>

View File

@ -85,6 +85,8 @@ import {
import { protocolList } from '@/utils/consts';
import { getProviders } from '@/api/data-collect/channel';
import { isNoCommunity } from '@/utils/utils';
import { USER_CENTER_MENU_DATA } from '@/views/init-home/data/baseMenu'
const selectedKeys: any = ref([]);
const treeData = ref<any>([]);
const systemMenu: any = ref([]);
@ -195,6 +197,7 @@ const handleOk = async () => {
const _dataArr = dealTree(cloneDeep(treeData.value),selectedKeys.value)
const _dataSorts = handleSorts(_dataArr)
loading.value = true;
_dataSorts.push(USER_CENTER_MENU_DATA)
const res = await updateMenus(_dataSorts).catch(() => {});
if (res?.status === 200) {
onlyMessage('操作成功', 'success');

View File

@ -22,7 +22,7 @@
draggable
block-node
v-model:expandedKeys="expandedKeys"
v-model:selectedKeys="selectedKeys"
:selectedKeys="selectedKeys"
:tree-data="_treeData"
:show-line="{ showLeafIcon: false }"
:show-icon="true"
@ -62,18 +62,18 @@
</j-button>
</j-tooltip>
<j-tooltip title="删除">
<j-popconfirm
@confirm="onRemove(_data?.id)"
>
<j-button
@click.stop
class="actions-btn"
<PermissionButton
type="link"
style="margin: 0; padding: 0"
danger
:popConfirm="{
title: '确认删除?',
onConfirm: () =>
onRemove(_data?.id),
}"
>
<AIcon type="DeleteOutlined" />
</j-button>
</j-popconfirm>
<AIcon type="DeleteOutlined"
/></PermissionButton>
</j-tooltip>
</j-space>
</div>
@ -87,8 +87,8 @@
</div>
</div>
<Save
:mode="mode"
v-if="visible"
:mode="mode"
:data="current"
:treeData="_treeData"
:areaTree="areaTree"
@ -100,11 +100,18 @@
import { cloneDeep, debounce } from 'lodash-es';
import { onMounted, ref, watch } from 'vue';
import Save from '../Save/index.vue';
import { getRegionTree, delRegion } from '@/api/system/region';
import { useArea } from '../hooks';
import {
getRegionTree,
delRegion,
updateRegion,
saveRegion,
} from '@/api/system/region';
import { useArea, useRegion } from '../hooks';
import ResizeObserver from 'ant-design-vue/lib/vc-resize-observer';
import { onlyMessage } from '@/utils/comm';
import { title } from 'process';
const regionState = useRegion();
const treeData = ref<any[]>([]);
const _treeData = ref<any[]>([]);
const visible = ref<boolean>(false);
@ -119,7 +126,7 @@ const type = ref<string | undefined>(undefined);
const { areaTree } = useArea();
const emit = defineEmits(['select']);
const emit = defineEmits(['select', 'close']);
const filterTreeNodes = (tree: any[], condition: string) => {
return tree.filter((item) => {
@ -169,6 +176,7 @@ const onSave = () => {
const onClose = () => {
visible.value = false;
emit('close');
};
const divResize = ({ height }) => {
@ -181,14 +189,19 @@ const onEdit = (_data: any) => {
mode.value = 'edit';
current.value = _data;
visible.value = true;
selectedKeys.value = [_data.id];
emit('select', _data?.code, _data);
};
const onRemove = async (id: string) => {
const resp = await delRegion(id);
const onRemove = (id: string) => {
const response = delRegion(id);
response.then((resp) => {
if (resp.success) {
onlyMessage('操作成功!');
handleSearch();
}
});
return response
};
const onAdd = (_data?: any) => {
@ -217,13 +230,19 @@ const onDrop = (info: any) => {
const dropPos = info.node.pos.split('-');
const dropPosition =
info.dropPosition - Number(dropPos[dropPos.length - 1]);
const loop = (data: any, key: string | number, callback: any) => {
const loop = (
data: any,
key: string | number,
callback: any,
parent?: any,
) => {
data.forEach((item: any, index: number) => {
if (item.key === key) {
return callback(item, index, data);
if (item.id === key) {
return callback(item, index, data, parent);
}
if (item.children) {
return loop(item.children, key, callback);
return loop(item.children, key, callback, item);
}
});
};
@ -234,36 +253,67 @@ const onDrop = (info: any) => {
arr.splice(index, 1);
dragObj = item;
});
if (!info.dropToGap) {
// Drop on the content
loop(data, dropKey, (item: any) => {
item.children = item.children || [];
/// where to insert
dragObj.parentId = item.id;
item.children.unshift(dragObj);
item.children = item.children.map((cl: any, clIndex: number) => {
cl.sortIndex = clIndex + 1;
return cl;
});
updateRegion(dragObj);
});
} else if (
(info.node.children || []).length > 0 && // Has children
info.node.expanded && // Is expanded
dropPosition === 1 // On the bottom gap
) {
loop(data, dropKey, (item: any) => {
loop(
data,
dropKey,
(item: any, index: number, _data: any[], parent: any) => {
item.children = item.children || [];
// where to insert
item.children.unshift(dragObj);
});
dragObj.parentId = item.parentId;
item.children = item.children.map(
(cl: any, clIndex: number) => {
cl.sortIndex = clIndex + 1;
return cl;
},
);
_data.splice(index + 1, 0, dragObj);
// itemdragObj
updateRegion(item);
updateRegion(dragObj);
},
);
} else {
let ar: any[] = [];
let i = 0;
loop(data, dropKey, (_item: any, index: number, arr: any[]) => {
ar = arr;
i = index;
loop(
data,
dropKey,
(_item: any, index: number, arr: any[], parent: any) => {
dragObj.parentId = parent ? parent.id : '';
dragObj.sortIndex = dropPosition === -1 ? index : index + 1;
arr.splice(dragObj.sortIndex, 0, dragObj);
const sortArray = arr.map((cl: any, clIndex: number) => {
cl.sortIndex = clIndex + 1;
return cl;
});
if (dropPosition === -1) {
ar.splice(i, 0, dragObj);
if (parent) {
parent.children = sortArray;
updateRegion(parent);
} else {
ar.splice(i + 1, 0, dragObj);
updateRegion(arr);
}
},
);
}
treeData.value = data;
};
@ -286,8 +336,9 @@ watch(
* 区域选择
*/
const areaSelect = (key, { node }) => {
if (!key.length) return;
selectedKeys.value = key;
emit('select', node?.code);
emit('select', node?.code, node);
};
const handleSearch = async () => {
@ -301,11 +352,24 @@ const handleSearch = async () => {
const dt = treeData.value?.[0];
if (dt) {
selectedKeys.value = dt?.id ? [dt?.id] : [];
emit('select', dt?.code);
emit('select', dt?.code, dt);
}
}
};
const openSave = (geoJson: Record<string, any>) => {
if (geoJson) {
regionState.saveCache.geoJson = geoJson;
}
current.value = regionState.saveCache;
visible.value = true;
regionState.treeMask = false;
};
defineExpose({
openSave: openSave,
});
onMounted(() => {
handleSearch();
});
@ -318,9 +382,8 @@ onMounted(() => {
}
.tree-content {
display: flex;
flex-grow: 1;
height: 0;
flex: 1 1 0;
min-height: 0;
width: 100%;
.tree-empty {

View File

@ -1,91 +1,372 @@
<template>
<div class="region-map">
<AMapComponent @init="initMap"/>
<div class="region-map-loading" v-if="_loading || loading">
<a-spin :spinning="_loading || loading"></a-spin>
<AMapComponent
ref="mapRef"
>
<el-amap-polygon
v-if="showPolygon && pathData?.length"
:path="pathData"
:editable="isEdit"
:key="layerId"
@dragend="dragend"
@adjust="dragend"
@removenode="dragend"
@addnode="dragend"
@init="polygonInit"
/>
<el-amap-circle
v-if="showCircle"
:radius="pathData.radius"
:center="pathData.center"
:editable="isEdit"
@dragend="dragend"
/>
<el-amap-rectangle
v-if="showRectangle && pathData?.length"
:bounds="pathData"
:editable="isEdit"
:key="layerId"
@dragend="dragend"
@adjust="dragend"
@removenode="dragend"
@addnode="dragend"
@init="polygonInit"
/>
<DistrictSearch
v-if="showDistrict"
:adcode="adbode"
:styles="{
'stroke-width': 2,
'fill': 'rgba(0,176,255, 0.2)'
}"
/>
<GeoJson
v-if="showGeoJson"
:geo="pathData"
/>
<el-amap-mouse-tool
v-if="showTool"
:type="toolType"
@draw="toolDraw"
/>
</AMapComponent>
<div class="map-tool" v-if="showToolDom">
<div class="map-tool-content">
<div class="tool-item-group">
<div class="tool-item" @click="toolSave">
<j-tooltip title="保存描点" >
<AIcon type="SaveOutlined" />
</j-tooltip>
</div>
<div class="tool-item" @click="toolClose">
<j-tooltip title="取消操作" >
<AIcon type="CloseOutlined" />
</j-tooltip>
</div>
</div>
<div class="tool-item-group">
<div :class="{'tool-item': true, 'active': toolType === 'rectangle'}" @click="changeToolType(MAP_TOOL.rectangle)">
<j-tooltip title="矩形" >
<AIcon type="icon-huajuxing" />
</j-tooltip>
</div>
<div :class="{'tool-item': true, 'active': toolType === 'polygon'}" @click="changeToolType(MAP_TOOL.polygon)">
<j-tooltip title="多边形" >
<AIcon type="icon-huaduobianxing" />
</j-tooltip>
</div>
</div>
<div class="tool-item-group">
<div :class="{'tool-item': true, 'disabled': !hasHistory }" @click="onRevoke">
<j-tooltip title="撤销" >
<AIcon type="RollbackOutlined" />
</j-tooltip>
</div>
<div class="tool-item" @click="onDelete">
<j-tooltip title="删除">
<AIcon type="DeleteOutlined" />
</j-tooltip>
</div>
</div>
</div>
</div>
</div>
</template>
<script name="RegionMap" setup>
const props = defineProps({
selectCode: {
type: String,
default: undefined
},
import {useHistory, useRegion} from "../hooks";
import { MAP_TOOL } from '../util'
import { DistrictSearch, GeoJson } from '@/components/AMapComponent'
import { randomNumber } from '@/utils/utils'
import {onlyMessage} from "@/utils/comm";
const regionState = useRegion()
const { revoke, addRecord, reset, hasHistory } = useHistory()
const toolType = ref()
const showTool = ref(false)
const showToolDom = ref(false)
const adbode = ref()
const pathData = ref()
const isEdit = ref(false)
const layerId = ref('layer')
const mapRef = ref()
const toolDrawCache = ref()
const showPolygon = computed(() => {
return regionState.type === MAP_TOOL.polygon
})
const MapRef = ref()
const loading = ref(true)
const _loading = ref(true)
let polygon = null
let district = null
const initMap = (e) => {
loading.value = true
MapRef.value = e
loading.value = false
const showRectangle = computed(() => {
return regionState.type === MAP_TOOL.rectangle
})
const showCircle = computed(() => {
return regionState.type === MAP_TOOL.circle
})
const showDistrict = computed(() => {
return regionState.type === MAP_TOOL.district
})
const showGeoJson = computed(() => {
return regionState.type === MAP_TOOL.geoJson
})
const toolDraw = (e) => {
regionState.type = toolType.value
isEdit.value = true
pathData.value = e
handleGeoJson(toolType.value, e)
showTool.value = false
toolType.value = undefined
if (!hasHistory.value) {
addRecord(null)
}
addRecord({
isEdit: isEdit.value,
pathData: e,
toolType: toolType.value,
id: randomNumber()
})
}
const queryBounds = (code) => {
_loading.value = true
if (!district) {
//DistrictSearch
const opts = {
subdistrict: 0,
extensions: 'all',
level: 'district'
};
district = new AMap.DistrictSearch(opts);
const dragend = (e) => {
let paths = []
if (e.getPath) {
paths = e.getPath()
} else if(e.target?.getPath){
paths = e.target.getPath()
} if (e.bounds) {
const { northEast, southWest} = e.bounds
paths = [
[northEast.lng, northEast.lat],
[southWest.lng, southWest.lat],
]
}
district.search(code, function (status, result) {
if (polygon) {
MapRef.value.remove(polygon)//
polygon = null;
}
const bounds = result?.districtList?.[0]?.boundaries;
if (bounds) {
//polygon
for (let i = 0; i < bounds.length; i += 1) {// MultiPolygonpath
bounds[i] = [bounds[i]]
}
polygon = new AMap.Polygon({
strokeWeight: 1,
path: bounds,
fillOpacity: 0.4,
fillColor: '#80d8ff',
strokeColor: '#0091ea'
});
MapRef.value.add(polygon)
MapRef.value.setFitView(polygon);//
}
_loading.value = false
});
addRecord({
isEdit: isEdit.value,
pathData: paths,
toolType: regionState.type,
id: randomNumber()
})
handleGeoJson(regionState.type, paths)
}
watch(() => [props.selectCode, loading.value], () => {
if (props.selectCode && !loading.value) {
queryBounds(String(props.selectCode).padEnd(6, '0'))
const handleGeoJson = (type, data) => {
toolDrawCache.value = {
type: 'Feature',
toolType: type,
features: [{
type: 'FeatureCollection',
properties: {
type: type
},
geometry: {
type:"Polygon",
coordinates: type === MAP_TOOL.polygon ? [data] : data
}
}, {immediate: true})
}]
}
}
const changeToolType = (type) => {
showTool.value = true
toolType.value = type
isEdit.value = false
regionState.type = undefined
}
const showToolFn = (geoJson) => {
toolDrawCache.value = geoJson
showToolDom.value = true
}
const toolSave = () => {
if (toolDrawCache.value) {
regionState.openSave(toolDrawCache.value)
} else {
onlyMessage('请绘制区域范围','warning')
}
}
const toolClose = () => { //
showTool.value = false
toolType.value = undefined
regionState.openSave()
}
const showDistrictFn = (code) => {
adbode.value = code.toString().padEnd(6, '0')
}
const showGeoJsonFn = (geoJson) => {
pathData.value = geoJson
addRecord({
isEdit: true,
pathData: geoJson,
toolType: regionState.type,
id: randomNumber()
})
}
const openEdit = () => {
isEdit.value = true
layerId.value = randomNumber()
}
const onDelete = () => {
isEdit.value = false
regionState.type = undefined
toolType.value = undefined
pathData.value = undefined
toolDrawCache.value = undefined
addRecord({
isEdit: false,
pathData: [],
toolType: '',
id: randomNumber()
})
}
const readOnly = () => {
isEdit.value = false
showToolDom.value = false
}
const onRevoke = () => {
const item = revoke()
isEdit.value = item?.isEdit || false
pathData.value = item?.pathData || []
regionState.type = item?.toolType
layerId.value = item?.id || randomNumber()
}
const initState = () => {
isEdit.value = false
showTool.value = false
showToolDom.value = false
adbode.value = undefined
toolType.value = undefined
pathData.value = undefined
}
const polygonInit = (e) => {
const bounds = e.getBounds()
mapRef.value?.setBounds(bounds)
}
const init = () => {
initState()
reset()
}
defineExpose({
showTool: showToolFn,
showDistrict: showDistrictFn,
showGeoJson: showGeoJsonFn,
openEdit: openEdit,
init: init,
readOnly: readOnly,
})
</script>
<style lang="less" scoped>
.region-map {
position: relative;
//height: 800px;
height: 100%;
.region-map-loading {
.map-tool{
position: absolute;
left: 0;
top: 0;
width: 100%;
background-color: yellow;
height: 100%;
top: 20%;
right: 20px;
z-index: 3;
.map-tool-content {
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(#000, 0.35);
gap: 24px;
flex-direction: column;
.tool-item-group {
display: flex;
flex-direction: column;
border: 1px solid #e3e3e3;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 0 16px rgba(#000, .15);
.tool-item {
padding: 4px 6px;
color: #333;
font-size: 16px;
&:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
&:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
&:not(:first-child) {
border-top: 1px solid #e3e3e3;
}
&.active {
background-color: var(--ant-primary-color);
color: #fff;
}
&.disabled {
cursor: not-allowed !important;
background-color: #efefef;
> span {
cursor: not-allowed !important;
color: #666;
}
}
}
}
}
}
}
</style>

View File

@ -16,7 +16,7 @@
</template>
</j-tree-select>
<j-checkbox
v-model:checked="_checked"
v-model:checked="mySync"
@change="onCheckChange"
style="margin-top: 5px"
>同步添加下一级区域</j-checkbox
@ -43,13 +43,17 @@ const props = defineProps({
type: Array,
default: () => [],
},
sync: {
type: Boolean,
default: true
}
});
const emits = defineEmits(['update:value', 'update:name', 'update:children']);
const emits = defineEmits(['update:value', 'update:name', 'update:children', 'update:sync']);
const features = ref<any>({});
const _value = ref<string>();
const _checked = ref<boolean>(props.children?.length ?? false);
const mySync = ref<boolean>(props.sync);
const findChildren = (data: any, code: string) => {
@ -73,18 +77,18 @@ const findChildren = (data: any, code: string) => {
}
const onCheckChange = (e: any) => {
console.log('e',props.children, e.target.checked)
if (e.target.checked) {
const children = features.value?.children ? features.value?.children : findChildren(props.areaTree, _value.value)
emits('update:children', children.map((item, index) => {
if (!item.sortIndex) {
item.sortIndex = index + 1
}
return item
}));
} else {
emits('update:children', []);
}
// if (e.target.checked) {
// const children = features.value?.children ? features.value?.children : findChildren(props.areaTree, _value.value)
// emits('update:children', children.map((item, index) => {
// if (!item.sortIndex) {
// item.sortIndex = index + 1
// }
// return item
// }));
// } else {
// emits('update:children', []);
// }
emits('update:sync', e.target.checked)
};
const getObj = (node: any): any => {
@ -104,19 +108,19 @@ const getObj = (node: any): any => {
};
const onSelect = (val: string, node: any) => {
features.value = getObj(node);
// features.value = getObj(node);
_value.value = val;
emits('update:name', features.value?.name);
emits('update:value', features.value?.code);
emits('update:name', node.name);
emits('update:value', node.code);
if (_checked.value) {
emits('update:children', node?.children.map(item => ({
code: item.code,
name: item.name,
parentId: item.parentId,
})));
}
// if (mySync.value) {
// emits('update:children', node?.children.map(item => ({
// code: item.code,
// name: item.name,
// parentId: item.parentId,
// })));
// }
};
@ -127,7 +131,7 @@ watch(
_value.value = props.value
} else {
emits('update:name', '中国');
emits('update:value', 100000);
emits('update:value', '100000');
}
},
{
@ -135,4 +139,8 @@ watch(
immediate: true,
},
);
watch(() => props.sync, () => {
mySync.value = props.sync
}, { immediate: true})
</script>

View File

@ -26,14 +26,33 @@
@change="treeSelect"
/>
</j-form-item>
<j-form-item label="内置行政区" v-if="modelRef.properties.type === 'builtin'">
<j-form-item label="添加方式">
<RadioButton
v-model:value="modelRef.properties.type"
:options="[
{
label: '内置行政区',
value: 'builtin'
},
{
label: '自定义数据',
value: 'custom'
},
]"
@select="typeChange"
/>
</j-form-item>
<template v-if="modelRef.properties.type === 'builtin'">
<j-form-item>
<BuildIn
v-model:value="modelRef.code"
v-model:children="modelRef.children"
v-model:name="modelRef.name"
v-model:sync="modelRef.properties.sync"
:areaTree="areaTree"
/>
</j-form-item>
</template>
<template v-else>
<j-form-item
label="区域名称"
name="name"
@ -59,13 +78,13 @@
/>
</j-form-item>
<j-form-item
label="行政区划代码"
label="区划代码"
name="code"
required
:rules="[
{
required: true,
message: '请输入行政区划代码',
message: '请输入区划代码',
},
{
validator: vailCode,
@ -76,26 +95,73 @@
<j-input-number
v-model:value="modelRef.code"
style="width: 100%"
:disabled="modelRef.properties.type === 'builtin'"
placeholder="请输入行政区划代码"
placeholder="请输入区划代码"
/>
</j-form-item>
<j-form-item
label="区域划分"
>
<RadioButton
v-model:value="modelRef.properties.partition"
:options="[
{
label: '无',
value: 'none'
},
{
label: '手动描点',
value: 'manual'
},
{
label: 'GeoJson',
value: 'geoJson'
},
]"
@select="typeChange"
/>
</j-form-item>
<div v-if="modelRef.properties.partition === 'manual'">
<a-button v-if="!modelRef.geoJson" type="link" style="padding: 0" @click="showEditMap(false)">请在地图上描点</a-button>
<template v-else>
<a-space>
<span>区域已圈定完成</span>
<a-button type="link" style="padding: 0" @click="showEditMap(true)">编辑</a-button>
</a-space>
</template>
</div>
<div v-else-if="modelRef.properties.partition === 'geoJson'">
<a-button v-if="!modelRef.geoJson" type="link" style="padding: 0" @click="geoJsonVisible = true">点击上传GeoJson</a-button>
<template v-else>
<a-space>
<span>已上传</span>
<a-button type="link" style="padding: 0" @click="geoJsonVisible = true">编辑</a-button>
</a-space>
</template>
</div>
</template>
</j-form>
<GeoJsonModal
v-if="geoJsonVisible"
:value="modelRef.geoJson"
@cancel="geoJsonVisible = false"
@ok="updateGeoJson"
/>
</div>
</j-modal>
</template>
<script lang="ts" setup name="Save">
import {ref, watch, reactive} from 'vue';
import type {PropType} from 'vue';
import {reactive, ref, watch} from 'vue';
import BuildIn from './BuildIn.vue';
import {
validateName,
validateCode,
updateRegion,
} from '@/api/system/region';
import {updateRegion, validateName, validateCode} from '@/api/system/region';
import {omit} from "lodash-es";
import {onlyMessage} from "@/utils/comm";
import RadioButton from '@/components/CardSelect/RadioButton.vue'
import GeoJsonModal from './GeoJsonModal.vue'
import {useRegion} from "@/views/system/Region/hooks";
import {syncChildren} from "@/views/system/Region/util";
const emit = defineEmits(['close', 'save']);
const props = defineProps({
@ -117,10 +183,13 @@ const props = defineProps({
default: 'add',
},
});
const areaList = ref<Record<string, any>[]>([]);
const loading = ref<boolean>(false);
const geoJsonVisible = ref<boolean>(false);
const formRef = ref();
const regionState = useRegion()
const init = {
parentId: undefined,
@ -131,36 +200,52 @@ const init = {
children: [],
properties: {
type: 'builtin',
partition: 'none',
sync: true
},
sortIndex: props.data.sortIndex || 1
sortIndex: props.data.sortIndex || 1,
geoJson: undefined,
};
const modelRef = reactive(init);
watch(
() => props.data,
() => {
Object.assign(modelRef, {});
if (props.mode === 'add') {
//
Object.assign(modelRef, {
...init,
...props.data,
});
} else if (props.mode === 'edit') {
//
Object.assign(modelRef, props.data);
} else {
Object.assign(modelRef, init);
}
const modelRef = reactive({
parentId: undefined,
id: undefined,
name: undefined,
code: undefined,
features: undefined,
children: [],
properties: {
type: 'builtin',
partition: 'none',
sync: true
},
{immediate: true, deep: true},
);
sortIndex: props.data.sortIndex || 1,
geoJson: undefined,
});
const updateGeoJson = (json: string) => {
modelRef.geoJson = json
geoJsonVisible.value = false
}
const handleCancel = (data: any) => {
if (modelRef.properties.type === 'custom') {
if (props.mode === 'add') {
regionState.mapInit()
} else {
regionState.mapReadOnly(modelRef.geoJson)
}
}
emit('close', data);
};
const typeChange = (type: string) => {
modelRef.geoJson = undefined
modelRef.children = []
modelRef.properties.sync =false
}
const traceEdit = () => {
const newData: any = {
...props.data,
@ -170,6 +255,21 @@ const traceEdit = () => {
handleCancel(newData)
}
const showEditMap = (type: boolean) => {
regionState.treeMask = true
regionState.saveCache = modelRef
regionState.showTool()
if (type) {
regionState.layerSetData(modelRef.geoJson)
} else {
regionState.type = undefined
}
regionState.editType = props.mode
emit('close')
}
const treeSelect = (id: string, label: string, extra: any) => {
let children: any[]
if (extra) {
@ -177,7 +277,7 @@ const treeSelect = (id: string, label: string, extra: any) => {
} else {
children = props.treeData
}
const lastItem = children.length ? children[children.length-1] : {}
const lastItem = children.length ? children[children.length - 1] : {}
modelRef.sortIndex = lastItem.sortIndex ? lastItem.sortIndex + 1 : 1
}
@ -192,25 +292,34 @@ const handleSave = () => {
newData.fullName = props.data.parentFullName ? props.data.parentFullName + modelRef.name : modelRef.name
newData.parentId = newData.parentId || ''
const arr = areaList.value.map(item=>item.code)
if (newData.properties.sync) {
const _syncChildren = syncChildren(newData.code, props.areaTree)
const different = _syncChildren.filter(item => {
if (newData.children && newData.children.some(oldItem => oldItem.code === item.code)) {
return false
}
if (newData.children?.length) {
newData.children = newData.children.map(item => {
if (!item.fullName) {
item.fullName = newData.fullName + item.name
}
item = {
...item,
children: []
}
return item
}).filter(item =>!arr.includes(item.code))
return true
})
newData.children = [
...(newData.children || []),
...different
]
}
loading.value = true;
const resp = await updateRegion(newData).finally(() => {
loading.value = false;
});
if (resp.status === 200) {
regionState.stateInit()
onlyMessage('操作成功!');
emit('save');
}
@ -250,11 +359,28 @@ const onChange = () => {
modelRef.features = undefined;
};
watch(() => JSON.stringify(props.treeData), () => {
const item = JSON.parse(JSON.stringify(props.treeData))
areaList.value = item
if(props.mode === 'add'){
modelRef.children = props.areaTree?.[0]?.children
watch(
() => JSON.stringify(props.data),
(val) => {
if (props.mode === 'add') {
//
Object.assign(modelRef, init, JSON.parse(val || '{}'));
} else if (props.mode === 'edit') {
//
Object.assign(modelRef, JSON.parse(val || '{}'));
} else {
Object.assign(modelRef, init);
}
},
{immediate: true},
);
watch(() => JSON.stringify(props.treeData), () => {
areaList.value = JSON.parse(JSON.stringify(props.treeData))
// if (props.mode === 'add' && modelRef.properties.sync) {
// // modelRef.children = props.areaTree?.[0]?.children
// }
}, {immediate: true})
</script>

View File

@ -49,3 +49,61 @@ export const useArea = () => {
getParentNameById
}
}
export const REGION_KEY = Symbol('region_key')
export const useRegion = () => {
return inject(REGION_KEY, {
edit: false,
type: undefined, // MAP_TOOL
})
}
export const useHistory = () => {
const history = ref([])
const addRecord = (record: Record<string, any>) => {
history.value.push(record)
if (history.value.length > 10) { // 最多记录10条
history.value = history.value.slice(1)
}
}
/**
*
*/
const revoke = () => {
if (!hasHistory.value) {
return
}
if (history.value.length) {
// 删除最后一条
history.value.pop()
}
return getLastHistory()
}
const getLastHistory = () => {
return history.value[history.value.length - 1]
}
const hasHistory = computed(() => {
return history.value.length > 1
})
const reset = () => {
history.value = []
}
return {
revoke,
getLastHistory,
addRecord,
reset,
hasHistory
}
}

View File

@ -3,7 +3,10 @@
<full-page fixed>
<div class="region">
<div class="left">
<LeftTree @select="onSelect" />
<div v-if="regionState.treeMask" class="left-mask"></div>
<div class="left-content">
<LeftTree ref="treeRef" @select="onSelect" @close="close"/>
</div>
</div>
<div class="right">
<Map ref="mapRef" :selectCode="selectCode" />
@ -17,12 +20,92 @@
import LeftTree from './LeftTree/index.vue'
import Map from './MapTool/map.vue'
import FullPage from "@/components/Layout/FullPage.vue";
import {REGION_KEY} from "@/views/system/Region/hooks";
import {MAP_TOOL} from "@/views/system/Region/util";
const selectCode = ref('')
const mapRef = ref()
const treeRef = ref()
const onSelect = (dt: string) => {
selectCode.value = dt
const regionState = reactive({
showTool: showTool,
openSave: openSave,
openEdit: openEdit,
layerSetData: layerSetData,
mapInit: mapInit,
edit: false,
editType: 'add',
treeMask: false,
saveCache: undefined,
stateInit: stateInit,
mapReadOnly: mapReadOnly,
prevSelect: {}
})
provide(REGION_KEY, regionState)
const onSelect = (code: string, node: Record<string, any>) => {
if (!node) return
if (node.properties?.partition === 'geoJson') {
mapRef.value?.showGeoJson(node.geoJson)
regionState.type = MAP_TOOL.geoJson
} else if(node.properties?.partition === 'manual') {
layerSetData(node.geoJson, false)
} else {
mapRef.value?.showDistrict(code)
regionState.type = MAP_TOOL.district
}
regionState.prevSelect = {
code, node
}
}
const close = () => {
if (regionState.prevSelect.code) {
onSelect(regionState.prevSelect.code, regionState.prevSelect.node)
}
}
function stateInit() {
regionState.edit = false
regionState.treeMask = false
regionState.saveCache = undefined
regionState.type = undefined
regionState.editType = 'add'
mapInit()
}
function openSave(geoJson: Record<string, any>){
treeRef.value?.openSave(geoJson)
}
function showTool(type: string) {
mapRef.value?.showTool(regionState.saveCache?.geoJson)
}
function mapReadOnly(geoJson: any) {
layerSetData(geoJson, false)
mapRef.value?.readOnly(geoJson)
}
function openEdit() {
mapRef.value?.openEdit()
}
function layerSetData(geoJson: Record<string, any>, edit = true) {
regionState.type = geoJson.features[0].properties.type
mapRef.value?.showGeoJson(geoJson.features[0].geometry.coordinates)
if (edit) {
mapRef.value?.openEdit()
}
}
function mapInit() {
mapRef.value?.init()
}
</script>
<style lang="less" scoped>
@ -36,6 +119,22 @@ const onSelect = (dt: string) => {
width: 300px;
position: relative;
.left-content {
display: flex;
flex-direction: column;
height: 100%;
}
.left-mask {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 2;
background-color: rgba(0,0,0, .2);
}
.btn {
width: 100%;
margin: 18px 0;

View File

@ -27,7 +27,7 @@
</j-button>
<PermissionButton
:popConfirm="{
title: `是否批量解除绑定`,
title: `确认批量解除绑定?`,
placement: 'topRight',
onConfirm: () => table.unbind(),
}"
@ -170,12 +170,14 @@ const table = {
onlyMessage('请勾选数据', 'warning');
return;
}
unbindUser_api(roleId, data).then((resp) => {
const response = unbindUser_api(roleId, data)
response.then((resp) => {
if (resp.status === 200) {
onlyMessage('操作成功');
table.refresh();
}
});
return response
},
//
refresh: () => {

View File

@ -29,6 +29,11 @@
<AIcon type="PlusOutlined" />新增
</PermissionButton>
</template>
<template #username="slotProps">
<div class="username">
<Ellipsis>{{ slotProps.username }}</Ellipsis>
</div>
</template>
<template #type="slotProps">
{{ slotProps.type?.name }}
</template>
@ -74,9 +79,9 @@
}`,
}"
:popConfirm="{
title: `${
title: `${
slotProps.status ? '禁用' : '启用'
}`,
}`,
onConfirm: () =>
table.changeStatus(slotProps),
}"
@ -160,7 +165,7 @@ const columns = [
title: '用户名',
dataIndex: 'username',
key: 'username',
ellipsis: true,
scopedSlots: true,
search: {
type: 'string',
},
@ -276,17 +281,21 @@ const table = {
status: status === 0 ? 1 : 0,
id,
};
changeUserStatus_api(params).then(() => {
const response = changeUserStatus_api(params);
response.then(() => {
onlyMessage('操作成功');
table.refresh();
});
return response;
},
//
clickDel: (id: string) => {
deleteUser_api(id).then(() => {
const response = deleteUser_api(id);
response.then(() => {
onlyMessage('操作成功');
table.refresh();
});
return response;
},
//
refresh: () => {
@ -361,19 +370,22 @@ const handleParams = (params: any) => {
};
}
}
if(termsItem.column === 'roleList'){
if(termsItem.termType === 'eq' || termsItem.termType === 'in'){
if (termsItem.column === 'roleList') {
if (
termsItem.termType === 'eq' ||
termsItem.termType === 'in'
) {
return {
column: 'id$in-dimension$role',
type: termsItem.type,
value: termsItem.value
}
}else{
value: termsItem.value,
};
} else {
return {
column: 'id$in-dimension$role$not',
type: termsItem.type,
value: termsItem.value
}
value: termsItem.value,
};
}
}
return termsItem;
@ -401,4 +413,12 @@ const handleParams = (params: any) => {
}
}
}
.username {
display: inline-block;
border: 1px solid #91caff;
padding: 0 8px;
border-radius: 4px;
color: #1677ff;
background: #e6f4ff;
}
</style>

View File

@ -94,14 +94,15 @@ export default defineConfig(({ mode}) => {
[env.VITE_APP_BASE_API]: {
// target: 'http://192.168.32.226:8844',
// target: 'http://192.168.32.244:8881',
// target: 'http://192.168.32.163:8844', //张本地
// target: 'http://192.168.32.217:8844', //张本地
// target: 'http://120.77.179.54:8844', // 120测试
target: 'http://192.168.33.46:8844', // 本地开发环境
// target: 'http://192.168.32.167:8844', // 本地开发环境1
// target: 'http://192.168.33.6:8848', // 社区版开发环境
// target: 'http://192.168.33.6:38848', // 社区版开发环境
// target: 'http://192.168.32.207:8844', // 刘本地
// target: 'http://192.168.32.187:8844', // 谭本地
// target: 'http://192.168.33.66:8844', // 苟本地
// target: 'http://192.168.35.155:8844', // 王本地
ws: 'ws://192.168.33.46:8844',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
@ -113,6 +114,7 @@ export default defineConfig(({ mode}) => {
less: {
modifyVars: {
'root-entry-name': 'variable',
'primary-color': '#1677FF',
hack: `true; @import (reference) "${path.resolve('src/style/variable.less')}";`,
},
javascriptEnabled: true,