Merge pull request #54 from jetlinks/dev-hub

feat: 设备接入网关 接口调试 加入权限
This commit is contained in:
胡彪 2023-02-28 21:19:25 +08:00 committed by GitHub
commit fdb3323bcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 2278 additions and 1874 deletions

View File

@ -49,8 +49,10 @@ export const queryProduct = (data?: any) =>
export const queryDevice = () =>
server.get(`/device/instance/_query/no-paging?paging=false`);
export const validateVersion = (productId: string, versionOrder: number) =>
server.get(`/firmware/${productId}/${versionOrder}/exists`);
export const validateVersion = (
productId: string,
versionOrder: number | string,
) => server.get(`/firmware/${productId}/${versionOrder}/exists`);
export const queryDetailList = (data: Record<string, unknown>) =>
server.post(`/device-instance/detail/_query`, data);

11
src/api/link/dashboard.ts Normal file
View File

@ -0,0 +1,11 @@
import server from '@/utils/request';
export const dashboard = (data: object) =>
server.post(`/dashboard/_multi`, data);
export const productCount = (data: object) =>
server.post(`/device-product/_count`, data);
export const getGeo = (data: object) =>
server.post(`/geo/object/device/_search/geo.json`, data);
export const deviceCount = (data: object) =>
server.get(`/device/instance/_count`, data);
export const serverNode = () => server.get(`/dashboard/cluster/nodes`);

View File

