feat: 仪表盘
This commit is contained in:
@@ -15,486 +15,12 @@ import { hasPermissions } from '@/plugins/auth-user';
|
|||||||
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||||||
import useI18n from '@/hooks/useI18n';
|
import useI18n from '@/hooks/useI18n';
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
/**请求点击 */
|
|
||||||
let isClick = ref<boolean>(false);
|
|
||||||
|
|
||||||
/**缓存内容信息 */
|
onMounted(() => {});
|
||||||
let cacheKeyInfo = reactive({
|
|
||||||
loading: true,
|
|
||||||
data: {
|
|
||||||
cacheKey: '',
|
|
||||||
cacheName: '',
|
|
||||||
cacheValue: '',
|
|
||||||
remark: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询缓存内容详细
|
|
||||||
* @param cacheKey
|
|
||||||
*/
|
|
||||||
function fnCacheKeyInfo(cacheKey: string) {
|
|
||||||
if (!hasPermissions(['monitor:cache:query'])) return;
|
|
||||||
if (isClick.value) return;
|
|
||||||
isClick.value = true;
|
|
||||||
cacheKeyInfo.loading = true;
|
|
||||||
getCacheValue(cacheKeyTable.cacheName, cacheKey).then(res => {
|
|
||||||
isClick.value = false;
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS) {
|
|
||||||
cacheKeyInfo.data = Object.assign(cacheKeyInfo.data, res.data);
|
|
||||||
cacheKeyInfo.loading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**键名列表表格字段列 */
|
|
||||||
let cacheKeyTableColumns: ColumnsType = [
|
|
||||||
{
|
|
||||||
title: t('common.rowId'),
|
|
||||||
dataIndex: 'num',
|
|
||||||
width: '50px',
|
|
||||||
align: 'center',
|
|
||||||
customRender(opt) {
|
|
||||||
return opt.index + 1;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('views.monitor.cache.cacheKey'),
|
|
||||||
dataIndex: 'cacheKey',
|
|
||||||
align: 'left',
|
|
||||||
ellipsis: true,
|
|
||||||
// 渲染值处理
|
|
||||||
customRender(opt) {
|
|
||||||
return opt.text;
|
|
||||||
},
|
|
||||||
// 自定义过滤控件
|
|
||||||
customFilterDropdown: true,
|
|
||||||
onFilter: (value, record) => {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const nameLower = record.cacheKey.toLowerCase();
|
|
||||||
return nameLower.includes(value.toLowerCase());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('common.operate'),
|
|
||||||
key: 'option',
|
|
||||||
align: 'center',
|
|
||||||
width: '50px',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**键名列表表格数据 */
|
|
||||||
let cacheKeyTable = reactive({
|
|
||||||
loading: false,
|
|
||||||
data: [],
|
|
||||||
/**当前键名列表的缓存名称 */
|
|
||||||
cacheName: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理指定缓存键名
|
|
||||||
* @param cacheKey 键名列表中的缓存键名
|
|
||||||
*/
|
|
||||||
function fnCacheKeyClear(cacheKey: string) {
|
|
||||||
if (isClick.value) return;
|
|
||||||
isClick.value = true;
|
|
||||||
const hide = message.loading(t('common.loading'), 0);
|
|
||||||
clearCacheKey(cacheKeyTable.cacheName, cacheKey).then(res => {
|
|
||||||
hide();
|
|
||||||
isClick.value = false;
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS) {
|
|
||||||
message.success({
|
|
||||||
content: t('views.monitor.cache.clearCacheKeyOk', { txt: cacheKey }),
|
|
||||||
duration: 3,
|
|
||||||
});
|
|
||||||
// 缓存内容显示且是删除的缓存键名,需要进行加载显示
|
|
||||||
if (!cacheKeyInfo.loading && cacheKeyInfo.data.cacheKey === cacheKey) {
|
|
||||||
cacheKeyInfo.loading = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message.error({
|
|
||||||
content: res.msg,
|
|
||||||
duration: 3,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fnCacheKeyList();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 查询缓存键名列表 */
|
|
||||||
function fnCacheKeyList(cacheName: string = 'load') {
|
|
||||||
if (cacheName === 'load') {
|
|
||||||
cacheName = cacheKeyTable.cacheName;
|
|
||||||
}
|
|
||||||
if (!cacheName) {
|
|
||||||
message.warning(t('views.monitor.cache.cacheKeyListErr'), 3);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isClick.value) return;
|
|
||||||
isClick.value = true;
|
|
||||||
cacheKeyTable.loading = true;
|
|
||||||
listCacheKey(cacheName).then(res => {
|
|
||||||
isClick.value = false;
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS && res.data) {
|
|
||||||
cacheKeyTable.cacheName = cacheName;
|
|
||||||
cacheKeyTable.data = res.data;
|
|
||||||
cacheKeyTable.loading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**缓存列表表格数据 */
|
|
||||||
let cacheNameTable = reactive({
|
|
||||||
loading: true,
|
|
||||||
data: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
/**缓存列表表格字段列 */
|
|
||||||
let cacheNameTableColumns: ColumnsType = [
|
|
||||||
{
|
|
||||||
title: t('common.rowId'),
|
|
||||||
dataIndex: 'num',
|
|
||||||
width: '50px',
|
|
||||||
align: 'center',
|
|
||||||
customRender(opt) {
|
|
||||||
return opt.index + 1;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('views.monitor.cache.cacheName'),
|
|
||||||
dataIndex: 'cacheName',
|
|
||||||
align: 'left',
|
|
||||||
ellipsis: true,
|
|
||||||
// 渲染值处理
|
|
||||||
customRender(opt) {
|
|
||||||
return opt.text;
|
|
||||||
},
|
|
||||||
// 自定义过滤控件
|
|
||||||
customFilterDropdown: true,
|
|
||||||
onFilter: (value, record) => {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const nameLower = record.cacheName.toLowerCase();
|
|
||||||
return nameLower.includes(value.toLowerCase());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('views.monitor.cache.remark'),
|
|
||||||
dataIndex: 'remark',
|
|
||||||
align: 'left',
|
|
||||||
ellipsis: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('common.operate'),
|
|
||||||
key: 'option',
|
|
||||||
align: 'center',
|
|
||||||
width: '50px',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**安全清理缓存 */
|
|
||||||
function fnClearCacheSafe() {
|
|
||||||
if (isClick.value) return;
|
|
||||||
isClick.value = true;
|
|
||||||
const hide = message.loading(t('common.loading'), 0);
|
|
||||||
clearCacheSafe().then(res => {
|
|
||||||
hide();
|
|
||||||
isClick.value = false;
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS) {
|
|
||||||
message.success({
|
|
||||||
content: t('views.monitor.cache.clearCacheSafeOk'),
|
|
||||||
duration: 3,
|
|
||||||
});
|
|
||||||
cacheKeyTable.loading = true;
|
|
||||||
cacheKeyInfo.loading = true;
|
|
||||||
} else {
|
|
||||||
message.error({
|
|
||||||
content: res.msg,
|
|
||||||
duration: 3,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理指定缓存名称
|
|
||||||
* @param cacheName 缓存名称
|
|
||||||
*/
|
|
||||||
function fnCacheNameClear(cacheName: string) {
|
|
||||||
if (isClick.value) return;
|
|
||||||
isClick.value = true;
|
|
||||||
const hide = message.loading(t('common.loading'), 0);
|
|
||||||
clearCacheName(cacheName).then(res => {
|
|
||||||
hide();
|
|
||||||
isClick.value = false;
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS) {
|
|
||||||
message.success({
|
|
||||||
content: t('views.monitor.cache.clearCacheNameOk', { txt: cacheName }),
|
|
||||||
duration: 3,
|
|
||||||
});
|
|
||||||
// 缓存内容显示且是删除的缓存名称,需要进行加载显示
|
|
||||||
if (!cacheKeyInfo.loading && cacheKeyInfo.data.cacheName === cacheName) {
|
|
||||||
cacheKeyInfo.loading = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message.error({
|
|
||||||
content: res.msg,
|
|
||||||
duration: 3,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fnCacheKeyList(cacheName);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**查询缓存名称列表 */
|
|
||||||
function fnCacheNameList() {
|
|
||||||
if (isClick.value) return;
|
|
||||||
isClick.value = true;
|
|
||||||
cacheNameTable.loading = true;
|
|
||||||
listCacheName().then(res => {
|
|
||||||
isClick.value = false;
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS && res.data) {
|
|
||||||
cacheNameTable.data = res.data;
|
|
||||||
cacheNameTable.loading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fnCacheNameList();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PageContainer>
|
<div></div>
|
||||||
<a-row :gutter="20">
|
|
||||||
<a-col :lg="8" :md="8" :xs="24">
|
|
||||||
<a-card
|
|
||||||
:title="t('views.monitor.cache.cacheList')"
|
|
||||||
:bordered="false"
|
|
||||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
|
||||||
>
|
|
||||||
<template #extra>
|
|
||||||
<a-space :size="8" align="center">
|
|
||||||
<a-tooltip>
|
|
||||||
<template #title>
|
|
||||||
{{ t('common.reloadText') }}
|
|
||||||
</template>
|
|
||||||
<a-button type="text" @click.prevent="fnCacheNameList">
|
|
||||||
<template #icon><ReloadOutlined /></template>
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip>
|
|
||||||
<template #title>
|
|
||||||
{{ t('views.monitor.cache.clearCacheSafe') }}
|
|
||||||
</template>
|
|
||||||
<a-popconfirm
|
|
||||||
placement="bottomRight"
|
|
||||||
:title="t('views.monitor.cache.clearCacheSafeTip')"
|
|
||||||
:ok-text="t('common.ok')"
|
|
||||||
:cancel-text="t('common.cancel')"
|
|
||||||
@confirm="fnClearCacheSafe()"
|
|
||||||
>
|
|
||||||
<a-button type="text" v-perms:has="['monitor:cache:remove']">
|
|
||||||
<template #icon><ClearOutlined /></template>
|
|
||||||
</a-button>
|
|
||||||
</a-popconfirm>
|
|
||||||
</a-tooltip>
|
|
||||||
</a-space>
|
|
||||||
</template>
|
|
||||||
<a-table
|
|
||||||
row-key="cacheName"
|
|
||||||
size="small"
|
|
||||||
:columns="cacheNameTableColumns"
|
|
||||||
:data-source="cacheNameTable.data"
|
|
||||||
:loading="cacheNameTable.loading"
|
|
||||||
:scroll="{ y: 'calc(100vh - 350px)' }"
|
|
||||||
:pagination="false"
|
|
||||||
:row-selection="{
|
|
||||||
type: 'radio',
|
|
||||||
onChange: (selectedRowKeys: (string|number)[]) => fnCacheKeyList(selectedRowKeys[0] as string),
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
#customFilterDropdown="{
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
column,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div style="padding: 8px">
|
|
||||||
<a-input
|
|
||||||
:placeholder="
|
|
||||||
t('views.monitor.cache.filterPlace', { txt: column.title })
|
|
||||||
"
|
|
||||||
:value="selectedKeys[0]"
|
|
||||||
style="width: 188px; margin-bottom: 8px; display: block"
|
|
||||||
@change="
|
|
||||||
(e:any)=> setSelectedKeys(e.target.value ? [e.target.value] : [])
|
|
||||||
"
|
|
||||||
@pressEnter="confirm()"
|
|
||||||
/>
|
|
||||||
<a-button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
style="width: 90px; margin-right: 8px"
|
|
||||||
@click="confirm()"
|
|
||||||
>
|
|
||||||
{{ t('views.monitor.cache.filter') }}
|
|
||||||
</a-button>
|
|
||||||
<a-button
|
|
||||||
size="small"
|
|
||||||
style="width: 90px"
|
|
||||||
@click="clearFilters({ confirm: true })"
|
|
||||||
>
|
|
||||||
{{ t('common.reset') }}
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #bodyCell="{ column, record }">
|
|
||||||
<template v-if="column.key === 'option'">
|
|
||||||
<a-popconfirm
|
|
||||||
placement="topRight"
|
|
||||||
:title="t('views.monitor.cache.clearCacheNameTip')"
|
|
||||||
,
|
|
||||||
:ok-text="t('common.ok')"
|
|
||||||
:cancel-text="t('common.cancel')"
|
|
||||||
@confirm="fnCacheNameClear(record.cacheName)"
|
|
||||||
>
|
|
||||||
<a-button type="text" v-perms:has="['monitor:cache:remove']">
|
|
||||||
<template #icon><ClearOutlined /></template>
|
|
||||||
</a-button>
|
|
||||||
</a-popconfirm>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</a-table>
|
|
||||||
</a-card>
|
|
||||||
</a-col>
|
|
||||||
|
|
||||||
<a-col :lg="8" :md="8" :xs="24">
|
|
||||||
<a-card
|
|
||||||
:title="t('views.monitor.cache.keyNameList')"
|
|
||||||
:bordered="false"
|
|
||||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
|
||||||
>
|
|
||||||
<template #extra>
|
|
||||||
<a-tooltip>
|
|
||||||
<template #title>
|
|
||||||
{{ t('common.reloadText') }}
|
|
||||||
</template>
|
|
||||||
<a-button type="text" @click.prevent="fnCacheKeyList()">
|
|
||||||
<template #icon><ReloadOutlined /></template>
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
</template>
|
|
||||||
<a-table
|
|
||||||
row-key="cacheKey"
|
|
||||||
size="small"
|
|
||||||
:columns="cacheKeyTableColumns"
|
|
||||||
:data-source="cacheKeyTable.data"
|
|
||||||
:loading="cacheKeyTable.loading"
|
|
||||||
:scroll="{ y: 'calc(100vh - 350px)' }"
|
|
||||||
:pagination="false"
|
|
||||||
:row-selection="{
|
|
||||||
type: 'radio',
|
|
||||||
onChange: (selectedRowKeys: (string|number)[]) => fnCacheKeyInfo(selectedRowKeys[0] as string),
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
#customFilterDropdown="{
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
column,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div style="padding: 8px">
|
|
||||||
<a-input
|
|
||||||
:placeholder="
|
|
||||||
t('views.monitor.cache.filterPlace', { txt: column.title })
|
|
||||||
"
|
|
||||||
:value="selectedKeys[0]"
|
|
||||||
style="width: 188px; margin-bottom: 8px; display: block"
|
|
||||||
@change="
|
|
||||||
(e:any) => setSelectedKeys(e.target.value ? [e.target.value] : [])
|
|
||||||
"
|
|
||||||
@pressEnter="confirm()"
|
|
||||||
/>
|
|
||||||
<a-button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
style="width: 90px; margin-right: 8px"
|
|
||||||
@click="confirm()"
|
|
||||||
>
|
|
||||||
{{ t('views.monitor.cache.filter') }}
|
|
||||||
</a-button>
|
|
||||||
<a-button
|
|
||||||
size="small"
|
|
||||||
style="width: 90px"
|
|
||||||
@click="clearFilters({ confirm: true })"
|
|
||||||
>
|
|
||||||
{{ t('common.reset') }}
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #bodyCell="{ column, record }">
|
|
||||||
<template v-if="column.key === 'option'">
|
|
||||||
<a-popconfirm
|
|
||||||
placement="topRight"
|
|
||||||
:title="t('views.monitor.cache.clearCacheKeyTip')"
|
|
||||||
,
|
|
||||||
:ok-text="t('common.ok')"
|
|
||||||
:cancel-text="t('common.cancel')"
|
|
||||||
@confirm="fnCacheKeyClear(record.cacheKey)"
|
|
||||||
>
|
|
||||||
<a-button type="text" v-perms:has="['monitor:cache:remove']">
|
|
||||||
<template #icon><DeleteOutlined /></template>
|
|
||||||
</a-button>
|
|
||||||
</a-popconfirm>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</a-table>
|
|
||||||
</a-card>
|
|
||||||
</a-col>
|
|
||||||
|
|
||||||
<a-col :lg="8" :md="8" :xs="24" v-perms:has="['monitor:cache:query']">
|
|
||||||
<a-card
|
|
||||||
:title="t('views.monitor.cache.keyContent')"
|
|
||||||
:bordered="false"
|
|
||||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
|
||||||
:loading="cacheKeyInfo.loading"
|
|
||||||
>
|
|
||||||
<a-descriptions
|
|
||||||
size="small"
|
|
||||||
layout="vertical"
|
|
||||||
:bordered="true"
|
|
||||||
:column="1"
|
|
||||||
>
|
|
||||||
<a-descriptions-item :label="t('views.monitor.cache.cacheName')">
|
|
||||||
{{ cacheKeyInfo.data.cacheName }}
|
|
||||||
</a-descriptions-item>
|
|
||||||
<a-descriptions-item :label="t('views.monitor.cache.cacheKey')">
|
|
||||||
{{ cacheKeyInfo.data.cacheKey }}
|
|
||||||
</a-descriptions-item>
|
|
||||||
<a-descriptions-item :label="t('views.monitor.cache.cacheValue')">
|
|
||||||
<a-typography-paragraph>
|
|
||||||
<a-textarea
|
|
||||||
:value="cacheKeyInfo.data.cacheValue"
|
|
||||||
:auto-size="{ minRows: 4, maxRows: 20 }"
|
|
||||||
:maxlength="4000"
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
</a-typography-paragraph>
|
|
||||||
</a-descriptions-item>
|
|
||||||
</a-descriptions>
|
|
||||||
</a-card>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</PageContainer>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<style lang="less" scoped></style>
|
||||||
|
|||||||
419
src/views/dashboard/overview/components/Topology/index.vue
Normal file
419
src/views/dashboard/overview/components/Topology/index.vue
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, onMounted, ref, onBeforeUnmount } from 'vue';
|
||||||
|
import { PageContainer } from 'antdv-pro-layout';
|
||||||
|
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||||||
|
import { listNe, stateNe } from '@/api/ne/ne';
|
||||||
|
import { message } from 'ant-design-vue/lib';
|
||||||
|
import { getGraphData } from '@/api/monitor/topology';
|
||||||
|
import { parseDateToStr } from '@/utils/date-utils';
|
||||||
|
import { Graph, GraphData, Menu, Tooltip } from '@antv/g6';
|
||||||
|
import {
|
||||||
|
edgeCubicAnimateCircleMove,
|
||||||
|
edgeCubicAnimateLineDash,
|
||||||
|
edgeLineAnimateState,
|
||||||
|
} from '@/views/monitor/topologyBuild/hooks/registerEdge';
|
||||||
|
import {
|
||||||
|
nodeCircleAnimateShapeR,
|
||||||
|
nodeCircleAnimateShapeStroke,
|
||||||
|
nodeImageAnimateState,
|
||||||
|
nodeRectAnimateState,
|
||||||
|
} from '@/views/monitor/topologyBuild/hooks/registerNode';
|
||||||
|
import useI18n from '@/hooks/useI18n';
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
/**图DOM节点实例对象 */
|
||||||
|
const graphG6Dom = ref<HTMLElement | undefined>(undefined);
|
||||||
|
|
||||||
|
/**图状态 */
|
||||||
|
const graphState = reactive<Record<string, any>>({
|
||||||
|
/**当前图组名 */
|
||||||
|
group: '5GC System Architecture2',
|
||||||
|
/**图数据 */
|
||||||
|
data: {
|
||||||
|
combos: [],
|
||||||
|
edges: [],
|
||||||
|
nodes: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**非网元元素 */
|
||||||
|
const notNeNodes = ['5GC', 'DN', 'UE', 'Base'];
|
||||||
|
|
||||||
|
/**图实例对象 */
|
||||||
|
const graphG6 = ref<any>(null);
|
||||||
|
|
||||||
|
/**图节点右击菜单 */
|
||||||
|
const graphNodeMenu = new Menu({
|
||||||
|
offsetX: 6,
|
||||||
|
offseY: 10,
|
||||||
|
itemTypes: ['node'],
|
||||||
|
getContent(evt) {
|
||||||
|
if (!evt) return t('views.monitor.topologyBuild.graphNotInfo');
|
||||||
|
const { id, label, neState }: any = evt.item?.getModel();
|
||||||
|
if (notNeNodes.includes(id)) {
|
||||||
|
return `<div><span>${label || id}</span></div>`;
|
||||||
|
}
|
||||||
|
if (!neState) {
|
||||||
|
return `<div><span>${label || id}</span></div>`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 140px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h3 style="margin-bottom: 8px">
|
||||||
|
${t('views.monitor.topology.name')}:
|
||||||
|
${neState.neName ?? '--'}
|
||||||
|
</h3>
|
||||||
|
<div id="restart" style="cursor: pointer; margin-bottom: 4px">
|
||||||
|
> ${t('views.configManage.neManage.restart')}
|
||||||
|
</div>
|
||||||
|
<div id="stop" style="cursor: pointer; margin-bottom: 4px;">
|
||||||
|
> ${t('views.configManage.neManage.stop')}
|
||||||
|
</div>
|
||||||
|
<div id="log" style="cursor: pointer; margin-bottom: 4px;">
|
||||||
|
> ${t('views.monitor.topology.viewLogFile')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
handleMenuClick(target, item) {
|
||||||
|
const { neInfo }: any = item?.getModel();
|
||||||
|
const { neName, neType, neId } = neInfo;
|
||||||
|
const targetId = target.id;
|
||||||
|
console.log(targetId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**图节点展示 */
|
||||||
|
const graphNodeTooltip = new Tooltip({
|
||||||
|
offsetX: 10,
|
||||||
|
offsetY: 20,
|
||||||
|
getContent(evt) {
|
||||||
|
if (!evt) return t('views.monitor.topologyBuild.graphNotInfo');
|
||||||
|
const { id, label, neState }: any = evt.item?.getModel();
|
||||||
|
if (notNeNodes.includes(id)) {
|
||||||
|
return `<div><span>${label || id}</span></div>`;
|
||||||
|
}
|
||||||
|
if (!neState) {
|
||||||
|
return `<div><span>${label || id}</span></div>`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 200px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div><strong>${t('views.monitor.topology.state')}:</strong><span>
|
||||||
|
${
|
||||||
|
neState.online
|
||||||
|
? t('views.monitor.topology.normalcy')
|
||||||
|
: t('views.monitor.topology.exceptions')
|
||||||
|
}
|
||||||
|
</span></div>
|
||||||
|
<div><strong>${t('views.monitor.topology.refreshTime')}:</strong><span>
|
||||||
|
${neState.refreshTime ?? '--'}
|
||||||
|
</span></div>
|
||||||
|
<div>========================</div>
|
||||||
|
<div><strong>ID:</strong><span>${neState.neId}</span></div>
|
||||||
|
<div><strong>${t('views.monitor.topology.name')}:</strong><span>
|
||||||
|
${neState.neName ?? '--'}
|
||||||
|
</span></div>
|
||||||
|
<div><strong>IP:</strong><span>${neState.neIP}</span></div>
|
||||||
|
<div><strong>${t('views.monitor.topology.version')}:</strong><span>
|
||||||
|
${neState.version ?? '--'}
|
||||||
|
</span></div>
|
||||||
|
<div><strong>${t('views.monitor.topology.serialNum')}:</strong><span>
|
||||||
|
${neState.sn ?? '--'}
|
||||||
|
</span></div>
|
||||||
|
<div><strong>${t('views.monitor.topology.expiryDate')}:</strong><span>
|
||||||
|
${neState.expire ?? '--'}
|
||||||
|
</span></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
itemTypes: ['node'],
|
||||||
|
});
|
||||||
|
|
||||||
|
/**注册自定义边或节点 */
|
||||||
|
function registerEdgeNode() {
|
||||||
|
// 边
|
||||||
|
edgeCubicAnimateLineDash();
|
||||||
|
edgeCubicAnimateCircleMove();
|
||||||
|
edgeLineAnimateState();
|
||||||
|
// 节点
|
||||||
|
nodeCircleAnimateShapeR();
|
||||||
|
nodeCircleAnimateShapeStroke();
|
||||||
|
nodeRectAnimateState();
|
||||||
|
nodeImageAnimateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**图数据渲染 */
|
||||||
|
function handleRanderGraph(
|
||||||
|
container: HTMLElement | undefined,
|
||||||
|
data: GraphData
|
||||||
|
) {
|
||||||
|
if (!container) return;
|
||||||
|
const { clientHeight, clientWidth } = container;
|
||||||
|
|
||||||
|
// 注册自定义边或节点
|
||||||
|
registerEdgeNode();
|
||||||
|
|
||||||
|
const graph = new Graph({
|
||||||
|
container: container,
|
||||||
|
width: clientWidth,
|
||||||
|
height: clientHeight,
|
||||||
|
fitCenter: true,
|
||||||
|
modes: {
|
||||||
|
default: [
|
||||||
|
'drag-combo',
|
||||||
|
'drag-canvas',
|
||||||
|
'zoom-canvas',
|
||||||
|
'collapse-expand-combo',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
groupByTypes: false,
|
||||||
|
nodeStateStyles: {
|
||||||
|
selected: {
|
||||||
|
fill: 'transparent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [graphNodeMenu, graphNodeTooltip],
|
||||||
|
animate: true, // 是否使用动画过度,默认为 false
|
||||||
|
animateCfg: {
|
||||||
|
duration: 500, // Number,一次动画的时长
|
||||||
|
easing: 'linearEasing', // String,动画函数
|
||||||
|
},
|
||||||
|
});
|
||||||
|
graph.data(data);
|
||||||
|
graph.render();
|
||||||
|
|
||||||
|
graphG6.value = graph;
|
||||||
|
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取图组数据渲染到画布
|
||||||
|
* @param reload 是否重载数据
|
||||||
|
*/
|
||||||
|
function fnGraphDataLoad(reload: boolean = false) {
|
||||||
|
Promise.all([
|
||||||
|
getGraphData(graphState.group),
|
||||||
|
listNe({
|
||||||
|
bandStatus: false,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.then(resArr => {
|
||||||
|
const graphRes = resArr[0];
|
||||||
|
const neRes = resArr[1];
|
||||||
|
if (
|
||||||
|
graphRes.code === RESULT_CODE_SUCCESS &&
|
||||||
|
Array.isArray(graphRes.data.nodes) &&
|
||||||
|
graphRes.data.nodes.length > 0 &&
|
||||||
|
neRes.code === RESULT_CODE_SUCCESS &&
|
||||||
|
Array.isArray(neRes.data) &&
|
||||||
|
neRes.data.length > 0
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
graphData: graphRes.data,
|
||||||
|
neList: neRes.data,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
message.warning({
|
||||||
|
content: t('views.monitor.topology.noData'),
|
||||||
|
duration: 5,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
console.log('fnGraphDataLoad', res);
|
||||||
|
if (!res) return;
|
||||||
|
const { combos, edges, nodes } = res.graphData;
|
||||||
|
|
||||||
|
// 节点过滤
|
||||||
|
const nf: Record<string, any>[] = nodes.filter(
|
||||||
|
(node: Record<string, any>) => {
|
||||||
|
Reflect.set(node, 'neState', { online: false });
|
||||||
|
const nodeID: string = node.id;
|
||||||
|
const hasNe = res.neList.some(ne => {
|
||||||
|
Reflect.set(node, 'neInfo', ne.neType === nodeID ? ne : {});
|
||||||
|
return ne.neType === nodeID;
|
||||||
|
});
|
||||||
|
if (hasNe) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (notNeNodes.includes(nodeID)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(nf);
|
||||||
|
|
||||||
|
// 边过滤
|
||||||
|
const ef: Record<string, any>[] = edges.filter(
|
||||||
|
(edge: Record<string, any>) => {
|
||||||
|
const edgeSource: string = edge.source;
|
||||||
|
const edgeTarget: string = edge.target;
|
||||||
|
const hasNeS = nf.some(n => n.id === edgeSource);
|
||||||
|
const hasNeT = nf.some(n => n.id === edgeTarget);
|
||||||
|
// console.log(hasNeS, edgeSource, hasNeT, edgeTarget);
|
||||||
|
if (hasNeS && hasNeT) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hasNeS && notNeNodes.includes(edgeTarget)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hasNeT && notNeNodes.includes(edgeSource)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 分组过滤
|
||||||
|
combos.forEach((combo: Record<string, any>) => {
|
||||||
|
const comboChildren: Record<string, any>[] = combo.children;
|
||||||
|
combo.children = comboChildren.filter(c => nf.some(n => n.id === c.id));
|
||||||
|
return combo;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 图数据
|
||||||
|
graphState.data = { combos, edges: ef, nodes: nf };
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (graphState.data.length < 0) return;
|
||||||
|
// 重载数据
|
||||||
|
if (reload) {
|
||||||
|
graphG6.value.read(graphState.data);
|
||||||
|
} else {
|
||||||
|
handleRanderGraph(graphG6Dom.value, graphState.data);
|
||||||
|
}
|
||||||
|
fnGetState(); // 获取网元状态
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**网元状态定时器 */
|
||||||
|
const stateTimeout = ref<any>(null);
|
||||||
|
|
||||||
|
/**查询网元状态 */
|
||||||
|
async function fnGetState() {
|
||||||
|
clearTimeout(stateTimeout.value);
|
||||||
|
const { combos, edges, nodes } = graphState.data;
|
||||||
|
// console.log({ combos, edges, nodes })
|
||||||
|
|
||||||
|
// 获取节点状态
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (notNeNodes.includes(node.id)) continue;
|
||||||
|
const { neType, neId, neName } = node.neInfo;
|
||||||
|
if (!neType || !neId) continue;
|
||||||
|
const result = await stateNe(neType, neId);
|
||||||
|
if (result.code === RESULT_CODE_SUCCESS) {
|
||||||
|
// 更新网元状态
|
||||||
|
const newNeState = Object.assign(node.neState, result.data, {
|
||||||
|
refreshTime: parseDateToStr(result.data.refreshTime, 'HH:mm:ss'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 通过 ID 查询节点实例
|
||||||
|
const item = graphG6.value.findById(node.id);
|
||||||
|
if (item) {
|
||||||
|
const stateColor = newNeState.online ? '#52c41a' : '#f5222d'; // 状态颜色
|
||||||
|
// 图片类型不能填充
|
||||||
|
if (node.type.startsWith('image')) {
|
||||||
|
// 更新节点
|
||||||
|
graphG6.value.updateItem(item, {
|
||||||
|
label: neName,
|
||||||
|
});
|
||||||
|
// 设置状态
|
||||||
|
graphG6.value.setItemState(item, 'top-right-dot', stateColor);
|
||||||
|
} else {
|
||||||
|
// 更新节点
|
||||||
|
graphG6.value.updateItem(item, {
|
||||||
|
label: neName,
|
||||||
|
// neState: newNeState,
|
||||||
|
style: {
|
||||||
|
fill: stateColor, // 填充色
|
||||||
|
stroke: stateColor, // 填充色
|
||||||
|
},
|
||||||
|
// labelCfg: {
|
||||||
|
// style: {
|
||||||
|
// fill: '#ffffff', // 标签文本色
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
// 设置状态
|
||||||
|
graphG6.value.setItemState(item, 'stroke', newNeState.online);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置边状态
|
||||||
|
for (const edge of edges) {
|
||||||
|
const edgeSource: string = edge.source;
|
||||||
|
const edgeTarget: string = edge.target;
|
||||||
|
const neS = nodes.find((n: any) => n.id === edgeSource);
|
||||||
|
const neT = nodes.find((n: any) => n.id === edgeTarget);
|
||||||
|
// console.log(neS, edgeSource, neT, edgeTarget);
|
||||||
|
// 通过 ID 查询节点实例
|
||||||
|
const item = graphG6.value.findById(edge.id);
|
||||||
|
if (neS && neT && item) {
|
||||||
|
// console.log(
|
||||||
|
// `${edgeSource} - ${edgeTarget}`,
|
||||||
|
// neS.neState.online && neT.neState.online
|
||||||
|
// );
|
||||||
|
const stateColor =
|
||||||
|
neS.neState.online && neT.neState.online ? '#000000' : '#ff4d4f'; // 状态颜色
|
||||||
|
// 更新边
|
||||||
|
// graphG6.value.updateItem(item, {
|
||||||
|
// label: `${edgeSource} - ${edgeTarget}`,
|
||||||
|
// style: {
|
||||||
|
// stroke: stateColor, // 填充色
|
||||||
|
// },
|
||||||
|
// labelCfg: {
|
||||||
|
// style: {
|
||||||
|
// fill: '#ffffff', // 标签文本色
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// 设置状态
|
||||||
|
// graphG6.value.setItemState(
|
||||||
|
// item,
|
||||||
|
// 'line-dash',
|
||||||
|
// neS.neState.online && neT.neState.online
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
if (neS && notNeNodes.includes(edgeTarget)) {
|
||||||
|
}
|
||||||
|
if (neT && notNeNodes.includes(edgeSource)) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stateTimeout.value = setTimeout(() => fnGetState(), 30_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fnGraphDataLoad(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearTimeout(stateTimeout.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="graphG6Dom" class="chart"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
625
src/views/dashboard/overview/css/index.css
Normal file
625
src/views/dashboard/overview/css/index.css
Normal file
@@ -0,0 +1,625 @@
|
|||||||
|
.viewport {
|
||||||
|
/* 限定大小 */
|
||||||
|
min-width: 1024px;
|
||||||
|
max-width: 1920px;
|
||||||
|
min-height: 780px;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding: 5rem 0.833rem 0;
|
||||||
|
line-height: 1.15;
|
||||||
|
background-color: #101129;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 总览标题 */
|
||||||
|
.brand {
|
||||||
|
background-image: url(../images/brand.png);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.833rem;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.brand .brand-title {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
letter-spacing: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.brand .brand-desc {
|
||||||
|
color: #d9d9d9;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拓扑图 */
|
||||||
|
.topology {
|
||||||
|
height: 24.1rem;
|
||||||
|
margin-bottom: 0.833rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.topology .topology-title {
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0.667rem 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.833rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.topology .topology-chart {
|
||||||
|
flex: 1;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
flex: 3;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
/* 边框 */
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 2px solid red;
|
||||||
|
border-image: url(../images/border.png) 51 38 21 132;
|
||||||
|
border-width: 2.125rem 1.583rem 0.875rem 5.5rem;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 0.833rem;
|
||||||
|
}
|
||||||
|
.panel .inner {
|
||||||
|
/* 装内容 */
|
||||||
|
/* height: 60px; */
|
||||||
|
position: absolute;
|
||||||
|
top: -2.125rem;
|
||||||
|
right: -1.583rem;
|
||||||
|
bottom: -0.875rem;
|
||||||
|
left: -5.5rem;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
.panel h3 {
|
||||||
|
font-size: 0.833rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
/* 概览区域 */
|
||||||
|
.overview {
|
||||||
|
height: 4.583rem;
|
||||||
|
}
|
||||||
|
.overview .inner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.overview h4 {
|
||||||
|
font-size: 1.167rem;
|
||||||
|
padding-left: 0.2rem;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 0.333rem;
|
||||||
|
}
|
||||||
|
.overview span {
|
||||||
|
font-size: 0.667rem;
|
||||||
|
color: #4c9bfd;
|
||||||
|
}
|
||||||
|
/* 监控 */
|
||||||
|
.monitor {
|
||||||
|
height: 20rem;
|
||||||
|
}
|
||||||
|
.monitor .inner {
|
||||||
|
padding: 1rem 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.monitor .tabs {
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.monitor .tabs a {
|
||||||
|
color: #1950c4;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor .tabs a:first-child {
|
||||||
|
border-right: 0.083rem solid #00f2f1;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor .tabs a.active {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.monitor .content {
|
||||||
|
flex: 1;
|
||||||
|
display: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.monitor .head {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
font-size: 0.583rem;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
color: #68d8fe;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
line-height: 1.05;
|
||||||
|
}
|
||||||
|
.monitor .col:nth-child(1) {
|
||||||
|
width: 3.2rem;
|
||||||
|
}
|
||||||
|
.monitor .col:nth-child(2) {
|
||||||
|
width: 8.4rem;
|
||||||
|
/* 不换行 一行省略*/
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.monitor .col:nth-child(3) {
|
||||||
|
width: 3.2rem;
|
||||||
|
}
|
||||||
|
.monitor .marquee-view {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.6rem;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor .row {
|
||||||
|
line-height: 1.05;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
color: #61a8ff;
|
||||||
|
font-size: 0.5rem;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.monitor .row:hover {
|
||||||
|
color: #68d8ff;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.monitor .row:hover .icon-dot {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor .icon-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.64rem;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.monitor .marquee-view {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.6rem;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor .row {
|
||||||
|
line-height: 1.05;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
color: #61a8ff;
|
||||||
|
font-size: 0.5rem;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.monitor .row:hover {
|
||||||
|
color: #68d8ff;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.monitor .row:hover .icon-dot {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor .icon-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.64rem;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
/* ------------------------------------------------------------动画 */
|
||||||
|
@keyframes row {
|
||||||
|
0% {
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 调用动画 */
|
||||||
|
.monitor .marquee {
|
||||||
|
/* //infinite永久调用动画 */
|
||||||
|
animation: row 10s linear infinite;
|
||||||
|
}
|
||||||
|
/*鼠标划入 停止动画 */
|
||||||
|
.monitor .marquee:hover {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
/* 点位 */
|
||||||
|
.point {
|
||||||
|
height: 14.167rem;
|
||||||
|
}
|
||||||
|
.point .chart {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 1rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.point .pie {
|
||||||
|
width: 13rem;
|
||||||
|
height: 10rem;
|
||||||
|
margin-left: -0.4rem;
|
||||||
|
}
|
||||||
|
.point .data {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 7rem;
|
||||||
|
padding: 1.5rem 1.25rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-image: url(../images/rect.png);
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
.point h4 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.167rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.point span {
|
||||||
|
display: block;
|
||||||
|
color: #4c9bfd;
|
||||||
|
font-size: 0.667rem;
|
||||||
|
}
|
||||||
|
/* 地图 */
|
||||||
|
.map {
|
||||||
|
height: 24.1rem;
|
||||||
|
margin-bottom: 0.833rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.map h3 {
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0.667rem 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.833rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.map .icon-cube {
|
||||||
|
color: #68d8fe;
|
||||||
|
}
|
||||||
|
.map .chart {
|
||||||
|
flex: 1;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
.map .geo {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
/* 用户模块 */
|
||||||
|
.users {
|
||||||
|
height: 14.167rem;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.users .chart {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.users .bar {
|
||||||
|
width: 24.5rem;
|
||||||
|
height: 10rem;
|
||||||
|
}
|
||||||
|
.users .data {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 7rem;
|
||||||
|
padding: 1.5rem 1.25rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-image: url(../images/rect.png);
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
.users h4 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.167rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.users span {
|
||||||
|
display: block;
|
||||||
|
color: #4c9bfd;
|
||||||
|
font-size: 0.667rem;
|
||||||
|
}
|
||||||
|
/* 订单 */
|
||||||
|
.order {
|
||||||
|
height: 6.167rem;
|
||||||
|
}
|
||||||
|
.order .filter {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.order .filter a {
|
||||||
|
display: block;
|
||||||
|
height: 0.75rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
color: #1950c4;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border-right: 0.083rem solid #00f2f1;
|
||||||
|
}
|
||||||
|
.order .filter a:first-child {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.order .filter a:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.order .filter a.active {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.833rem;
|
||||||
|
}
|
||||||
|
.order .data {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 0.833rem;
|
||||||
|
}
|
||||||
|
.order .item {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
.order h4 {
|
||||||
|
font-size: 1.167rem;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 0.417rem;
|
||||||
|
}
|
||||||
|
.order span {
|
||||||
|
display: block;
|
||||||
|
color: #4c9bfd;
|
||||||
|
font-size: 0.667rem;
|
||||||
|
}
|
||||||
|
/* 销售区域 */
|
||||||
|
.sales {
|
||||||
|
height: 10.333rem;
|
||||||
|
}
|
||||||
|
.sales .caption {
|
||||||
|
display: flex;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.sales h3 {
|
||||||
|
height: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
border-right: 0.083rem solid #00f2f1;
|
||||||
|
}
|
||||||
|
.sales a {
|
||||||
|
padding: 0.167rem;
|
||||||
|
font-size: 0.667rem;
|
||||||
|
margin: -0.125rem 0 0 0.875rem;
|
||||||
|
border-radius: 0.125rem;
|
||||||
|
color: #0bace6;
|
||||||
|
}
|
||||||
|
.sales a.active {
|
||||||
|
background-color: #4c9bfd;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.sales .inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.sales .chart {
|
||||||
|
flex: 1;
|
||||||
|
padding-top: 0.6rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.sales .label {
|
||||||
|
position: absolute;
|
||||||
|
left: 1.75rem;
|
||||||
|
top: 0.75rem;
|
||||||
|
color: #4996f5;
|
||||||
|
font-size: 0.583rem;
|
||||||
|
}
|
||||||
|
.sales .line {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
/* 渠道区块 */
|
||||||
|
.wrap {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.channel,
|
||||||
|
.quarter {
|
||||||
|
flex: 1;
|
||||||
|
height: 9.667rem;
|
||||||
|
}
|
||||||
|
.channel {
|
||||||
|
margin-right: 0.833rem;
|
||||||
|
}
|
||||||
|
.channel .data {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.channel .item {
|
||||||
|
margin-top: 0.85rem;
|
||||||
|
}
|
||||||
|
.channel .item:first-child {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.channel .item:last-child {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.channel h4 {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.333rem;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
.channel small {
|
||||||
|
font-size: 50%;
|
||||||
|
}
|
||||||
|
.channel span {
|
||||||
|
display: block;
|
||||||
|
color: #4c9bfd;
|
||||||
|
font-size: 0.583rem;
|
||||||
|
}
|
||||||
|
/* 季度区块 */
|
||||||
|
.quarter .inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0 -0.25rem;
|
||||||
|
}
|
||||||
|
.quarter .chart {
|
||||||
|
flex: 1;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
}
|
||||||
|
.quarter .box {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.quarter .label {
|
||||||
|
transform: translate(-50%, -30%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
.quarter .label small {
|
||||||
|
font-size: 50%;
|
||||||
|
}
|
||||||
|
.quarter .gauge {
|
||||||
|
height: 3.5rem;
|
||||||
|
}
|
||||||
|
.quarter .data {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.quarter .item {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
.quarter h4 {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
.quarter span {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #4c9bfd;
|
||||||
|
font-size: 0.583rem;
|
||||||
|
}
|
||||||
|
/* 排行榜 */
|
||||||
|
.top {
|
||||||
|
height: 11.8rem;
|
||||||
|
}
|
||||||
|
.top .inner {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.top .all {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 7rem;
|
||||||
|
color: #4c9bfd;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.top .all ul {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
.top .all li {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.top .all [class^='icon-'] {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.top .province {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.top .province i {
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
margin-top: 0.208rem;
|
||||||
|
float: right;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 0.583rem;
|
||||||
|
color: #0bace6;
|
||||||
|
}
|
||||||
|
.top .province s {
|
||||||
|
display: inline-block;
|
||||||
|
transform: scale(0.8);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.top .province .icon-up {
|
||||||
|
color: #dc3c33;
|
||||||
|
}
|
||||||
|
.top .province .icon-down {
|
||||||
|
color: #36be90;
|
||||||
|
}
|
||||||
|
.top .province .data {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
}
|
||||||
|
.top .province ul {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.top .province ul li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.top .province ul span {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.top .province ul.sup {
|
||||||
|
font-size: 0.583rem;
|
||||||
|
}
|
||||||
|
.top .province ul.sup li {
|
||||||
|
color: #4995f4;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
.top .province ul.sup li.active {
|
||||||
|
color: #a3c6f2;
|
||||||
|
background-color: rgba(10, 67, 188, 0.2);
|
||||||
|
}
|
||||||
|
.top .province ul.sub {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
font-size: 0.5rem;
|
||||||
|
background-color: rgba(10, 67, 188, 0.2);
|
||||||
|
}
|
||||||
|
.top .province ul.sub li {
|
||||||
|
color: #52ffff;
|
||||||
|
padding: 0.417rem 0.6rem;
|
||||||
|
}
|
||||||
|
.clock {
|
||||||
|
position: absolute;
|
||||||
|
top: -1.5rem;
|
||||||
|
right: 1.667rem;
|
||||||
|
font-size: 0.833rem;
|
||||||
|
color: #0bace6;
|
||||||
|
}
|
||||||
|
.clock i {
|
||||||
|
margin-right: 5px;
|
||||||
|
font-size: 0.833rem;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 1600px) {
|
||||||
|
.top span {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
.top .province ul.sup li {
|
||||||
|
padding: 0.4rem 0.5rem;
|
||||||
|
}
|
||||||
|
.top .province ul.sub li {
|
||||||
|
padding: 0.23rem 0.5rem;
|
||||||
|
}
|
||||||
|
.quarter span {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/views/dashboard/overview/images/border.png
Normal file
BIN
src/views/dashboard/overview/images/border.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/views/dashboard/overview/images/brand.png
Normal file
BIN
src/views/dashboard/overview/images/brand.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
src/views/dashboard/overview/images/line.png
Normal file
BIN
src/views/dashboard/overview/images/line.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 237 B |
BIN
src/views/dashboard/overview/images/rect.png
Normal file
BIN
src/views/dashboard/overview/images/rect.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,500 +1,310 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref, onMounted } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import {
|
|
||||||
listCacheName,
|
|
||||||
listCacheKey,
|
|
||||||
getCacheValue,
|
|
||||||
clearCacheName,
|
|
||||||
clearCacheKey,
|
|
||||||
clearCacheSafe,
|
|
||||||
} from '@/api/monitor/cache';
|
|
||||||
import { PageContainer } from 'antdv-pro-layout';
|
|
||||||
import { ColumnsType } from 'ant-design-vue/lib/table/Table';
|
|
||||||
import { message } from 'ant-design-vue/lib';
|
|
||||||
import { hasPermissions } from '@/plugins/auth-user';
|
|
||||||
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
|
||||||
import useI18n from '@/hooks/useI18n';
|
import useI18n from '@/hooks/useI18n';
|
||||||
|
import Topology from './components/Topology/index.vue';
|
||||||
|
import { useFullscreen } from '@vueuse/core';
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
/**请求点击 */
|
|
||||||
let isClick = ref<boolean>(false);
|
|
||||||
|
|
||||||
/**缓存内容信息 */
|
/**总览节点 */
|
||||||
let cacheKeyInfo = reactive({
|
const viewportDom = ref<HTMLElement | null>(null);
|
||||||
loading: true,
|
const { isFullscreen, toggle } = useFullscreen(viewportDom);
|
||||||
data: {
|
|
||||||
cacheKey: '',
|
|
||||||
cacheName: '',
|
|
||||||
cacheValue: '',
|
|
||||||
remark: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
onMounted(() => {});
|
||||||
* 查询缓存内容详细
|
|
||||||
* @param cacheKey
|
|
||||||
*/
|
|
||||||
function fnCacheKeyInfo(cacheKey: string) {
|
|
||||||
if (!hasPermissions(['monitor:cache:query'])) return;
|
|
||||||
if (isClick.value) return;
|
|
||||||
isClick.value = true;
|
|
||||||
cacheKeyInfo.loading = true;
|
|
||||||
getCacheValue(cacheKeyTable.cacheName, cacheKey).then(res => {
|
|
||||||
isClick.value = false;
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS) {
|
|
||||||
cacheKeyInfo.data = Object.assign(cacheKeyInfo.data, res.data);
|
|
||||||
cacheKeyInfo.loading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**键名列表表格字段列 */
|
|
||||||
let cacheKeyTableColumns: ColumnsType = [
|
|
||||||
{
|
|
||||||
title: t('common.rowId'),
|
|
||||||
dataIndex: 'num',
|
|
||||||
width: '50px',
|
|
||||||
align: 'center',
|
|
||||||
customRender(opt) {
|
|
||||||
return opt.index + 1;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('views.monitor.cache.cacheKey'),
|
|
||||||
dataIndex: 'cacheKey',
|
|
||||||
align: 'left',
|
|
||||||
ellipsis: true,
|
|
||||||
// 渲染值处理
|
|
||||||
customRender(opt) {
|
|
||||||
return opt.text;
|
|
||||||
},
|
|
||||||
// 自定义过滤控件
|
|
||||||
customFilterDropdown: true,
|
|
||||||
onFilter: (value, record) => {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const nameLower = record.cacheKey.toLowerCase();
|
|
||||||
return nameLower.includes(value.toLowerCase());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('common.operate'),
|
|
||||||
key: 'option',
|
|
||||||
align: 'center',
|
|
||||||
width: '50px',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**键名列表表格数据 */
|
|
||||||
let cacheKeyTable = reactive({
|
|
||||||
loading: false,
|
|
||||||
data: [],
|
|
||||||
/**当前键名列表的缓存名称 */
|
|
||||||
cacheName: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理指定缓存键名
|
|
||||||
* @param cacheKey 键名列表中的缓存键名
|
|
||||||
*/
|
|
||||||
function fnCacheKeyClear(cacheKey: string) {
|
|
||||||
if (isClick.value) return;
|
|
||||||
isClick.value = true;
|
|
||||||
const hide = message.loading(t('common.loading'), 0);
|
|
||||||
clearCacheKey(cacheKeyTable.cacheName, cacheKey).then(res => {
|
|
||||||
hide();
|
|
||||||
isClick.value = false;
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS) {
|
|
||||||
message.success({
|
|
||||||
content: t('views.monitor.cache.clearCacheKeyOk', { txt: cacheKey }),
|
|
||||||
duration: 3,
|
|
||||||
});
|
|
||||||
// 缓存内容显示且是删除的缓存键名,需要进行加载显示
|
|
||||||
if (!cacheKeyInfo.loading && cacheKeyInfo.data.cacheKey === cacheKey) {
|
|
||||||
cacheKeyInfo.loading = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message.error({
|
|
||||||
content: res.msg,
|
|
||||||
duration: 3,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fnCacheKeyList();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 查询缓存键名列表 */
|
|
||||||
function fnCacheKeyList(cacheName: string = 'load') {
|
|
||||||
if (cacheName === 'load') {
|
|
||||||
cacheName = cacheKeyTable.cacheName;
|
|
||||||
}
|
|
||||||
if (!cacheName) {
|
|
||||||
message.warning(t('views.monitor.cache.cacheKeyListErr'), 3);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isClick.value) return;
|
|
||||||
isClick.value = true;
|
|
||||||
cacheKeyTable.loading = true;
|
|
||||||
listCacheKey(cacheName).then(res => {
|
|
||||||
isClick.value = false;
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS && res.data) {
|
|
||||||
cacheKeyTable.cacheName = cacheName;
|
|
||||||
cacheKeyTable.data = res.data;
|
|
||||||
cacheKeyTable.loading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**缓存列表表格数据 */
|
|
||||||
let cacheNameTable = reactive({
|
|
||||||
loading: true,
|
|
||||||
data: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
/**缓存列表表格字段列 */
|
|
||||||
let cacheNameTableColumns: ColumnsType = [
|
|
||||||
{
|
|
||||||
title: t('common.rowId'),
|
|
||||||
dataIndex: 'num',
|
|
||||||
width: '50px',
|
|
||||||
align: 'center',
|
|
||||||
customRender(opt) {
|
|
||||||
return opt.index + 1;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('views.monitor.cache.cacheName'),
|
|
||||||
dataIndex: 'cacheName',
|
|
||||||
align: 'left',
|
|
||||||
ellipsis: true,
|
|
||||||
// 渲染值处理
|
|
||||||
customRender(opt) {
|
|
||||||
return opt.text;
|
|
||||||
},
|
|
||||||
// 自定义过滤控件
|
|
||||||
customFilterDropdown: true,
|
|
||||||
onFilter: (value, record) => {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const nameLower = record.cacheName.toLowerCase();
|
|
||||||
return nameLower.includes(value.toLowerCase());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('views.monitor.cache.remark'),
|
|
||||||
dataIndex: 'remark',
|
|
||||||
align: 'left',
|
|
||||||
ellipsis: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('common.operate'),
|
|
||||||
key: 'option',
|
|
||||||
align: 'center',
|
|
||||||
width: '50px',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**安全清理缓存 */
|
|
||||||
function fnClearCacheSafe() {
|
|
||||||
if (isClick.value) return;
|
|
||||||
isClick.value = true;
|
|
||||||
const hide = message.loading(t('common.loading'), 0);
|
|
||||||
clearCacheSafe().then(res => {
|
|
||||||
hide();
|
|
||||||
isClick.value = false;
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS) {
|
|
||||||
message.success({
|
|
||||||
content: t('views.monitor.cache.clearCacheSafeOk'),
|
|
||||||
duration: 3,
|
|
||||||
});
|
|
||||||
cacheKeyTable.loading = true;
|
|
||||||
cacheKeyInfo.loading = true;
|
|
||||||
} else {
|
|
||||||
message.error({
|
|
||||||
content: res.msg,
|
|
||||||
duration: 3,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理指定缓存名称
|
|
||||||
* @param cacheName 缓存名称
|
|
||||||
*/
|
|
||||||
function fnCacheNameClear(cacheName: string) {
|
|
||||||
if (isClick.value) return;
|
|
||||||
isClick.value = true;
|
|
||||||
const hide = message.loading(t('common.loading'), 0);
|
|
||||||
clearCacheName(cacheName).then(res => {
|
|
||||||
hide();
|
|
||||||
isClick.value = false;
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS) {
|
|
||||||
message.success({
|
|
||||||
content: t('views.monitor.cache.clearCacheNameOk', { txt: cacheName }),
|
|
||||||
duration: 3,
|
|
||||||
});
|
|
||||||
// 缓存内容显示且是删除的缓存名称,需要进行加载显示
|
|
||||||
if (!cacheKeyInfo.loading && cacheKeyInfo.data.cacheName === cacheName) {
|
|
||||||
cacheKeyInfo.loading = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message.error({
|
|
||||||
content: res.msg,
|
|
||||||
duration: 3,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fnCacheKeyList(cacheName);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**查询缓存名称列表 */
|
|
||||||
function fnCacheNameList() {
|
|
||||||
if (isClick.value) return;
|
|
||||||
isClick.value = true;
|
|
||||||
cacheNameTable.loading = true;
|
|
||||||
listCacheName().then(res => {
|
|
||||||
isClick.value = false;
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS && res.data) {
|
|
||||||
cacheNameTable.data = res.data;
|
|
||||||
cacheNameTable.loading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fnCacheNameList();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PageContainer>
|
<div class="viewport" ref="viewportDom">
|
||||||
<a-row :gutter="20">
|
<div class="brand">
|
||||||
<a-col :lg="8" :md="8" :xs="24">
|
<div class="brand-title" @click="toggle">
|
||||||
<a-card
|
核心网系统看板
|
||||||
:title="t('views.monitor.cache.cacheList')"
|
<FullscreenExitOutlined v-if="isFullscreen" />
|
||||||
:bordered="false"
|
<FullscreenOutlined v-else />
|
||||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
</div>
|
||||||
>
|
<div class="brand-desc">5GC 核心网数据</div>
|
||||||
<template #extra>
|
</div>
|
||||||
<a-space :size="8" align="center">
|
|
||||||
<a-tooltip>
|
|
||||||
<template #title>
|
|
||||||
{{ t('common.reloadText') }}
|
|
||||||
</template>
|
|
||||||
<a-button type="text" @click.prevent="fnCacheNameList">
|
|
||||||
<template #icon><ReloadOutlined /></template>
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip>
|
|
||||||
<template #title>
|
|
||||||
{{ t('views.monitor.cache.clearCacheSafe') }}
|
|
||||||
</template>
|
|
||||||
<a-popconfirm
|
|
||||||
placement="bottomRight"
|
|
||||||
:title="t('views.monitor.cache.clearCacheSafeTip')"
|
|
||||||
:ok-text="t('common.ok')"
|
|
||||||
:cancel-text="t('common.cancel')"
|
|
||||||
@confirm="fnClearCacheSafe()"
|
|
||||||
>
|
|
||||||
<a-button type="text" v-perms:has="['monitor:cache:remove']">
|
|
||||||
<template #icon><ClearOutlined /></template>
|
|
||||||
</a-button>
|
|
||||||
</a-popconfirm>
|
|
||||||
</a-tooltip>
|
|
||||||
</a-space>
|
|
||||||
</template>
|
|
||||||
<a-table
|
|
||||||
row-key="cacheName"
|
|
||||||
size="small"
|
|
||||||
:columns="cacheNameTableColumns"
|
|
||||||
:data-source="cacheNameTable.data"
|
|
||||||
:loading="cacheNameTable.loading"
|
|
||||||
:scroll="{ y: 'calc(100vh - 350px)' }"
|
|
||||||
:pagination="false"
|
|
||||||
:row-selection="{
|
|
||||||
type: 'radio',
|
|
||||||
onChange: (selectedRowKeys: (string|number)[]) => fnCacheKeyList(selectedRowKeys[0] as string),
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
#customFilterDropdown="{
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
column,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div style="padding: 8px">
|
|
||||||
<a-input
|
|
||||||
:placeholder="
|
|
||||||
t('views.monitor.cache.filterPlace', { txt: column.title })
|
|
||||||
"
|
|
||||||
:value="selectedKeys[0]"
|
|
||||||
style="width: 188px; margin-bottom: 8px; display: block"
|
|
||||||
@change="
|
|
||||||
(e:any)=> setSelectedKeys(e.target.value ? [e.target.value] : [])
|
|
||||||
"
|
|
||||||
@pressEnter="confirm()"
|
|
||||||
/>
|
|
||||||
<a-button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
style="width: 90px; margin-right: 8px"
|
|
||||||
@click="confirm()"
|
|
||||||
>
|
|
||||||
{{ t('views.monitor.cache.filter') }}
|
|
||||||
</a-button>
|
|
||||||
<a-button
|
|
||||||
size="small"
|
|
||||||
style="width: 90px"
|
|
||||||
@click="clearFilters({ confirm: true })"
|
|
||||||
>
|
|
||||||
{{ t('common.reset') }}
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #bodyCell="{ column, record }">
|
|
||||||
<template v-if="column.key === 'option'">
|
|
||||||
<a-popconfirm
|
|
||||||
placement="topRight"
|
|
||||||
:title="t('views.monitor.cache.clearCacheNameTip')"
|
|
||||||
,
|
|
||||||
:ok-text="t('common.ok')"
|
|
||||||
:cancel-text="t('common.cancel')"
|
|
||||||
@confirm="fnCacheNameClear(record.cacheName)"
|
|
||||||
>
|
|
||||||
<a-button type="text" v-perms:has="['monitor:cache:remove']">
|
|
||||||
<template #icon><ClearOutlined /></template>
|
|
||||||
</a-button>
|
|
||||||
</a-popconfirm>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</a-table>
|
|
||||||
</a-card>
|
|
||||||
</a-col>
|
|
||||||
|
|
||||||
<a-col :lg="8" :md="8" :xs="24">
|
<div class="column">
|
||||||
<a-card
|
<!-- 基站数量 -->
|
||||||
:title="t('views.monitor.cache.keyNameList')"
|
<div class="wrap">
|
||||||
:bordered="false"
|
<div class="channel panel">
|
||||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
<div class="inner">
|
||||||
>
|
<h3>UE数量</h3>
|
||||||
<template #extra>
|
<div class="data">
|
||||||
<a-tooltip>
|
<div class="item">
|
||||||
<template #title>
|
<h4>39 <small>%</small></h4>
|
||||||
{{ t('common.reloadText') }}
|
<span>
|
||||||
</template>
|
<i class="icon-plane"></i>
|
||||||
<a-button type="text" @click.prevent="fnCacheKeyList()">
|
IMS
|
||||||
<template #icon><ReloadOutlined /></template>
|
</span>
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
</template>
|
|
||||||
<a-table
|
|
||||||
row-key="cacheKey"
|
|
||||||
size="small"
|
|
||||||
:columns="cacheKeyTableColumns"
|
|
||||||
:data-source="cacheKeyTable.data"
|
|
||||||
:loading="cacheKeyTable.loading"
|
|
||||||
:scroll="{ y: 'calc(100vh - 350px)' }"
|
|
||||||
:pagination="false"
|
|
||||||
:row-selection="{
|
|
||||||
type: 'radio',
|
|
||||||
onChange: (selectedRowKeys: (string|number)[]) => fnCacheKeyInfo(selectedRowKeys[0] as string),
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
#customFilterDropdown="{
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
column,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div style="padding: 8px">
|
|
||||||
<a-input
|
|
||||||
:placeholder="
|
|
||||||
t('views.monitor.cache.filterPlace', { txt: column.title })
|
|
||||||
"
|
|
||||||
:value="selectedKeys[0]"
|
|
||||||
style="width: 188px; margin-bottom: 8px; display: block"
|
|
||||||
@change="
|
|
||||||
(e:any) => setSelectedKeys(e.target.value ? [e.target.value] : [])
|
|
||||||
"
|
|
||||||
@pressEnter="confirm()"
|
|
||||||
/>
|
|
||||||
<a-button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
style="width: 90px; margin-right: 8px"
|
|
||||||
@click="confirm()"
|
|
||||||
>
|
|
||||||
{{ t('views.monitor.cache.filter') }}
|
|
||||||
</a-button>
|
|
||||||
<a-button
|
|
||||||
size="small"
|
|
||||||
style="width: 90px"
|
|
||||||
@click="clearFilters({ confirm: true })"
|
|
||||||
>
|
|
||||||
{{ t('common.reset') }}
|
|
||||||
</a-button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
<template #bodyCell="{ column, record }">
|
<div class="data">
|
||||||
<template v-if="column.key === 'option'">
|
<div class="item">
|
||||||
<a-popconfirm
|
<h4>20 <small>%</small></h4>
|
||||||
placement="topRight"
|
<span>
|
||||||
:title="t('views.monitor.cache.clearCacheKeyTip')"
|
<i class="icon-train"></i>
|
||||||
,
|
AMF
|
||||||
:ok-text="t('common.ok')"
|
</span>
|
||||||
:cancel-text="t('common.cancel')"
|
</div>
|
||||||
@confirm="fnCacheKeyClear(record.cacheKey)"
|
</div>
|
||||||
>
|
</div>
|
||||||
<a-button type="text" v-perms:has="['monitor:cache:remove']">
|
</div>
|
||||||
<template #icon><DeleteOutlined /></template>
|
<div class="quarter panel">
|
||||||
</a-button>
|
<div class="inner">
|
||||||
</a-popconfirm>
|
<h3>基站数量</h3>
|
||||||
</template>
|
<div class="chart">
|
||||||
</template>
|
<div class="data">
|
||||||
</a-table>
|
<div class="item">
|
||||||
</a-card>
|
<h4>1,321</h4>
|
||||||
</a-col>
|
<span>
|
||||||
|
<i class="icon-dot" style="color: #6acca3"></i>
|
||||||
|
在线
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<h4>150%</h4>
|
||||||
|
<span>
|
||||||
|
<i class="icon-dot" style="color: #ed3f35"></i>
|
||||||
|
不在线
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a-col :lg="8" :md="8" :xs="24" v-perms:has="['monitor:cache:query']">
|
<!--本月告警统计-->
|
||||||
<a-card
|
<div class="point panel">
|
||||||
:title="t('views.monitor.cache.keyContent')"
|
<div class="inner">
|
||||||
:bordered="false"
|
<h3>本月告警统计</h3>
|
||||||
:body-style="{ marginBottom: '24px', padding: 0 }"
|
<div class="chart">
|
||||||
:loading="cacheKeyInfo.loading"
|
<div class="pie"></div>
|
||||||
>
|
<div class="data">
|
||||||
<a-descriptions
|
<div class="item">
|
||||||
size="small"
|
<h4>320,11</h4>
|
||||||
layout="vertical"
|
<span>
|
||||||
:bordered="true"
|
<i class="icon-dot" style="color: #ed3f35"></i>
|
||||||
:column="1"
|
告警总数
|
||||||
>
|
</span>
|
||||||
<a-descriptions-item :label="t('views.monitor.cache.cacheName')">
|
</div>
|
||||||
{{ cacheKeyInfo.data.cacheName }}
|
<div class="item">
|
||||||
</a-descriptions-item>
|
<h4>418</h4>
|
||||||
<a-descriptions-item :label="t('views.monitor.cache.cacheKey')">
|
<span>
|
||||||
{{ cacheKeyInfo.data.cacheKey }}
|
<i class="icon-dot" style="color: #eacf19"></i>
|
||||||
</a-descriptions-item>
|
本月新增
|
||||||
<a-descriptions-item :label="t('views.monitor.cache.cacheValue')">
|
</span>
|
||||||
<a-typography-paragraph>
|
</div>
|
||||||
<a-textarea
|
</div>
|
||||||
:value="cacheKeyInfo.data.cacheValue"
|
</div>
|
||||||
:auto-size="{ minRows: 4, maxRows: 20 }"
|
</div>
|
||||||
:maxlength="4000"
|
</div>
|
||||||
:disabled="true"
|
|
||||||
/>
|
<!-- 告警趋势 -->
|
||||||
</a-typography-paragraph>
|
<div class="top panel">
|
||||||
</a-descriptions-item>
|
<div class="inner">
|
||||||
</a-descriptions>
|
<div class="all">
|
||||||
</a-card>
|
<h3>告警趋势</h3>
|
||||||
</a-col>
|
<ul>
|
||||||
</a-row>
|
<li>
|
||||||
</PageContainer>
|
<i class="icon-cup1" style="color: #d93f36"></i>
|
||||||
|
可爱多
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="icon-cup2" style="color: #68d8fe"></i>
|
||||||
|
娃哈啥
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="icon-cup3" style="color: #4c9bfd"></i>
|
||||||
|
喜之郎
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column" style="flex: 4; margin: 1.333rem 0.833rem 0">
|
||||||
|
<!-- 实时流量 -->
|
||||||
|
<div class="users panel">
|
||||||
|
<div class="inner">
|
||||||
|
<h3>实时流量</h3>
|
||||||
|
<div class="chart">
|
||||||
|
<div class="bar"></div>
|
||||||
|
<div class="data">
|
||||||
|
<div class="item">
|
||||||
|
<h4>120,899</h4>
|
||||||
|
<span>
|
||||||
|
<i class="icon-dot" style="color: #ed3f35"></i>
|
||||||
|
用户总量
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<h4>248</h4>
|
||||||
|
<span>
|
||||||
|
<i class="icon-dot" style="color: #eacf19"></i>
|
||||||
|
本月新增
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 拓扑图 -->
|
||||||
|
<div class="topology panel">
|
||||||
|
<div class="topology-title">
|
||||||
|
<PartitionOutlined style="color: #68d8fe" />
|
||||||
|
<DeploymentUnitOutlined style="color: #68d8fe" />
|
||||||
|
网络拓扑
|
||||||
|
</div>
|
||||||
|
<div class="topology-chart">
|
||||||
|
<Topology />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<!-- 流量 -->
|
||||||
|
<div class="order panel">
|
||||||
|
<div class="inner">
|
||||||
|
<!-- 筛选 -->
|
||||||
|
<div class="filter">
|
||||||
|
<a href="javascript:;" data-key="day1" class="active">24小时</a>
|
||||||
|
<a href="javascript:;" data-key="day30">7天</a>
|
||||||
|
<a href="javascript:;" data-key="day30">30天</a>
|
||||||
|
</div>
|
||||||
|
<!-- 数据 -->
|
||||||
|
<div class="data">
|
||||||
|
<div class="item">
|
||||||
|
<h4>20,301 TB</h4>
|
||||||
|
<span>
|
||||||
|
<ArrowUpOutlined style="color: #ed3f35" />
|
||||||
|
上行
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<h4>99834 TB</h4>
|
||||||
|
<span>
|
||||||
|
<ArrowDownOutlined style="color: #eacf19" />
|
||||||
|
下行
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 资源情况 -->
|
||||||
|
<div class="users panel">
|
||||||
|
<div class="inner">
|
||||||
|
<h3>资源情况</h3>
|
||||||
|
<div class="chart">
|
||||||
|
<div class="bar"></div>
|
||||||
|
<div class="data">
|
||||||
|
<div class="item">
|
||||||
|
<h4>120,899</h4>
|
||||||
|
<span>
|
||||||
|
<i class="icon-dot" style="color: #ed3f35"></i>
|
||||||
|
磁盘
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<h4>248</h4>
|
||||||
|
<span>
|
||||||
|
<i class="icon-dot" style="color: #eacf19"></i>
|
||||||
|
cpu
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<h4>248</h4>
|
||||||
|
<span>
|
||||||
|
<i class="icon-dot" style="color: #eacf19"></i>
|
||||||
|
内存
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--会话监控-->
|
||||||
|
<div class="monitor panel">
|
||||||
|
<div class="inner">
|
||||||
|
<div class="tabs">
|
||||||
|
<a href="javascript:;" data-index="0" class="active">会话监控</a>
|
||||||
|
</div>
|
||||||
|
<div class="content" style="display: block">
|
||||||
|
<div class="head">
|
||||||
|
<span class="col">故障时间</span>
|
||||||
|
<span class="col">设备地址</span>
|
||||||
|
<span class="col">异常代码</span>
|
||||||
|
</div>
|
||||||
|
<div class="marquee-view">
|
||||||
|
<div class="marquee">
|
||||||
|
<div class="row">
|
||||||
|
<span class="col">20180701</span>
|
||||||
|
<span class="col">11北京市昌平西路金燕龙写字楼</span>
|
||||||
|
<span class="col">1000001</span>
|
||||||
|
<span class="icon-dot"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="col">20190601</span>
|
||||||
|
<span class="col">北京市昌平区城西路金燕龙写字楼</span>
|
||||||
|
<span class="col">1000002</span>
|
||||||
|
<span class="icon-dot"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="col">20190704</span>
|
||||||
|
<span class="col">北京市昌平区建材城西路金燕龙写字楼</span>
|
||||||
|
<span class="col">1000003</span>
|
||||||
|
<span class="icon-dot"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="col">20180701</span>
|
||||||
|
<span class="col">北京市昌平区建路金燕龙写字楼</span>
|
||||||
|
<span class="col">1000004</span>
|
||||||
|
<span class="icon-dot"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="col">20190701</span>
|
||||||
|
<span class="col">北京市昌平区建材城西路金燕龙写字楼</span>
|
||||||
|
<span class="col">1000005</span>
|
||||||
|
<span class="icon-dot"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="col">20190701</span>
|
||||||
|
<span class="col">北京市昌平区建材城西路金燕龙写字楼</span>
|
||||||
|
<span class="col">1000006</span>
|
||||||
|
<span class="icon-dot"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="col">20190701</span>
|
||||||
|
<span class="col">北京市昌平区建西路金燕龙写字楼</span>
|
||||||
|
<span class="col">1000007</span>
|
||||||
|
<span class="icon-dot"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="col">20190701</span>
|
||||||
|
<span class="col">北京市昌平区建材城西路金燕龙写字楼</span>
|
||||||
|
<span class="col">1000008</span>
|
||||||
|
<span class="icon-dot"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="col">20190701</span>
|
||||||
|
<span class="col">北京市昌平区建材城西路金燕龙写字楼</span>
|
||||||
|
<span class="col">1000009</span>
|
||||||
|
<span class="icon-dot"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="col">20190710</span>
|
||||||
|
<span class="col">北京市昌平区建材城西路金燕龙写字楼</span>
|
||||||
|
<span class="col">1000010</span>
|
||||||
|
<span class="icon-dot"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<style lang="less" scoped>
|
||||||
|
@import url('./css/index.css');
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user