From 7d69d3c21df1eba6180dfb15d5e143b5bc598d2e Mon Sep 17 00:00:00 2001 From: zhongzm Date: Sat, 23 Nov 2024 17:01:41 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E6=A0=B7=E5=BC=8F=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=81=E6=98=8E=E6=9A=97=E4=B8=BB=E9=A2=98=E9=80=82?= =?UTF-8?q?=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/perfManage/kpiKeyTarget/index.vue | 809 ++++++++++++++++---- 1 file changed, 639 insertions(+), 170 deletions(-) diff --git a/src/views/perfManage/kpiKeyTarget/index.vue b/src/views/perfManage/kpiKeyTarget/index.vue index a4798631..d14640c5 100644 --- a/src/views/perfManage/kpiKeyTarget/index.vue +++ b/src/views/perfManage/kpiKeyTarget/index.vue @@ -20,7 +20,14 @@ import { TableColumnType } from 'ant-design-vue'; const { t, currentLocale } = useI18n(); const neInfoStore = useNeInfoStore(); - +//日期快捷选择 +const ranges = ref([ + {label:t('views.perfManage.customTarget.sixHoursAgo'),value:[dayjs().subtract(6, 'hours'), + dayjs(),]}, + {label:t('views.perfManage.customTarget.threeHoursAgo'),value:[dayjs().subtract(3, 'hours'), + dayjs(),]}, + {label:t('views.monitor.monitor.today'),value:[dayjs().startOf('day'), dayjs()]}, +]) //WebSocket连接 const ws = ref(null); @@ -51,26 +58,61 @@ const selectedNeTypes = ref([]); const latestSelectedTypes = ref([]); // watch 监控函数 -watch(selectedNeTypes, async (newTypes) => { - networkElementTypes.value = [...newTypes]; // 使用展开运算符创建新数组 +watch(selectedNeTypes, async (newTypes, oldTypes) => { + networkElementTypes.value = [...newTypes]; latestSelectedTypes.value = [...newTypes]; + // 获取被移除和新增的类型 + const removedTypes = oldTypes.filter(type => !newTypes.includes(type)); + const addedTypes = newTypes.filter(type => !oldTypes.includes(type)); + + // 清理被移除的图表状态 + removedTypes.forEach(type => { + const state = chartStates[type]; + if (state) { + if (state.chart.value) { + state.chart.value.dispose(); + } + if (state.observer.value) { + state.observer.value.disconnect(); + } + state.chart.value = null; + state.observer.value = null; + state.chartDataXAxisData = []; + state.chartDataYSeriesData = []; + state.tableState.data = []; + } + }); + // 确保每个选中的类型都有对应的图表状态 newTypes.forEach(type => { if (!chartStates[type]) { - chartStates[type] = createChartState(); + chartStates[type] = createChartState(type); } }); // 初始化新选中的图表 for (const type of newTypes) { try { - if (!chartStates[type].chart.value) { + // 只有当图表不存在或者是新增的类型时才初始化 + const needsInit = !chartStates[type].chart.value || addedTypes.includes(type); + + if (needsInit) { await fetchKPITitle(type); await nextTick(); await initChart(type); + + // 只有在非实时模式下且是新增的网元时才获取数据 + if (!realTimeEnabled.value && addedTypes.includes(type)) { + await fetchData(type); + } + } + + // 恢复选中状态 + if (selectedRows.value[type]) { + await nextTick(); + updateChartLegendSelect(type, selectedRows.value[type]); } - await fetchData(type); } catch (error) { console.error(`Error initializing chart for ${type}:`, error); } @@ -106,7 +148,7 @@ useDebounceFn(() => { // 确保 chartStates 包含新的网元类型 newTypes.forEach((type) => { if (!chartStates[type]) { - chartStates[type] = createChartState(); + chartStates[type] = createChartState(type); } }); @@ -120,6 +162,10 @@ useDebounceFn(() => { }, 300); // 改变状态时重新初始化图表 数 const initCharts = async () => { + // 保存当前的选中状态和图表状态 + const currentSelectedStates = { ...selectedRows.value }; + const savedChartStates = new Map(Object.entries(chartStates)); + // 清除不再需要的图表 Object.keys(chartStates).forEach((key) => { if (!networkElementTypes.value.includes(key as AllChartType)) { @@ -130,34 +176,39 @@ const initCharts = async () => { if (state.observer.value) { state.observer.value.disconnect(); } - delete chartStates[key as AllChartType]; + // 不要删除状态,只是清理图表实例 + state.chart.value = null; + state.observer.value = null; } }); // 初始化或更新需要的图表 for (const type of networkElementTypes.value) { - if (!chartStates[type]) { - chartStates[type] = createChartState(); - } try { + if (!chartStates[type]) { + // 检查是否有保存的状态 + const savedState = savedChartStates.get(type); + if (savedState) { + chartStates[type] = savedState; + } else { + chartStates[type] = createChartState(type); + } + } + await fetchKPITitle(type); await nextTick(); await initChart(type); await fetchData(type); + + // 恢复选中状态 + if (currentSelectedStates[type]) { + await nextTick(); + updateChartLegendSelect(type, currentSelectedStates[type]); + } } catch (error) { console.error(`Error initializing chart for ${type}:`, error); } } - - // 添加延时检查 - setTimeout(() => { - networkElementTypes.value.forEach(type => { - const state = chartStates[type]; - if (state && !state.chart.value) { - initChart(type); - } - }); - }, 200); }; // 定义表格状态类型 @@ -170,10 +221,12 @@ type TableStateType = { }; // 创建可复用的状态 -const createChartState = () => { +const createChartState = (neType: AllChartType) => { const chartDom = ref(null); const chart = ref(null); const observer = ref(null); + const currentTheme = ref(document.documentElement.getAttribute('data-theme')); + const themeObserver = ref(null); return { chartDom, @@ -190,12 +243,16 @@ const createChartState = () => { chartLegendSelected: {} as Record, chartDataXAxisData: [] as string[], chartDataYSeriesData: [] as CustomSeriesOption[], + seriesColors: reactive(new Map()), + currentTheme, + themeObserver, + neType, }; }; // 图表类型状态 const chartStates: Record> = Object.fromEntries( - networkElementTypes.value.map(type => [type, createChartState()]) + networkElementTypes.value.map(type => [type, createChartState(type)]) ) as Record>; //期择器 @@ -225,6 +282,9 @@ const initChart = (type: AllChartType) => { return; } + // 更新当前主题 + state.currentTheme.value = document.documentElement.getAttribute('data-theme'); + // 等待 DOM 更新 await nextTick(); @@ -239,68 +299,147 @@ const initChart = (type: AllChartType) => { return; } + // 确保旧的图表实例被正确销毁 if (state.chart.value) { state.chart.value.dispose(); + state.chart.value = null; } + // 创建新的图表实例 state.chart.value = markRaw(echarts.init(container)); - const option: echarts.EChartsOption = { - tooltip: { - trigger: 'axis', - position: function(pt: any) { - const [x, y] = pt; - const chartDom = state.chartDom.value; - if (!chartDom) return [x, y]; - const rect = chartDom.getBoundingClientRect(); - const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; - const viewportHeight = window.innerHeight; - const relativeY = rect.top + y - scrollTop; - if (relativeY + 100 > viewportHeight) { - return [x, '10%']; - } - return [x, y + 10]; - }, - axisPointer: { - type: 'line', - z: 0 - }, - className: `chart-tooltip-${type}`, - z: 1000, - extraCssText: 'z-index: 1000; pointer-events: none;', - confine: true, - }, - 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', - }, - show:false, - icon: 'circle', - selected: state.chartLegendSelected, - }, - grid: { - left: '10%', - right: '10%', - bottom: '15%', - }, - series: state.chartDataYSeriesData as SeriesOption[], - }; - state.chart.value.setOption(option); - state.chart.value.resize(); - // 创建 ResizeObserver 实例 + // 更新图表配置 + const updateChartTheme = (type: AllChartType) => { + if (!state.chart.value) return; + + const splitLineColor = getSplitLineColor(); + const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; + + const option = { + tooltip: { + trigger: 'axis', + position: function (point: number[], _params: any, _dom: HTMLElement, _rect: any, size: { viewSize: number[], contentSize: number[] }) { + const [x] = point; + const [viewWidth] = size.viewSize; + const [tooltipWidth] = size.contentSize; + + // 计算距离右边界的距离 + const rightSpace = viewWidth - x; + + // 如果右侧空间不足以显示 tooltip(加上20px的安全距离) + if (rightSpace < tooltipWidth + 20) { + // 向左显示,tooltip 右边界对齐鼠标位置 + return [x - tooltipWidth - 10, point[1]]; + } + + // 默认向右显示,tooltip 左边界对齐鼠标位置 + return [x + 10, point[1]]; + }, + axisPointer: { + type: 'line', + z: 0 + }, + className: `chart-tooltip-${type}`, + z: 1000, + extraCssText: 'z-index: 1000; pointer-events: none;', + confine: true, + backgroundColor: isDark ? 'rgba(48, 48, 48, 0.8)' : 'rgba(255, 255, 255, 0.9)', + borderColor: isDark ? '#555' : '#ddd', + textStyle: { + color: isDark ? '#CACADA' : '#333' + } + }, + xAxis: { + type: 'category', + boundaryGap: false, + data: state.chartDataXAxisData, + axisLabel: { + formatter: '{value}', + color: isDark ? '#CACADA' : '#333' + }, + splitLine: { + show: true, + lineStyle: { + color: splitLineColor + } + } + }, + yAxis: { + type: 'value', + boundaryGap: [0, '100%'], + axisLabel: { + formatter: '{value}', + color: isDark ? '#CACADA' : '#333' + }, + splitNumber: 5, + scale: true, + splitLine: { + show: true, + lineStyle: { + color: splitLineColor + } + } + }, + legend: { + type: 'scroll', + orient: 'horizontal', + top: -5, + itemWidth: 20, + textStyle: { + color: isDark ? '#CACADA' : '#646A73', + }, + show: false, + icon: 'circle', + selected: state.chartLegendSelected, + backgroundColor: isDark ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.9)', + borderColor: isDark ? '#555' : '#ddd', + }, + grid: { + left: '10%', + right: '10%', + bottom: '15%', + }, + series: state.chartDataYSeriesData.map(series => { + const color = state.seriesColors.get(series.customKey as string) || generateColorRGBA(); + return { + ...series, + itemStyle: { color }, + lineStyle: { 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)') }, + ]), + }, + }; + }) as SeriesOption[], + }; + + state.chart.value.setOption(option, true); + }; + + // 初始化时更新一次 + updateChartTheme(type); + + // 添加主题变化监听 + if (state.themeObserver.value) { + state.themeObserver.value.disconnect(); + } + + const observer = new MutationObserver(() => { + state.currentTheme.value = document.documentElement.getAttribute('data-theme'); + updateChartTheme(type); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-theme'] + }); + + // 正确地设置 ref 的值 + state.themeObserver.value = observer; + + // 重新绑定 ResizeObserver if (state.observer.value) { state.observer.value.disconnect(); } @@ -309,12 +448,7 @@ const initChart = (type: AllChartType) => { state.chart.value.resize(); } }); - state.observer.value.observe(container); - - // 确保在图表初始化后更新统计信息 - await nextTick(); - await renderChart(type); }); }; @@ -358,9 +492,10 @@ function fnRealTimeSwitch(bool: boolean) { if (!ws.value) { ws.value = new WS(); } - // 清空所有图表的现有数据 + // 清空所有图表的现有数据并设置 loading 状态 Object.values(chartStates).forEach(state => { state.tableState.seached = false; + state.tableState.loading = true; state.tableState.data = []; // 清空表格数据 state.chartDataXAxisData = []; state.chartDataYSeriesData.forEach(series => { @@ -387,14 +522,11 @@ function fnRealTimeSwitch(bool: boolean) { if (ws.value) { Object.values(chartStates).forEach(state => { state.tableState.seached = true; + state.tableState.loading = false; }); ws.value.close(); ws.value = null; - - // 重新获取历史数据 - selectedNeTypes.value.forEach(type => { - fetchData(type); - }); + // 关闭实时数据时保留当前数据,不清空也不重新获取 } } } @@ -430,6 +562,11 @@ function wsMessage(res: Record) { // 只有在实时数据模式下才更新图表 if (realTimeEnabled.value) { + // 如果是第一条数据,关闭 loading 状态 + if (state.tableState.data.length === 0) { + state.tableState.loading = false; + } + // 更新表格数据 state.tableState.data.unshift(kpiEvent); if (state.tableState.data.length > 100) { @@ -464,9 +601,9 @@ function wsMessage(res: Record) { // 更新统计信息 nextTick(() => { - const stats = getKpiStats(neType); + const stats = getKpiStats(state.neType); if (stats.length > 0) { - renderChart(neType); + renderChart(state.neType); } }); } @@ -476,11 +613,14 @@ interface CustomSeriesOption extends Omit { customKey?: string; data: Array<[string, number]>; } -// 创建可复用的图表渲染函数 +// 建可复用的图表渲染函数 const renderChart = async (type: AllChartType) => { const state = chartStates[type]; if (!state?.chart.value) return; + // 保存当前的选中状态 + const currentSelectedKpiIds = selectedRows.value[type] || []; + // 重置数据 state.chartLegendSelected = {}; state.chartDataXAxisData = []; @@ -489,14 +629,23 @@ const renderChart = async (type: AllChartType) => { // 处理数据 for (const column of state.tableColumns.value) { if (['neName', 'startIndex', 'timeGroup'].includes(column.key as string)) continue; - const color = generateColorRGBA(); - state.chartDataYSeriesData.push({ + + // 检查是否已有该指标的颜色,如果没有才生成新颜色 + let color = state.seriesColors.get(column.key as string); + if (!color) { + color = generateColorRGBA(); + state.seriesColors.set(column.key as string, color); + } + + const seriesData = { name: column.title as string, customKey: column.key as string, type: 'line', symbol: 'none', + smooth:0.6, sampling: 'lttb', itemStyle: { color }, + lineStyle: { color }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: color.replace(')', ',0.8)') }, @@ -504,7 +653,9 @@ const renderChart = async (type: AllChartType) => { ]), }, data: [], - } as CustomSeriesOption); + } as CustomSeriesOption; + + state.chartDataYSeriesData.push(seriesData); state.chartLegendSelected[column.title as string] = true; } @@ -517,14 +668,51 @@ const renderChart = async (type: AllChartType) => { } } + // 更新图表时考虑当前选中状态 + const legendSelected: Record = {}; + state.chartDataYSeriesData.forEach(series => { + const title = state.tableColumns.value.find(col => col.key === series.customKey)?.title || series.customKey; + if (typeof title === 'string') { + // 如果没有选中的指标或选中列表为空,显示所有指标 + // 否则只显示选中的指标 + legendSelected[title] = !currentSelectedKpiIds.length || + currentSelectedKpiIds.includes(series.customKey as string); + } + }); + // 更新图表 state.chart.value.setOption( { - legend: { selected: state.chartLegendSelected }, + legend: { + selected: legendSelected, + textStyle: { + color: document.documentElement.getAttribute('data-theme') === 'dark' + ? '#CACADA' + : '#646A73', + }, + backgroundColor: document.documentElement.getAttribute('data-theme') === 'dark' + ? 'rgba(0, 0, 0, 0.2)' + : 'rgba(255, 255, 255, 0.9)', + borderColor: document.documentElement.getAttribute('data-theme') === 'dark' + ? '#555' + : '#ddd', + }, xAxis: { data: state.chartDataXAxisData, type: 'category', boundaryGap: false, + axisLabel: { + formatter: '{value}', + color: document.documentElement.getAttribute('data-theme') === 'dark' + ? '#CACADA' + : '#333' + }, + splitLine: { + show: true, + lineStyle: { + color: getSplitLineColor() + } + } }, series: state.chartDataYSeriesData as SeriesOption[], }, @@ -539,7 +727,10 @@ const fetchKPITitle = async (type: AllChartType) => { try { const res = await getKPITitle(type.toUpperCase()); if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { - // 添加时间列作为第一列 + // 保存现有的颜色映 + const existingColors = new Map(chartStates[type].seriesColors); + + // 设置新的列 chartStates[type].tableColumns.value = [ { title: t('views.perfManage.kpiKeyTarget.time'), @@ -552,17 +743,28 @@ const fetchKPITitle = async (type: AllChartType) => { return dayjs(Number(text)).format('YYYY-MM-DD HH:mm:ss'); } }, - ...res.data.map(item => ({ - title: item[`${language}Title`], - dataIndex: item.kpiId, - align: 'left', - key: item.kpiId, - resizable: true, - width: 100, - minWidth: 150, - maxWidth: 300, - })) + ...res.data.map(item => { + const kpiId = item.kpiId; + // 如果没有现有的颜色,生成新的颜色 + if (!existingColors.has(kpiId)) { + existingColors.set(kpiId, generateColorRGBA()); + } + + return { + title: item[`${language}Title`], + dataIndex: kpiId, + align: 'left', + key: kpiId, + resizable: true, + width: 100, + minWidth: 150, + maxWidth: 300, + }; + }) ]; + + // 更新颜色映射 + chartStates[type].seriesColors = existingColors; } } catch (error) { console.error(error); @@ -573,7 +775,168 @@ const fetchKPITitle = async (type: AllChartType) => { // 定义默认选择的网元类型 const DEFAULT_NE_TYPES: AllChartType[] = ['udm', 'amf', 'upf', 'ims']; -// 在 onMounted 钩子中 +// 添加一个函数来获取当前主题下的网格线颜色 +const getSplitLineColor = () => { + return document.documentElement.getAttribute('data-theme') === 'dark' + ? '#333333' + : '#E8E8E8'; +}; + +// 修改主题观察器 +const themeObserver = new MutationObserver(() => { + const splitLineColor = getSplitLineColor(); + const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; + + Object.values(chartStates).forEach(state => { + const chart = state.chart?.value; + if (!chart) return; + + requestAnimationFrame(() => { + // 获取当前选中的指标IDs + const currentSelectedKpiIds = selectedRows.value[state.neType] || []; + + // 重新生成所有指标的颜色并更新到 seriesColors + state.chartDataYSeriesData.forEach(series => { + const color = generateColorRGBA(); + state.seriesColors.set(series.customKey as string, color); + }); + + // 重新计算图例选中状态 + const legendSelected: Record = {}; + state.chartDataYSeriesData.forEach(series => { + const title = state.tableColumns.value.find(col => col.key === series.customKey)?.title || series.customKey; + if (typeof title === 'string') { + // 如果没有选中的指标或选中列表为空,显示所有指标 + // 否则只显示选中的指标 + legendSelected[title] = !currentSelectedKpiIds.length || + currentSelectedKpiIds.includes(series.customKey as string); + } + }); + + const option = { + tooltip: { + trigger: 'axis', + position: function (point: number[], _params: any, _dom: HTMLElement, _rect: any, size: { viewSize: number[], contentSize: number[] }) { + const [x] = point; + const [viewWidth] = size.viewSize; + const [tooltipWidth] = size.contentSize; + + // 计算距离右边界的距离 + const rightSpace = viewWidth - x; + + // 如果右侧空间不足以显示 tooltip(加上20px的安全距离) + if (rightSpace < tooltipWidth + 20) { + // 向左显示,tooltip 右边界对齐鼠标位置 + return [x - tooltipWidth - 10, point[1]]; + } + + // 默认向右显示,tooltip 左边界对齐鼠标位置 + return [x + 10, point[1]]; + }, + axisPointer: { + type: 'line', + z: 0 + }, + className: `chart-tooltip-${state.neType}`, + z: 1000, + extraCssText: 'z-index: 1000; pointer-events: none;', + confine: true, + backgroundColor: isDark ? 'rgba(48, 48, 48, 0.8)' : 'rgba(255, 255, 255, 0.9)', + borderColor: isDark ? '#555' : '#ddd', + textStyle: { + color: isDark ? '#CACADA' : '#333' + } + }, + xAxis: { + type: 'category', + boundaryGap: false, + data: state.chartDataXAxisData, + axisLabel: { + color: isDark ? '#CACADA' : '#333' + }, + splitLine: { + show: true, + lineStyle: { + color: splitLineColor + } + } + }, + yAxis: { + type: 'value', + boundaryGap: [0, '100%'], + axisLabel: { + color: isDark ? '#CACADA' : '#333' + }, + splitNumber: 5, + scale: true, + splitLine: { + show: true, + lineStyle: { + color: splitLineColor + } + } + }, + legend: { + show: false, + selected: legendSelected, + textStyle: { + color: isDark ? '#CACADA' : '#646A73' + }, + backgroundColor: isDark ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.9)', + borderColor: isDark ? '#555' : '#ddd', + }, + series: state.chartDataYSeriesData.map(series => { + const color = state.seriesColors.get(series.customKey as string) as string; + return { + ...series, + type: 'line', + symbol: 'none', + smooth:0.6, + sampling: 'lttb', + itemStyle: { color }, + lineStyle: { 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: series.data + }; + }) + }; + + chart.setOption(option, { + notMerge: false, + lazyUpdate: false + }); + + // 强制重新渲染 + chart.resize(); + + // 如果有选中的指标,确保图例状态正确 + if (currentSelectedKpiIds.length > 0) { + nextTick(() => { + updateChartLegendSelect(state.neType, currentSelectedKpiIds); + }); + } + + // 强制更新表格以刷新图标颜色 + nextTick(() => { + const stats = getKpiStats(state.neType); + if (stats.length > 0) { + // 触发表格重新渲染 + state.tableState.loading = true; + nextTick(() => { + state.tableState.loading = false; + }); + } + }); + }); + }); +}); + +// 在 onMounted 中添加题观察器 onMounted(async () => { ws.value = new WS(); await neInfoStore.fnNelist(); @@ -585,16 +948,21 @@ onMounted(async () => { 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)); } + + // 添加主题观察器 + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-theme'] + }); + await initCharts(); }); -// 在组件卸载时销毁图表实例 +// 在 onUnmounted 中断开主题观察器 onUnmounted(() => { if(ws.value && ws.value.state() === WebSocket.OPEN) { ws.value.close(); @@ -606,7 +974,11 @@ onUnmounted(() => { if (state.observer.value) { state.observer.value.disconnect(); } + if (state.themeObserver.value) { + state.themeObserver.value.disconnect(); + } }); + themeObserver.disconnect(); }); // 添加 KPIStats 接口定义 @@ -641,7 +1013,69 @@ const getKpiStats = (type: AllChartType) => { }).filter((item): item is KPIStats => item !== null); }; -// 添加表格列配置 +// 修改选中行的状态,改为数组存储个选中的指标ID +const selectedRows = ref>({} as Record); + +// 修改处理行点击的方法,支持多选 +const handleRowClick = (record: KPIStats, type: AllChartType) => { + const state = chartStates[type]; + if (!state?.chart.value) return; + + // 确保选中数组已初始化 + if (!selectedRows.value[type]) { + selectedRows.value[type] = []; + } + + const index = selectedRows.value[type].indexOf(record.kpiId); + if (index > -1) { + // 如果已选中,则取消选中 + selectedRows.value[type].splice(index, 1); + } else { + // 如果未选中,则添加到选中列表 + selectedRows.value[type].push(record.kpiId); + } + + // 更新图表显示 + updateChartLegendSelect(type, selectedRows.value[type]); +}; + +// 修改更新图表图例选中的方法,支持多选 +const updateChartLegendSelect = (type: AllChartType, selectedKpiIds?: string[]) => { + const state = chartStates[type]; + const chart = state?.chart?.value; + if (!chart) return; + + // 确保图表数据已经准备好 + if (state.chartDataYSeriesData.length === 0) { + return; + } + + const legendSelected: Record = {}; + state.chartDataYSeriesData.forEach(series => { + const title = state.tableColumns.value.find(col => col.key === series.customKey)?.title || series.customKey; + if (typeof title === 'string') { + // 如果没有选中的指标或选中列表为空,显示所有指标 + // 否则只显示选中的指标 + legendSelected[title] = !selectedKpiIds?.length || selectedKpiIds.includes(series.customKey as string); + } + }); + + const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; + + chart.setOption({ + legend: { + show: false, + selected: legendSelected, + textStyle: { + color: isDark ? '#CACADA' : '#646A73', + }, + backgroundColor: isDark ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.9)', + borderColor: isDark ? '#555' : '#ddd', + } + }); +}; + +// 修改表格行的样式配置 const statsColumns: TableColumnType[] = [ { title: '', @@ -649,10 +1083,12 @@ const statsColumns: TableColumnType[] = [ width: 80, customRender: ({ record }: { record: KPIStats }) => { const state = chartStates[record.neType]; - const series = state?.chartDataYSeriesData.find(s => s.customKey === record.kpiId); + // 从 seriesColors 中获取最新的颜色 + const color = state?.seriesColors.get(record.kpiId); + return h(LineOutlined, { style: { - color: series?.itemStyle?.color || '#000', + color: color || '#000', fontSize: '40px', fontWeight: 'bold', } as Record @@ -683,46 +1119,6 @@ const statsColumns: TableColumnType[] = [ } ]; -// 添加选中行的状态 -const selectedRows = ref>({} as Record); - -// 添加处理行点击的方法 -const handleRowClick = (record: KPIStats, type: AllChartType) => { - const state = chartStates[type]; - if (!state?.chart.value) return; - - if (selectedRows.value[type] === record.kpiId) { - // 如果点击的是当前中的行,则取消选中 - selectedRows.value[type] = null; - // 更新图表,显示所有指标 - updateChartLegendSelect(type); - } else { - // 选中新行 - selectedRows.value[type] = record.kpiId; - // 更新图表,只显示选中的指标 - updateChartLegendSelect(type, record.kpiId); - } -}; - -// 添加更新图表图例选中态的方法 -const updateChartLegendSelect = (type: AllChartType, selectedKpiId?: string) => { - const state = chartStates[type]; - if (!state?.chart.value) return; - - const legendSelected = Object.fromEntries( - state.chartDataYSeriesData.map(series => [ - state.tableColumns.value.find(col => col.key === series.customKey)?.title || series.customKey, - selectedKpiId ? series.customKey === selectedKpiId : true - ]) - ); - - state.chart.value.setOption({ - legend: { - selected: legendSelected - } - }); -}; - // 1. 添加 tab 切换处理函数 const handleTabChange = async (activeKey: string, type: AllChartType) => { if (activeKey === 'stats') { @@ -733,7 +1129,7 @@ const handleTabChange = async (activeKey: string, type: AllChartType) => { await nextTick(); const stats = getKpiStats(type); if (stats.length === 0 && state.chartDataYSeriesData.length > 0) { - // 如果统计信息为空图表有数,重新渲染图表 + // 如果统计信为空图表有,重新渲染图表 await renderChart(type); } } @@ -754,7 +1150,11 @@ const handleTabChange = async (activeKey: string, type: AllChartType) => { - + @@ -776,7 +1176,9 @@ const handleTabChange = async (activeKey: string, type: AllChartType) => { { :loading="chartStates[type].tableState.loading" :custom-row="(record) => ({ onClick: () => handleRowClick(record, type), - class: record.kpiId === selectedRows[type] ? 'selected-row' : '' + class: selectedRows[type]?.includes(record.kpiId) ? 'selected-row' : '' })" /> @@ -835,15 +1237,38 @@ const handleTabChange = async (activeKey: string, type: AllChartType) => { display: flex; flex-direction: column; gap: 16px; + min-width: 1200px; + overflow-x: auto; + overflow-y: visible; + + &::-webkit-scrollbar { + width: 0; + height: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #ccc; + border-radius: 3px; + + &:hover { + background: #999; + } + } + + &::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; + } } .chart-card { width: 100%; height: 500px; - position:relative; + position: relative; + min-width: 1168px; :deep(.ant-card-body) { - height: calc(100% - 57px); // 减去卡片头部高度 + height: calc(100% - 57px); padding: 16px !important; display: flex; flex-direction: column; @@ -961,16 +1386,16 @@ const handleTabChange = async (activeKey: string, type: AllChartType) => { } &::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.2); + background: #ccc; border-radius: 3px; &:hover { - background: rgba(0, 0, 0, 0.3); + background: #999; } } &::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.05); + background: #f1f1f1; border-radius: 3px; } } @@ -978,22 +1403,36 @@ const handleTabChange = async (activeKey: string, type: AllChartType) => { // 暗色主题下的滚动条样式 [data-theme='dark'] { - :deep(.ant-table-body) { + .charts-container { &::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.2); + background: #4c4c4c; &:hover { - background: rgba(255, 255, 255, 0.3); + background: #666; } } &::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); + background: #2a2a2a; + } + } + + :deep(.ant-table-body) { + &::-webkit-scrollbar-thumb { + background: #4c4c4c; + + &:hover { + background: #666; + } + } + + &::-webkit-scrollbar-track { + background: #2a2a2a; } } } -// 表格交互样式 +// 表格交样式 :deep(.ant-table-tbody) { tr { cursor: pointer; @@ -1003,13 +1442,13 @@ const handleTabChange = async (activeKey: string, type: AllChartType) => { } &.selected-row { - background-color: rgba(24, 144, 255, 0.1); + background-color: rgba(24, 144, 255, 0.1) !important; } } } [data-theme='dark'] :deep(.selected-row) { - background-color: rgba(24, 144, 255, 0.2); + background-color: rgba(24, 144, 255, 0.2) !important; } // 基础组件样式覆盖 @@ -1033,4 +1472,34 @@ const handleTabChange = async (activeKey: string, type: AllChartType) => { flex-shrink: 0; max-width: 100%; } + +:deep(.ant-checkbox-group) { + &.ant-checkbox-group-disabled { + cursor: not-allowed; + + .ant-checkbox-wrapper-disabled { + cursor: not-allowed; + + &:hover { + .ant-checkbox-inner { + border-color: #d9d9d9; + } + } + } + } +} + +[data-theme='dark'] { + :deep(.ant-checkbox-group) { + &.ant-checkbox-group-disabled { + .ant-checkbox-wrapper-disabled { + &:hover { + .ant-checkbox-inner { + border-color: #434343; + } + } + } + } + } +}