@ -184,7 +184,12 @@
import { message, Form } from 'ant-design-vue';
import { getImage } from '@/utils/comm';
import FileUpload from './FileUpload.vue';
import { save, update, queryProduct } from '@/api/device/firmware';
import {
save,
update,
queryProduct,
validateVersion,
} from '@/api/device/firmware';
import type { FormInstance } from 'ant-design-vue';
import type { Properties } from '../type';
@ -246,6 +251,21 @@ const validatorSign = async (_: Record<string, any>, value: string) => {
return Promise.resolve();
}
};
const validatorVersionOrder = async (_: Record<string, any>, value: string) => {
const { signMethod, productId } = formData.value;
if (value && !!signMethod && productId) {
const res = await validateVersion(productId, value);
if (res.status === 200) {
if (id && props.data.versionOrder === value) {
formData.value.versionOrder = '';
} else {
Promise.reject(res.result ? ['版本序号已存在'] : '');
}
}
} else {
return Promise.resolve();
}
};
const { resetFields, validate, validateInfos } = useForm(
formData,
@ -258,8 +278,12 @@ const { resetFields, validate, validateInfos } = useForm(
version: [
{ required: true, message: '请输入版本号' },
{ max: 64, message: '最多可输入64个字符', trigger: 'change' },
{ validator: validatorVersionOrder, trigger: 'blur' },
],
versionOrder: [
{ required: true, message: '请输入版本序号' },
{ validator: validatorVersionOrder, trigger: 'blur' },
],
versionOrder: [{ required: true, message: '请输入版本序号' }],
signMethod: [{ required: true, message: '请选择签名方式' }],
sign: [
{ required: true, message: '请输入签名' },
@ -280,10 +304,10 @@ const onSubmit = async () => {
validate()
.then(async (res) => {
const product = productOptions.value.find(
(item) => item.value === res.productId,
(item) => item?.value === res.productId,
);
const productName = product.label || props.data?.url;
const size = extraValue.value.length || props.data?.size;
const productName = product?.label || props.data?.url;
const size = extraValue.value?.length || props.data?.size;
const params = {
...toRaw(formData.value),

View File

@ -9,8 +9,10 @@
></Provider>
</div>
<div v-else>
<div v-if="!id"><a @click="goBack">返回</a></div>
<AccessNetwork
<div class="go-back" v-if="id === ':id'">
<a @click="goBack">返回</a>
</div>
<Network
v-if="showType === 'network'"
:provider="provider"
:data="data"
@ -42,18 +44,15 @@
</template>
<script lang="ts" setup name="AccessConfigDetail">
import { getImage } from '@/utils/comm';
import AccessNetwork from '../components/Network.vue';
import Network from '../components/Network/index.vue';
import Provider from '../components/Provider/index.vue';
import { getProviders, detail } from '@/api/link/accessConfig';
import Media from '../components/Media/index.vue';
import Channel from '../components/Channel/index.vue';
import Edge from '../components/Edge/index.vue';
import Cloud from '../components/Cloud/index.vue';
import { getProviders, detail } from '@/api/link/accessConfig';
const route = useRoute();
const view = route.query.view as string;
const id = route.params.id as string;
const dataSource = ref([]);
@ -74,7 +73,7 @@ const goBack = () => {
type.value = true;
};
const getTypeList = (result: any[]) => {
const getTypeList = (result: Record<string, any>) => {
const list = [];
const media: any[] = [];
const network: any[] = [];
@ -184,76 +183,7 @@ onMounted(() => {
</script>
<style lang="less" scoped>
.provider {
position: relative;
width: 100%;
padding: 20px;
background: url('/public/images/access/background.png') no-repeat;
background-size: 100% 100%;
border: 1px solid #e6e6e6;
&::before {
position: absolute;
top: 0;
left: 40px;
display: block;
width: 15%;
min-width: 64px;
height: 2px;
background-image: url('/public/images/access/rectangle.png');
background-repeat: no-repeat;
background-size: 100% 100%;
// border: 1px #8da1f4 solid;
// border-bottom-left-radius: 10%;
// border-bottom-right-radius: 10%;
content: ' ';
}
&:hover {
box-shadow: 0 0 24px rgba(#000, 0.1);
}
}
.box {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.left {
display: flex;
width: calc(100% - 70px);
.images {
width: 64px;
height: 64px;
img {
width: 100%;
}
}
.context {
width: calc(100% - 84px);
margin: 10px;
.title {
font-weight: 600;
}
.desc {
width: 100%;
margin-top: 10px;
overflow: hidden;
color: rgba(0, 0, 0, 0.55);
font-weight: 400;
font-size: 13px;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.right {
width: 70px;
}
.go-back {
margin: 0 0 20px 0;
}
</style>

View File

@ -39,12 +39,16 @@
/>
</a-form-item>
<a-form-item>
<a-button
<PermissionButton
v-if="view === 'false'"
type="primary"
html-type="submit"
>保存</a-button
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</PermissionButton>
</a-form-item>
</a-form>
</div>
@ -86,10 +90,9 @@
</template>
<script lang="ts" setup name="AccessChannel">
import { message, Form } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { update, save } from '@/api/link/accessConfig';
import { ProtocolMapping } from '../../Detail/data';
import { ProtocolMapping } from '../../data';
interface FormState {
name: string;
@ -129,16 +132,7 @@ const onFinish = async (values: any) => {
id === ':id' ? await save(params) : await update({ ...params, id });
if (resp.status === 200) {
message.success('操作成功!');
// if (params.get('save')) {
// if ((window as any).onTabSaveSuccess) {
// if (resp.result) {
// (window as any).onTabSaveSuccess(resp.result);
// setTimeout(() => window.close(), 300);
// }
// }
// } else {
history.back();
// }
}
};
@ -164,8 +158,6 @@ onMounted(() => {
}
.config-right {
padding: 20px;
// color: rgba(0, 0, 0, 0.8);
// background: rgba(0, 0, 0, 0.04);
.config-right-item {
margin-bottom: 10px;

View File

@ -6,7 +6,7 @@
<div class="steps-content">
<div class="steps-box" v-if="current === 0">
<div class="alert">
<info-circle-outlined />
<AIcon type="InfoCircleOutlined" />
通过CTWing平台的HTTP推送服务进行数据接入
</div>
<div style="margin-top: 15px">
@ -160,7 +160,7 @@
<div class="steps-content">
<div class="steps-box" v-if="current === 1">
<div class="alert">
<info-circle-outlined />
<AIcon type="InfoCircleOutlined" />
只能选择HTTP通信方式的协议
</div>
<div class="search">
@ -170,9 +170,14 @@
style="width: 300px"
@search="procotolSearch"
/>
<a-button type="primary" @click="addProcotol"
>新增</a-button
<PermissionButton
type="primary"
@click="addProcotol"
hasPermission="link/Protocol:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
</div>
<div class="card-item">
<a-row :gutter="[24, 24]" v-if="procotolList.length > 0">
@ -282,96 +287,39 @@
>
下一步
</a-button>
<a-button
<PermissionButton
v-if="current === 2 && view === 'false'"
type="primary"
style="margin-right: 8px"
@click="saveData"
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</a-button>
</PermissionButton>
<a-button v-if="current > 0" @click="prev"> 上一步 </a-button>
</div>
</div>
</template>
<script lang="ts" setup name="AccessCloudCtwing">
import { message, Form } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue';
import { update, save, getNetworkList } from '@/api/link/accessConfig';
import { ProtocolMapping, NetworkTypeMapping } from '../../Detail/data';
import { InfoCircleOutlined } from '@ant-design/icons-vue';
import { update, save, getProtocolList } from '@/api/link/accessConfig';
import { ProtocolMapping } from '../../data';
import AccessCard from '../AccessCard/index.vue';
import { randomString } from '@/utils/utils';
import { getImage } from '@/utils/comm';
import { useMenuStore } from 'store/menu';
const menuStory = useMenuStore();
const origin = window.location.origin;
const img1 = getImage('/network/01.png');
const img2 = getImage('/network/02.jpg');
const img3 = getImage('/network/03.png');
const img4 = getImage('/network/04.jpg');
//1{
const resultList1 = [
{
id: '1612354213444087808',
name: '大华烟感协议',
},
{
id: '1610475299002855424',
name: '宇视摄像头协议',
},
{
id: '1610466717670780928',
name: '官方协议',
},
{
id: '1610205217785524224',
name: 'demo协议',
},
{
id: '1610204985806958592',
name: '水压协议',
},
{
id: '1605459961693745152',
name: '测试设备诊断日志显示',
},
{
id: '1582302200020783104',
name: 'demo',
},
{
id: '1581839391887794176',
name: '海康闸机协议',
},
{
id: '1567062365030637568',
name: '协议20220906160914',
},
{
id: '1561650927208628224',
name: 'local',
},
{
id: '1552881998413754368',
name: '官方协议V3-支持固件升级3',
},
{
id: '2b283b28a16d61e5fc2bdf39ceff34f8',
name: 'JetLinks官方协议',
description: 'JetLinks官方协议包',
},
{
id: '1551510679466844160',
name: '官方协议3.1',
},
{
id: '1551509716811161600',
name: '官方协议3.0',
},
];
interface FormState {
apiAddress: string;
appKey: string;
@ -397,7 +345,6 @@ const props = defineProps({
},
});
const channel = ref(props.provider.channel);
const formRef1 = ref<FormInstance>();
const formRef2 = ref<FormInstance>();
@ -459,37 +406,24 @@ const saveData = async () => {
});
if (resp.status === 200) {
message.success('操作成功!');
//
// if (window.onTabSaveSuccess) {
// window.onTabSaveSuccess(resp);
// setTimeout(() => window.close(), 300);
// } else {
// // this.$store.dispatch('jumpPathByKey', { key: MenuKeys['Link/AccessConfig'] })
// }
history.back();
}
// onFinish(data);
};
const queryProcotolList = async (id: string, params = {}) => {
// const resp = await getProtocolList(ProtocolMapping.get(id), {
// ...params,
// 'sorts[0].name': 'createTime',
// 'sorts[0].order': 'desc',
// });
// if (resp.status === 200) {
// procotolList.value = resp.result;
// allProcotolList.value = resp.result;
// }
//使1
procotolList.value = resultList1;
allProcotolList.value = resultList1;
const resp = await getProtocolList(ProtocolMapping.get(id), {
...params,
'sorts[0].name': 'createTime',
'sorts[0].order': 'desc',
});
if (resp.status === 200) {
procotolList.value = resp.result;
allProcotolList.value = resp.result;
}
};
const addProcotol = () => {
// const url = this.$store.state.permission.routes['Link/Protocol']
const url = '/iot/link/protocol';
const url = menuStory.menus['link/Protocol']?.path;
const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?save=true`,
);
@ -503,7 +437,7 @@ const addProcotol = () => {
const next = async () => {
if (current.value === 0) {
let data1: any = await formRef1.value?.validate();
await formRef1.value?.validate();
queryProcotolList(props.provider.id);
current.value = current.value + 1;
} else if (current.value === 1) {
@ -514,9 +448,11 @@ const next = async () => {
}
}
};
const prev = () => {
current.value = current.value - 1;
};
onMounted(() => {
if (id !== ':id') {
formState.value = props.data.configuration;
@ -527,6 +463,7 @@ onMounted(() => {
};
}
});
watch(
current,
(v) => {
@ -590,9 +527,6 @@ watch(
}
.config-right {
padding: 20px;
// color: rgba(0, 0, 0, 0.8);
// background: rgba(0, 0, 0, 0.04);
.config-right-item {
margin-bottom: 10px;

View File

@ -6,7 +6,7 @@
<div class="steps-content">
<div class="steps-box" v-if="current === 0">
<div class="alert">
<info-circle-outlined />
<AIcon type="InfoCircleOutlined" />
通过OneNet平台的HTTP推送服务进行数据接入
</div>
<div style="margin-top: 15px">
@ -41,7 +41,9 @@
同步物联网平台设备数据到OneNet
</p>
</template>
<question-circle-outlined />
<AIcon
type="QuestionCircleOutlined"
/>
</a-tooltip>
</div>
<a-input
@ -105,7 +107,9 @@
接收OneNet推送的Token地址
</p>
</template>
<question-circle-outlined />
<AIcon
type="QuestionCircleOutlined"
/>
</a-tooltip>
</div>
<a-input
@ -136,7 +140,9 @@
端生成的消息加密key
</p>
</template>
<question-circle-outlined />
<AIcon
type="QuestionCircleOutlined"
/>
</a-tooltip>
</div>
<a-input
@ -253,7 +259,7 @@
<div class="steps-content">
<div class="steps-box" v-if="current === 1">
<div class="alert">
<info-circle-outlined />
<AIcon type="InfoCircleOutlined" />
只能选择HTTP通信方式的协议
</div>
<div class="search">
@ -263,9 +269,14 @@
style="width: 300px"
@search="procotolSearch"
/>
<a-button type="primary" @click="addProcotol"
>新增</a-button
<PermissionButton
type="primary"
@click="addProcotol"
hasPermission="link/Protocol:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
</div>
<div class="card-item">
<a-row :gutter="[24, 24]" v-if="procotolList.length > 0">
@ -375,98 +386,38 @@
>
下一步
</a-button>
<a-button
<PermissionButton
style="margin-right: 8px"
v-if="current === 2 && view === 'false'"
type="primary"
@click="saveData"
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</a-button>
</PermissionButton>
<a-button v-if="current > 0" @click="prev"> 上一步 </a-button>
</div>
</div>
</template>
<script lang="ts" setup name="AccessCloudOneNet">
import { message, Form } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue';
import { update, save, getNetworkList } from '@/api/link/accessConfig';
import { ProtocolMapping, NetworkTypeMapping } from '../../Detail/data';
import {
InfoCircleOutlined,
QuestionCircleOutlined,
} from '@ant-design/icons-vue';
import { update, save, getProtocolList } from '@/api/link/accessConfig';
import AccessCard from '../AccessCard/index.vue';
import { randomString } from '@/utils/utils';
import { getImage } from '@/utils/comm';
import { ProtocolMapping } from '../../data';
import { useMenuStore } from 'store/menu';
const menuStory = useMenuStore();
const origin = window.location.origin;
const img5 = getImage('/network/05.jpg');
const img6 = getImage('/network/06.jpg');
const img = getImage('/network/OneNet.jpg');
//1{
const resultList1 = [
{
id: '1612354213444087808',
name: '大华烟感协议',
},
{
id: '1610475299002855424',
name: '宇视摄像头协议',
},
{
id: '1610466717670780928',
name: '官方协议',
},
{
id: '1610205217785524224',
name: 'demo协议',
},
{
id: '1610204985806958592',
name: '水压协议',
},
{
id: '1605459961693745152',
name: '测试设备诊断日志显示',
},
{
id: '1582302200020783104',
name: 'demo',
},
{
id: '1581839391887794176',
name: '海康闸机协议',
},
{
id: '1567062365030637568',
name: '协议20220906160914',
},
{
id: '1561650927208628224',
name: 'local',
},
{
id: '1552881998413754368',
name: '官方协议V3-支持固件升级3',
},
{
id: '2b283b28a16d61e5fc2bdf39ceff34f8',
name: 'JetLinks官方协议',
description: 'JetLinks官方协议包',
},
{
id: '1551510679466844160',
name: '官方协议3.1',
},
{
id: '1551509716811161600',
name: '官方协议3.0',
},
];
interface FormState {
apiAddress: string;
apiKey: string;
@ -478,9 +429,6 @@ interface Form {
name: string;
description: string;
}
const route = useRoute();
const view = route.query.view as string;
const id = route.params.id as string;
const props = defineProps({
provider: {
@ -493,7 +441,10 @@ const props = defineProps({
},
});
const channel = ref(props.provider.channel);
const route = useRoute();
const view = route.query.view as string;
const id = route.params.id as string;
const formRef1 = ref<FormInstance>();
const formRef2 = ref<FormInstance>();
@ -557,36 +508,24 @@ const saveData = async () => {
if (resp.status === 200) {
message.success('操作成功!');
//
// if (window.onTabSaveSuccess) {
// window.onTabSaveSuccess(resp);
// setTimeout(() => window.close(), 300);
// } else {
// // this.$store.dispatch('jumpPathByKey', { key: MenuKeys['Link/AccessConfig'] })
// }
history.back();
}
};
const queryProcotolList = async (id: string, params = {}) => {
// const resp = await getProtocolList(ProtocolMapping.get(id), {
// ...params,
// 'sorts[0].name': 'createTime',
// 'sorts[0].order': 'desc',
// });
// if (resp.status === 200) {
// procotolList.value = resp.result;
// allProcotolList.value = resp.result;
// }
//使1
procotolList.value = resultList1;
allProcotolList.value = resultList1;
const resp = await getProtocolList(ProtocolMapping.get(id), {
...params,
'sorts[0].name': 'createTime',
'sorts[0].order': 'desc',
});
if (resp.status === 200) {
procotolList.value = resp.result;
allProcotolList.value = resp.result;
}
};
const addProcotol = () => {
// const url = this.$store.state.permission.routes['Link/Protocol']
const url = '/iot/link/protocol';
const url = menuStory.menus['link/Protocol']?.path;
const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?save=true`,
);
@ -689,9 +628,6 @@ watch(
}
.config-right {
padding: 20px;
// color: rgba(0, 0, 0, 0.8);
// background: rgba(0, 0, 0, 0.04);
.config-right-item {
margin-bottom: 10px;

View File

@ -10,7 +10,7 @@
<div v-if="channel !== 'edge-child-device'" class="steps-content">
<div class="steps-box" v-if="current === 0">
<div class="alert">
<question-circle-outlined />
<AIcon type="InfoCircleOutlined" />
选择与设备通信的网络组件
</div>
<div class="search">
@ -20,7 +20,14 @@
style="width: 300px"
@search="networkSearch"
/>
<a-button type="primary" @click="addNetwork">新增</a-button>
<PermissionButton
type="primary"
@click="addNetwork"
hasPermission="link/Type:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
</div>
<div class="card-item">
<a-row :gutter="[24, 24]" v-if="networkList.length > 0">
@ -103,51 +110,53 @@
<a-row :gutter="[24, 24]">
<a-col :span="12">
<title-component data="基本信息" />
<div>
<a-form
:model="formState"
name="basic"
autocomplete="off"
layout="vertical"
@finish="onFinish"
ref="formRef"
<a-form
:model="formState"
name="basic"
autocomplete="off"
layout="vertical"
@finish="onFinish"
ref="formRef"
>
<a-form-item
label="名称"
name="name"
:rules="[
{
required: true,
message: '请输入名称',
trigger: 'blur',
},
{ max: 64, message: '最多可输入64个字符' },
]"
>
<a-form-item
label="名称"
name="name"
:rules="[
{
required: true,
message: '请输入名称',
trigger: 'blur',
},
{ max: 64, message: '最多可输入64个字符' },
]"
<a-input
placeholder="请输入名称"
v-model:value="formState.name"
/>
</a-form-item>
<a-form-item label="说明" name="description">
<a-textarea
placeholder="请输入说明"
:rows="4"
v-model:value="formState.description"
show-count
:maxlength="200"
/>
</a-form-item>
<a-form-item>
<PermissionButton
v-if="current !== 1 && view === 'false'"
type="primary"
html-type="submit"
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
<a-input
placeholder="请输入名称"
v-model:value="formState.name"
/>
</a-form-item>
<a-form-item label="说明" name="description">
<a-textarea
placeholder="请输入说明"
:rows="4"
v-model:value="formState.description"
show-count
:maxlength="200"
/>
</a-form-item>
<a-form-item>
<a-button
v-if="current !== 1 && view === 'false'"
type="primary"
html-type="submit"
>保存</a-button
>
</a-form-item>
</a-form>
</div>
保存
</PermissionButton>
</a-form-item>
</a-form>
</a-col>
<a-col :span="12">
<div class="config-right">
@ -178,119 +187,35 @@
>
下一步
</a-button>
<a-button
<PermissionButton
v-if="current === 1 && view === 'false'"
type="primary"
style="margin-right: 8px"
@click="saveData"
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</a-button>
</PermissionButton>
<a-button v-if="current > 0" @click="prev"> 上一步 </a-button>
</div>
</div>
</template>
<script lang="ts" setup name="AccessEdge">
import { message, Form } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue';
import { update, save, getNetworkList } from '@/api/link/accessConfig';
import {
descriptionList,
ProtocolMapping,
NetworkTypeMapping,
} from '../../Detail/data';
import { QuestionCircleOutlined } from '@ant-design/icons-vue';
} from '../../data';
import AccessCard from '../AccessCard/index.vue';
import { useMenuStore } from 'store/menu';
//1
const networkListTest = {
message: 'success',
result: [
{
id: '1585192878304051200',
name: 'MQTT网络组件',
addresses: [
{
address: 'mqtt://120.77.179.54:8101',
health: 1,
ok: true,
bad: false,
disabled: false,
},
],
},
{
id: '1583268266806009856',
name: '我的第一个MQTT服务组件',
description: '',
addresses: [
{
address: 'mqtt://120.77.179.54:8100',
health: 1,
ok: true,
bad: false,
disabled: false,
},
],
},
{
id: '1570335308902912000',
name: '0915MQTT网络组件_勿动',
description: '测试,勿动!',
addresses: [
{
address: 'mqtt://120.77.179.54:8083',
health: 1,
ok: true,
bad: false,
disabled: false,
},
],
},
{
id: '1567062350140858368',
name: '网络组件20220906160907',
addresses: [
{
address: 'mqtt://120.77.179.54:8083',
health: 1,
ok: true,
bad: false,
disabled: false,
},
],
},
{
id: '1556563257890742272',
name: 'MQTT网络组件',
addresses: [
{
address: 'mqtt://0.0.0.0:8104',
health: 1,
ok: true,
bad: false,
disabled: false,
},
],
},
{
id: '1534774770408108032',
name: 'MQTT',
addresses: [
{
address: 'mqtt://120.77.179.54:8088',
health: 1,
ok: true,
bad: false,
disabled: false,
},
],
},
],
status: 200,
timestamp: 1674960624150,
};
const menuStory = useMenuStore();
interface FormState {
name: string;
@ -327,6 +252,7 @@ const stepCurrent = ref(0);
const steps = ref(['网络组件', '完成']);
const networkCurrent = ref('');
const networkList = ref([]);
const allNetworkList = ref([]);
const onFinish = async (values: any) => {
const providerId = props.provider.id;
@ -341,16 +267,7 @@ const onFinish = async (values: any) => {
id === ':id' ? await save(params) : await update({ ...params, id });
if (resp.status === 200) {
message.success('操作成功!');
// if (params.get('save')) {
// if ((window as any).onTabSaveSuccess) {
// if (resp.result) {
// (window as any).onTabSaveSuccess(resp.result);
// setTimeout(() => window.close(), 300);
// }
// }
// } else {
history.back();
// }
}
};
@ -359,28 +276,27 @@ const checkedChange = (id: string) => {
};
const queryNetworkList = async (id: string, include: string, data = {}) => {
// const resp = await getNetworkList(
// NetworkTypeMapping.get(id),
// include,
// data,
// );
// if (resp.status === 200) {
// networkList.value = resp.result;
// }
//使1
networkList.value = networkListTest.result;
const resp = await getNetworkList(
NetworkTypeMapping.get(id),
include,
data,
);
if (resp.status === 200) {
networkList.value = resp.result;
allNetworkList.value = resp.result;
}
};
const networkSearch = (value: string) => {
queryNetworkList(props.provider.id, networkCurrent.value || '', {
terms: [
{
column: 'name$LIKE',
value: `%${value}%`,
},
],
});
if (value) {
networkList.value = allNetworkList.value.filter(
(i: any) =>
i.name &&
i.name.toLocaleLowerCase().includes(value.toLocaleLowerCase()),
);
} else {
networkList.value = allNetworkList.value;
}
};
const saveData = async () => {
@ -389,8 +305,7 @@ const saveData = async () => {
};
const addNetwork = () => {
// const url = this.$store.state.permission.routes['Link/Type/Detail']
const url = '/iot/link/type/detail/:id';
const url = menuStory.menus['link/Type/Detail']?.path;
const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?type=${
NetworkTypeMapping.get(props.provider?.id) || ''
@ -426,17 +341,17 @@ onMounted(() => {
};
networkCurrent.value = props.data.channelId;
}
}),
watch(
current,
(v) => {
stepCurrent.value = v;
},
{
deep: true,
immediate: true,
},
);
});
watch(
current,
(v) => {
stepCurrent.value = v;
},
{
deep: true,
immediate: true,
},
);
</script>
<style lang="less" scoped>

View File

@ -6,7 +6,7 @@
<div class="steps-content">
<div class="steps-box" v-if="current === 0">
<div class="alert">
<info-circle-outlined />
<AIcon type="InfoCircleOutlined" />
配置设备信令参数
</div>
<div>
@ -85,7 +85,7 @@
独立配置:集群下不同节点使用不同配置
</p>
</template>
<question-circle-outlined />
<AIcon type="QuestionCircleOutlined" />
</a-tooltip>
</div>
@ -218,9 +218,7 @@
:header="`#${index + 1}.节点`"
>
<template #extra>
<delete-outlined
@click="removeCluster(cluster)"
/>
<AIcon type="DeleteOutlined" />
</template>
<a-row :gutter="[24, 24]">
<a-col :span="8">
@ -274,7 +272,9 @@
绑定到服务器上的网卡地址,绑定到所有网卡:0.0.0.0
</p>
</template>
<question-circle-outlined />
<AIcon
type="QuestionCircleOutlined"
/>
</a-tooltip>
</div>
@ -363,7 +363,9 @@
监听指定端口的请求
</p>
</template>
<question-circle-outlined />
<AIcon
type="QuestionCircleOutlined"
/>
</a-tooltip>
</div>
<a-input
@ -417,7 +419,7 @@
block
@click="addCluster"
>
<PlusOutlined />
<AIcon type="PlusOutlined" />
新增
</a-button>
</a-form-item>
@ -502,14 +504,17 @@
>
下一步
</a-button>
<a-button
<PermissionButton
v-if="current === 1 && view === 'false'"
type="primary"
style="margin-right: 8px"
@click="saveData"
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</a-button>
</PermissionButton>
<a-button v-if="current > 0" @click="prev"> 上一步 </a-button>
</div>
</div>
@ -519,12 +524,6 @@
import { message, Form } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue';
import { getResourcesCurrent, getClusters } from '@/api/link/accessConfig';
import {
DeleteOutlined,
PlusOutlined,
QuestionCircleOutlined,
InfoCircleOutlined,
} from '@ant-design/icons-vue';
import { update, save } from '@/api/link/accessConfig';
interface Form2 {

View File

@ -39,12 +39,16 @@
/>
</a-form-item>
<a-form-item>
<a-button
<PermissionButton
v-if="view === 'false'"
type="primary"
html-type="submit"
>保存</a-button
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</PermissionButton>
</a-form-item>
</a-form>
</div>
@ -122,16 +126,7 @@ const onFinish = async (values: any) => {
id === ':id' ? await save(params) : await update({ ...params, id });
if (resp.status === 200) {
message.success('操作成功!');
// if (params.get('save')) {
// if ((window as any).onTabSaveSuccess) {
// if (resp.result) {
// (window as any).onTabSaveSuccess(resp.result);
// setTimeout(() => window.close(), 300);
// }
// }
// } else {
history.back();
// }
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,701 @@
<template>
<div>
<a-steps :current="stepCurrent">
<a-step v-for="item in steps" :key="item" :title="item" />
</a-steps>
<div class="steps-content">
<div class="steps-box" v-if="current === 0">
<div class="alert">
<AIcon type="InfoCircleOutlined" />
选择与设备通信的网络组件
</div>
<div class="search">
<a-input-search
allowClear
placeholder="请输入"
style="width: 300px"
@search="networkSearch"
/>
<PermissionButton
type="primary"
@click="addNetwork"
hasPermission="link/Type:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
</div>
<div class="card-item">
<a-row :gutter="[24, 24]" v-if="networkList.length > 0">
<a-col
:span="8"
v-for="item in networkList"
:key="item.id"
>
<access-card
@checkedChange="checkedChange"
:checked="networkCurrent"
:data="{
...item,
description: item.description
? item.description
: descriptionList[provider.id],
}"
>
<template #other>
<div class="other">
<a-tooltip placement="topLeft">
<div
v-if="
(item.addresses || [])
.length > 1
"
>
<div
v-for="i in item.addresses ||
[]"
:key="i.address"
class="item"
>
<a-badge
:color="getColor(i)"
/>{{ i.address }}
</div>
</div>
<div
v-for="i in (
item.addresses || []
).slice(0, 1)"
:key="i.address"
class="item"
>
<a-badge
:color="getColor(i)"
:text="i.address"
/>
<span
v-if="
(item.addresses || [])
.length > 1
"
>...</span
>
</div>
</a-tooltip>
</div>
</template>
</access-card>
</a-col>
</a-row>
<a-empty v-else description="暂无数据" />
</div>
</div>
<div class="steps-box" v-else-if="current === 1">
<div class="alert">
<AIcon type="InfoCircleOutlined" />
使用选择的消息协议对网络组件通信数据进行编解码认证等操作
</div>
<div class="search">
<a-input-search
allowClear
placeholder="请输入"
style="width: 300px"
@search="procotolSearch"
/>
<PermissionButton
type="primary"
@click="addProcotol"
hasPermission="link/Protocol:add"
>
<template #icon><AIcon type="PlusOutlined" /></template>
新增
</PermissionButton>
</div>
<div class="card-item">
<a-row :gutter="[24, 24]" v-if="procotolList.length > 0">
<a-col
:span="8"
v-for="item in procotolList"
:key="item?.id"
>
<access-card
@checkedChange="procotolChange"
:checked="procotolCurrent"
:data="item"
>
</access-card>
</a-col>
</a-row>
<a-empty v-else description="暂无数据" />
</div>
</div>
<div class="steps-box" v-else>
<div
class="card-last"
:style="`max-height:${
clientHeight > 900 ? 750 : clientHeight * 0.7
}px`"
>
<a-row :gutter="[24, 24]">
<a-col :span="12">
<title-component data="基本信息" />
<div>
<a-form
ref="formRef"
:model="formData"
layout="vertical"
>
<a-form-item
label="名称"
v-bind="validateInfos.name"
>
<a-input
v-model:value="formData.name"
allowClear
placeholder="请输入名称"
/>
</a-form-item>
<a-form-item
label="说明"
v-bind="validateInfos.description"
>
<a-textarea
placeholder="请输入说明"
:rows="4"
v-model:value="formData.description"
show-count
:maxlength="200"
/>
</a-form-item>
</a-form>
</div>
</a-col>
<a-col :span="12">
<div class="config-right">
<div class="config-right-item">
<div class="config-right-item-title">
接入方式
</div>
<div class="config-right-item-context">
{{ provider.name }}
</div>
<div class="config-right-item-context">
{{ provider.description }}
</div>
</div>
<div class="config-right-item">
<div class="config-right-item-title">
消息协议
</div>
<div class="config-right-item-context">
{{
procotolList.find(
(i) => i.id === procotolCurrent,
).name
}}
</div>
<div
class="config-right-item-context"
v-if="config.document"
>
<Markdown :source="config.document" />
</div>
</div>
<div
class="config-right-item"
v-if="getNetworkCurrent()"
>
<div class="config-right-item-title">
网络组件
</div>
<div
v-for="i in getNetworkCurrentData()"
:key="i.address"
>
<a-badge
:color="getColor(i)"
:text="i.address"
/>
</div>
</div>
<div
class="config-right-item"
v-if="
config.routes &&
config.routes.length > 0
"
>
<div class="config-right-item-title">
{{
data.provider ===
'mqtt-server-gateway' ||
data.provider ===
'mqtt-client-gateway'
? 'topic'
: 'URL信息'
}}
</div>
<a-table
:pagination="false"
:rowKey="generateUUID()"
:data-source="config.routes || []"
bordered
:columns="
config.id === 'MQTT'
? columnsMQTT
: columnsHTTP
"
:scroll="{ y: 300 }"
>
<template
#bodyCell="{ column, text, record }"
>
<template
v-if="
column.dataIndex ===
'stream'
"
>
<span>{{
getStream(record)
}}</span>
</template>
</template>
</a-table>
</div>
</div>
</a-col>
</a-row>
</div>
</div>
</div>
<div class="steps-action">
<a-button
v-if="[0, 1].includes(current)"
type="primary"
style="margin-right: 8px"
@click="next"
>
下一步
</a-button>
<PermissionButton
v-if="current === 2 && view === 'false'"
type="primary"
style="margin-right: 8px"
@click="saveData"
:hasPermission="`link/AccessConfig:${
id === ':id' ? 'add' : 'update'
}`"
>
保存
</PermissionButton>
<a-button
v-if="type === 'child-device' ? current > 1 : current > 0"
@click="prev"
>
上一步
</a-button>
</div>
</div>
</template>
<script lang="ts" setup name="AccessNetwork">
import {
getNetworkList,
getProtocolList,
getConfigView,
save,
update,
getChildConfigView,
} from '@/api/link/accessConfig';
import {
descriptionList,
NetworkTypeMapping,
ProtocolMapping,
ColumnsMQTT,
ColumnsHTTP,
} from '../../data';
import AccessCard from '../AccessCard/index.vue';
import { message, Form } from 'ant-design-vue';
import type { FormInstance, TableColumnType } from 'ant-design-vue';
import Markdown from 'vue3-markdown-it';
import { useMenuStore } from 'store/menu';
const menuStory = useMenuStore();
function generateUUID() {
var d = new Date().getTime();
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
/[xy]/g,
function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
},
);
}
const props = defineProps({
provider: {
type: Object,
default: () => {},
},
data: {
type: Object,
default: () => {},
},
});
const clientHeight = document.body.clientHeight;
const type = props.provider.channel;
const route = useRoute();
const view = route.query.view as string;
const id = route.params.id as string;
const formRef = ref<FormInstance>();
const useForm = Form.useForm;
const current = ref(0);
const stepCurrent = ref(0);
const steps = ref(['网络组件', '消息协议', '完成']);
const networkList: any = ref([]);
const allNetworkList: any = ref([]);
const procotolList = ref([]);
const allProcotolList = ref([]);
const networkCurrent: any = ref('');
const procotolCurrent: any = ref('');
const config: any = ref({});
const columnsMQTT = ref(<TableColumnType>[]);
const columnsHTTP = ref(<TableColumnType>[]);
const formData = ref({
name: '',
description: '',
});
const { resetFields, validate, validateInfos } = useForm(
formData,
reactive({
name: [
{ required: true, message: '请输入名称', trigger: 'blur' },
{ max: 64, message: '最多可输入64个字符' },
],
description: [{ max: 200, message: '最多可输入200个字符' }],
}),
);
const queryNetworkList = async (id: string, include: string, data = {}) => {
const resp = await getNetworkList(
NetworkTypeMapping.get(id),
include,
data,
);
if (resp.status === 200) {
networkList.value = resp.result;
}
};
const queryProcotolList = async (id: string, params = {}) => {
const resp = await getProtocolList(ProtocolMapping.get(id), {
...params,
'sorts[0].name': 'createTime',
'sorts[0].order': 'desc',
});
if (resp.status === 200) {
procotolList.value = resp.result;
allProcotolList.value = resp.result;
}
};
const addNetwork = () => {
const url = menuStory.menus['link/Type/Detail']?.path;
const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?type=${
NetworkTypeMapping.get(props.provider?.id) || ''
}`,
);
tab.onTabSaveSuccess = (value) => {
if (value.success) {
networkCurrent.value = value.result.id;
queryNetworkList(props.provider?.id, networkCurrent.value || '');
}
};
};
const addProcotol = () => {
const url = menuStory.menus['link/Protocol']?.path;
const tab = window.open(
`${window.location.origin + window.location.pathname}#${url}?save=true`,
);
tab.onTabSaveSuccess = (value) => {
if (value.success) {
procotolCurrent.value = value.result?.id;
queryProcotolList(props.provider?.id);
}
};
};
const getNetworkCurrent = () =>
networkList.value.find((i) => i.id === networkCurrent) &&
(networkList.value.find((i) => i.id === networkCurrent).addresses || [])
.length > 0;
const getNetworkCurrentData = () =>
getNetworkCurrent()
? networkList.value.find((i) => i.id === networkCurrent).addresses
: [];
const getColor = (i) => (i.health === -1 ? 'red' : 'green');
const getStream = (record: any) => {
let stream = '';
if (record.upstream && record.downstream) stream = '上行、下行';
else if (record.upstream) stream = '上行';
else if (record.downstream) stream = '下行';
return stream;
};
const checkedChange = (id: string) => {
networkCurrent.value = id;
};
const networkSearch = (value: string) => {
if (value) {
networkList.value = allNetworkList.value.filter(
(i: any) =>
i.name &&
i.name.toLocaleLowerCase().includes(value.toLocaleLowerCase()),
);
} else {
networkList.value = allNetworkList.value;
}
};
const procotolChange = (id: string) => {
if (!props.data.id) {
procotolCurrent.value = id;
}
};
const procotolSearch = (value: string) => {
if (value) {
const list = allProcotolList.value.filter((i: any) => {
return (
i.name &&
i.name.toLocaleLowerCase().includes(value.toLocaleLowerCase())
);
});
procotolList.value = list;
} else {
procotolList.value = allProcotolList.value;
}
};
const saveData = () => {
validate()
.then(async (values) => {
const params = {
...props.data,
...values,
protocol: procotolCurrent.value,
channel: 'network', //
channelId: networkCurrent.value,
};
const resp =
id === ':id'
? await save(params)
: await update({
...params,
id,
provider: props.provider.id,
transport:
props.provider?.id === 'child-device'
? 'Gateway'
: ProtocolMapping.get(props.provider.id),
});
if (resp.status === 200) {
message.success('操作成功!');
history.back();
}
})
.catch((err) => {});
};
const next = async () => {
if (current.value === 0) {
if (!networkCurrent.value) {
message.error('请选择网络组件!');
} else {
queryProcotolList(props.provider.id);
current.value = current.value + 1;
}
} else if (current.value === 1) {
if (!procotolCurrent.value) {
message.error('请选择消息协议!');
} else {
const resp =
type !== 'child-device'
? await getConfigView(
procotolCurrent.value,
ProtocolMapping.get(props.provider.id),
)
: await getChildConfigView(procotolCurrent.value);
if (resp.status === 200) {
config.value = resp.result;
console.log(222, config.value);
current.value = current.value + 1;
const Group = {
title: '分组',
dataIndex: 'group',
key: 'group',
ellipsis: true,
align: 'center',
width: 100,
customCell: (record: any, rowIndex: number) => {
const obj = {
children: record,
rowSpan: 0,
};
const list = config.value?.routes || [];
const arr = list.filter(
(res: any) => res.group === record.group,
);
const isRowIndex =
rowIndex === 0 ||
list[rowIndex - 1].group !== record.group;
isRowIndex && (obj.rowSpan = arr.length);
return obj;
},
};
columnsMQTT.value = [Group, ...ColumnsMQTT];
columnsHTTP.value = [Group, ...ColumnsHTTP];
}
}
}
};
const prev = () => {
current.value = current.value - 1;
};
onMounted(() => {
if (props.data && props.data.id) {
if (props.data.provider !== 'child-device') {
procotolCurrent.value = props.data.protocol;
current.value = 0;
networkCurrent.value = props.data.channelId;
queryNetworkList(props.provider.id, networkCurrent.value);
procotolCurrent.value = props.data.protocol;
steps.value = ['网络组件', '消息协议', '完成'];
} else {
steps.value = ['消息协议', '完成'];
current.value = 1;
queryProcotolList(props.provider.id);
}
} else {
if (props.provider?.id) {
if (type !== 'child-device') {
queryNetworkList(props.provider.id, '');
steps.value = ['网络组件', '消息协议', '完成'];
current.value = 0;
} else {
steps.value = ['消息协议', '完成'];
current.value = 1;
queryProcotolList(props.provider.id);
}
}
}
});
onMounted(() => {
if (id !== ':id') {
procotolCurrent.value = props.data.protocol;
formData.value = {
name: props.data.name,
description: props.data.description,
};
}
});
watch(
current,
(v) => {
stepCurrent.value = type === 'child-device' ? v - 1 : v;
},
{
deep: true,
immediate: true,
},
);
</script>
<style lang="less" scoped>
.steps-content {
margin: 20px;
}
.steps-box {
min-height: 400px;
.card-item {
padding-right: 5px;
max-height: 480px;
overflow-y: auto;
overflow-x: hidden;
}
.card-last {
padding-right: 5px;
overflow-y: auto;
overflow-x: hidden;
}
}
.steps-action {
width: 100%;
margin-top: 24px;
margin-left: 20px;
}
.alert {
height: 40px;
padding-left: 10px;
color: rgba(0, 0, 0, 0.55);
line-height: 40px;
background-color: #f6f6f6;
}
.search {
display: flex;
margin: 15px 0;
justify-content: space-between;
}
.other {
width: 100%;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
.item {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.config-right {
padding: 20px;
color: rgba(0, 0, 0, 0.8);
background: rgba(0, 0, 0, 0.04);
.config-right-item {
margin-bottom: 10px;
.config-right-item-title {
width: 100%;
margin-bottom: 10px;
font-weight: 600;
}
.config-right-item-context {
margin: 5px 0;
color: rgba(0, 0, 0, 0.8);
}
}
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div v-for="items in dataSource" :key="items.type">
<a-card class="card-items">
<div v-for="items in dataSource" :key="items.type" class="card-items">
<div class="card-items-container">
<TitleComponent :data="items.title"></TitleComponent>
<a-row :gutter="[24, 24]">
<a-col :span="12" v-for="item in items.list" :key="item.id">
@ -8,7 +8,7 @@
<div class="box">
<div class="left">
<div class="images">
<img :src="backMap.get(item.id)" />
<img :src="BackMap.get(item.id)" />
</div>
<div class="context">
<div class="title">
@ -30,13 +30,13 @@
</div>
</a-col>
</a-row>
</a-card>
</div>
</div>
</template>
<script lang="ts" setup name="AccessConfigProvider">
import TitleComponent from '@/components/TitleComponent/index.vue';
import { getImage } from '@/utils/comm';
import { BackMap } from '../../data';
const props = defineProps({
dataSource: {
@ -47,32 +47,16 @@ const props = defineProps({
const emit = defineEmits(['onClick']);
const backMap = new Map();
backMap.set('mqtt-server-gateway', getImage('/access/mqtt.png'));
backMap.set('websocket-server', getImage('/access/websocket.png'));
backMap.set('modbus-tcp', getImage('/access/modbus.png'));
backMap.set('coap-server-gateway', getImage('/access/coap.png'));
backMap.set('tcp-server-gateway', getImage('/access/tcp.png'));
backMap.set('Ctwing', getImage('/access/ctwing.png'));
backMap.set('child-device', getImage('/access/child-device.png'));
backMap.set('opc-ua', getImage('/access/opc-ua.png'));
backMap.set('http-server-gateway', getImage('/access/http.png'));
backMap.set('fixed-media', getImage('/access/video-device.png'));
backMap.set('udp-device-gateway', getImage('/access/udp.png'));
backMap.set('OneNet', getImage('/access/onenet.png'));
backMap.set('gb28181-2016', getImage('/access/gb28181.png'));
backMap.set('mqtt-client-gateway', getImage('/access/mqtt-broke.png'));
backMap.set('edge-child-device', getImage('/access/child-device.png'));
backMap.set('official-edge-gateway', getImage('/access/edge.png'));
const click = (value: object) => {
emit('onClick', value);
};
</script>
<style lang="less" scoped>
.card-items{
.card-items {
margin-bottom: 24px;
.card-items-container {
}
}
.provider {
position: relative;
@ -93,9 +77,6 @@ const click = (value: object) => {
background-image: url('/public/images/access/rectangle.png');
background-repeat: no-repeat;
background-size: 100% 100%;
// border: 1px #8da1f4 solid;
// border-bottom-left-radius: 10%;
// border-bottom-right-radius: 10%;
content: ' ';
}
@ -109,7 +90,6 @@ const click = (value: object) => {
align-items: center;
justify-content: space-between;
width: 100%;
.left {
display: flex;
width: calc(100% - 70px);

View File

@ -1,3 +1,4 @@
import { getImage } from '@/utils/comm';
const ProtocolMapping = new Map();
ProtocolMapping.set('websocket-server', 'WebSocket');
@ -25,6 +26,23 @@ NetworkTypeMapping.set('mqtt-server-gateway', 'MQTT_SERVER');
NetworkTypeMapping.set('tcp-server-gateway', 'TCP_SERVER');
NetworkTypeMapping.set('official-edge-gateway', 'MQTT_SERVER');
const BackMap = new Map();
BackMap.set('mqtt-server-gateway', getImage('/access/mqtt.png'));
BackMap.set('websocket-server', getImage('/access/websocket.png'));
BackMap.set('modbus-tcp', getImage('/access/modbus.png'));
BackMap.set('coap-server-gateway', getImage('/access/coap.png'));
BackMap.set('tcp-server-gateway', getImage('/access/tcp.png'));
BackMap.set('Ctwing', getImage('/access/ctwing.png'));
BackMap.set('child-device', getImage('/access/child-device.png'));
BackMap.set('opc-ua', getImage('/access/opc-ua.png'));
BackMap.set('http-server-gateway', getImage('/access/http.png'));
BackMap.set('fixed-media', getImage('/access/video-device.png'));
BackMap.set('udp-device-gateway', getImage('/access/udp.png'));
BackMap.set('OneNet', getImage('/access/onenet.png'));
BackMap.set('gb28181-2016', getImage('/access/gb28181.png'));
BackMap.set('mqtt-client-gateway', getImage('/access/mqtt-broke.png'));
BackMap.set('edge-child-device', getImage('/access/child-device.png'));
BackMap.set('official-edge-gateway', getImage('/access/edge.png'));
const descriptionList = {
'udp-device-gateway':
@ -43,21 +61,21 @@ const descriptionList = {
'CoAP是针对只有少量的内存空间和有限的计算能力提供的一种基于UDP的协议。便于低功耗或网络受限的设备与平台通信仅支持设备和平台之间单对单的请求-响应模式。',
};
const columnsMQTT = [
{
title: '分组',
dataIndex: 'group',
key: 'group',
ellipsis: true,
align: 'center',
width: 100,
scopedSlots: { customRender: 'group' },
},
const ColumnsMQTT = [
// {
// title: '分组',
// dataIndex: 'group',
// key: 'group',
// ellipsis: true,
// align: 'center',
// width: 100,
// scopedSlots: { customRender: 'group' },
// },
{
title: 'topic',
dataIndex: 'topic',
key: 'topic',
scopedSlots: { customRender: 'topic' },
ellipsis: true,
},
{
title: '上下行',
@ -72,37 +90,58 @@ const columnsMQTT = [
title: '说明',
dataIndex: 'description',
key: 'description',
scopedSlots: { customRender: 'description' },
},
]
const columnsHTTP = [
{
title: '分组',
dataIndex: 'group',
key: 'group',
ellipsis: true,
width: 100,
scopedSlots: { customRender: 'group' },
},
{
},
];
const ColumnsHTTP = [
// {
// title: '分组',
// dataIndex: 'group',
// key: 'group',
// ellipsis: true,
// width: 100,
// scopedSlots: { customRender: 'group' },
// },
{
title: '地址',
dataIndex: 'address',
key: 'address',
scopedSlots: { customRender: 'address' },
},
{
ellipsis: true,
// scopedSlots: { customRender: 'address' },
},
{
title: '示例',
dataIndex: 'example',
key: 'example',
scopedSlots: { customRender: 'example' },
},
{
ellipsis: true,
// scopedSlots: { customRender: 'example' },
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
scopedSlots: { customRender: 'description' }
},
]
ellipsis: true,
// scopedSlots: { customRender: 'description' },
},
];
export { NetworkTypeMapping, ProtocolMapping, descriptionList, columnsMQTT, columnsHTTP };
const TiTlePermissionButtonStyle = {
padding: 0,
color: ' #1890ff !important',
'font-weight': 700,
'font-size': '16px',
overflow: 'hidden',
'text-overflow': 'ellipsis',
'white-space': 'nowrap',
};
export {
NetworkTypeMapping,
ProtocolMapping,
BackMap,
descriptionList,
ColumnsMQTT,
ColumnsHTTP,
TiTlePermissionButtonStyle,
};

View File

@ -11,12 +11,23 @@
:defaultParams="{
sorts: [{ name: 'createTime', order: 'desc' }],
}"
gridColumn="2"
gridColumns="[2]"
:params="params"
>
<template #headerTitle>
<a-button type="primary" @click="handlAdd"
><AIcon type="PlusOutlined" />新增</a-button
>
<a-space>
<PermissionButton
type="primary"
@click="handlAdd"
hasPermission="link/AccessConfig:add"
>
<template #icon
><AIcon type="PlusOutlined"
/></template>
新增
</PermissionButton>
</a-space>
</template>
<template #card="slotProps">
<CardBox
@ -43,12 +54,15 @@
</template>
<template #content>
<div class="card-item-content">
<h3
<PermissionButton
type="link"
@click="handlEye(slotProps.id)"
class="card-item-content-title card-item-content-title-a"
hasPermission="link/AccessConfig:view"
:style="TiTlePermissionButtonStyle"
>
{{ slotProps.name }}
</h3>
</PermissionButton>
<a-row class="card-item-content-box">
<a-col
:span="12"
@ -65,13 +79,7 @@
"
>
<a-badge
:status="
slotProps.channelInfo
.addresses[0].health ===
-1
? 'error'
: 'success'
"
:status="getStatus(slotProps)"
/>
<a-tooltip>
<template #title>{{
@ -112,24 +120,12 @@
<a-tooltip>
<template #title>
{{
slotProps.description
? slotProps.description
: providersList.find(
(item) =>
item.id ===
slotProps.provider,
)?.description
getDescription(
slotProps,
)
}}
</template>
{{
slotProps.description
? slotProps.description
: providersList.find(
(item) =>
item.id ===
slotProps.provider,
)?.description
}}
{{ getDescription(slotProps) }}
</a-tooltip>
</div>
</a-col>
@ -138,42 +134,24 @@
</template>
<template #actions="item">
<a-tooltip
v-bind="item.tooltip"
:title="item.disabled && item.tooltip.title"
<PermissionButton
:disabled="item.disabled"
:popConfirm="item.popConfirm"
:tooltip="{
...item.tooltip,
}"
@click="item.onClick"
:hasPermission="'link/AccessConfig:' + item.key"
>
<a-popconfirm
v-if="item.popConfirm"
v-bind="item.popConfirm"
:disabled="item.disabled"
>
<a-button :disabled="item.disabled">
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</template>
</a-button>
</a-popconfirm>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<a-button
:disabled="item.disabled"
@click="item.onClick"
>
<AIcon
type="DeleteOutlined"
v-if="item.key === 'delete'"
/>
<template v-else>
<AIcon :type="item.icon" />
<span>{{ item.text }}</span>
</template>
</a-button>
<AIcon :type="item.icon" />
<span>{{ item?.text }}</span>
</template>
</a-tooltip>
</PermissionButton>
</template>
</CardBox>
</template>
@ -198,12 +176,14 @@ import {
deploy,
} from '@/api/link/accessConfig';
import { message } from 'ant-design-vue';
import { useMenuStore } from 'store/menu';
import { TiTlePermissionButtonStyle } from './data';
const menuStory = useMenuStore();
const tableRef = ref<Record<string, any>>({});
const router = useRouter();
const params = ref<Record<string, any>>({});
let providersList = ref([]);
let providersList = ref<Record<string, any>>([]);
const statusMap = new Map();
statusMap.set('enabled', 'success');
@ -225,8 +205,6 @@ const columns = [
key: 'provider',
search: {
type: 'select',
// options: providersList,
// options: getProvidersList
options: async () => {
const res = await getProviders();
return (res?.result || []).map((item) => ({
@ -275,9 +253,10 @@ const columns = [
const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
if (!data) return [];
const state = data.state.value;
const stateText = state === 'enabled' ? '禁用' : '启用';
return [
{
key: 'edit',
key: 'update',
text: '编辑',
tooltip: {
title: '编辑',
@ -289,13 +268,13 @@ const getActions = (data: Partial<Record<string, any>>): ActionsType[] => {
},
{
key: 'action',
text: state === 'enabled' ? '禁用' : '启用',
text: stateText,
tooltip: {
title: state === 'enabled' ? '禁用' : '启用',
title: stateText,
},
icon: state === 'enabled' ? 'StopOutlined' : 'CheckCircleOutlined',
popConfirm: {
title: `确认${state === 'enabled' ? '禁用' : '启用'}?`,
title: `确认${stateText}?`,
onConfirm: async () => {
let res =
state === 'enabled'
@ -342,27 +321,29 @@ const getProvidersList = async () => {
getProvidersList();
const handlAdd = () => {
// router.push('/link/accessConfig/detail/add/new');
router.push({
path: `/iot/link/accessConfig/detail/:id`,
query: { view: false },
});
menuStory.jumpPage(
`link/AccessConfig/Detail`,
{ id: ':id' },
{ view: false },
);
};
const handlEdit = (id: string) => {
// router.push(`/link/accessConfig/detail/edit/${id}`);
router.push({
path: `/iot/link/accessConfig/detail/${id}`,
query: { view: false },
});
menuStory.jumpPage(`link/AccessConfig/Detail`, { id }, { view: false });
};
const handlEye = (id: string) => {
// router.push(`/link/accessConfig/detail/view/${id}`);
router.push({
path: `/iot/link/accessConfig/detail/${id}`,
query: { view: true },
});
menuStory.jumpPage(`link/AccessConfig/Detail`, { id }, { view: true });
};
const getDescription = (slotProps: Record<string, any>) =>
slotProps.description
? slotProps.description
: providersList?.find(
(item: Record<string, any>) => item.id === slotProps.provider,
)?.description;
const getStatus = (slotProps: Record<string, any>) =>
slotProps.channelInfo.addresses[0].health === -1 ? 'error' : 'success';
/**
* 搜索
* @param params
@ -370,18 +351,6 @@ const handlEye = (id: string) => {
const handleSearch = (e: any) => {
params.value = e;
};
// const handlAdd = () => {
// router.push({
// path: '/link/accessConfig/detail/add',
// query: {
// id: '1610475400026861568',
// },
// });
// };
// const handlAdd = () => {
// router.push('/link/accessConfig/detail/add');
// }
</script>
<style lang="less" scoped>
.tableCardDisabled {

View File

@ -0,0 +1,189 @@
<template>
<a-spin :spinning="loading">
<div class="dash-board">
<div class="header">
<h3>CPU使用率趋势</h3>
<a-range-picker
@change="pickerTimeChange"
:allowClear="false"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
v-model="data.time"
>
<template #suffixIcon><a-icon type="calendar" /></template>
<template #renderExtraFooter>
<a-radio-group
default-value="a"
button-style="solid"
style="margin-right: 10px"
v-model:value="data.type"
>
<a-radio-button value="hour">
最近1小时
</a-radio-button>
<a-radio-button value="today">
今日
</a-radio-button>
<a-radio-button value="week">
近一周
</a-radio-button>
</a-radio-group></template
>
</a-range-picker>
</div>
<div ref="chartRef" style="width: 100%; height: 300px"></div>
</div>
</a-spin>
</template>
m
<script lang="ts" setup name="Cpu">
import * as echarts from 'echarts';
import { dashboard } from '@/api/link/dashboard';
import moment from 'moment';
import {
getTimeFormat,
getTimeByType,
arrayReverse,
defulteParamsData,
areaStyleCpu,
typeDataLine,
} from './tool.ts';
const chartRef = ref<Record<string, any>>({});
const loading = ref(false);
const data = ref({
type: 'hour',
time: [null, null],
});
const pickerTimeChange = () => {
data.value.type = undefined;
};
const getCPUEcharts = async (val) => {
loading.value = true;
const res = await dashboard(defulteParamsData('cpu', val));
if (res.success) {
const _cpuOptions = {};
const _cpuXAxis = new Set();
if (res.result?.length) {
res.result.forEach((item) => {
const value = item.data.value;
const nodeID = item.data.clusterNodeId;
_cpuXAxis.add(
moment(value.timestamp).format(
getTimeFormat(data.value.type),
),
);
if (!_cpuOptions[nodeID]) {
_cpuOptions[nodeID] = [];
}
_cpuOptions[nodeID].push(
Number(value.cpuSystemUsage).toFixed(2),
);
});
}
handleCpuOptions(_cpuOptions, [..._cpuXAxis.keys()]);
}
setTimeout(() => {
loading.value = false;
}, 300);
};
const setOptions = (optionsData, key) => ({
data: arrayReverse(optionsData[key]),
name: key,
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: areaStyleCpu,
});
const handleCpuOptions = (optionsData, xAxis) => {
const chart = chartRef.value;
if (chart) {
const myChart = echarts.init(chart);
const dataKeys = Object.keys(optionsData);
const options = {
xAxis: {
type: 'category',
boundaryGap: false,
data: arrayReverse(xAxis),
},
tooltip: {
trigger: 'axis',
valueFormatter: (value) => `${value}%`,
},
yAxis: {
type: 'value',
},
grid: {
left: '50px',
right: '50px',
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
color: ['#2CB6E0'],
series: dataKeys.length
? dataKeys.map((key) => setOptions(optionsData, key))
: typeDataLine,
};
myChart.setOption(options);
window.addEventListener('resize', function () {
myChart.resize();
});
}
};
watch(
() => data.value.type,
(val) => {
const endTime = moment(new Date());
const startTime = getTimeByType(val);
data.value.time = [startTime, endTime];
},
{ immediate: true, deep: true },
);
watch(
() => data.value,
(val) => {
const { time } = val;
if (time && Array.isArray(time) && time.length === 2 && time[0]) {
getCPUEcharts(val);
}
},
{ immediate: true, deep: true },
);
</script>
<style lang="less" scoped>
.dash-board {
display: flex;
flex-direction: column;
height: 100%;
padding: 24px;
background-color: #fff;
box-shadow: 0px 2.73036px 5.46071px rgba(31, 89, 245, 0.2);
border-radius: 2px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
width: 200px;
margin-top: 8px;
}
}
</style>

View File

@ -0,0 +1,193 @@
<template>
<a-spin :spinning="loading">
<div class="dash-board">
<div class="header">
<h3>JVM内存使用率趋势</h3>
<a-range-picker
@change="pickerTimeChange"
:allowClear="false"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
v-model="data.time"
>
<template #suffixIcon><a-icon type="calendar" /></template>
<template #renderExtraFooter>
<a-radio-group
default-value="a"
button-style="solid"
style="margin-right: 10px"
v-model:value="data.type"
>
<a-radio-button value="hour">
最近1小时
</a-radio-button>
<a-radio-button value="today">
今日
</a-radio-button>
<a-radio-button value="week">
近一周
</a-radio-button>
</a-radio-group></template
>
</a-range-picker>
</div>
<div ref="chartRef" style="width: 100%; height: 300px"></div>
</div>
</a-spin>
</template>
<script lang="ts" setup name="Jvm">
import * as echarts from 'echarts';
import { dashboard } from '@/api/link/dashboard';
import moment from 'moment';
import {
getTimeFormat,
getTimeByType,
arrayReverse,
typeDataLine,
areaStyleJvm,
defulteParamsData,
} from './tool.ts';
const chartRef = ref<Record<string, any>>({});
const loading = ref(false);
const data = ref({
type: 'hour',
time: [null, null],
});
const pickerTimeChange = () => {
data.value.type = undefined;
};
const getJVMEcharts = async (val) => {
loading.value = true;
const res = await dashboard(defulteParamsData('jvm', val));
if (res.success) {
const _jvmOptions = {};
const _jvmXAxis = new Set();
if (res.result?.length) {
res.result.forEach((item) => {
const value = item.data.value;
const memoryJvmHeapFree = value.memoryJvmHeapFree;
const memoryJvmHeapTotal = value.memoryJvmHeapTotal;
const nodeID = item.data.clusterNodeId;
const _value = (
((memoryJvmHeapTotal - memoryJvmHeapFree) /
memoryJvmHeapTotal) *
100
).toFixed(2);
if (!_jvmOptions[nodeID]) {
_jvmOptions[nodeID] = [];
}
_jvmXAxis.add(
moment(value.timestamp).format(
getTimeFormat(data.value.type),
),
);
_jvmOptions[nodeID].push(_value);
});
}
handleJVMOptions(_jvmOptions, [..._jvmXAxis.keys()]);
}
setTimeout(() => {
loading.value = false;
}, 300);
};
const setOptions = (optionsData, key) => ({
data: arrayReverse(optionsData[key]),
name: key,
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: areaStyleJvm,
});
const handleJVMOptions = (optionsData, xAxis) => {
const chart = chartRef.value;
if (chart) {
const myChart = echarts.init(chart);
const dataKeys = Object.keys(optionsData);
const options = {
xAxis: {
type: 'category',
boundaryGap: false,
data: arrayReverse(xAxis),
},
tooltip: {
trigger: 'axis',
valueFormatter: (value: any) => `${value}%`,
},
yAxis: {
type: 'value',
},
grid: {
left: '50px',
right: '50px',
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
color: ['#60DFC7'],
series: dataKeys.length
? dataKeys.map((key) => setOptions(optionsData, key))
: typeDataLine,
};
myChart.setOption(options);
window.addEventListener('resize', function () {
myChart.resize();
});
}
};
watch(
() => data.value.type,
(val) => {
const endTime = moment(new Date());
const startTime = getTimeByType(val);
data.value.time = [startTime, endTime];
},
{ immediate: true, deep: true },
);
watch(
() => data.value,
(val) => {
const { time } = val;
if (time && Array.isArray(time) && time.length === 2 && time[0]) {
getJVMEcharts(val);
}
},
{ immediate: true, deep: true },
);
</script>
<style lang="less" scoped>
.dash-board {
display: flex;
flex-direction: column;
height: 100%;
padding: 24px;
background-color: #fff;
box-shadow: 0px 2.73036px 5.46071px rgba(31, 89, 245, 0.2);
border-radius: 2px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
width: 200px;
margin-top: 8px;
}
}
</style>

View File

@ -0,0 +1,219 @@
<template>
<a-spin :spinning="loading">
<div class="dash-board">
<div class="header">
<div class="left">
<h3 style="width: 80px">网络流量</h3>
<a-radio-group
button-style="solid"
v-model:value="data.type"
>
<a-radio-button value="bytesRead">
上行
</a-radio-button>
<a-radio-button value="bytesSent">
下行
</a-radio-button>
</a-radio-group>
</div>
<div class="right">
<a-radio-group
default-value="a"
button-style="solid"
style="margin-right: 10px"
v-model:value="data.time.type"
>
<a-radio-button value="hour">
最近1小时
</a-radio-button>
<a-radio-button value="today"> 今日 </a-radio-button>
<a-radio-button value="week"> 近一周 </a-radio-button>
</a-radio-group>
<a-range-picker
:allowClear="false"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
v-model="data.time.time"
@change="pickerTimeChange"
>
<template #suffixIcon
><a-icon type="calendar"
/></template>
</a-range-picker>
</div>
</div>
<div>
<div
ref="chartRef"
v-if="flag"
style="width: 100%; height: 350px"
></div>
<a-empty v-else style="height: 300px; margin-top: 120px" />
</div>
</div>
</a-spin>
</template>
<script lang="ts" setup name="Network">
import { dashboard } from '@/api/link/dashboard';
import {
getTimeByType,
typeDataLine,
areaStyle,
networkParams,
arrayReverse,
} from './tool.ts';
import moment from 'moment';
import * as echarts from 'echarts';
const chartRef = ref<Record<string, any>>({});
const flag = ref(true);
const loading = ref(false);
const myChart = ref(null);
const data = ref({
type: 'bytesRead',
time: {
type: 'today',
time: [null, null],
},
});
const pickerTimeChange = () => {
data.value.time.type = undefined;
};
const getNetworkEcharts = async (val) => {
loading.value = true;
const resp = await dashboard(networkParams(val));
if (resp.success) {
const _networkOptions = {};
const _networkXAxis = new Set();
if (resp.result.length) {
resp.result.forEach((item) => {
const value = item.data.value;
const _data = [];
const nodeID = item.data.clusterNodeId;
value.forEach((item) => {
_data.push(item.value);
_networkXAxis.add(item.timeString);
});
_networkOptions[nodeID] = {
_data: _networkOptions[nodeID]
? _networkOptions[nodeID]._data.concat(_data)
: _data,
};
});
handleNetworkOptions(_networkOptions, [..._networkXAxis.keys()]);
} else {
handleNetworkOptions([], []);
}
}
setTimeout(() => {
loading.value = false;
}, 300);
};
const networkValueRender = (obj) => {
const { value } = obj;
let _data = '';
if (value >= 1024 && value < 1024 * 1024) {
_data = `${Number((value / 1024).toFixed(2))}KB`;
} else if (value >= 1024 * 1024) {
_data = `${Number((value / 1024 / 1024).toFixed(2))}M`;
} else {
_data = `${value}B`;
}
return `${obj?.axisValueLabel}<br />${obj?.marker}${obj?.seriesName}: ${_data}`;
};
const setOptions = (data, key) => ({
data: data[key]._data, // .map((item) => Number((item / 1024 / 1024).toFixed(2))),
name: key,
type: 'line',
smooth: true,
areaStyle,
});
const handleNetworkOptions = (optionsData, xAxis) => {
const chart = chartRef.value;
if (chart) {
const myChart = echarts.init(chart);
const dataKeys = Object.keys(optionsData);
const options = {
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxis,
},
yAxis: {
type: 'value',
},
grid: {
left: '80px',
right: '50px',
},
tooltip: {
trigger: 'axis',
formatter: (_value) => networkValueRender(_value[0]),
},
color: ['#979AFF'],
series: dataKeys.length
? dataKeys.map((key) => setOptions(optionsData, key))
: typeDataLine,
};
myChart.setOption(options);
window.addEventListener('resize', function () {
myChart.resize();
});
}
};
watch(
() => data.value.time.type,
(value) => {
const endTime = moment(new Date());
const startTime = getTimeByType(value);
data.value.time.time = [startTime, endTime];
},
{ immediate: true, deep: true },
);
watch(
() => data.value,
(value) => {
const {
time: { time },
} = value;
if (time && Array.isArray(time) && time.length === 2 && time[0]) {
getNetworkEcharts(value);
}
},
{ immediate: true, deep: true },
);
</script>
<style lang="less" scoped>
.dash-board {
display: flex;
flex-direction: column;
height: 100%;
padding: 24px;
background-color: #fff;
box-shadow: 0px 2.73036px 5.46071px rgba(31, 89, 245, 0.2);
border-radius: 2px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
.left h3 {
width: 200px;
margin-top: 8px;
}
}
.left,
.right {
display: flex;
align-items: center;
}
</style>

View File

@ -0,0 +1,141 @@
<template>
<div>
<a-select
style="width: 300px; margin-bottom: 20px"
@change="serverIdChange"
:value="serverId"
:options="serverNodeOptions"
v-if="serverNodeOptions.length > 1"
></a-select>
<div class="dash-board">
<div class="dash-board-item">
<TopEchartsItemNode title="CPU使用率" :value="topValues.cpu" />
</div>
<div class="dash-board-item">
<TopEchartsItemNode
title="JVM内存"
:max="topValues.jvmTotal"
:bottom="`总JVM内存 ${topValues.jvmTotal}G`"
formatter="G"
:value="topValues.jvm"
/>
</div>
<div class="dash-board-item">
<TopEchartsItemNode
title="磁盘占用"
:max="topValues.usageTotal"
:bottom="`总磁盘大小 ${topValues.usageTotal}G`"
formatter="G"
:value="topValues.usage"
/>
</div>
<div class="dash-board-item">
<TopEchartsItemNode
title="系统内存"
:max="topValues.systemUsageTotal"
:bottom="`系统内存 ${topValues.systemUsageTotal}G`"
formatter="G"
:value="topValues.systemUsage"
/>
</div>
</div>
</div>
</template>
<script lang="ts" setup name="TopCard">
import { serverNode } from '@/api/link/dashboard';
import TopEchartsItemNode from './TopEchartsItemNode.vue';
import { getWebSocket } from '@/utils/websocket';
import { map } from 'rxjs/operators';
const serverId = ref();
const serverNodeOptions = ref([]);
const topValues = ref({
cpu: 0,
jvm: 0,
jvmTotal: 0,
usage: 0,
usageTotal: 0,
systemUsage: 0,
systemUsageTotal: 0,
});
const serverIdChange = (val: string) => {
serverId.value = val;
};
const getData = () => {
const id = 'operations-statistics-system-info-realTime';
const topic = '/dashboard/systemMonitor/stats/info/realTime';
getWebSocket(id, topic, {
type: 'all',
serverNodeId: serverId.value,
interval: '1s',
agg: 'avg',
})
.pipe(map((res) => res.payload))
.subscribe((payload) => {
const {
value: { cpu, memory, disk },
} = payload;
topValues.value = {
cpu: cpu.systemUsage,
jvm: Number(
(
(memory.jvmHeapUsage / 100) *
(memory.jvmHeapTotal / 1024)
).toFixed(1),
),
jvmTotal: Math.ceil(memory.jvmHeapTotal / 1024),
usage: Number(
((disk.total / 1024) * (disk.usage / 100)).toFixed(1),
),
usageTotal: Math.ceil(disk.total / 1024),
systemUsage: Number(
(
(memory.systemTotal / 1024) *
(memory.systemUsage / 100)
).toFixed(1),
),
systemUsageTotal: Math.ceil(memory.systemTotal / 1024),
};
});
};
onMounted(() => {
serverNode().then((resp) => {
if (resp.success) {
serverNodeOptions.value = resp.result.map((item) => ({
label: item.name,
value: item.id,
}));
if (serverNodeOptions.value.length) {
serverId.value = serverNodeOptions.value[0]?.value;
}
}
});
});
watch(
() => serverId.value,
(val) => {
val && getData();
},
);
</script>
<style lang="less" scoped>
.dash-board {
display: flex;
flex-wrap: wrap;
height: 100%;
background-color: #fff;
box-shadow: 0px 2.73036px 5.46071px rgba(31, 89, 245, 0.2);
border-radius: 2px;
justify-content: space-between;
.dash-board-item {
flex: 1;
margin: 24px 12px;
min-width: 250px;
}
}
</style>

View File

@ -0,0 +1,174 @@
<template>
<div class="echarts-item">
<div class="echarts-item-left">
<div class="echarts-item-title">{{ title }}</div>
<div class="echarts-item-value">
{{ value || 0 }} {{ formatter || '%' }}
</div>
<div v-if="!!bottom" class="echarts-item-bottom">{{ bottom }}</div>
</div>
<div class="echarts-item-right">
<div ref="chartRef" style="width: 100%; height: 100px"></div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts';
import { topOptionsSeries } from './tool';
export default {
name: 'TopEchartsItemNode',
props: {
title: {
type: String,
default: '',
},
value: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 0,
},
bottom: {
type: String,
default: '',
},
formatter: {
type: String,
default: '%',
},
},
data() {
return {
options: {},
};
},
methods: {
createChart(val) {
const chart = this.$refs.chartRef;
if (chart && Object.keys(val).length > 0) {
const myChart = echarts.init(chart);
myChart.setOption(val);
window.addEventListener('resize', function () {
myChart.resize();
});
}
},
getOptions(max, formatter, val) {
let formatterCount = 0;
this.options = {
series: [
{
...topOptionsSeries,
max: max || 100,
axisLabel: {
distance: -22,
color: 'auto',
fontSize: 12,
width: 30,
padding: [6, 10, 0, 10],
formatter: (value) => {
formatterCount += 1;
if ([1, 3, 6, 9, 11].includes(formatterCount)) {
return value + (formatter || '%');
}
return '';
},
},
data: [{ value: val || 0 }],
},
],
};
},
},
watch: {
options: {
handler(val) {
this.createChart(val);
},
immediate: true,
deep: true,
},
max: {
handler(val) {
this.getOptions(val, this.formatter, this.value);
},
immediate: true,
deep: true,
},
value: {
handler(val) {
this.getOptions(this.max, this.formatter, val);
},
immediate: true,
deep: true,
},
formatter: {
handler(val) {
this.getOptions(this.max, val, this.value);
},
immediate: true,
deep: true,
},
},
};
</script>
<style lang="less" scoped>
.echarts-item {
display: flex;
height: 150px;
padding: 16px;
background-color: #fff;
box-shadow: 0px 2.73036px 5.46071px rgba(31, 89, 245, 0.2);
.echarts-item-left {
display: flex;
flex-direction: column;
width: 45%;
}
.echarts-item-right {
width: 55%;
}
.echarts-item-title {
margin-bottom: 8px;
color: rgba(#000, 0.6);
font-size: 16px;
}
.echarts-item-value {
font-weight: bold;
font-size: 36px;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-align: left;
text-overflow: ellipsis;
}
.echarts-item-bottom {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: center;
height: 0;
padding-left: 12px;
&::before {
position: absolute;
top: 50%;
left: 0;
width: 4px;
height: 12px;
background-color: #ff595e;
transform: translateY(-50%);
content: ' ';
}
}
}
</style>

View File

@ -0,0 +1,220 @@
import moment from 'moment';
import * as echarts from 'echarts';
export const getInterval = (type) => {
switch (type) {
case 'year':
return '30d';
case 'month':
case 'week':
return '1d';
case 'hour':
return '1m';
default:
return '1h';
}
};
export const getTimeFormat = (type) => {
switch (type) {
case 'year':
return 'YYYY-MM-DD';
case 'month':
case 'week':
return 'MM-DD';
case 'hour':
return 'HH:mm';
default:
return 'HH';
}
};
export const getTimeByType = (type) => {
switch (type) {
case 'hour':
return moment().subtract(1, 'hours');
case 'week':
return moment().subtract(6, 'days');
case 'month':
return moment().subtract(29, 'days');
case 'year':
return moment().subtract(365, 'days');
default:
return moment().startOf('day');
}
};
export const arrayReverse = (data) => {
const newArray = [];
for (let i = data.length - 1; i >= 0; i--) {
newArray.push(data[i]);
}
return newArray;
};
export const networkParams = (val) => [
{
dashboard: 'systemMonitor',
object: 'network',
measurement: 'traffic',
dimension: 'agg',
group: 'network',
params: {
type: val.type,
interval: getInterval(val.time.type),
from: moment(val.time.time[0]).valueOf(),
to: moment(val.time.time[1]).valueOf(),
},
},
];
export const defulteParamsData = (group, val) => [
{
dashboard: 'systemMonitor',
object: 'stats',
measurement: 'info',
dimension: 'history',
group,
params: {
from: moment(val.time[0]).valueOf(),
to: moment(val.time[1]).valueOf(),
},
},
];
export const areaStyle = {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 1,
color: 'rgba(151, 154, 255, 0)',
},
{
offset: 0,
color: 'rgba(151, 154, 255, .24)',
},
]),
};
export const areaStyleCpu = {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 1,
color: 'rgba(44, 182, 224, 0)',
},
{
offset: 0,
color: 'rgba(44, 182, 224, .24)',
},
]),
};
export const areaStyleJvm = {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 1,
color: 'rgba(96, 223, 199, 0)',
},
{
offset: 0,
color: 'rgba(96, 223, 199, .24)',
},
]),
};
export const typeDataLine = [
{
data: [],
type: 'line',
},
];
export const topOptionsSeries = {
type: 'gauge',
min: 0,
startAngle: 200,
endAngle: -20,
center: ['50%', '67%'],
title: {
show: false,
},
axisTick: {
distance: -20,
lineStyle: {
width: 1,
color: 'rgba(0,0,0,0.15)',
},
},
splitLine: {
distance: -22,
length: 9,
lineStyle: {
width: 1,
color: '#000',
},
},
pointer: {
length: '80%',
width: 4,
itemStyle: {
color: 'auto',
},
},
anchor: {
show: true,
showAbove: true,
size: 20,
itemStyle: {
borderWidth: 3,
borderColor: '#fff',
shadowBlur: 20,
shadowColor: 'rgba(0, 0, 0, .25)',
color: 'auto',
},
},
axisLine: {
lineStyle: {
width: 10,
color: [
[0.25, 'rgba(36, 178, 118, 1)'],
[
0.4,
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(66, 147, 255, 1)',
},
{
offset: 1,
color: 'rgba(36, 178, 118, 1)',
},
]),
],
[
0.5,
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(250, 178, 71, 1)',
},
{
offset: 1,
color: 'rgba(66, 147, 255, 1)',
},
]),
],
[
1,
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(250, 178, 71, 1)',
},
{
offset: 1,
color: 'rgba(247, 111, 93, 1)',
},
]),
],
],
},
},
detail: {
show: false,
},
};

View File

@ -0,0 +1,21 @@
<template>
<page-container>
<div>
<a-row :gutter="[24, 24]">
<a-col :span="24"><TopCard /> </a-col>
<a-col :span="24"><Network /></a-col>
<a-col :span="12"><Cpu /></a-col>
<a-col :span="12"><Jvm /></a-col>
</a-row>
</div>
</page-container>
</template>
<script lang="ts" setup name="DashBoardPage">
import TopCard from './components/TopCard.vue';
import Network from './components/Network.vue';
import Cpu from './components/Cpu.vue';
import Jvm from './components/Jvm.vue';
</script>
<style lang="less" scoped></style>

View File

@ -159,7 +159,7 @@ import _ from 'lodash';
const tableRef = ref<Record<string, any>>({});
const params = ref<Record<string, any>>({});
const route = useRoute();
const visible = ref(false);
const current = ref({});
@ -276,6 +276,14 @@ const saveChange = (value: object) => {
}
};
watch(
() => route.query?.save,
(value) => {
value === 'true' && handlAdd();
},
{ deep: true, immediate: true },
);
/**
* 搜索
* @param params

View File

@ -1032,11 +1032,10 @@ import { Store } from 'jetlinks-store';
import MonacoEditor from '@/components/MonacoEditor/index.vue';
const route = useRoute();
const view = route.query.view as string;
const NetworkType = route.query.type as string;
const view = NetworkType ? 'false' : (route.query.view as string);
const id = route.params.id as string;
const activeKey = ref(['1']);
const loading = ref(false);
const formRef1 = ref<FormInstance>();
const formRef2 = ref<FormInstance>();
@ -1250,7 +1249,6 @@ watch(
}
},
{ deep: true },
// { deep: true, immediate: true },
);
watch(
@ -1263,7 +1261,6 @@ watch(
updateClustersListIndex();
},
{ deep: true },
// { deep: true, immediate: true },
);
watch(
() => dynamicValidateForm.cluster?.length,
@ -1272,6 +1269,17 @@ watch(
},
{ deep: true, immediate: true },
);
watch(
() => NetworkType,
(value) => {
if (value) {
const { cluster } = dynamicValidateForm;
formData.value.type = value;
cluster[0].configuration.host = '0.0.0.0';
}
},
{ deep: true, immediate: true },
);
</script>
<style lang="less" scoped>