Files
fe.ems.vue3/src/views/perfManage/kpiKeyTarget/index.vue
2024-11-06 15:25:23 +08:00

1016 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { GridLayout, GridItem } from 'grid-layout-plus'
import * as echarts from 'echarts';
import { PageContainer } from 'antdv-pro-layout';
import { onMounted, reactive, ref, markRaw, nextTick, onUnmounted, watch } from 'vue';
import { RESULT_CODE_ERROR, RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { listKPIData, getKPITitle } from '@/api/perfManage/goldTarget';
import useI18n from '@/hooks/useI18n';
import { parseDateToStr } from '@/utils/date-utils';
import dayjs, { Dayjs } from 'dayjs';
import useNeInfoStore from '@/store/modules/neinfo';
import { message } from 'ant-design-vue';
import { generateColorRGBA } from '@/utils/generate-utils';
import { LineSeriesOption } from 'echarts/charts';
import { SeriesOption } from 'echarts';
import { OptionsType, WS } from '@/plugins/ws-websocket';
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();
//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 定义之后添加 小写转大写
const neTypeOptions = ALL_NE_TYPES.map(type => ({
label: type.toUpperCase(),
value: 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(selectedNeTypes, (newTypes) => {
//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;
}
const newTypes = selectedNeTypes.value;
// 更新 chartOrder
chartOrder.value = chartOrder.value.filter(item => newTypes.includes(item.i));
newTypes.forEach((type) => {
if (!chartOrder.value.some(item => item.i === type)) {
chartOrder.value.push({
x: (chartOrder.value.length % 2) * 6,
y: Math.floor(chartOrder.value.length / 2) * 4,
w: 6,
h: 4,
i: type,
});
}
// 确保 chartStates 包含新的网元类型
if (!chartStates[type]) {
chartStates[type] = createChartState();
}
});
// 保存选中的网元类型到本地存储
localStorage.setItem('selectedNeTypes', JSON.stringify(newTypes));
// 重新初始化图表
nextTick(() => {
initCharts();
});
}, 300);
// 改变状态时重新初始化图表 数
const initCharts = async () => {
// 清除不再需要的图表
Object.keys(chartStates).forEach((key) => {
if (!networkElementTypes.value.includes(key as AllChartType)) {
const state = chartStates[key as AllChartType];
if (state.chart.value) {
state.chart.value.dispose();
}
if (state.observer.value) {
state.observer.value.disconnect();
}
delete chartStates[key as AllChartType];
}
});
// 初始化或更新需要的图表
for (const type of networkElementTypes.value) {
if (!chartStates[type]) {
chartStates[type] = createChartState();
}
try {
await fetchKPITitle(type);
await nextTick();
initChart(type);
await fetchData(type);
} catch (error) {
console.error(`Error initializing chart for ${type}:`, error);
}
}
// 不要在这里保存布局,因为这可能会覆盖我们刚刚设置的布局
// localStorage.setItem('chartOrder', JSON.stringify(chartOrder.value));
};
// 位置类型定义(记录布局)
interface LayoutItem {
x: number;
y: number;
w: number;
h: number;
i: AllChartType;
}
type Layout = LayoutItem[];
//构建响应式数组储存图表类型数据
const chartOrder = ref<Layout>(
JSON.parse(localStorage.getItem('chartOrder') || 'null') ||
networkElementTypes.value.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 = 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.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();
}
});
});
}
}, { deep: true });
// 定义表格状态类型
type TableStateType = {
loading: boolean;
size: SizeType;
seached: boolean;
data: Record<string, any>[];
selectedRowKeys: (string | number)[];
};
// 创建可复用的状态
const createChartState = () => {
const chartDom = ref<HTMLElement | null>(null);
const chart = ref<echarts.ECharts | null>(null);
const observer = ref<ResizeObserver | null>(null);
return {
chartDom,
chart,
observer,
tableColumns: ref<any[]>([]),
tableState: reactive<TableStateType>({
loading: false,
size: 'small',
seached: true,
data: [],
selectedRowKeys: [],
}),
chartLegendSelected: {} as Record<string, boolean>,
chartDataXAxisData: [] as string[],
chartDataYSeriesData: [] as CustomSeriesOption[],
};
};
// 图表类型状态
const chartStates: Record<AllChartType, ReturnType<typeof createChartState>> = Object.fromEntries(
networkElementTypes.value.map(type => [type, createChartState()])
) as Record<AllChartType, ReturnType<typeof createChartState>>;
//日期选择器
interface RangePicker extends Record<AllChartType, [string, string]> {
placeholder: [string, string];
}
// 日期选择器状态
const rangePicker = reactive<RangePicker>({
...Object.fromEntries(networkElementTypes.value.map(type => [
type,
[
dayjs().startOf('hour').valueOf().toString(), // 当前小时内
dayjs().endOf('hour').valueOf().toString()
]
])) as Record<AllChartType, [string, string]>,
placeholder: [t('views.monitor.monitor.startTime'), t('views.monitor.monitor.endTime')] as [string, string],
});
// 可复用的图表初始化函数
const initChart = (type: AllChartType) => {
const tryInit = (retries = 3) => {
nextTick(() => {
const state = chartStates[type];
if (!state) {
console.warn(`Chart state for ${type} not found`);
return;
}
const container = state.chartDom.value;
if (!container) {
if (retries > 0) {
console.warn(`Chart container for ${type} not found, retrying... (${retries} attempts left)`);
setTimeout(() => tryInit(retries - 1), 100);
} else {
console.error(`Chart container for ${type} not found after multiple attempts`);
}
return;
}
if (state.chart.value) {
state.chart.value.dispose();
}
state.chart.value = markRaw(echarts.init(container));
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'axis',
position: function(pt: any) {
return [pt[0], '10%'];
},
},
xAxis: {
type: 'category',
boundaryGap: false,
data: state.chartDataXAxisData,
},
yAxis: {
type: 'value',
boundaryGap: [0, '100%'],
},
legend: {
type: 'scroll',
orient: 'horizontal',
top: -5,
itemWidth: 20,
textStyle: {
color: '#646A73',
},
icon: 'circle',
selected: state.chartLegendSelected,
},
grid: {
left: '10%',
right: '10%',
bottom: '15%',
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: state.chartDataYSeriesData as SeriesOption[],
};
state.chart.value.setOption(option);
state.chart.value.resize();
// 创建 ResizeObserver 实例
if (state.observer.value) {
state.observer.value.disconnect();
}
state.observer.value = new ResizeObserver(() => {
if (state.chart.value) {
state.chart.value.resize();
}
});
// 观察图表容器
state.observer.value.observe(container);
});
};
tryInit();
};
// 可复用的数据获取函数
const fetchData = async (type: AllChartType) => {
const state = chartStates[type]; // 直使用 type
const neId = '001';
state.tableState.loading = true;
try {
const dateRange = rangePicker[type] as [string, string];
const [startTime, endTime] = dateRange;
const res = await listKPIData({
neType: type.toUpperCase(),
neId,
startTime,
endTime,
sortField: 'timeGroup',
sortOrder: 'desc',
interval: 5,
});
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
state.tableState.data = res.data;
await nextTick(() => {
renderChart(type);
});
}
} catch (error) {
console.error(error);
message.error(t('common.getInfoFail'));
} finally {
state.tableState.loading = false;
}
};
//实时数连接开关
function fnRealTimeSwitch(bool: boolean) {
realTimeEnabled.value = bool;
if (bool) {
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: selectedNeTypes.value.map(type => `10_${type.toUpperCase()}_001`).join(','),
},
onmessage: wsMessage,
onerror: wsError,
};
ws.value.connect(options);
} else if(ws.value) {
Object.values(chartStates).forEach(state => {
state.tableState.seached = true;
});
ws.value.close();
ws.value = null;
}
}
// 接收数据后错误回调
function wsError() {
message.error(t('common.websocketError'));
}
// 接收数据回调
function wsMessage(res: Record<string, any>) {
const { code, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
if (!data?.groupId) {
return;
}
const neType = data.groupId.split('_')[1].toLowerCase() as AllChartType;
const state = chartStates[neType];
if (!state) {
console.warn(`No chart state found for ${neType}`);
return;
}
const kpiEvent = data.data;
if (!kpiEvent) {
console.warn(`No data found for ${neType}`);
return;
}
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[]
});
}
}
}
interface CustomSeriesOption extends Omit<LineSeriesOption, 'data'> {
customKey?: string;
data: Array<[string, number]>;
}
// 创建可复用的图表渲染函数
const renderChart = (type: AllChartType) => {
const state = chartStates[type];
if (state.chart.value == null) {
return;
}
// 置数据
state.chartLegendSelected = {};
state.chartDataXAxisData = [];
state.chartDataYSeriesData = [];
// 处理数据
for (const column of state.tableColumns.value) {
if (['neName', 'startIndex', 'timeGroup'].includes(column.key as string)) continue;
const color = generateColorRGBA();
state.chartDataYSeriesData.push({
name: column.title as string,
customKey: column.key as string,
type: 'line',
symbol: 'none',
sampling: 'lttb',
itemStyle: { color },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: color.replace(')', ',0.8)') },
{ offset: 1, color: color.replace(')', ',0.3)') },
]),
},
data: [],
} as CustomSeriesOption);
state.chartLegendSelected[column.title as string] = true;
}
const orgData = [...state.tableState.data].reverse();
for (const item of orgData) {
state.chartDataXAxisData.push(parseDateToStr(+item.timeGroup));
for (const series of state.chartDataYSeriesData) {
series.data.push([parseDateToStr(+item.timeGroup), +item[series.customKey as string]]);
}
}
// 更新图表
state.chart.value.setOption(
{
legend: { selected: state.chartLegendSelected },
xAxis: {
data: state.chartDataXAxisData,
type: 'category',
boundaryGap: false,
},
series: state.chartDataYSeriesData as SeriesOption[],
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
},
{ replaceMerge: ['xAxis', 'series'] }
);
};
// 获网元指标数据
const fetchKPITitle = async (type: AllChartType) => {
const language = currentLocale.value.split('_')[0] === 'zh' ? 'cn' : currentLocale.value.split('_')[0];
try {
const res = await getKPITitle(type.toUpperCase());
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
chartStates[type].tableColumns.value = res.data.map(item => ({
title: item[`${language}Title`],
dataIndex: item.kpiId,
align: 'left',
key: item.kpiId,
resizable: true,
width: 100,
minWidth: 150,
maxWidth: 300,
}));
}
} catch (error) {
console.error(error);
message.warning(t('common.getInfoFail'));
}
};
// 定义默认选择的网元类型
const DEFAULT_NE_TYPES: AllChartType[] = ['udm', 'amf', 'upf', 'ims'];
// 在 onMounted 钩子中
onMounted(async () => {
ws.value = new WS();
await neInfoStore.fnNelist();
// 从本地存储中读取选中的网元类型
const savedSelectedNeTypes = localStorage.getItem('selectedNeTypes');
if (savedSelectedNeTypes) {
const parsedSelectedNeTypes = JSON.parse(savedSelectedNeTypes) as AllChartType[];
selectedNeTypes.value = parsedSelectedNeTypes;
networkElementTypes.value = parsedSelectedNeTypes;
} else {
// 如果没有保存的选中网元类型,则用默认选择
selectedNeTypes.value = [...DEFAULT_NE_TYPES];
networkElementTypes.value = [...DEFAULT_NE_TYPES];
// 保存这个默认选择到本地存储
localStorage.setItem('selectedNeTypes', JSON.stringify(DEFAULT_NE_TYPES));
}
// 初始化或更新 chartOrder
const savedLayout = localStorage.getItem('chartOrder');
if (savedLayout) {
chartOrder.value = JSON.parse(savedLayout).filter((item: LayoutItem) =>
networkElementTypes.value.includes(item.i)
);
}
// 如果 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,
});
});
await initCharts();
});
// 在组件卸载时销毁图表实例
onUnmounted(() => {
if(ws.value && ws.value.state() === WebSocket.OPEN) {
ws.value.close();
}
Object.values(chartStates).forEach((state) => {
if (state.chart.value) {
state.chart.value.dispose();
}
if (state.observer.value) {
state.observer.value.disconnect();
}
});
});
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('views.perfManage.kpiKeyTarget.saveSuccess', { name: t(`views.perfManage.kpiKeyTarget.${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('views.perfManage.kpiKeyTarget.restoreSavedSuccess', { name: t(`views.perfManage.kpiKeyTarget.${layoutName}`)}));
} else {
message.warning(t('views.perfManage.kpiKeyTarget.noSavedLayout', { name: t(`views.perfManage.kpiKeyTarget.${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="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>
<GridLayout
v-model:layout="chartOrder"
:col-num="12"
:row-height="100"
:margin="[10, 10]"
:is-draggable="true"
:is-resizable="true"
:vertical-compact="true"
:use-css-transforms="true"
:responsive="true"
:prevent-collision="false"
@layout-updated="handleLayoutUpdated"
class="charts-container"
>
<GridItem
v-for="item in chartOrder.filter(i => networkElementTypes.includes(i.i))"
:key="item.i"
:x="item.x"
:y="item.y"
:w="item.w"
:h="item.h"
:i="item.i"
:min-w="4"
:min-h="3"
:is-draggable="true"
:is-resizable="true"
:resizable-handles="['se']"
drag-allow-from=".ant-card-head"
drag-ignore-from=".no-drag"
class="grid-item"
>
<a-card :bordered="false" class="card-container">
<template #title>
<div class="card-header">
<div class="card-title">
<span>{{ (item.i as string).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"
style="width: 360px"
@change="() => fetchData(item.i)"
class="no-drag"
></a-range-picker>
</div>
</template>
<div class='chart'>
<div :ref="el => { if (el && chartStates[item.i]) chartStates[item.i].chartDom.value = el as HTMLElement }"></div>
</div>
</a-card>
</GridItem>
</GridLayout>
</PageContainer>
</template>
<style lang="less" scoped>
.control-card {
margin-bottom: 16px;
}
.charts-container {
width: 100%;
min-height: 600px;
}
.grid-item {
overflow: visible;
position: relative;
}
.date-picker-wrapper {
position: absolute;
top: -40px;
left: 0;
right: 0;
z-index: 1;
}
.card-container {
height: 100%;
display: flex;
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;
flex-shrink: 0;
span {
font-size: 16px;
font-weight: 500;
}
}
.chart {
flex: 1;
min-height: 0;
> div {
width: 100%;
height: 100%;
}
}
.switch-label {
margin-left: 8px;
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
white-space: nowrap;
}
:deep(.ant-card-body) {
flex: 1;
display: flex;
flex-direction: column;
padding: 16px !important;
overflow: hidden;
}
:deep(.ant-form-item) {
margin-bottom: 0;
}
:deep(.ant-form-item-label) {
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
:deep(.anticon) {
font-size: 16px;
color: #1890ff;
}
:deep(.ant-select) {
min-width: 200px;
}
:deep(.ant-card-head) {
padding: 8px 16px; /* 减小上下内边距 */
}
:deep(.ant-card-head-title) {
padding: 8px 0; /* 减小上下内边距 */
width: 100%; // 确保标题占据全宽
}
:deep(.ant-range-picker) {
flex-shrink: 0; // 防止日期选择被压缩
max-width: 100%; // 确保在小屏幕上不会溢出
}
</style>