feat:快速布局功能以及ws连接修复
This commit is contained in:
@@ -1072,7 +1072,19 @@ export default {
|
||||
element:'Element',
|
||||
granularity:'Granularity',
|
||||
unit:'Unit',
|
||||
}
|
||||
},
|
||||
kpiKeyTarget:{
|
||||
"fullWidthLayout":"Full Width",
|
||||
"twoColumnLayout":"Two Column",
|
||||
"saveLayout": "Save Layout",
|
||||
"restoreSaved": "Restore Layout",
|
||||
"saveSuccess": " '{name}' saved successfully",
|
||||
"restoreSavedSuccess": " '{name}' restored successfully",
|
||||
"noSavedLayout": "No saved layout found for '{name}'",
|
||||
"layout1": "Layout 1",
|
||||
"layout2": "Layout 2",
|
||||
"layout3": "Layout 3"
|
||||
},
|
||||
},
|
||||
traceManage: {
|
||||
analysis: {
|
||||
@@ -1288,7 +1300,7 @@ export default {
|
||||
},
|
||||
exportFile:{
|
||||
fileName:'File Source',
|
||||
downTip: "Confirm the download file name is [{fileName}] File?",
|
||||
downTip: "Confirm the download file name is [{fileName}] File?",
|
||||
downTipErr: "Failed to get file",
|
||||
deleteTip: "Confirm the delete file name is [{fileName}] File?",
|
||||
deleteTipErr: "Failed to delete file",
|
||||
|
||||
@@ -1072,7 +1072,19 @@ export default {
|
||||
element:'元素',
|
||||
granularity:'颗粒度',
|
||||
unit:'单位',
|
||||
}
|
||||
},
|
||||
kpiKeyTarget:{
|
||||
"fullWidthLayout":"全宽布局",
|
||||
"twoColumnLayout":"两列布局",
|
||||
"saveLayout": "保存布局",
|
||||
"restoreSaved": "恢复布局",
|
||||
"saveSuccess": " {name} 保存成功",
|
||||
"restoreSavedSuccess": " {name} 恢复成功",
|
||||
"noSavedLayout": "没有找到保存的布局 {name}",
|
||||
"layout1": "布局1",
|
||||
"layout2": "布局2",
|
||||
"layout3": "布局3"
|
||||
},
|
||||
},
|
||||
traceManage: {
|
||||
analysis: {
|
||||
@@ -1127,7 +1139,7 @@ export default {
|
||||
stopNotRun: "{title} 任务未运行",
|
||||
},
|
||||
task: {
|
||||
traceId: '跟踪编号',
|
||||
traceId: '跟踪编号',
|
||||
trackType: '跟踪类型',
|
||||
trackTypePlease: '请选择跟踪类型',
|
||||
creater: '创建人',
|
||||
@@ -1288,7 +1300,7 @@ export default {
|
||||
},
|
||||
exportFile:{
|
||||
fileName:'文件来源',
|
||||
downTip: "确认下载文件名为 【{fileName}】 文件?",
|
||||
downTip: "确认下载文件名为 【{fileName}】 文件?",
|
||||
downTipErr: "文件获取失败",
|
||||
deleteTip: "确认删除文件名为 【{fileName}】 文件?",
|
||||
deleteTipErr: "文件删除失败",
|
||||
@@ -2110,7 +2122,7 @@ export default {
|
||||
hostSelectMore: "加载更多 {num}",
|
||||
hostSelectHeader: "主机列表",
|
||||
},
|
||||
ps:{
|
||||
ps:{
|
||||
realTimeHigh:"高",
|
||||
realTimeLow:"低",
|
||||
realTimeRegular:"常规",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { DragOutlined } from '@ant-design/icons-vue';
|
||||
import { GridLayout, GridItem } from 'grid-layout-plus'
|
||||
import * as echarts from 'echarts';
|
||||
import { PageContainer } from 'antdv-pro-layout';
|
||||
@@ -15,9 +14,11 @@ import { message } from 'ant-design-vue';
|
||||
import { ColumnsType } from 'ant-design-vue/es/table';
|
||||
import { generateColorRGBA } from '@/utils/generate-utils';
|
||||
import { LineSeriesOption } from 'echarts/charts';
|
||||
import { SeriesOption } from 'echarts';
|
||||
import { OptionsType, WS } from '@/plugins/ws-websocket';
|
||||
import { Select } from 'ant-design-vue';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { DownOutlined } from '@ant-design/icons-vue';
|
||||
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
|
||||
const { t, currentLocale } = useI18n();
|
||||
|
||||
const neInfoStore = useNeInfoStore();
|
||||
@@ -25,19 +26,18 @@ const neInfoStore = useNeInfoStore();
|
||||
//WebSocket连接
|
||||
const ws = ref<WS | null>(null);
|
||||
|
||||
//添加实时数据开关状态
|
||||
//添加实时数据状态
|
||||
const realTimeEnabled = ref(false);
|
||||
//实时数据开关
|
||||
const handleRealTimeSwitch = (checked: any) => {
|
||||
fnRealTimeSwitch(!!checked);
|
||||
};
|
||||
|
||||
|
||||
// 定义所有可能的网元类型
|
||||
// 定义所有网元类型
|
||||
const ALL_NE_TYPES = ['ims', 'amf', 'udm', 'smf', 'pcf','upf','mme','mocngw','smsc','cbc','ausf'] as const;
|
||||
type AllChartType = typeof ALL_NE_TYPES[number];
|
||||
|
||||
// 在 ALL_NE_TYPES 定义之后添加
|
||||
// 在 ALL_NE_TYPES 定义之后添加 小写转大写
|
||||
const neTypeOptions = ALL_NE_TYPES.map(type => ({
|
||||
label: type.toUpperCase(),
|
||||
value: type
|
||||
@@ -46,28 +46,43 @@ const neTypeOptions = ALL_NE_TYPES.map(type => ({
|
||||
// 使用 ref 来使 networkElementTypes 变为响应式,并使用 ALL_NE_TYPES 初始化
|
||||
const networkElementTypes = ref<AllChartType[]>([...ALL_NE_TYPES]);
|
||||
|
||||
// 添加选择的网元类型
|
||||
// 选择的网元类型
|
||||
const selectedNeTypes = ref<AllChartType[]>([]);
|
||||
|
||||
// 添加一个临时状态来存储最新的选择
|
||||
// 临时状态 存储最新的选择
|
||||
const latestSelectedTypes = ref<AllChartType[]>([]);
|
||||
//手动更新跟踪
|
||||
const isManuallyUpdating = ref(false);
|
||||
|
||||
// 修改 watch 函数
|
||||
// watch 监控函数
|
||||
watch(selectedNeTypes, (newTypes) => {
|
||||
console.log('Selected types changed:', newTypes);
|
||||
// 立即更新 UI
|
||||
//if (isManuallyUpdating.value) return;
|
||||
networkElementTypes.value = newTypes;
|
||||
// 更新临时状态
|
||||
latestSelectedTypes.value = newTypes;
|
||||
// 触发防抖函数
|
||||
debouncedUpdateCharts();
|
||||
|
||||
// 更新 WebSocket 订阅
|
||||
if (realTimeEnabled.value && ws.value) {
|
||||
// 关闭现有连接
|
||||
ws.value.close();
|
||||
// 创建新的连接和更新后的订
|
||||
const options: OptionsType = {
|
||||
url: '/ws',
|
||||
params: {
|
||||
subGroupID: newTypes.map(type => `10_${type.toUpperCase()}_001`).join(','),
|
||||
},
|
||||
onmessage: wsMessage,
|
||||
onerror: wsError,
|
||||
};
|
||||
ws.value.connect(options);
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// 修改防抖函数
|
||||
// 防抖函数
|
||||
const debouncedUpdateCharts = useDebounceFn(() => {
|
||||
// 比较当前选择和最新选择
|
||||
if (JSON.stringify(latestSelectedTypes.value) !== JSON.stringify(selectedNeTypes.value)) {
|
||||
// 如果不致,以最新选择为准
|
||||
// 如果不一致,以最新选择为准
|
||||
selectedNeTypes.value = latestSelectedTypes.value;
|
||||
}
|
||||
|
||||
@@ -92,8 +107,6 @@ const debouncedUpdateCharts = useDebounceFn(() => {
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Updated chartOrder:', chartOrder.value);
|
||||
|
||||
// 保存选中的网元类型到本地存储
|
||||
localStorage.setItem('selectedNeTypes', JSON.stringify(newTypes));
|
||||
|
||||
@@ -103,10 +116,8 @@ const debouncedUpdateCharts = useDebounceFn(() => {
|
||||
});
|
||||
}, 300);
|
||||
|
||||
// 修改 initCharts 函数
|
||||
// 改变状态时重新初始化图表 数
|
||||
const initCharts = async () => {
|
||||
console.log('Initializing charts for:', networkElementTypes.value);
|
||||
|
||||
// 清除不再需要的图表
|
||||
Object.keys(chartStates).forEach((key) => {
|
||||
if (!networkElementTypes.value.includes(key as AllChartType)) {
|
||||
@@ -123,7 +134,6 @@ const initCharts = async () => {
|
||||
|
||||
// 初始化或更新需要的图表
|
||||
for (const type of networkElementTypes.value) {
|
||||
console.log('Initializing chart for:', type);
|
||||
if (!chartStates[type]) {
|
||||
chartStates[type] = createChartState();
|
||||
}
|
||||
@@ -137,19 +147,17 @@ const initCharts = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Finished initializing charts');
|
||||
|
||||
// 保存更新后的布局
|
||||
localStorage.setItem('chartOrder', JSON.stringify(chartOrder.value));
|
||||
// 不要在这里保存布局,因为这可能会覆盖我们刚刚设置的布局
|
||||
// localStorage.setItem('chartOrder', JSON.stringify(chartOrder.value));
|
||||
};
|
||||
|
||||
// 添加类型定义
|
||||
// 位置类型定义(记录布局)
|
||||
interface LayoutItem {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
i: AllChartType; // 将 ChartType 改为 AllChartType
|
||||
i: AllChartType;
|
||||
}
|
||||
|
||||
type Layout = LayoutItem[];
|
||||
@@ -157,7 +165,7 @@ type Layout = LayoutItem[];
|
||||
//构建响应式数组储存图表类型数据
|
||||
const chartOrder = ref<Layout>(
|
||||
JSON.parse(localStorage.getItem('chartOrder') || 'null') ||
|
||||
networkElementTypes.value.map((type, index) => ({
|
||||
networkElementTypes.value.map((type, index) => ({//系统默认布局
|
||||
x: index % 2 * 6, // 每行两个图表,宽度为6
|
||||
y: Math.floor(index / 2) * 4, // 每个图表据 4 个单位高度
|
||||
w: 6, // 宽度为6单位
|
||||
@@ -166,34 +174,41 @@ const chartOrder = ref<Layout>(
|
||||
}))
|
||||
);
|
||||
|
||||
// 改变布局触发更新
|
||||
const handleLayoutUpdated = (newLayout: Layout) => {
|
||||
// 监听带防抖
|
||||
const handleLayoutUpdated = useDebounceFn((newLayout: Layout) => {
|
||||
if (isManuallyUpdating.value) {
|
||||
return;
|
||||
}
|
||||
const filteredLayout = newLayout.filter(item => networkElementTypes.value.includes(item.i));
|
||||
if (JSON.stringify(filteredLayout) !== JSON.stringify(chartOrder.value)) {
|
||||
chartOrder.value = filteredLayout;
|
||||
// 保存布局到 localStorage
|
||||
localStorage.setItem('chartOrder', JSON.stringify(chartOrder.value));
|
||||
nextTick(() => {
|
||||
chartOrder.value.forEach((item) => {
|
||||
const state = chartStates[item.i];
|
||||
if (state?.chart.value) {
|
||||
state.chart.value.resize();
|
||||
renderChart(item.i);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.log('No change in layout, skipping update');
|
||||
}
|
||||
}, 200); // 200ms 的防抖时间
|
||||
|
||||
// 监听 chartOrder 的变化
|
||||
watch(chartOrder, (newOrder, oldOrder) => {
|
||||
if (JSON.stringify(newOrder) !== JSON.stringify(oldOrder)) {
|
||||
nextTick(() => {
|
||||
Object.values(chartStates).forEach(state => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}, { deep: true });
|
||||
|
||||
// 定义表格状态类型
|
||||
type TableStateType = {
|
||||
@@ -204,7 +219,6 @@ type TableStateType = {
|
||||
selectedRowKeys: (string | number)[];
|
||||
};
|
||||
|
||||
|
||||
// 创建可复用的状态
|
||||
const createChartState = () => {
|
||||
const chartDom = ref<HTMLElement | null>(null);
|
||||
@@ -229,7 +243,7 @@ const createChartState = () => {
|
||||
};
|
||||
};
|
||||
|
||||
// 为每种图表类型创建状
|
||||
// 图表类型状态
|
||||
const chartStates: Record<AllChartType, ReturnType<typeof createChartState>> = Object.fromEntries(
|
||||
networkElementTypes.value.map(type => [type, createChartState()])
|
||||
) as Record<AllChartType, ReturnType<typeof createChartState>>;
|
||||
@@ -240,17 +254,13 @@ interface RangePicker extends Record<AllChartType, [string, string]> {
|
||||
ranges: Record<string, [Dayjs, Dayjs]>;
|
||||
}
|
||||
|
||||
// 创建日期选择器状态
|
||||
// 日期选择器状态
|
||||
const rangePicker = reactive<RangePicker>({
|
||||
...Object.fromEntries(networkElementTypes.value.map(type => [
|
||||
type,
|
||||
// [
|
||||
// dayjs('2024-09-20 00:00:00').valueOf().toString(),//拟数据的日期设2024.9.20
|
||||
// dayjs('2024-09-20 23:59:59').valueOf().toString()
|
||||
// ]
|
||||
[
|
||||
dayjs().startOf('day').valueOf().toString(), // 天 0 点 0 分 0 秒
|
||||
dayjs().valueOf().toString() // 当前时间
|
||||
dayjs().startOf('day').valueOf().toString(), // 0 点 0 分 0 秒
|
||||
dayjs().valueOf().toString() // 此时
|
||||
]
|
||||
])) as Record<AllChartType, [string, string]>,
|
||||
placeholder: [t('views.monitor.monitor.startTime'), t('views.monitor.monitor.endTime')] as [string, string],
|
||||
@@ -262,7 +272,7 @@ const rangePicker = reactive<RangePicker>({
|
||||
} as Record<string, [Dayjs, Dayjs]>,
|
||||
});
|
||||
|
||||
// 创建可复用的图表初始化函数
|
||||
// 可复用的图表初始化函数
|
||||
const initChart = (type: AllChartType) => {
|
||||
const tryInit = (retries = 3) => {
|
||||
nextTick(() => {
|
||||
@@ -297,7 +307,7 @@ const initChart = (type: AllChartType) => {
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: [],
|
||||
data: state.chartDataXAxisData,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
@@ -312,7 +322,7 @@ const initChart = (type: AllChartType) => {
|
||||
color: '#646A73',
|
||||
},
|
||||
icon: 'circle',
|
||||
selected: {},
|
||||
selected: state.chartLegendSelected,
|
||||
},
|
||||
grid: {
|
||||
left: '10%',
|
||||
@@ -330,10 +340,10 @@ const initChart = (type: AllChartType) => {
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
series: [],
|
||||
series: state.chartDataYSeriesData as SeriesOption[],
|
||||
};
|
||||
state.chart.value.setOption(option);
|
||||
state.chart.value.resize(); // 确保图表正确调整大小
|
||||
state.chart.value.resize();
|
||||
|
||||
// 创建 ResizeObserver 实例
|
||||
if (state.observer.value) {
|
||||
@@ -345,7 +355,7 @@ const initChart = (type: AllChartType) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 开始观察图表容器
|
||||
// 观察图表容器
|
||||
state.observer.value.observe(container);
|
||||
});
|
||||
};
|
||||
@@ -353,11 +363,9 @@ const initChart = (type: AllChartType) => {
|
||||
tryInit();
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 可复用的数据获函数
|
||||
// 可复用的数据获取函数
|
||||
const fetchData = async (type: AllChartType) => {
|
||||
const state = chartStates[type]; // 直接使用 type
|
||||
const state = chartStates[type]; // 直使用 type
|
||||
const neId = '001';
|
||||
state.tableState.loading = true;
|
||||
try {
|
||||
@@ -386,31 +394,37 @@ const fetchData = async (type: AllChartType) => {
|
||||
}
|
||||
};
|
||||
|
||||
//建立实时数据连接
|
||||
//实时数连接开关
|
||||
function fnRealTimeSwitch(bool: boolean) {
|
||||
|
||||
realTimeEnabled.value = bool;
|
||||
if (bool) {
|
||||
if(!ws.value){
|
||||
|
||||
if(!ws.value) {
|
||||
ws.value = new WS();
|
||||
}
|
||||
// 清空所有图表的现有数据
|
||||
Object.values(chartStates).forEach(state => {
|
||||
state.tableState.seached = false;
|
||||
state.chartDataXAxisData = [];
|
||||
state.chartDataYSeriesData.forEach(series => {
|
||||
series.data = [];
|
||||
});
|
||||
if (state.chart.value) {
|
||||
state.chart.value.setOption({
|
||||
xAxis: { data: [] },
|
||||
series: state.chartDataYSeriesData
|
||||
});
|
||||
}
|
||||
});
|
||||
// 建立连接
|
||||
const options: OptionsType = {
|
||||
url: '/ws',
|
||||
params: {
|
||||
subGroupID: networkElementTypes.value.map(type => `10_${type.toUpperCase()}_001`).join(','),
|
||||
subGroupID: selectedNeTypes.value.map(type => `10_${type.toUpperCase()}_001`).join(','),
|
||||
},
|
||||
onmessage: wsMessage,
|
||||
onerror: wsError,
|
||||
|
||||
};
|
||||
ws.value.connect(options);
|
||||
} else if(ws.value){
|
||||
|
||||
} else if(ws.value) {
|
||||
Object.values(chartStates).forEach(state => {
|
||||
state.tableState.seached = true;
|
||||
});
|
||||
@@ -419,13 +433,13 @@ function fnRealTimeSwitch(bool: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
// 接收数据后错误回
|
||||
// 接收数据后错误回调
|
||||
function wsError() {
|
||||
|
||||
message.error(t('common.websocketError'));
|
||||
}
|
||||
|
||||
// 修改 wsMessage 数
|
||||
// 接收数据回调
|
||||
function wsMessage(res: Record<string, any>) {
|
||||
const { code, data } = res;
|
||||
if (code === RESULT_CODE_ERROR) {
|
||||
@@ -437,53 +451,55 @@ function wsMessage(res: Record<string, any>) {
|
||||
return;
|
||||
}
|
||||
|
||||
networkElementTypes.value.forEach((type) => {
|
||||
const state = chartStates[type];
|
||||
const kpiEvent:any = data.data[type.toUpperCase()];
|
||||
const neType = data.groupId.split('_')[1].toLowerCase() as AllChartType;
|
||||
|
||||
if (kpiEvent) {
|
||||
if (kpiEvent.timeGroup) {
|
||||
const newTime = parseDateToStr(+kpiEvent.timeGroup);
|
||||
state.chartDataXAxisData.push(newTime);
|
||||
if (state.chartDataXAxisData.length > 100) {
|
||||
state.chartDataXAxisData.shift();
|
||||
}
|
||||
const state = chartStates[neType];
|
||||
if (!state) {
|
||||
console.warn(`No chart state found for ${neType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 appendData 方法追加数据
|
||||
state.chartDataYSeriesData.forEach(series => {
|
||||
if (kpiEvent[series.customKey as string] !== undefined) {
|
||||
const newValue = +kpiEvent[series.customKey as string];
|
||||
if (state.chart.value) {
|
||||
state.chart.value.appendData({
|
||||
seriesIndex: state.chartDataYSeriesData.indexOf(series),
|
||||
data: [[newTime, newValue]]
|
||||
});
|
||||
}
|
||||
// 保持数据长度不超过100
|
||||
if (series.data.length > 100) {
|
||||
series.data.shift();
|
||||
}
|
||||
}
|
||||
});
|
||||
const kpiEvent = data.data;
|
||||
if (!kpiEvent) {
|
||||
console.warn(`No data found for ${neType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新 X 轴
|
||||
if (state.chart.value) {
|
||||
state.chart.value.setOption({
|
||||
xAxis: { data: state.chartDataXAxisData }
|
||||
});
|
||||
const newTime = parseDateToStr(kpiEvent.timeGroup ? +kpiEvent.timeGroup : Date.now());
|
||||
|
||||
// 只有在实时数据模式下才更新图表
|
||||
if (realTimeEnabled.value) {
|
||||
// 更新X轴数据
|
||||
state.chartDataXAxisData.push(newTime);
|
||||
if (state.chartDataXAxisData.length > 100) {
|
||||
state.chartDataXAxisData.shift();
|
||||
}
|
||||
|
||||
// 更新每个系列的数据
|
||||
state.chartDataYSeriesData.forEach(series => {
|
||||
const kpiKey = series.customKey as string;
|
||||
if (kpiEvent[kpiKey] !== undefined) {
|
||||
const newValue = +kpiEvent[kpiKey];
|
||||
series.data.push([newTime, newValue]);
|
||||
if (series.data.length > 100) {
|
||||
series.data.shift();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 立即更新图表
|
||||
if (state.chart.value) {
|
||||
state.chart.value.setOption({
|
||||
xAxis: { data: state.chartDataXAxisData },
|
||||
series: state.chartDataYSeriesData as SeriesOption[]
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type LineDataItem = {
|
||||
value: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
interface CustomSeriesOption extends Omit<LineSeriesOption, 'data'> {
|
||||
customKey?: string;
|
||||
data: (number | LineDataItem)[];
|
||||
data: Array<[string, number]>;
|
||||
}
|
||||
// 创建可复用的图表渲染函数
|
||||
const renderChart = (type: AllChartType) => {
|
||||
@@ -491,7 +507,7 @@ const renderChart = (type: AllChartType) => {
|
||||
if (state.chart.value == null) {
|
||||
return;
|
||||
}
|
||||
// 重置数据
|
||||
// 置数据
|
||||
state.chartLegendSelected = {};
|
||||
state.chartDataXAxisData = [];
|
||||
state.chartDataYSeriesData = [];
|
||||
@@ -522,7 +538,7 @@ const renderChart = (type: AllChartType) => {
|
||||
for (const item of orgData) {
|
||||
state.chartDataXAxisData.push(parseDateToStr(+item.timeGroup));
|
||||
for (const series of state.chartDataYSeriesData) {
|
||||
series.data.push(+item[series.customKey as string]);
|
||||
series.data.push([parseDateToStr(+item.timeGroup), +item[series.customKey as string]]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,7 +551,7 @@ const renderChart = (type: AllChartType) => {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
},
|
||||
series: state.chartDataYSeriesData,
|
||||
series: state.chartDataYSeriesData as SeriesOption[],
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
@@ -553,7 +569,7 @@ const renderChart = (type: AllChartType) => {
|
||||
};
|
||||
|
||||
|
||||
// 获取头数据
|
||||
// 获网元指标数据
|
||||
const fetchKPITitle = async (type: AllChartType) => {
|
||||
const language = currentLocale.value.split('_')[0] === 'zh' ? 'cn' : currentLocale.value.split('_')[0];
|
||||
try {
|
||||
@@ -591,7 +607,7 @@ onMounted(async () => {
|
||||
selectedNeTypes.value = parsedSelectedNeTypes;
|
||||
networkElementTypes.value = parsedSelectedNeTypes;
|
||||
} else {
|
||||
// 如果没有保存的选中网元类型,则使用默认选择
|
||||
// 如果没有保存的选中网元类型,则用默认选择
|
||||
selectedNeTypes.value = [...DEFAULT_NE_TYPES];
|
||||
networkElementTypes.value = [...DEFAULT_NE_TYPES];
|
||||
// 保存这个默认选择到本地存储
|
||||
@@ -601,24 +617,22 @@ onMounted(async () => {
|
||||
// 初始化或更新 chartOrder
|
||||
const savedLayout = localStorage.getItem('chartOrder');
|
||||
if (savedLayout) {
|
||||
const parsedLayout = JSON.parse(savedLayout);
|
||||
// 只保留当前选中的网元类型的布局
|
||||
chartOrder.value = parsedLayout.filter((item: LayoutItem) => networkElementTypes.value.includes(item.i));
|
||||
chartOrder.value = JSON.parse(savedLayout).filter((item: LayoutItem) =>
|
||||
networkElementTypes.value.includes(item.i)
|
||||
);
|
||||
}
|
||||
|
||||
// 如果 chartOrder 为空或者不包含所有选中的网元,重新创建布局
|
||||
if (chartOrder.value.length === 0 || chartOrder.value.length !== networkElementTypes.value.length) {
|
||||
chartOrder.value = networkElementTypes.value.map((type, index) => ({
|
||||
x: index % 2 * 6,
|
||||
y: Math.floor(index / 2) * 4,
|
||||
// 如果 chartOrder 为空或者不包含所有选中的网元,添加缺失的网元
|
||||
const missingTypes = networkElementTypes.value.filter(type => !chartOrder.value.some(item => item.i === type));
|
||||
missingTypes.forEach((type) => {
|
||||
chartOrder.value.push({
|
||||
x: (chartOrder.value.length % 2) * 6,
|
||||
y: Math.floor(chartOrder.value.length / 2) * 4,
|
||||
w: 6,
|
||||
h: 4,
|
||||
i: type,
|
||||
}));
|
||||
}
|
||||
|
||||
console.log('Initialized networkElementTypes:', networkElementTypes.value);
|
||||
console.log('Initialized chartOrder:', chartOrder.value);
|
||||
});
|
||||
});
|
||||
|
||||
await initCharts();
|
||||
});
|
||||
@@ -636,27 +650,208 @@ onUnmounted(() => {
|
||||
state.observer.value.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// 不需要显式取消防抖函数
|
||||
});
|
||||
</script>
|
||||
|
||||
const isSaving = ref(false);
|
||||
const isRestoring = ref(false);
|
||||
//保存布局按钮
|
||||
const handleSaveLayout = async (info: MenuInfo) => {
|
||||
if (typeof info.key === 'string' && !isSaving.value) {
|
||||
isSaving.value = true;
|
||||
isRestoring.value = true;
|
||||
try {
|
||||
await saveCurrentLayout(info.key);
|
||||
} finally {
|
||||
setTimeout(()=>{
|
||||
isSaving.value = false;
|
||||
isRestoring.value = false;
|
||||
},700)
|
||||
}
|
||||
} else {
|
||||
console.error('Invalid layout key or operation in progress');
|
||||
}
|
||||
};
|
||||
//恢复布局按钮
|
||||
const handleRestoreLayout = async (info: MenuInfo) => {
|
||||
if (typeof info.key === 'string' && !isRestoring.value) {
|
||||
isRestoring.value = true;
|
||||
isSaving.value = true;
|
||||
try {
|
||||
await restoreSavedLayout(info.key);
|
||||
} finally {
|
||||
setTimeout(()=>{
|
||||
isSaving.value = false;
|
||||
isRestoring.value = false;
|
||||
},700)
|
||||
}
|
||||
} else {
|
||||
console.error('Invalid layout key or operation in progress');
|
||||
}
|
||||
};
|
||||
|
||||
// 保存当前布局
|
||||
const saveCurrentLayout = async (layoutName: string) => {
|
||||
const savedLayouts = JSON.parse(localStorage.getItem('savedLayouts') || '{}');
|
||||
savedLayouts[layoutName] = {
|
||||
layout: chartOrder.value,
|
||||
selectedTypes: selectedNeTypes.value
|
||||
};
|
||||
localStorage.setItem('savedLayouts', JSON.stringify(savedLayouts));
|
||||
message.success(t('layout.saveSuccess', { name: t(`layout.${layoutName}`) }));
|
||||
};
|
||||
//恢复已保存的布局
|
||||
const restoreSavedLayout = async (layoutName: string) => {
|
||||
const savedLayouts = JSON.parse(localStorage.getItem('savedLayouts') || '{}');
|
||||
const savedLayout = savedLayouts[layoutName];
|
||||
if (savedLayout && Array.isArray(savedLayout.selectedTypes) && Array.isArray(savedLayout.layout)) {
|
||||
selectedNeTypes.value = savedLayout.selectedTypes;
|
||||
networkElementTypes.value = savedLayout.selectedTypes;
|
||||
|
||||
// 更新布局
|
||||
chartOrder.value = savedLayout.layout.filter((item: LayoutItem) =>
|
||||
savedLayout.selectedTypes.includes(item.i)
|
||||
);
|
||||
|
||||
// 如果有当前选中的网元类型不在保存的布局中,添加它们
|
||||
const missingTypes = savedLayout.selectedTypes.filter((type: AllChartType) =>
|
||||
!chartOrder.value.some(item => item.i === type)
|
||||
);
|
||||
missingTypes.forEach((type: AllChartType) => {
|
||||
chartOrder.value.push({
|
||||
x: (chartOrder.value.length % 2) * 6,
|
||||
y: Math.floor(chartOrder.value.length / 2) * 4,
|
||||
w: 6,
|
||||
h: 4,
|
||||
i: type,
|
||||
});
|
||||
});
|
||||
|
||||
localStorage.setItem('chartOrder', JSON.stringify(chartOrder.value));
|
||||
localStorage.setItem('selectedNeTypes', JSON.stringify(selectedNeTypes.value));
|
||||
|
||||
await nextTick();
|
||||
await initCharts();
|
||||
|
||||
message.success(t('layout.restoreSavedSuccess', { name: t(`layout.${layoutName}`) }));
|
||||
} else {
|
||||
message.warning(t('layout.noSavedLayout', { name: t(`layout.${layoutName}`) }));
|
||||
}
|
||||
};
|
||||
|
||||
// 应用全宽布局
|
||||
const applyFullWidthLayout = () => {
|
||||
isManuallyUpdating.value = true;
|
||||
const newLayout = selectedNeTypes.value.map((type, index) => ({
|
||||
x: 0,
|
||||
y: index * 8,
|
||||
w: 12,
|
||||
h: 8,
|
||||
i: type,
|
||||
}));
|
||||
|
||||
nextTick(() => {
|
||||
Object.assign(chartOrder.value, newLayout)
|
||||
// chartOrder.value = newLayout;
|
||||
localStorage.setItem('chartOrder', JSON.stringify(newLayout));
|
||||
initCharts();
|
||||
// 直接更新图表,不调用 handleLayoutUpdated
|
||||
chartOrder.value.forEach((item) => {
|
||||
const state = chartStates[item.i];
|
||||
if (state?.chart.value) {
|
||||
state.chart.value.resize();
|
||||
renderChart(item.i);
|
||||
}
|
||||
});
|
||||
isManuallyUpdating.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// 应用两列布局
|
||||
const applyTwoColumnLayout = () => {
|
||||
isManuallyUpdating.value = true;
|
||||
const newLayout = selectedNeTypes.value.map((type, index) => ({
|
||||
x: (index % 2) * 6,
|
||||
y: Math.floor(index / 2) * 4,
|
||||
w: 6,
|
||||
h: 4,
|
||||
i: type,
|
||||
}));
|
||||
nextTick(() => {
|
||||
// chartOrder.value = newLayout;
|
||||
Object.assign(chartOrder.value, newLayout)
|
||||
localStorage.setItem('chartOrder', JSON.stringify(newLayout));
|
||||
initCharts();
|
||||
// 直接更新图表,不调用 handleLayoutUpdated
|
||||
chartOrder.value.forEach((item) => {
|
||||
const state = chartStates[item.i];
|
||||
if (state?.chart.value) {
|
||||
state.chart.value.resize();
|
||||
renderChart(item.i);
|
||||
}
|
||||
});
|
||||
isManuallyUpdating.value = false;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<PageContainer>
|
||||
<a-card :bordered="false" class="control-card">
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<a-switch
|
||||
v-model:checked="realTimeEnabled"
|
||||
@change="handleRealTimeSwitch"
|
||||
/>
|
||||
</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-item :label="t('views.ne.common.neType')" class="ne-type-select">
|
||||
<a-checkbox-group v-model:value="selectedNeTypes" :options="neTypeOptions" />
|
||||
</a-form-item>
|
||||
<a-form layout="horizontal">
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="4" :md="24" :xs="24">
|
||||
<a-form-item :label="realTimeEnabled ? t('views.dashboard.cdr.realTimeDataStart') : t('views.dashboard.cdr.realTimeDataStop')">
|
||||
<a-switch
|
||||
v-model:checked="realTimeEnabled"
|
||||
@change="handleRealTimeSwitch"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="12" :md="24" :xs="24">
|
||||
<a-form-item :label="t('views.ne.common.neType')" class="ne-type-select">
|
||||
<a-checkbox-group v-model:value="selectedNeTypes" :options="neTypeOptions" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="24" :xs="24">
|
||||
|
||||
<a-form-item>
|
||||
<a-dropdown :disabled="isSaving">
|
||||
<a-button :loading="isSaving">
|
||||
{{ t('views.perfManage.kpiKeyTarget.saveLayout') }}
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="handleSaveLayout">
|
||||
<a-menu-item key="layout1">{{ t('views.perfManage.kpiKeyTarget.layout1') }}</a-menu-item>
|
||||
<a-menu-item key="layout2">{{ t('views.perfManage.kpiKeyTarget.layout2') }}</a-menu-item>
|
||||
<a-menu-item key="layout3">{{ t('views.perfManage.kpiKeyTarget.layout3') }}</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<a-dropdown :disabled="isRestoring">
|
||||
<a-button :loading="isRestoring">
|
||||
{{ t('views.perfManage.kpiKeyTarget.restoreSaved') }}
|
||||
<DownOutlined />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="handleRestoreLayout">
|
||||
<a-menu-item key="layout1">{{ t('views.perfManage.kpiKeyTarget.layout1') }}</a-menu-item>
|
||||
<a-menu-item key="layout2">{{ t('views.perfManage.kpiKeyTarget.layout2') }}</a-menu-item>
|
||||
<a-menu-item key="layout3">{{ t('views.perfManage.kpiKeyTarget.layout3') }}</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<a-button @click="applyFullWidthLayout">
|
||||
{{ t('views.perfManage.kpiKeyTarget.fullWidthLayout') }}
|
||||
</a-button>
|
||||
|
||||
<a-button @click="applyTwoColumnLayout">
|
||||
{{ t('views.perfManage.kpiKeyTarget.twoColumnLayout') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
@@ -691,25 +886,25 @@ onUnmounted(() => {
|
||||
drag-ignore-from=".no-drag"
|
||||
class="grid-item"
|
||||
>
|
||||
<div class="date-picker-wrapper">
|
||||
<a-range-picker
|
||||
v-model:value="rangePicker[item.i]"
|
||||
:allow-clear="false"
|
||||
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>
|
||||
</div>
|
||||
<a-card :bordered="false" class="card-container">
|
||||
<template #title>
|
||||
<div class="card-title">
|
||||
<DragOutlined />
|
||||
<span>{{ item.i.toUpperCase() }}</span>
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<span>{{ item.i.toUpperCase() }}</span>
|
||||
</div>
|
||||
<a-range-picker
|
||||
v-model:value="rangePicker[item.i]"
|
||||
:allow-clear="false"
|
||||
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: 360px"
|
||||
@change="() => fetchData(item.i)"
|
||||
class="no-drag"
|
||||
></a-range-picker>
|
||||
</div>
|
||||
</template>
|
||||
<div class='chart'>
|
||||
@@ -750,16 +945,20 @@ onUnmounted(() => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
cursor: move;
|
||||
flex-wrap: wrap; /* 添加这行,允许内容换行 */
|
||||
gap: 8px; /* 添加这行,为换行时的元素之间添加间隔 */
|
||||
}
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: move;
|
||||
|
||||
.anticon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
color: #1890ff;
|
||||
}
|
||||
flex-shrink: 0;
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
@@ -784,33 +983,7 @@ onUnmounted(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: move;
|
||||
|
||||
.anticon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.ne-type-select {
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
|
||||
:deep(.ant-checkbox-group) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep {
|
||||
.ant-card-body {
|
||||
@@ -840,7 +1013,17 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.ant-card-head {
|
||||
cursor: move;
|
||||
padding: 8px 16px; /* 减小上下内边距 */
|
||||
}
|
||||
|
||||
.ant-card-head-title {
|
||||
padding: 8px 0; /* 减小上下内边距 */
|
||||
width: 100%; // 确保标题占据全宽
|
||||
}
|
||||
|
||||
.ant-range-picker {
|
||||
flex-shrink: 0; // 防止日期选择被压缩
|
||||
max-width: 100%; // 确保在小屏幕上不会溢出
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user