Files
fe.ems.vue3/src/views/perfManage/kpiCReport/index.vue
2024-11-22 17:27:27 +08:00

1069 lines
28 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 * as echarts from 'echarts/core';
import {
TooltipComponent,
TooltipComponentOption,
GridComponent,
GridComponentOption,
LegendComponent,
LegendComponentOption,
DataZoomComponent,
DataZoomComponentOption,
} from 'echarts/components';
import { LineChart, LineSeriesOption } from 'echarts/charts';
import { UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import {
reactive,
ref,
onMounted,
toRaw,
markRaw,
nextTick,
onBeforeUnmount,
h,
} from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { message, Modal, TableColumnType } from 'ant-design-vue/es';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import TableColumnsDnd from '@/components/TableColumnsDnd/index.vue';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import useNeInfoStore from '@/store/modules/neinfo';
import useI18n from '@/hooks/useI18n';
import { listCustom } from '@/api/perfManage/customTarget';
import { listCustomData } from '@/api/perfManage/customData';
import { parseDateToStr } from '@/utils/date-utils';
import { writeSheet } from '@/utils/execl-utils';
import saveAs from 'file-saver';
import { generateColorRGBA } from '@/utils/generate-utils';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import { LineOutlined } from '@ant-design/icons-vue';
import { useRoute } from 'vue-router';
import dayjs, { Dayjs } from 'dayjs';
const neInfoStore = useNeInfoStore();
const route = useRoute();
const { t, currentLocale } = useI18n();
const ws = new WS();
echarts.use([
TooltipComponent,
GridComponent,
LegendComponent,
DataZoomComponent,
LineChart,
CanvasRenderer,
UniversalTransition,
]);
type EChartsOption = echarts.ComposeOption<
| TooltipComponentOption
| GridComponentOption
| LegendComponentOption
| DataZoomComponentOption
| LineSeriesOption
>;
/**图DOM节点实例对象 */
const kpiChartDom = ref<HTMLElement | undefined>(undefined);
/**图实例对象 */
const kpiChart = ref<any>(null);
/**网元参数 */
let neCascaderOptions = ref<Record<string, any>[]>([]);
/**记录开始结束时间 */
let queryRangePicker = ref<[string, string]>(['', '']);
/**时间选择 */
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()],
},
]);
/**表格字段列 */
let tableColumns = ref<any[]>([]);
/**表格字段列排序 */
let tableColumnsDnd = ref<any[]>([]);
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => t('common.tablePaginationTotal', { total }),
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
},
});
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: Record<string, any>[];
/**显示表格 */
showTable: boolean;
};
/**表格状态 */
let tableState: TabeStateType = reactive({
tableColumns: [],
loading: false,
size: 'middle',
seached: true,
data: [],
showTable: false,
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**查询参数 */
let queryParams: any = reactive({
/**网元类型 */
neType: '',
/**网元标识 */
neId: '',
/**开始时间 */
startTime: '',
/**结束时间 */
endTime: '',
/**排序字段 */
sortField: 'created_at',
/**排序方式 */
sortOrder: 'desc',
});
/**表格分页、排序、筛选变化时触发操作, 排序方式,取值为 ascend descend */
function fnTableChange(pagination: any, filters: any, sorter: any, extra: any) {
const { columnKey, order } = sorter;
if (!order) return;
if (order.startsWith(queryParams.sortOrder)) return;
if (order) {
queryParams.sortField = columnKey;
queryParams.sortOrder = order.replace('end', '');
} else {
queryParams.sortOrder = 'asc';
}
fnGetList();
}
/**对象信息状态类型 */
type StateType = {
/**网元类型 */
neType: string[];
/**图表实时统计 */
chartRealTime: boolean;
/**图表标签选择 */
chartLegendSelectedFlag: boolean;
};
/**对象信息状态 */
let state: StateType = reactive({
neType: [],
chartRealTime: false,
chartLegendSelectedFlag: true,
});
// 存储每个指标的临时固定颜色
const kpiColors = new Map<string, string>();
//legend表格数据
const kpiStats: any = ref([]);
// 添加一个函数来获取当前主题下的网格线颜色
function getSplitLineColor() {
return document.documentElement.getAttribute('data-theme') === 'dark'
? '#333333'
: '#E8E8E8'; // 亮色模式返回 undefined使用默认颜色
}
// 添加表格列定义
const statsColumns: TableColumnType<any>[] = [
{
title: '',
key: 'icon',
width: 50,
customRender: ({ record }: { record: any }) => {
return h(LineOutlined, {
style: {
color: kpiColors.get(record.kpiId) || '#000', // 使用与折线图相同的颜色
fontSize: '30px', // 增大图标尺寸到30px
fontWeight: 'bold', // 加粗
},
});
},
},
{
title: t('views.perfManage.kpiOverView.kpiName'),
dataIndex: 'title',
key: 'title',
width: '65%',
},
{
title: t('views.perfManage.kpiOverView.maxValue'),
dataIndex: 'max',
key: 'max',
width: '17%',
sorter: (a: any, b: any) => a.max - b.max, // 添加排序函数
sortDirections: ['ascend', 'descend'],
},
{
title: t('views.perfManage.kpiOverView.minValue'),
dataIndex: 'min',
key: 'min',
width: '17%',
sorter: (a: any, b: any) => a.min - b.min, // 添加排序函数
sortDirections: ['ascend', 'descend'],
},
];
/**
* 数据列表导出
*/
function fnRecordExport() {
Modal.confirm({
title: 'Tip',
content: t('views.perfManage.goldTarget.exportSure'),
onOk() {
const key = 'exportKPI';
message.loading({ content: t('common.loading'), key });
if (tableState.data.length <= 0) {
message.error({
content: t('views.perfManage.goldTarget.exportEmpty'),
key,
duration: 2,
});
return;
}
const tableColumnsTitleArr: string[] = [];
const tableColumnsKeyArr: string[] = [];
for (const columns of tableColumnsDnd.value) {
tableColumnsTitleArr.push(`${columns.title}`);
tableColumnsKeyArr.push(`${columns.key}`);
}
const kpiDataArr = [];
for (const item of tableState.data) {
const kpiData: Record<string, any> = {};
const keys = Object.keys(item);
for (let i = 0; i <= tableColumnsKeyArr.length; i++) {
for (const key of keys) {
if (tableColumnsKeyArr[i] === key) {
const title = tableColumnsTitleArr[i];
kpiData[title] = item[key];
}
}
}
kpiDataArr.push(kpiData);
}
writeSheet(kpiDataArr, 'KPI', { header: tableColumnsTitleArr })
.then(fileBlob => saveAs(fileBlob, `kpi_data_${Date.now()}.xlsx`))
.finally(() => {
message.success({
content: t('common.msgSuccess', { msg: t('common.export') }),
key,
duration: 2,
});
});
},
});
}
/**查询数据列表表头 */
function fnGetListTitle() {
// 当前语言
var language = currentLocale.value.split('_')[0];
if (language === 'zh') language = 'cn';
// 获取表头文字
listCustom({ neType: state.neType[0], status: 'Active' })
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
if (res.data.length === 0) {
message.error({
content: t('views.perfManage.customTarget.kpiIdTip'),
duration: 2,
});
tableState.data = [];
tableColumns.value = [];
tableColumnsDnd.value = [];
kpiStats.value = []; //清空数据
fnRanderChartData();
return false;
}
tableColumns.value = [];
const columns: any[] = [];
for (const item of res.data) {
const kpiDisplay = item[`unit`]
? item[`title`] + `(${item['unit']})`
: item[`title`];
const kpiValue = item[`kpiId`];
columns.push({
title: kpiDisplay,
dataIndex: kpiValue,
align: 'left',
key: kpiValue,
resizable: true,
width: 100,
minWidth: 150,
maxWidth: 300,
});
}
columns.push({
title: t('views.perfManage.perfData.neName'),
dataIndex: 'neName',
key: 'neName',
align: 'left',
width: 100,
});
columns.push({
title: t('views.perfManage.goldTarget.time'),
dataIndex: 'timeGroup',
align: 'left',
fixed: 'right',
key: 'timeGroup',
sorter: true,
width: 100,
});
nextTick(() => {
tableColumns.value = columns;
});
return true;
} else {
message.warning({
content: t('common.getInfoFail'),
duration: 2,
});
return false;
}
})
.then(result => {
result && fnGetList();
});
}
/**查询数据列表 */
function fnGetList() {
if (tableState.loading) return;
tableState.loading = true;
queryParams.neType = state.neType[0];
queryParams.neId = state.neType[1];
queryParams.startTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
listCustomData(toRaw(queryParams))
.then(res => {
tableState.loading = false;
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
tablePagination.total = res.data.length;
tableState.data = res.data;
if (!res.data.length) {
message.warning({
content: t('common.noData'),
duration: 2,
});
tableState.data = [];
tableColumns.value = [];
tableColumnsDnd.value = [];
kpiStats.value = []; //清空数据
fnRanderChartData();
return false;
}
return true;
}
return false;
})
.then(result => {
if (result) {
fnRanderChartData();
//封装legend表格数据
kpiStats.value = [];
for (const columns of tableColumns.value) {
if (
columns.key === 'neName' ||
columns.key === 'startIndex' ||
columns.key === 'timeGroup'
) {
continue;
}
const values = tableState.data.map((item: any) => {
return item[columns.key] ? Number(item[columns.key]) : 0;
});
kpiStats.value.push({
kpiId: columns.key,
title: columns.title,
max: values.length > 0 ? Math.max(...values) : 0,
min: values.length > 0 ? Math.min(...values) : 0,
});
}
}
});
}
/**切换显示类型 图或表格 */
function fnChangShowType() {
tableState.showTable = !tableState.showTable;
}
/**绘制图表 */
function fnRanderChart() {
const container: HTMLElement | undefined = kpiChartDom.value;
if (!container) return;
kpiChart.value = markRaw(echarts.init(container, 'light'));
const option: EChartsOption = {
tooltip: {
trigger: 'axis',
position: function (pt: any) {
return [pt[0], '10%'];
},
},
xAxis: {
type: 'category',
boundaryGap: false,
data: [], // 数据x轴
},
yAxis: {
type: 'value',
boundaryGap: [0, '100%'],
},
legend: {
show: false,
type: 'scroll',
orient: 'vertical',
top: 40,
right: 20,
itemWidth: 20,
itemGap: 25,
textStyle: {
color: '#646A73',
},
icon: 'circle',
selected: {},
},
grid: {
//网格区域边距
left: '7%',
right: '7%',
bottom: '7%',
containLabel: true,
},
series: [], // 数据y轴
};
kpiChart.value.setOption(option);
// 创建 ResizeObserver 实例
var observer = new ResizeObserver(entries => {
if (kpiChart.value) {
kpiChart.value.resize();
}
});
// 监听元素大小变化
observer.observe(container);
}
/**图表标签选择 */
let chartLegendSelected: Record<string, boolean> = {};
/**图表配置数据x轴 */
let chartDataXAxisData: string[] = [];
/**图表配置数据y轴 */
let chartDataYSeriesData: Record<string, any>[] = [];
/**图表数据渲染 */
function fnRanderChartData() {
if (kpiChart.value == null && tableState.data.length <= 0) {
return;
}
// 重置
chartLegendSelected = {};
chartDataXAxisData = [];
chartDataYSeriesData = [];
for (var columns of tableColumns.value) {
if (
columns.key === 'neName' ||
columns.key === 'startIndex' ||
columns.key === 'timeGroup'
) {
continue;
}
const color = kpiColors.get(columns.key) || generateColorRGBA();
kpiColors.set(columns.key, color);
chartDataYSeriesData.push({
name: `${columns.title}`,
key: `${columns.key}`,
type: 'line',
symbol: 'none',
sampling: 'lttb',
itemStyle: {
color: 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: [],
});
chartLegendSelected[`${columns.title}`] = state.chartLegendSelectedFlag;
}
// 用降序就反转
let orgData = tableState.data;
if (queryParams.sortOrder === 'desc') {
orgData = orgData.toReversed();
}
for (const item of orgData) {
const keys = Object.keys(item);
//console.log(keys,item);//
for (const y of chartDataYSeriesData) {
for (const key of keys) {
if (y.key === key) {
y.data.push(+item[key]);
chartDataXAxisData.push(item['timeGroup']);
}
}
}
}
chartDataXAxisData = Array.from(new Set(chartDataXAxisData));
///console.log(queryParams.sortOrder, chartDataXAxisData);
//console.log(chartDataXAxisData, chartDataYSeriesData);
// 绘制图数据
kpiChart.value.setOption(
{
legend: {
selected: chartLegendSelected,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: chartDataXAxisData,
},
series: chartDataYSeriesData,
},
{
replaceMerge: ['xAxis', 'series'],
}
);
}
/**图表实时统计 */
function fnRealTimeSwitch(bool: any) {
if (bool) {
tableState.seached = false;
// 建立链接
const options: OptionsType = {
url: '/ws',
params: {
/**订阅通道组
*
* 指标(GroupID:10_neType_neId)
*/
subGroupID: `20_${queryParams.neType}_${queryParams.neId}`,
},
onmessage: wsMessage,
onerror: wsError,
};
ws.connect(options);
} else {
tableState.seached = true;
ws.close();
}
}
/**接收数据后回调 */
function wsError(ev: any) {
// 接收数据后回调
console.error(ev);
}
/**接收数据后回调 */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 订阅组信息
if (!data?.groupId) {
return;
}
// kpiEvent 黄金指标指标事件
const kpiEvent = data.data;
tableState.data.unshift(kpiEvent);
tablePagination.total++;
// 非对应网元类型
if (kpiEvent.neType !== queryParams.neType) return;
for (const key of Object.keys(data.data)) {
const v = kpiEvent[key];
// x轴
if (key === 'timeGroup') {
// chartDataXAxisData.shift();
chartDataXAxisData.push(v);
continue;
}
// y轴
const yItem = chartDataYSeriesData.find(item => item.key === key);
if (yItem) {
// yItem.data.shift();
yItem.data.push(+v);
}
}
// 绘制图数据
kpiChart.value.setOption({
xAxis: {
data: chartDataXAxisData,
},
series: chartDataYSeriesData,
});
}
// 添加一个变量来跟踪当前选中的行
const selectedRow = ref<string | null>(null);
// 添加处理行点击的方法
function handleRowClick(record: any) {
if (selectedRow.value === record.kpiId) {
// 如果点击的是当前选中的行,则取消选中
selectedRow.value = null;
// 更新图表,显示所有指标
for (let key in chartLegendSelected) {
chartLegendSelected[key] = true;
}
} else {
// 选中新行
selectedRow.value = record.kpiId;
// 更新图表,只显示选中的指标
for (let key in chartLegendSelected) {
if (key === record.title) {
chartLegendSelected[key] = true;
} else {
chartLegendSelected[key] = false;
}
}
}
kpiChart.value.setOption({
legend: {
selected: chartLegendSelected,
},
});
}
onMounted(() => {
// 目前支持的 AMF AUSF MME MOCNGW NSSF SMF UDM UPF PCF
// 获取网元网元列表
listCustom({ status: 'Active' }).then((res: any) => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
if (!res.data.length) {
message.warning({
content: '无可用的自定义指标,请先添加自定义指标',
duration: 2,
});
return false;
}
let typeArr: any = [];
res.data.forEach((item: any) => {
typeArr.push(item.neType);
});
neInfoStore.fnNelist().then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
if (res.data.length > 0) {
// 过滤不可用的网元
neCascaderOptions.value = neInfoStore.getNeCascaderOptions.filter(
(item: any) => {
return typeArr.includes(item.value);
}
);
if (neCascaderOptions.value.length === 0) {
message.warning({
content: t('common.noData'),
duration: 2,
});
return;
}
// 无查询参数neType时 默认选择UPF
const queryNeType = (route.query.neType as string) || 'UPF';
const item = neCascaderOptions.value.find(
s => s.value === queryNeType
);
if (item && item.children) {
const info = item.children[0];
state.neType = [info.neType, info.neId];
queryParams.neType = info.neType;
queryParams.neId = info.neId;
} else {
const info = neCascaderOptions.value[0].children[0];
state.neType = [info.neType, info.neId];
queryParams.neType = info.neType;
queryParams.neId = info.neId;
}
// 查询当前小时
const now = new Date();
now.setMinutes(0, 0, 0);
// 设置起始时间为整点前一小时
const startTime = new Date(now);
startTime.setHours(now.getHours() - 1);
queryRangePicker.value[0] = `${startTime.getTime()}`;
// 设置结束时间为整点
const endTime = new Date(now);
endTime.setMinutes(59, 59, 59);
queryRangePicker.value[1] = `${endTime.getTime()}`;
fnGetListTitle();
// 绘图
fnRanderChart();
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
});
}
});
});
onBeforeUnmount(() => {
ws.close();
});
</script>
<template>
<PageContainer>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParamsFrom" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item name="neType" :label="t('views.ne.common.neType')">
<a-cascader
v-model:value="state.neType"
:options="neCascaderOptions"
:allow-clear="false"
:placeholder="t('common.selectPlease')"
/>
</a-form-item>
</a-col>
<a-col :lg="10" :md="12" :xs="24">
<a-form-item
:label="t('views.perfManage.goldTarget.timeFrame')"
name="timeFrame"
>
<a-range-picker
v-model:value="queryRangePicker"
bordered
:allow-clear="false"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
value-format="x"
:presets="ranges"
style="width: 100%"
></a-range-picker>
</a-form-item>
</a-col>
<a-col :lg="2" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button
type="primary"
:loading="tableState.loading"
@click.prevent="fnGetListTitle()"
>
<template #icon><SearchOutlined /></template>
{{ t('common.search') }}
</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button
type="primary"
:loading="tableState.loading"
@click.prevent="fnChangShowType()"
>
<template #icon> <AreaChartOutlined /> </template>
{{
tableState.showTable
? t('views.perfManage.goldTarget.kpiChartTitle')
: t('views.perfManage.goldTarget.kpiTableTitle')
}}
</a-button>
<a-button
type="dashed"
:loading="tableState.loading"
@click.prevent="fnRecordExport()"
v-show="tableState.showTable"
>
<template #icon>
<ExportOutlined />
</template>
{{ t('common.export') }}
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center" v-show="tableState.showTable">
<a-tooltip>
<template #title>{{ t('common.reloadText') }}</template>
<a-button type="text" @click.prevent="fnGetList()">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<TableColumnsDnd
v-if="tableColumns.length > 0"
:cache-id="`kpiTarget_${state.neType[0]}`"
:columns="tableColumns"
v-model:columns-dnd="tableColumnsDnd"
></TableColumnsDnd>
<a-tooltip>
<template #title>{{ t('common.sizeText') }}</template>
<a-dropdown trigger="click" placement="bottomRight">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">
{{ t('common.size.default') }}
</a-menu-item>
<a-menu-item key="middle">
{{ t('common.size.middle') }}
</a-menu-item>
<a-menu-item key="small">
{{ t('common.size.small') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
<a-form layout="inline" v-show="!tableState.showTable">
<!-- <a-form-item
:label="t('views.perfManage.goldTarget.showChartSelected')"
name="chartLegendSelectedFlag"
>
<a-switch
:disabled="tableState.loading"
v-model:checked="state.chartLegendSelectedFlag"
:checked-children="t('common.switch.open')"
:un-checked-children="t('common.switch.shut')"
@change="fnLegendSelected"
size="small"
/>
</a-form-item> -->
<a-form-item
:label="t('views.perfManage.goldTarget.realTimeData')"
name="chartRealTime"
>
<a-switch
:disabled="tableState.loading"
v-model:checked="state.chartRealTime"
:checked-children="t('common.switch.open')"
:un-checked-children="t('common.switch.shut')"
@change="fnRealTimeSwitch"
size="small"
/>
</a-form-item>
</a-form>
</template>
<!-- 表格列表 -->
<a-table
v-show="tableState.showTable"
class="table"
row-key="id"
:columns="tableColumnsDnd"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ x: tableColumnsDnd.length * 200, y: 'calc(100vh - 480px)' }"
@resizeColumn="(w:number, col:any) => (col.width = w)"
:show-expand-column="false"
@change="fnTableChange"
>
</a-table>
<!-- 图表 -->
<div style="padding: 24px" v-show="!tableState.showTable">
<div
ref="kpiChartDom"
class="chart-container"
style="height: 450px; width: 100%"
></div>
<div class="table-container">
<a-table
:columns="statsColumns"
:data-source="kpiStats"
:pagination="false"
:scroll="{ y: 250 }"
size="small"
:custom-row="
record => ({
onClick: () => handleRowClick(record),
class: record.kpiId === selectedRow ? 'selected-row' : '',
})
"
/>
</div>
</div>
</a-card>
</PageContainer>
</template>
<style scoped>
.chart-container {
height: 800px;
width: 100%;
}
.table-container {
height: 282px;
width: 100%;
margin-top: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 表格布局相关样式 */
:deep(.ant-table-wrapper),
:deep(.ant-table),
:deep(.ant-table-container),
:deep(.ant-table-content) {
flex: 1;
display: flex;
flex-direction: column;
}
:deep(.ant-table-body) {
flex: 1;
overflow-y: auto !important;
min-height: 0;
}
/* 表格行和表头样式 */
:deep(.ant-table-thead tr th),
:deep(.ant-table-tbody tr td) {
padding: 8px;
height: 40px;
}
/* 美化滚动条样式 */
:deep(.ant-table-body::-webkit-scrollbar) {
width: 6px;
height: 6px;
}
:deep(.ant-table-body::-webkit-scrollbar-thumb) {
background: #ccc;
border-radius: 3px;
}
:deep(.ant-table-body::-webkit-scrollbar-track) {
background: #f1f1f1;
border-radius: 3px;
}
[data-theme='dark'] :deep(.ant-table-body::-webkit-scrollbar-thumb) {
background: #4c4c4c;
}
[data-theme='dark'] :deep(.ant-table-body::-webkit-scrollbar-track) {
background: #2a2a2a;
}
/* 选中行样式 */
:deep(.selected-row) {
background-color: rgba(24, 144, 255, 0.1);
}
[data-theme='dark'] :deep(.selected-row) {
background-color: rgba(24, 144, 255, 0.2);
}
/* 鼠标悬停样式 */
:deep(.ant-table-tbody tr:hover) {
cursor: pointer;
}
</style>