fix:关键指标界面自定义布局功能

This commit is contained in:
zhongzm
2024-10-15 17:48:35 +08:00
parent d77c4e43d4
commit 700bff6e38
2 changed files with 190 additions and 95 deletions

View File

@@ -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",

View File

@@ -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>