Files
fe.ems.vue3/src/views/perfManage/kpiKeyTarget/index.vue
2024-11-18 10:02:20 +08:00

1037 lines
27 KiB
Vue

<script setup lang="ts">
import * as echarts from 'echarts';
import { PageContainer } from 'antdv-pro-layout';
import { onMounted, reactive, ref, markRaw, nextTick, onUnmounted, watch, h } 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 { LineOutlined } from '@ant-design/icons-vue';
import { TableColumnType } from 'ant-design-vue';
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', 'upf','smf', 'pcf', 'mme', 'mocngw', 'smsc', 'cbc', 'ausf'] as const;
type AllChartType = (typeof ALL_NE_TYPES)[number] & string;
// 在 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[]>([]);
// watch 监控函数
watch(selectedNeTypes, async (newTypes) => {
networkElementTypes.value = [...newTypes]; // 使用展开运算符创建新数组
latestSelectedTypes.value = [...newTypes];
// 确保每个选中的类型都有对应的图表状态
newTypes.forEach(type => {
if (!chartStates[type]) {
chartStates[type] = createChartState();
}
});
// 初始化新选中的图表
for (const type of newTypes) {
try {
if (!chartStates[type].chart.value) {
await fetchKPITitle(type);
await nextTick();
await initChart(type);
}
await fetchData(type);
} catch (error) {
console.error(`Error initializing chart for ${type}:`, error);
}
}
// 更新 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);
}
// 保存选中的网元类型到本地存储
localStorage.setItem('selectedNeTypes', JSON.stringify(newTypes));
}, { deep: true });
// 防抖函数
useDebounceFn(() => {
// 比较当前选择和最新选择
if (JSON.stringify(latestSelectedTypes.value) !== JSON.stringify(selectedNeTypes.value)) {
// 如果不一致,以最新选择为准
selectedNeTypes.value = latestSelectedTypes.value;
}
const newTypes = selectedNeTypes.value;
// 确保 chartStates 包含新的网元类型
newTypes.forEach((type) => {
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();
await initChart(type);
await fetchData(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);
};
// 定义表格状态类型
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(async () => {
const state = chartStates[type];
if (!state) {
console.warn(`Chart state for ${type} not found`);
return;
}
// 等待 DOM 更新
await nextTick();
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) {
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 实例
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);
// 确保在图表初始化后更新统计信息
await nextTick();
await renderChart(type);
});
};
tryInit();
};
// 可复用的数据获取函数
const fetchData = async (type: AllChartType) => {
const state = chartStates[type];
state.tableState.loading = true;
try {
const dateRange = rangePicker[type] as [string, string];
const [startTime, endTime] = dateRange;
const res = await listKPIData({
neType: type.toUpperCase(),
neId: '001',
startTime,
endTime,
sortField: 'timeGroup',
sortOrder: 'desc',
interval: 5,
});
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
state.tableState.data = res.data;
await 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.tableState.data = []; // 清空表格数据
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;
// 重新获取历史数据
selectedNeTypes.value.forEach(type => {
fetchData(type);
});
}
}
}
// 接收数据后错误回调
function wsError() {
message.error(t('common.websocketError'));
}
// 接收数据回调
function wsMessage(res: Record<string, any>) {
const { code, data } = res;
if (code === RESULT_CODE_ERROR || !data?.groupId) {
console.warn(res.msg);
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) {
// 更新表格数据
state.tableState.data.unshift(kpiEvent);
if (state.tableState.data.length > 100) {
state.tableState.data.pop();
}
// 更新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[]
});
}
// 更新统计信息
nextTick(() => {
const stats = getKpiStats(neType);
if (stats.length > 0) {
renderChart(neType);
}
});
}
}
interface CustomSeriesOption extends Omit<LineSeriesOption, 'data'> {
customKey?: string;
data: Array<[string, number]>;
}
// 创建可复用的图表渲染函数
const renderChart = async (type: AllChartType) => {
const state = chartStates[type];
if (!state?.chart.value) 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) {
const timeStr = parseDateToStr(+item.timeGroup);
state.chartDataXAxisData.push(timeStr);
for (const series of state.chartDataYSeriesData) {
series.data.push([timeStr, +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[],
},
{ 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 = [
{
title: t('views.perfManage.kpiKeyTarget.time'),
dataIndex: 'timeGroup',
key: 'timeGroup',
align: 'left',
width: 180,
fixed: 'left',
customRender: ({ text }: { text: string }) => {
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,
}))
];
}
} 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));
}
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();
}
});
});
// 添加 KPIStats 接口定义
interface KPIStats {
kpiId: string;
title: string;
max: number;
min: number;
neType: AllChartType; // 添加网元类型字段
}
// 修改 getKpiStats 函数
const getKpiStats = (type: AllChartType) => {
const state = chartStates[type];
if (!state?.chartDataYSeriesData) return [];
return state.chartDataYSeriesData.map(series => {
const kpiId = series.customKey as string;
const column = state.tableColumns.value.find(col => col.key === kpiId);
if (!column) return null;
// 直接从图表数据中获取值
const values = series.data.map(item => Number(item[1]));
return {
kpiId,
title: column.title as string,
max: values.length > 0 ? Math.max(...values) : 0,
min: values.length > 0 ? Math.min(...values) : 0,
neType: type
};
}).filter((item): item is KPIStats => item !== null);
};
// 添加表格列配置
const statsColumns: TableColumnType<KPIStats>[] = [
{
title: '',
key: 'icon',
width: 80,
customRender: ({ record }: { record: KPIStats }) => {
const state = chartStates[record.neType];
const series = state?.chartDataYSeriesData.find(s => s.customKey === record.kpiId);
return h(LineOutlined, {
style: {
color: series?.itemStyle?.color || '#000',
fontSize: '40px',
fontWeight: 'bold',
} as Record<string, string>
});
}
},
{
title: t('views.perfManage.kpiOverView.kpiName'),
dataIndex: 'title',
key: 'title',
width: '40%',
},
{
title: t('views.perfManage.kpiOverView.maxValue'),
dataIndex: 'max',
key: 'max',
width: '30%',
sorter: (a: KPIStats, b: KPIStats) => a.max - b.max,
sortDirections: ['ascend', 'descend'] as ('ascend' | 'descend')[],
},
{
title: t('views.perfManage.kpiOverView.minValue'),
dataIndex: 'min',
key: 'min',
width: '30%',
sorter: (a: KPIStats, b: KPIStats) => a.min - b.min,
sortDirections: ['ascend', 'descend'] as ('ascend' | 'descend')[],
}
];
// 添加选中行的状态
const selectedRows = ref<Record<AllChartType, string | null>>({} as Record<AllChartType, string | null>);
// 添加处理行点击的方法
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') {
const state = chartStates[type];
if (!state?.chart.value) return;
// 制重新算统计信息
await nextTick();
const stats = getKpiStats(type);
if (stats.length === 0 && state.chartDataYSeriesData.length > 0) {
// 如果统计信息为空图表有数,重新渲染图表
await renderChart(type);
}
}
};
</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.realTimeDataStop'):t('views.dashboard.cdr.realTimeDataStart')">
<a-switch
v-model:checked="realTimeEnabled"
@change="handleRealTimeSwitch"
/>
</a-form-item>
</a-col>
<a-col :lg="20" :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-row>
</a-form>
</a-card>
<div class="charts-container">
<a-card
v-for="type in networkElementTypes"
:key="type"
:bordered="false"
class="chart-card"
>
<template #title>
<div class="card-header">
<div class="card-title">
<span>{{ (type as string).toUpperCase() }}</span>
</div>
<a-range-picker
v-model:value="rangePicker[type]"
: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(type)"
class="no-drag"
></a-range-picker>
</div>
</template>
<div class="card-content">
<div class="chart-container">
<div :ref="el => { if (el && chartStates[type]) chartStates[type].chartDom.value = el as HTMLElement }"></div>
</div>
<div class="table-container">
<a-tabs default-active-key="stats" @change="(key:any) => handleTabChange(key, type)">
<a-tab-pane key="stats" :tab="t('views.perfManage.kpiKeyTarget.statistics')">
<a-table
:columns="statsColumns"
:data-source="getKpiStats(type)"
:pagination="false"
size="small"
:scroll="{ y: 'true' }"
:loading="chartStates[type].tableState.loading"
:custom-row="(record) => ({
onClick: () => handleRowClick(record, type),
class: record.kpiId === selectedRows[type] ? 'selected-row' : ''
})"
/>
</a-tab-pane>
<a-tab-pane key="raw" :tab="t('views.perfManage.kpiKeyTarget.rawData')">
<a-table
:columns="chartStates[type].tableColumns.value"
:data-source="chartStates[type].tableState.data"
:loading="chartStates[type].tableState.loading"
:pagination="false"
size="small"
:scroll="{ y: '100%' }"
/>
</a-tab-pane>
</a-tabs>
</div>
</div>
</a-card>
</div>
</PageContainer>
</template>
<style lang="less" scoped>
.control-card {
margin-bottom: 16px;
}
.charts-container {
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.chart-card {
width: 100%;
height: 500px;
position:relative;
:deep(.ant-card-body) {
height: calc(100% - 57px); // 减去卡片头部高度
padding: 16px !important;
display: flex;
flex-direction: column;
overflow: visible;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 8px;
}
.card-title {
display: flex;
align-items: center;
flex-shrink: 0;
span {
font-size: 16px;
font-weight: 500;
}
}
.card-content {
height: 100%;
display: flex;
gap: 16px;
}
.chart-container {
flex: 3;
min-width: 0;
height: 100%;
> div {
width: 100%;
height: 100%;
}
}
.table-container {
flex: 2;
min-width: 500px;
height: 100%;
display: flex;
flex-direction: column;
:deep(.ant-tabs) {
height: 100%;
display: flex;
flex-direction: column;
}
:deep(.ant-tabs-content-holder) {
flex: 1;
overflow: hidden;
}
:deep(.ant-tabs-content) {
height: 100%;
}
:deep(.ant-tabs-tabpane) {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
:deep(.ant-table-wrapper) {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
:deep(.ant-spin-nested-loading) {
height: 100%;
}
:deep(.ant-spin-container) {
height: 100%;
display: flex;
flex-direction: column;
}
:deep(.ant-table) {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
:deep(.ant-table-container) {
height: 100%;
display: flex;
flex-direction: column;
}
:deep(.ant-table-header) {
flex-shrink: 0;
}
:deep(.ant-table-body) {
flex: 1;
overflow-y: auto !important;
min-height: 0;
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
&:hover {
background: rgba(0, 0, 0, 0.3);
}
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 3px;
}
}
}
// 暗色主题下的滚动条样式
[data-theme='dark'] {
:deep(.ant-table-body) {
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
&:hover {
background: rgba(255, 255, 255, 0.3);
}
}
&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
}
}
// 表格交互样式
:deep(.ant-table-tbody) {
tr {
cursor: pointer;
&:hover > td {
background-color: rgba(24, 144, 255, 0.1);
}
&.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-form-item) {
margin-bottom: 0;
&-label {
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
}
:deep(.ant-card-head) {
padding: 8px 16px;
&-title {
padding: 8px 0;
width: 100%;
}
}
:deep(.ant-range-picker) {
flex-shrink: 0;
max-width: 100%;
}
</style>