fix:关键指标界面自定义布局功能
This commit is contained in:
@@ -30,6 +30,7 @@
|
|||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"echarts": "~5.5.0",
|
"echarts": "~5.5.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"grid-layout-plus": "^1.0.5",
|
||||||
"intl-tel-input": "^23.8.1",
|
"intl-tel-input": "^23.8.1",
|
||||||
"js-base64": "^3.7.7",
|
"js-base64": "^3.7.7",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import draggable from 'vuedraggable';
|
import { GridLayout, GridItem } from 'grid-layout-plus'
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
import { PageContainer } from 'antdv-pro-layout';
|
import { PageContainer } from 'antdv-pro-layout';
|
||||||
import { onMounted, reactive, ref, markRaw, nextTick, onUnmounted} from 'vue';
|
import { onMounted, reactive, ref, markRaw, nextTick, onUnmounted, watch } from 'vue';
|
||||||
import { RESULT_CODE_ERROR, RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
import { RESULT_CODE_ERROR, RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||||||
import { SizeType } from 'ant-design-vue/es/config-provider';
|
import { SizeType } from 'ant-design-vue/es/config-provider';
|
||||||
import { listKPIData, getKPITitle } from '@/api/perfManage/goldTarget';
|
import { listKPIData, getKPITitle } from '@/api/perfManage/goldTarget';
|
||||||
@@ -10,12 +10,11 @@ import useI18n from '@/hooks/useI18n';
|
|||||||
import { parseDateToStr } from '@/utils/date-utils';
|
import { parseDateToStr } from '@/utils/date-utils';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import useNeInfoStore from '@/store/modules/neinfo';
|
import useNeInfoStore from '@/store/modules/neinfo';
|
||||||
import { message } from 'ant-design-vue';
|
import { message, Switch, Form } from 'ant-design-vue';
|
||||||
import { ColumnsType } from 'ant-design-vue/es/table';
|
import { ColumnsType } from 'ant-design-vue/es/table';
|
||||||
import { generateColorRGBA } from '@/utils/generate-utils';
|
import { generateColorRGBA } from '@/utils/generate-utils';
|
||||||
import { LineSeriesOption } from 'echarts/charts';
|
import { LineSeriesOption } from 'echarts/charts';
|
||||||
import { OptionsType, WS } from '@/plugins/ws-websocket';
|
import { OptionsType, WS } from '@/plugins/ws-websocket';
|
||||||
import { Switch } from 'ant-design-vue';
|
|
||||||
const { t, currentLocale } = useI18n();
|
const { t, currentLocale } = useI18n();
|
||||||
|
|
||||||
const neInfoStore = useNeInfoStore();
|
const neInfoStore = useNeInfoStore();
|
||||||
@@ -34,10 +33,53 @@ const handleRealTimeSwitch = (checked: any) => {
|
|||||||
// 网元数组
|
// 网元数组
|
||||||
const networkElementTypes = ['udm', 'upf', 'amf', 'smf', 'ims','ausf'] as const;
|
const networkElementTypes = ['udm', 'upf', 'amf', 'smf', 'ims','ausf'] as const;
|
||||||
// 定义 ChartType
|
// 定义 ChartType
|
||||||
type ChartType = typeof networkElementTypes[number];
|
type ChartType = typeof networkElementTypes[number]; // 将 i 的类型改为 ChartType
|
||||||
|
|
||||||
|
// 添加类型定义
|
||||||
|
interface LayoutItem {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
i: ChartType; // 将 i 的类型改为 ChartType
|
||||||
|
}
|
||||||
|
|
||||||
|
type Layout = LayoutItem[];
|
||||||
|
|
||||||
//构建响应式数组储存图表类型数据
|
//构建响应式数组储存图表类型数据
|
||||||
const chartOrder = ref([...networkElementTypes]);
|
const chartOrder = ref<Layout>(
|
||||||
|
networkElementTypes.map((type, index) => ({
|
||||||
|
x: index % 2 * 6, // 每行两个图表,宽度为6
|
||||||
|
y: Math.floor(index / 2) * 4, // 每个图表占据 4 个单位高度
|
||||||
|
w: 6, // 宽度为6个单位
|
||||||
|
h: 4, // 高度为4个单位
|
||||||
|
i: type, // 使用网元类型作为唯一标识符
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// 改变布局触发更新
|
||||||
|
const handleLayoutUpdated = (newLayout: Layout) => {
|
||||||
|
chartOrder.value = newLayout;
|
||||||
|
nextTick(() => {
|
||||||
|
newLayout.forEach((item) => {
|
||||||
|
const state = chartStates[item.i];
|
||||||
|
if (state.chart.value) {
|
||||||
|
state.chart.value.resize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 chartOrder 的变化
|
||||||
|
watch(chartOrder, () => {
|
||||||
|
nextTick(() => {
|
||||||
|
Object.values(chartStates).forEach(state => {
|
||||||
|
if (state.chart.value) {
|
||||||
|
state.chart.value.resize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 定义表格状态类型
|
// 定义表格状态类型
|
||||||
type TableStateType = {
|
type TableStateType = {
|
||||||
@@ -79,8 +121,7 @@ const chartStates: Record<ChartType, ReturnType<typeof createChartState>> = Obje
|
|||||||
) as Record<ChartType, ReturnType<typeof createChartState>>;
|
) as Record<ChartType, ReturnType<typeof createChartState>>;
|
||||||
|
|
||||||
//日期选择器
|
//日期选择器
|
||||||
interface RangePicker {
|
interface RangePicker extends Record<ChartType, [string, string]> {
|
||||||
[key: string]: [string, string] | Record<string, [Dayjs, Dayjs]>;
|
|
||||||
placeholder: [string, string];
|
placeholder: [string, string];
|
||||||
ranges: Record<string, [Dayjs, Dayjs]>;
|
ranges: Record<string, [Dayjs, Dayjs]>;
|
||||||
}
|
}
|
||||||
@@ -90,10 +131,10 @@ const rangePicker = reactive<RangePicker>({
|
|||||||
...Object.fromEntries(networkElementTypes.map(type => [
|
...Object.fromEntries(networkElementTypes.map(type => [
|
||||||
type,
|
type,
|
||||||
[
|
[
|
||||||
dayjs('2024-09-20 00:00:00').valueOf().toString(),//模拟数据日期为9月20日数据
|
dayjs('2024-09-20 00:00:00').valueOf().toString(),//模拟数据的日期设为默认日期
|
||||||
dayjs('2024-09-20 23:59:59').valueOf().toString()
|
dayjs('2024-09-20 23:59:59').valueOf().toString()
|
||||||
]
|
]
|
||||||
])),
|
])) as Record<ChartType, [string, string]>,
|
||||||
placeholder: [t('views.monitor.monitor.startTime'), t('views.monitor.monitor.endTime')] as [string, string],
|
placeholder: [t('views.monitor.monitor.startTime'), t('views.monitor.monitor.endTime')] as [string, string],
|
||||||
ranges: {
|
ranges: {
|
||||||
[t('views.monitor.monitor.yesterday')]: [dayjs().subtract(1, 'day').startOf('day'), dayjs().subtract(1, 'day').endOf('day')],
|
[t('views.monitor.monitor.yesterday')]: [dayjs().subtract(1, 'day').startOf('day'), dayjs().subtract(1, 'day').endOf('day')],
|
||||||
@@ -157,6 +198,7 @@ const initChart = (type: ChartType) => {
|
|||||||
series: [],
|
series: [],
|
||||||
};
|
};
|
||||||
state.chart.value.setOption(option);
|
state.chart.value.setOption(option);
|
||||||
|
state.chart.value.resize(); // 确保图表正确调整大小
|
||||||
|
|
||||||
// 创建 ResizeObserver 实例
|
// 创建 ResizeObserver 实例
|
||||||
state.observer.value = new ResizeObserver(() => {
|
state.observer.value = new ResizeObserver(() => {
|
||||||
@@ -173,11 +215,11 @@ const initChart = (type: ChartType) => {
|
|||||||
//结束拖拽事件
|
//结束拖拽事件
|
||||||
const onDragEnd = () => {
|
const onDragEnd = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
chartOrder.value.forEach((type) => {
|
chartOrder.value.forEach((type:any) => {
|
||||||
const state = chartStates[type as ChartType];
|
const state = chartStates[type as ChartType];
|
||||||
if (state.chart.value) {
|
if (state.chart.value) {
|
||||||
state.chart.value.resize(); // 调整图表大小
|
state.chart.value.resize(); // 调整图表大小
|
||||||
// 重新设置图表选项,保留原有数据
|
// 重新设置图表选项,保留原有数
|
||||||
state.chart.value.setOption({
|
state.chart.value.setOption({
|
||||||
xAxis: { data: state.chartDataXAxisData },
|
xAxis: { data: state.chartDataXAxisData },
|
||||||
series: state.chartDataYSeriesData,
|
series: state.chartDataYSeriesData,
|
||||||
@@ -188,9 +230,9 @@ const onDragEnd = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 创建可复用的数据获取函数
|
// 可复用的数据获取函数
|
||||||
const fetchData = async (type: ChartType) => {
|
const fetchData = async (type: ChartType) => {
|
||||||
const state = chartStates[type];
|
const state = chartStates[type]; // 直接使用 type
|
||||||
const neId = '001';
|
const neId = '001';
|
||||||
state.tableState.loading = true;
|
state.tableState.loading = true;
|
||||||
try {
|
try {
|
||||||
@@ -206,9 +248,8 @@ const fetchData = async (type: ChartType) => {
|
|||||||
interval: 5,
|
interval: 5,
|
||||||
});
|
});
|
||||||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||||
|
|
||||||
state.tableState.data = res.data;
|
state.tableState.data = res.data;
|
||||||
nextTick(()=> {
|
nextTick(() => {
|
||||||
renderChart(type);
|
renderChart(type);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -318,22 +359,22 @@ interface CustomSeriesOption extends Omit<LineSeriesOption, 'data'> {
|
|||||||
}
|
}
|
||||||
// 创建可复用的图表渲染函数
|
// 创建可复用的图表渲染函数
|
||||||
const renderChart = (type: ChartType) => {
|
const renderChart = (type: ChartType) => {
|
||||||
|
|
||||||
const state = chartStates[type];
|
const state = chartStates[type];
|
||||||
if (state.chart.value == null ) {
|
if (state.chart.value == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 重置数据
|
// 重置数据
|
||||||
state.chartLegendSelected = {};
|
state.chartLegendSelected = {};
|
||||||
state.chartDataXAxisData = [];
|
state.chartDataXAxisData = [];
|
||||||
state.chartDataYSeriesData = [];
|
state.chartDataYSeriesData = [];
|
||||||
|
|
||||||
// 处理数据
|
// 处理数据
|
||||||
for (const columns of state.tableColumns.value) {
|
for (const column of state.tableColumns.value) {
|
||||||
if (['neName', 'startIndex', 'timeGroup'].includes(columns.key as string)) continue;
|
if (['neName', 'startIndex', 'timeGroup'].includes(column.key as string)) continue;
|
||||||
const color = generateColorRGBA();
|
const color = generateColorRGBA();
|
||||||
state.chartDataYSeriesData.push({
|
state.chartDataYSeriesData.push({
|
||||||
name: columns.title as string,
|
name: column.title as string,
|
||||||
customKey: columns.key as string,
|
customKey: column.key as string,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
symbol: 'none',
|
symbol: 'none',
|
||||||
sampling: 'lttb',
|
sampling: 'lttb',
|
||||||
@@ -346,22 +387,23 @@ const renderChart = (type: ChartType) => {
|
|||||||
},
|
},
|
||||||
data: [],
|
data: [],
|
||||||
} as CustomSeriesOption);
|
} as CustomSeriesOption);
|
||||||
state.chartLegendSelected[columns.title as string] = true;
|
state.chartLegendSelected[column.title as string] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const orgData = [...state.tableState.data].reverse();
|
const orgData = [...state.tableState.data].reverse();
|
||||||
for (const item of orgData) {
|
for (const item of orgData) {
|
||||||
state.chartDataXAxisData.push(parseDateToStr(+item.timeGroup));
|
state.chartDataXAxisData.push(parseDateToStr(+item.timeGroup));
|
||||||
for (const y of state.chartDataYSeriesData) {
|
for (const series of state.chartDataYSeriesData) {
|
||||||
//const key = (y.emphasis as any).customKey;
|
series.data.push(+item[series.customKey as string]);
|
||||||
y.data.push(+item[y.customKey as string]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新图表
|
// 更新图表
|
||||||
state.chart.value.setOption(
|
state.chart.value.setOption(
|
||||||
{
|
{
|
||||||
legend: { selected: state.chartLegendSelected },
|
legend: { selected: state.chartLegendSelected },
|
||||||
xAxis: { data: state.chartDataXAxisData,
|
xAxis: {
|
||||||
|
data: state.chartDataXAxisData,
|
||||||
type: 'category',
|
type: 'category',
|
||||||
boundaryGap: false,
|
boundaryGap: false,
|
||||||
},
|
},
|
||||||
@@ -413,7 +455,7 @@ onMounted(async () => {
|
|||||||
for (const type of networkElementTypes) {
|
for (const type of networkElementTypes) {
|
||||||
await fetchKPITitle(type);
|
await fetchKPITitle(type);
|
||||||
initChart(type);
|
initChart(type);
|
||||||
fetchData(type);
|
fetchData(type); // 确保这行存在
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -437,99 +479,151 @@ onUnmounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<div class="control-row">
|
<div class="control-row">
|
||||||
<div class="real-time-switch">
|
<a-form layout="inline">
|
||||||
<Switch
|
<a-form-item>
|
||||||
v-model:checked="realTimeEnabled"
|
<a-switch
|
||||||
@change="handleRealTimeSwitch as any"
|
v-model:checked="realTimeEnabled"
|
||||||
/>
|
@change="handleRealTimeSwitch"
|
||||||
<span class="switch-label">{{ realTimeEnabled ? t('views.dashboard.cdr.realTimeDataStart') : t('views.dashboard.cdr.realTimeDataStop') }}</span>
|
/>
|
||||||
</div>
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<span class="switch-label">{{ realTimeEnabled ? t('views.dashboard.cdr.realTimeDataStart') : t('views.dashboard.cdr.realTimeDataStop') }}</span>
|
||||||
|
</a-form-item>
|
||||||
|
<!-- 这里可以添加更多的表单项 -->
|
||||||
|
</a-form>
|
||||||
</div>
|
</div>
|
||||||
<draggable
|
<GridLayout
|
||||||
v-model="chartOrder"
|
v-model:layout="chartOrder"
|
||||||
:animation="200"
|
:col-num="12"
|
||||||
item-key="type"
|
:row-height="100"
|
||||||
@end="onDragEnd"
|
:margin="[10, 10]"
|
||||||
class="row"
|
:is-draggable="true"
|
||||||
onscroll='false'
|
:is-resizable="true"
|
||||||
|
:vertical-compact="true"
|
||||||
|
:use-css-transforms="true"
|
||||||
|
:responsive="true"
|
||||||
|
:prevent-collision="false"
|
||||||
|
@layout-updated="handleLayoutUpdated"
|
||||||
|
class="charts-container"
|
||||||
>
|
>
|
||||||
<template #item="{element:type}">
|
<GridItem
|
||||||
|
v-for="item in chartOrder"
|
||||||
<div class="col-lg-6 col-md=-6 col-xs-12">
|
:key="item.i"
|
||||||
<a-card :bordered="false" :body-style="{ marginBottom: '24px', padding: '24px'}">
|
:x="item.x"
|
||||||
<template #title>{{ type.toUpperCase() }}</template>
|
:y="item.y"
|
||||||
<template #extra>
|
:w="item.w"
|
||||||
<a-range-picker
|
:h="item.h"
|
||||||
v-model:value="(rangePicker[type as keyof RangePicker]as[string,string])"
|
:i="item.i"
|
||||||
:allow-clear="false"
|
:min-w="4"
|
||||||
bordered
|
:min-h="3"
|
||||||
:show-time="{ format: 'HH:mm:ss' }"
|
:is-draggable="true"
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
:is-resizable="true"
|
||||||
value-format="x"
|
:resizable-handles="['ne']"
|
||||||
:placeholder="rangePicker.placeholder"
|
drag-allow-from=".drag-handle"
|
||||||
:ranges="rangePicker.ranges"
|
drag-ignore-from=".no-drag"
|
||||||
style="width: 100%"
|
class="grid-item"
|
||||||
@change="() => fetchData(type)"
|
>
|
||||||
></a-range-picker>
|
<div class="drag-handle"></div>
|
||||||
</template>
|
<a-card :bordered="false" class="card-container">
|
||||||
<div class='chart' style="padding: 12px">
|
<template #title>
|
||||||
<div :ref="el => { if (el) chartStates[type as ChartType].chartDom.value = el as HTMLElement }" style="height:400px;width:100%"></div>
|
<span class="no-drag">{{ item.i.toUpperCase() }}</span>
|
||||||
</div>
|
</template>
|
||||||
</a-card>
|
<template #extra>
|
||||||
</div>
|
<a-range-picker
|
||||||
|
v-model:value="rangePicker[item.i]"
|
||||||
</template>
|
:allow-clear="false"
|
||||||
</draggable>
|
bordered
|
||||||
|
:show-time="{ format: 'HH:mm:ss' }"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
value-format="x"
|
||||||
|
:placeholder="rangePicker.placeholder"
|
||||||
|
:ranges="rangePicker.ranges"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="() => fetchData(item.i)"
|
||||||
|
></a-range-picker>
|
||||||
|
</template>
|
||||||
|
<div class='chart'>
|
||||||
|
<div :ref="el => { if (el) chartStates[item.i].chartDom.value = el as HTMLElement }"></div>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
</GridItem>
|
||||||
|
</GridLayout>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.chart {
|
.charts-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 450px;
|
min-height: 600px; // 减小最小高度
|
||||||
}
|
}
|
||||||
|
|
||||||
.sortable-ghost {
|
.grid-item {
|
||||||
opacity: 0.5;
|
overflow: visible; // 改为 visible 以确保拖拽手柄可见
|
||||||
background: #c8ebfb;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sortable-drag {
|
.card-container {
|
||||||
opacity: 0.8;
|
height: 100%;
|
||||||
background: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
margin: -12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-lg-6 {
|
:deep(.ant-card-body) {
|
||||||
flex: 0 0 50%;
|
flex: 1;
|
||||||
max-width: 50%;
|
display: flex;
|
||||||
padding: 12px;
|
flex-direction: column;
|
||||||
|
padding: 12px !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart > div {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-row {
|
.control-row {
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px;
|
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
background-color: #f0f2f5;
|
background-color: #ffffff; // 改为白色背景
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03); // 添加轻微阴影
|
||||||
|
border: 1px solid #f0f0f0; // 添加浅色边框
|
||||||
}
|
}
|
||||||
|
|
||||||
.real-time-switch {
|
:deep(.ant-form-item) {
|
||||||
display: flex;
|
margin-bottom: 0;
|
||||||
align-items: center;
|
}
|
||||||
|
|
||||||
|
:deep(.ant-form-item-label) {
|
||||||
|
font-weight: 500; // 加粗标签文字
|
||||||
|
color: rgba(0, 0, 0, 0.85); // 调整标签颜色
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-label {
|
.switch-label {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: rgba(0, 0, 0, 0.65); // 调整文字颜色
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: #1890ff;
|
||||||
|
cursor: move;
|
||||||
|
z-index: 100; // 增加 z-index 确保在最上层
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 可以根据需要调整卡片标题的样式 */
|
||||||
|
:deep(.ant-card-head-title) {
|
||||||
|
padding-right: 25px; /* 为拖拽手柄留出空间 */
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user