feat:快速布局功能以及ws连接修复

This commit is contained in:
zhongzm
2024-10-22 16:01:53 +08:00
parent 46578ce97b
commit 208895c7d5
3 changed files with 408 additions and 201 deletions

View File

@@ -1072,7 +1072,19 @@ export default {
element:'Element', element:'Element',
granularity:'Granularity', granularity:'Granularity',
unit:'Unit', 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: { traceManage: {
analysis: { analysis: {
@@ -1288,7 +1300,7 @@ export default {
}, },
exportFile:{ exportFile:{
fileName:'File Source', 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", downTipErr: "Failed to get file",
deleteTip: "Confirm the delete file name is [{fileName}] File?", deleteTip: "Confirm the delete file name is [{fileName}] File?",
deleteTipErr: "Failed to delete file", deleteTipErr: "Failed to delete file",

View File

@@ -1072,7 +1072,19 @@ export default {
element:'元素', element:'元素',
granularity:'颗粒度', granularity:'颗粒度',
unit:'单位', unit:'单位',
} },
kpiKeyTarget:{
"fullWidthLayout":"全宽布局",
"twoColumnLayout":"两列布局",
"saveLayout": "保存布局",
"restoreSaved": "恢复布局",
"saveSuccess": " {name} 保存成功",
"restoreSavedSuccess": " {name} 恢复成功",
"noSavedLayout": "没有找到保存的布局 {name}",
"layout1": "布局1",
"layout2": "布局2",
"layout3": "布局3"
},
}, },
traceManage: { traceManage: {
analysis: { analysis: {
@@ -1127,7 +1139,7 @@ export default {
stopNotRun: "{title} 任务未运行", stopNotRun: "{title} 任务未运行",
}, },
task: { task: {
traceId: '跟踪编号', traceId: '跟踪编号',
trackType: '跟踪类型', trackType: '跟踪类型',
trackTypePlease: '请选择跟踪类型', trackTypePlease: '请选择跟踪类型',
creater: '创建人', creater: '创建人',
@@ -1288,7 +1300,7 @@ export default {
}, },
exportFile:{ exportFile:{
fileName:'文件来源', fileName:'文件来源',
downTip: "确认下载文件名为 【{fileName}】 文件?", downTip: "确认下载文件名为 【{fileName}】 文件?",
downTipErr: "文件获取失败", downTipErr: "文件获取失败",
deleteTip: "确认删除文件名为 【{fileName}】 文件?", deleteTip: "确认删除文件名为 【{fileName}】 文件?",
deleteTipErr: "文件删除失败", deleteTipErr: "文件删除失败",
@@ -2110,7 +2122,7 @@ export default {
hostSelectMore: "加载更多 {num}", hostSelectMore: "加载更多 {num}",
hostSelectHeader: "主机列表", hostSelectHeader: "主机列表",
}, },
ps:{ ps:{
realTimeHigh:"高", realTimeHigh:"高",
realTimeLow:"低", realTimeLow:"低",
realTimeRegular:"常规", realTimeRegular:"常规",

View File

@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { DragOutlined } from '@ant-design/icons-vue';
import { GridLayout, GridItem } from 'grid-layout-plus' 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';
@@ -15,9 +14,11 @@ import { message } 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 { SeriesOption } from 'echarts';
import { OptionsType, WS } from '@/plugins/ws-websocket'; import { OptionsType, WS } from '@/plugins/ws-websocket';
import { Select } from 'ant-design-vue';
import { useDebounceFn } from '@vueuse/core'; 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 { t, currentLocale } = useI18n();
const neInfoStore = useNeInfoStore(); const neInfoStore = useNeInfoStore();
@@ -25,19 +26,18 @@ const neInfoStore = useNeInfoStore();
//WebSocket连接 //WebSocket连接
const ws = ref<WS | null>(null); const ws = ref<WS | null>(null);
//添加实时数据开关状态 //添加实时数据状态
const realTimeEnabled = ref(false); const realTimeEnabled = ref(false);
//实时数据开关 //实时数据开关
const handleRealTimeSwitch = (checked: any) => { const handleRealTimeSwitch = (checked: any) => {
fnRealTimeSwitch(!!checked); fnRealTimeSwitch(!!checked);
}; };
// 定义所有网元类型
// 定义所有可能的网元类型
const ALL_NE_TYPES = ['ims', 'amf', 'udm', 'smf', 'pcf','upf','mme','mocngw','smsc','cbc','ausf'] as const; const ALL_NE_TYPES = ['ims', 'amf', 'udm', 'smf', 'pcf','upf','mme','mocngw','smsc','cbc','ausf'] as const;
type AllChartType = typeof ALL_NE_TYPES[number]; type AllChartType = typeof ALL_NE_TYPES[number];
// 在 ALL_NE_TYPES 定义之后添加 // 在 ALL_NE_TYPES 定义之后添加 小写转大写
const neTypeOptions = ALL_NE_TYPES.map(type => ({ const neTypeOptions = ALL_NE_TYPES.map(type => ({
label: type.toUpperCase(), label: type.toUpperCase(),
value: type value: type
@@ -46,28 +46,43 @@ const neTypeOptions = ALL_NE_TYPES.map(type => ({
// 使用 ref 来使 networkElementTypes 变为响应式,并使用 ALL_NE_TYPES 初始化 // 使用 ref 来使 networkElementTypes 变为响应式,并使用 ALL_NE_TYPES 初始化
const networkElementTypes = ref<AllChartType[]>([...ALL_NE_TYPES]); const networkElementTypes = ref<AllChartType[]>([...ALL_NE_TYPES]);
// 添加选择的网元类型 // 选择的网元类型
const selectedNeTypes = ref<AllChartType[]>([]); const selectedNeTypes = ref<AllChartType[]>([]);
// 添加一个临时状态存储最新的选择 // 临时状态 存储最新的选择
const latestSelectedTypes = ref<AllChartType[]>([]); const latestSelectedTypes = ref<AllChartType[]>([]);
//手动更新跟踪
const isManuallyUpdating = ref(false);
// 修改 watch 函数 // watch 监控函数
watch(selectedNeTypes, (newTypes) => { watch(selectedNeTypes, (newTypes) => {
console.log('Selected types changed:', newTypes); //if (isManuallyUpdating.value) return;
// 立即更新 UI
networkElementTypes.value = newTypes; networkElementTypes.value = newTypes;
// 更新临时状态
latestSelectedTypes.value = newTypes; latestSelectedTypes.value = newTypes;
// 触发防抖函数
debouncedUpdateCharts(); 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 }); }, { deep: true });
// 修改防抖函数 // 防抖函数
const debouncedUpdateCharts = useDebounceFn(() => { const debouncedUpdateCharts = useDebounceFn(() => {
// 比较当前选择和最新选择 // 比较当前选择和最新选择
if (JSON.stringify(latestSelectedTypes.value) !== JSON.stringify(selectedNeTypes.value)) { if (JSON.stringify(latestSelectedTypes.value) !== JSON.stringify(selectedNeTypes.value)) {
// 如果不致,以最新选择为准 // 如果不致,以最新选择为准
selectedNeTypes.value = latestSelectedTypes.value; selectedNeTypes.value = latestSelectedTypes.value;
} }
@@ -92,8 +107,6 @@ const debouncedUpdateCharts = useDebounceFn(() => {
} }
}); });
console.log('Updated chartOrder:', chartOrder.value);
// 保存选中的网元类型到本地存储 // 保存选中的网元类型到本地存储
localStorage.setItem('selectedNeTypes', JSON.stringify(newTypes)); localStorage.setItem('selectedNeTypes', JSON.stringify(newTypes));
@@ -103,10 +116,8 @@ const debouncedUpdateCharts = useDebounceFn(() => {
}); });
}, 300); }, 300);
// 修改 initCharts 函 // 改变状态时重新初始化图表
const initCharts = async () => { const initCharts = async () => {
console.log('Initializing charts for:', networkElementTypes.value);
// 清除不再需要的图表 // 清除不再需要的图表
Object.keys(chartStates).forEach((key) => { Object.keys(chartStates).forEach((key) => {
if (!networkElementTypes.value.includes(key as AllChartType)) { if (!networkElementTypes.value.includes(key as AllChartType)) {
@@ -123,7 +134,6 @@ const initCharts = async () => {
// 初始化或更新需要的图表 // 初始化或更新需要的图表
for (const type of networkElementTypes.value) { for (const type of networkElementTypes.value) {
console.log('Initializing chart for:', type);
if (!chartStates[type]) { if (!chartStates[type]) {
chartStates[type] = createChartState(); 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 { interface LayoutItem {
x: number; x: number;
y: number; y: number;
w: number; w: number;
h: number; h: number;
i: AllChartType; // 将 ChartType 改为 AllChartType i: AllChartType;
} }
type Layout = LayoutItem[]; type Layout = LayoutItem[];
@@ -157,7 +165,7 @@ type Layout = LayoutItem[];
//构建响应式数组储存图表类型数据 //构建响应式数组储存图表类型数据
const chartOrder = ref<Layout>( const chartOrder = ref<Layout>(
JSON.parse(localStorage.getItem('chartOrder') || 'null') || JSON.parse(localStorage.getItem('chartOrder') || 'null') ||
networkElementTypes.value.map((type, index) => ({ networkElementTypes.value.map((type, index) => ({//系统默认布局
x: index % 2 * 6, // 每行两个图表宽度为6 x: index % 2 * 6, // 每行两个图表宽度为6
y: Math.floor(index / 2) * 4, // 每个图表据 4 个单位高度 y: Math.floor(index / 2) * 4, // 每个图表据 4 个单位高度
w: 6, // 宽度为6单位 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)); const filteredLayout = newLayout.filter(item => networkElementTypes.value.includes(item.i));
if (JSON.stringify(filteredLayout) !== JSON.stringify(chartOrder.value)) { if (JSON.stringify(filteredLayout) !== JSON.stringify(chartOrder.value)) {
chartOrder.value = filteredLayout; chartOrder.value = filteredLayout;
// 保存布局到 localStorage
localStorage.setItem('chartOrder', JSON.stringify(chartOrder.value)); localStorage.setItem('chartOrder', JSON.stringify(chartOrder.value));
nextTick(() => { nextTick(() => {
chartOrder.value.forEach((item) => { chartOrder.value.forEach((item) => {
const state = chartStates[item.i]; const state = chartStates[item.i];
if (state?.chart.value) { if (state?.chart.value) {
state.chart.value.resize(); 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();
} }
}); });
}); });
} }
}; }, { deep: true });
// 监听 chartOrder 的变化
watch(chartOrder, () => {
nextTick(() => {
Object.values(chartStates).forEach(state => {
if (state.chart.value) {
state.chart.value.resize();
}
});
});
});
// 定义表格状态类型 // 定义表格状态类型
type TableStateType = { type TableStateType = {
@@ -204,7 +219,6 @@ type TableStateType = {
selectedRowKeys: (string | number)[]; selectedRowKeys: (string | number)[];
}; };
// 创建可复用的状态 // 创建可复用的状态
const createChartState = () => { const createChartState = () => {
const chartDom = ref<HTMLElement | null>(null); const chartDom = ref<HTMLElement | null>(null);
@@ -229,7 +243,7 @@ const createChartState = () => {
}; };
}; };
// 为每种图表类型创建 // 图表类型状
const chartStates: Record<AllChartType, ReturnType<typeof createChartState>> = Object.fromEntries( const chartStates: Record<AllChartType, ReturnType<typeof createChartState>> = Object.fromEntries(
networkElementTypes.value.map(type => [type, createChartState()]) networkElementTypes.value.map(type => [type, createChartState()])
) as Record<AllChartType, ReturnType<typeof createChartState>>; ) as Record<AllChartType, ReturnType<typeof createChartState>>;
@@ -240,17 +254,13 @@ interface RangePicker extends Record<AllChartType, [string, string]> {
ranges: Record<string, [Dayjs, Dayjs]>; ranges: Record<string, [Dayjs, Dayjs]>;
} }
// 创建日期选择器状态 // 日期选择器状态
const rangePicker = reactive<RangePicker>({ const rangePicker = reactive<RangePicker>({
...Object.fromEntries(networkElementTypes.value.map(type => [ ...Object.fromEntries(networkElementTypes.value.map(type => [
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().startOf('day').valueOf().toString(), // 0 点 0 分 0 秒
dayjs().valueOf().toString() // 当前时间 dayjs().valueOf().toString() // 此时
] ]
])) as Record<AllChartType, [string, string]>, ])) as Record<AllChartType, [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],
@@ -262,7 +272,7 @@ const rangePicker = reactive<RangePicker>({
} as Record<string, [Dayjs, Dayjs]>, } as Record<string, [Dayjs, Dayjs]>,
}); });
// 创建可复用的图表初始化函数 // 可复用的图表初始化函数
const initChart = (type: AllChartType) => { const initChart = (type: AllChartType) => {
const tryInit = (retries = 3) => { const tryInit = (retries = 3) => {
nextTick(() => { nextTick(() => {
@@ -297,7 +307,7 @@ const initChart = (type: AllChartType) => {
xAxis: { xAxis: {
type: 'category', type: 'category',
boundaryGap: false, boundaryGap: false,
data: [], data: state.chartDataXAxisData,
}, },
yAxis: { yAxis: {
type: 'value', type: 'value',
@@ -312,7 +322,7 @@ const initChart = (type: AllChartType) => {
color: '#646A73', color: '#646A73',
}, },
icon: 'circle', icon: 'circle',
selected: {}, selected: state.chartLegendSelected,
}, },
grid: { grid: {
left: '10%', left: '10%',
@@ -330,10 +340,10 @@ const initChart = (type: AllChartType) => {
end: 100, end: 100,
}, },
], ],
series: [], series: state.chartDataYSeriesData as SeriesOption[],
}; };
state.chart.value.setOption(option); state.chart.value.setOption(option);
state.chart.value.resize(); // 确保图表正确调整大小 state.chart.value.resize();
// 创建 ResizeObserver 实例 // 创建 ResizeObserver 实例
if (state.observer.value) { if (state.observer.value) {
@@ -345,7 +355,7 @@ const initChart = (type: AllChartType) => {
} }
}); });
// 开始观察图表容器 // 观察图表容器
state.observer.value.observe(container); state.observer.value.observe(container);
}); });
}; };
@@ -353,11 +363,9 @@ const initChart = (type: AllChartType) => {
tryInit(); tryInit();
}; };
// 可复用的数据获取函数
// 可复用的数据获函数
const fetchData = async (type: AllChartType) => { const fetchData = async (type: AllChartType) => {
const state = chartStates[type]; // 直使用 type const state = chartStates[type]; // 直使用 type
const neId = '001'; const neId = '001';
state.tableState.loading = true; state.tableState.loading = true;
try { try {
@@ -386,31 +394,37 @@ const fetchData = async (type: AllChartType) => {
} }
}; };
//建立实时数连接 //实时数连接开关
function fnRealTimeSwitch(bool: boolean) { function fnRealTimeSwitch(bool: boolean) {
realTimeEnabled.value = bool; realTimeEnabled.value = bool;
if (bool) { if (bool) {
if(!ws.value){ if(!ws.value) {
ws.value = new WS(); ws.value = new WS();
} }
// 清空所有图表的现有数据
Object.values(chartStates).forEach(state => { Object.values(chartStates).forEach(state => {
state.tableState.seached = false; 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 = { const options: OptionsType = {
url: '/ws', url: '/ws',
params: { params: {
subGroupID: networkElementTypes.value.map(type => `10_${type.toUpperCase()}_001`).join(','), subGroupID: selectedNeTypes.value.map(type => `10_${type.toUpperCase()}_001`).join(','),
}, },
onmessage: wsMessage, onmessage: wsMessage,
onerror: wsError, onerror: wsError,
}; };
ws.value.connect(options); ws.value.connect(options);
} else if(ws.value){ } else if(ws.value) {
Object.values(chartStates).forEach(state => { Object.values(chartStates).forEach(state => {
state.tableState.seached = true; state.tableState.seached = true;
}); });
@@ -419,13 +433,13 @@ function fnRealTimeSwitch(bool: boolean) {
} }
} }
// 接收数据后错误回 // 接收数据后错误回
function wsError() { function wsError() {
message.error(t('common.websocketError')); message.error(t('common.websocketError'));
} }
// 修改 wsMessage 数 // 接收数据回调
function wsMessage(res: Record<string, any>) { function wsMessage(res: Record<string, any>) {
const { code, data } = res; const { code, data } = res;
if (code === RESULT_CODE_ERROR) { if (code === RESULT_CODE_ERROR) {
@@ -437,53 +451,55 @@ function wsMessage(res: Record<string, any>) {
return; return;
} }
networkElementTypes.value.forEach((type) => { const neType = data.groupId.split('_')[1].toLowerCase() as AllChartType;
const state = chartStates[type];
const kpiEvent:any = data.data[type.toUpperCase()];
if (kpiEvent) { const state = chartStates[neType];
if (kpiEvent.timeGroup) { if (!state) {
const newTime = parseDateToStr(+kpiEvent.timeGroup); console.warn(`No chart state found for ${neType}`);
state.chartDataXAxisData.push(newTime); return;
if (state.chartDataXAxisData.length > 100) { }
state.chartDataXAxisData.shift();
}
// 使用 appendData 方法追加数据 const kpiEvent = data.data;
state.chartDataYSeriesData.forEach(series => { if (!kpiEvent) {
if (kpiEvent[series.customKey as string] !== undefined) { console.warn(`No data found for ${neType}`);
const newValue = +kpiEvent[series.customKey as string]; return;
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();
}
}
});
// 更新 X 轴 const newTime = parseDateToStr(kpiEvent.timeGroup ? +kpiEvent.timeGroup : Date.now());
if (state.chart.value) {
state.chart.value.setOption({ // 只有在实时数据模式下才更新图表
xAxis: { data: state.chartDataXAxisData } 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'> { interface CustomSeriesOption extends Omit<LineSeriesOption, 'data'> {
customKey?: string; customKey?: string;
data: (number | LineDataItem)[]; data: Array<[string, number]>;
} }
// 创建可复用的图表渲染函数 // 创建可复用的图表渲染函数
const renderChart = (type: AllChartType) => { const renderChart = (type: AllChartType) => {
@@ -491,7 +507,7 @@ const renderChart = (type: AllChartType) => {
if (state.chart.value == null) { if (state.chart.value == null) {
return; return;
} }
// 置数据 // 置数据
state.chartLegendSelected = {}; state.chartLegendSelected = {};
state.chartDataXAxisData = []; state.chartDataXAxisData = [];
state.chartDataYSeriesData = []; state.chartDataYSeriesData = [];
@@ -522,7 +538,7 @@ const renderChart = (type: AllChartType) => {
for (const item of orgData) { for (const item of orgData) {
state.chartDataXAxisData.push(parseDateToStr(+item.timeGroup)); state.chartDataXAxisData.push(parseDateToStr(+item.timeGroup));
for (const series of state.chartDataYSeriesData) { 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', type: 'category',
boundaryGap: false, boundaryGap: false,
}, },
series: state.chartDataYSeriesData, series: state.chartDataYSeriesData as SeriesOption[],
dataZoom: [ dataZoom: [
{ {
type: 'inside', type: 'inside',
@@ -553,7 +569,7 @@ const renderChart = (type: AllChartType) => {
}; };
// 获取头数据 // 获网元指标数据
const fetchKPITitle = async (type: AllChartType) => { const fetchKPITitle = async (type: AllChartType) => {
const language = currentLocale.value.split('_')[0] === 'zh' ? 'cn' : currentLocale.value.split('_')[0]; const language = currentLocale.value.split('_')[0] === 'zh' ? 'cn' : currentLocale.value.split('_')[0];
try { try {
@@ -591,7 +607,7 @@ onMounted(async () => {
selectedNeTypes.value = parsedSelectedNeTypes; selectedNeTypes.value = parsedSelectedNeTypes;
networkElementTypes.value = parsedSelectedNeTypes; networkElementTypes.value = parsedSelectedNeTypes;
} else { } else {
// 如果没有保存的选中网元类型,则使用默认选择 // 如果没有保存的选中网元类型,则用默认选择
selectedNeTypes.value = [...DEFAULT_NE_TYPES]; selectedNeTypes.value = [...DEFAULT_NE_TYPES];
networkElementTypes.value = [...DEFAULT_NE_TYPES]; networkElementTypes.value = [...DEFAULT_NE_TYPES];
// 保存这个默认选择到本地存储 // 保存这个默认选择到本地存储
@@ -601,24 +617,22 @@ onMounted(async () => {
// 初始化或更新 chartOrder // 初始化或更新 chartOrder
const savedLayout = localStorage.getItem('chartOrder'); const savedLayout = localStorage.getItem('chartOrder');
if (savedLayout) { if (savedLayout) {
const parsedLayout = JSON.parse(savedLayout); chartOrder.value = JSON.parse(savedLayout).filter((item: LayoutItem) =>
// 只保留当前选中的网元类型的布局 networkElementTypes.value.includes(item.i)
chartOrder.value = parsedLayout.filter((item: LayoutItem) => networkElementTypes.value.includes(item.i)); );
} }
// 如果 chartOrder 为空或者不包含所有选中的网元,重新创建布局 // 如果 chartOrder 为空或者不包含所有选中的网元,添加缺失的网元
if (chartOrder.value.length === 0 || chartOrder.value.length !== networkElementTypes.value.length) { const missingTypes = networkElementTypes.value.filter(type => !chartOrder.value.some(item => item.i === type));
chartOrder.value = networkElementTypes.value.map((type, index) => ({ missingTypes.forEach((type) => {
x: index % 2 * 6, chartOrder.value.push({
y: Math.floor(index / 2) * 4, x: (chartOrder.value.length % 2) * 6,
y: Math.floor(chartOrder.value.length / 2) * 4,
w: 6, w: 6,
h: 4, h: 4,
i: type, i: type,
})); });
} });
console.log('Initialized networkElementTypes:', networkElementTypes.value);
console.log('Initialized chartOrder:', chartOrder.value);
await initCharts(); await initCharts();
}); });
@@ -636,27 +650,208 @@ onUnmounted(() => {
state.observer.value.disconnect(); 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> <template>
<PageContainer> <PageContainer>
<a-card :bordered="false" class="control-card"> <a-card :bordered="false" class="control-card">
<a-form layout="inline"> <a-form layout="horizontal">
<a-form-item> <a-row :gutter="16">
<a-switch <a-col :lg="4" :md="24" :xs="24">
v-model:checked="realTimeEnabled" <a-form-item :label="realTimeEnabled ? t('views.dashboard.cdr.realTimeDataStart') : t('views.dashboard.cdr.realTimeDataStop')">
@change="handleRealTimeSwitch" <a-switch
/> v-model:checked="realTimeEnabled"
</a-form-item> @change="handleRealTimeSwitch"
<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> </a-col>
<a-form-item :label="t('views.ne.common.neType')" class="ne-type-select"> <a-col :lg="12" :md="24" :xs="24">
<a-checkbox-group v-model:value="selectedNeTypes" :options="neTypeOptions" /> <a-form-item :label="t('views.ne.common.neType')" class="ne-type-select">
</a-form-item> <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-form>
</a-card> </a-card>
@@ -691,25 +886,25 @@ onUnmounted(() => {
drag-ignore-from=".no-drag" drag-ignore-from=".no-drag"
class="grid-item" 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"> <a-card :bordered="false" class="card-container">
<template #title> <template #title>
<div class="card-title"> <div class="card-header">
<DragOutlined /> <div class="card-title">
<span>{{ item.i.toUpperCase() }}</span> <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> </div>
</template> </template>
<div class='chart'> <div class='chart'>
@@ -750,16 +945,20 @@ onUnmounted(() => {
flex-direction: column; 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 { .card-title {
display: flex; display: flex;
align-items: center; align-items: center;
cursor: move; flex-shrink: 0;
.anticon {
margin-right: 8px;
font-size: 16px;
color: #1890ff;
}
span { span {
font-size: 16px; font-size: 16px;
@@ -784,33 +983,7 @@ onUnmounted(() => {
white-space: nowrap; 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 { :deep {
.ant-card-body { .ant-card-body {
@@ -840,7 +1013,17 @@ onUnmounted(() => {
} }
.ant-card-head { .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> </style>