You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

957 lines
29 KiB

<script lang="ts">
import type { ComponentPublicInstance } from 'vue'
import type { Dayjs } from 'dayjs'
import { debounce } from 'lodash-es'
import dayjs from 'dayjs'
import { computed, defineComponent, onMounted, ref, toRaw, watch } from 'vue'
import { useRoute } from 'vue-router'
import {
Button,
Card,
Checkbox,
Col,
Descriptions,
Divider,
Form,
FormItem,
Input,
InputNumber,
Modal,
RangePicker,
Row,
Spin,
Steps,
Tabs,
Transfer,
} from 'ant-design-vue'
import VueECharts from 'vue-echarts'
import { pointTableSchema, sampleInfoTableSchema } from './data'
import { BasicTable, useTable } from '@/components/Table'
import { PageWrapper } from '@/components/Page'
import {
bottomModelApi,
modelInfoApi,
testModelApi,
trainModelApi,
updateModelInfo,
} from '@/api/alert/model/models'
import { getExaHistorys } from '@/api/alert/exa/index'
import { useECharts } from '@/hooks/web/useECharts'
import { useMessage } from '@/hooks/web/useMessage'
import { pointListApi } from '@/api/alert/model/select'
export default defineComponent({
components: {
BasicTable,
PageWrapper,
[Divider.name]: Divider,
[Card.name]: Card,
[Descriptions.name]: Descriptions,
[Descriptions.Item.name]: Descriptions.Item,
[Steps.name]: Steps,
[Steps.Step.name]: Steps.Step,
ATabs: Tabs,
ATabPane: Tabs.TabPane,
ARangePicker: RangePicker,
AForm: Form,
AFormItem: FormItem,
VueECharts,
AModal: Modal,
AInput: Input,
ACheckbox: Checkbox,
AInputNumber: InputNumber,
AButton: Button,
ASpin: Spin,
ATransfer: Transfer,
ARow: Row,
ACol: Col,
},
setup() {
const { createMessage } = useMessage()
const route = useRoute()
const id = route.params.id
const model = ref(null)
const brushActivated = ref<Set<number>>(new Set())
const spinning = ref(false)
let trainTimeCopy = ''
const fetchModelInfo = async () => {
const modelInfo = await modelInfoApi(id)
model.value = modelInfo
trainTimeCopy = JSON.stringify(model.value.trainTime)
getHistory()
}
const pointData = computed(() => model.value?.pointInfo || [])
const [pointTable] = useTable({
columns: pointTableSchema,
pagination: true,
dataSource: pointData,
scroll: { y: 300 },
})
const trainTime = computed(() => model.value?.trainTime || [])
const [trainTimeTable] = useTable({
columns: sampleInfoTableSchema,
dataSource: trainTime,
scroll: { y: 300 },
})
const activeKey = ref('1')
type RangeValue = [Dayjs, Dayjs]
const currentDate: Dayjs = dayjs('2023-10-29 00:00:00')
const lastMonthDate: Dayjs = dayjs('2023-10-28 16:00:00')
const rangeValue: RangeValue = [
lastMonthDate,
currentDate,
]
const historyTime = ref<RangeValue>(rangeValue)
const historyList = ref<any[]>([])
const echartsRefs = ref<any[]>([])
async function getHistory() {
if (!historyTime.value)
return
spinning.value = true
if (model.value.para) {
getTestData()
}
else {
const params = {
startTime: historyTime.value[0].format('YYYY-MM-DD HH:mm:ss'),
endTime: historyTime.value[1].format('YYYY-MM-DD HH:mm:ss'),
itemName: model.value?.pointInfo
.map(item => item.pointId)
.join(','),
interval: model.value.sampling,
}
const history = await getExaHistorys(params)
historyList.value = history.map((item, index) => {
const point = model.value?.pointInfo[index]
return {
data: [item],
name: `${index + 1}.${point?.description}(${point?.pointId})`,
}
})
echartsRefs.value = Array.from({ length: historyList.value.length })
brushActivated.value = new Set()
}
spinning.value = false
}
async function getTestData() {
const params = {
Model_id: id,
version: model.value?.Cur_Version ? model.value?.Cur_Version : 'v-test',
Test_Data: {
time: historyTime.value
.map(t => dayjs(t).format('YYYY-MM-DD HH:mm:ss'))
.join(','),
points: model.value.pointInfo.map(t => t.pointId).join(','),
interval: model.value.sampling * 1000,
},
}
const result = await testModelApi(params)
const sampleData = result.sampleData
const reconData = result.reconData
const xData = generateTimeList(
historyTime.value,
model.value.sampling * 1000,
)
historyList.value = sampleData.map((item, index) => {
const point = model.value?.pointInfo[index]
return {
data: [
item.map((t, i) => {
return [xData[i], t]
}),
reconData[index].map((t, i) => {
return [xData[i], t]
}),
],
name: `${index + 1}.${point?.description}(${point?.pointId})`,
}
})
brushActivated.value = new Set()
}
function generateTimeList(time: RangeValue, intervalMs: number) {
const [t1, t2] = time
const count = Math.floor(t2.diff(t1, 'millisecond') / intervalMs) + 1
return Array.from({ length: count }, (_, i) => t1.add(i * intervalMs, 'millisecond').valueOf())
}
function getOption(item: any) {
const name = ['测量值', '模型值', '']
const color = ['blue', '#ff6f00', 'red']
const yIndex = [0, 0, 1]
const option = {
xAxis: {
type: 'time',
axisLabel: {
formatter(value) {
const date = new Date(value)
return date.toLocaleString()
},
},
},
yAxis: [{ type: 'value' }, { type: 'value', max: 10, show: false }],
series: item.data.map((item, index) => {
return {
data: item,
type: 'line',
smooth: true,
symbol: 'none',
name: name[index],
color: color[index],
yAxisIndex: yIndex[index],
}
}),
legend: {},
dataZoom: [{}],
brush: {
toolbox: ['lineX'],
xAxisIndex: 0,
brushType: 'lineX',
},
grid: [
{
left: 60,
right: 50,
bottom: 50,
top: 30,
},
],
}
return option
}
function setChartRef(
el: Element | ComponentPublicInstance | null,
index: number,
) {
let dom: HTMLElement | null = null
if (el) {
if ('$el' in el && (el as any).$el instanceof HTMLElement)
dom = (el as any).$el
else if (el instanceof HTMLElement)
dom = el
}
if (dom)
useECharts(ref(dom as HTMLDivElement))
}
function setEchartsRef(el: any, index: number) {
if (el)
echartsRefs.value[index] = el
}
const isInitBrush = ref(true)
function onChartFinished(index: number) {
if (brushActivated.value.has(index))
return
const chart = echartsRefs.value[index]
if (!chart)
return
chart.dispatchAction({
type: 'takeGlobalCursor',
key: 'brush',
brushOption: {
brushType: 'lineX',
brushMode: 'multiple',
},
})
const areas = (model.value?.trainTime || []).map(row => ({
brushType: 'lineX',
xAxisIndex: 0,
coordRange: [
dayjs(row.st).format('YYYY-MM-DD HH:mm:ss'),
dayjs(row.et).format('YYYY-MM-DD HH:mm:ss'),
],
}))
chart.dispatchAction({
type: 'brush',
areas,
})
brushActivated.value.add(index)
isInitBrush.value = true
}
onMounted(async () => {
await fetchModelInfo()
})
const onBrushSelected = debounce((params) => {
if (isInitBrush.value) {
isInitBrush.value = false
return
}
const selected = params.batch[0].selected
if (selected.length > 0) {
const areas = mergeAreas(params.batch[0].areas).map(area => ({
brushType: area.brushType,
xAxisIndex: 0,
coordRange: area.coordRange,
}))
const trainTime = areas.map((area) => {
const [stRaw, etRaw] = area.coordRange
const st = typeof stRaw === 'string' ? dayjs(stRaw).valueOf() : stRaw
const et = typeof etRaw === 'string' ? dayjs(etRaw).valueOf() : etRaw
console.log('Selected area:', { st, et }, area)
return {
st: dayjs(st).format('YYYY-MM-DD HH:mm:ss'),
et: dayjs(et).format('YYYY-MM-DD HH:mm:ss'),
duration: Math.round((et - st) / 1000),
number: '', // 采样数量(如有数据可补充)
filter: '', // 清洗样本数(如有数据可补充)
mode: '', // 有效样本数(如有数据可补充)
}
})
console.log('Selected train time:', trainTime)
if (trainTimeCopy === JSON.stringify(trainTime))
return
model.value.trainTime = trainTime
trainTimeCopy = JSON.stringify(trainTime)
echartsRefs.value.forEach((chart, index) => {
if (chart) {
chart.dispatchAction({
type: 'brush',
areas,
})
isInitBrush.value = true
}
})
updateModelInfoDebounced()
}
}, 300)
function mergeAreas(areas: any[]) {
if (!areas.length)
return []
// 只合并 brushType 和 xAxisIndex 相同的区间
const sorted = [...areas].sort(
(a, b) => a.coordRange[0] - b.coordRange[0],
)
const merged: any[] = []
for (const area of sorted) {
if (
merged.length
&& merged[merged.length - 1].brushType === area.brushType
&& merged[merged.length - 1].xAxisIndex === area.xAxisIndex
&& merged[merged.length - 1].coordRange[1] >= area.coordRange[0]
) {
// 有交集或相邻,合并
merged[merged.length - 1].coordRange[1] = Math.max(
merged[merged.length - 1].coordRange[1],
area.coordRange[1],
)
}
else {
// 新区间
merged.push({
brushType: area.brushType,
xAxisIndex: area.xAxisIndex,
coordRange: [...area.coordRange],
})
}
}
return merged
}
// 防抖更新
const updateModelInfoDebounced = debounce(() => {
const val = toRaw(model.value)
if (val && val.id)
updateModelInfo(val)
}, 500)
function handleDeleteTrainTime(index: number) {
if (!model.value?.trainTime)
return
// 赋值新数组,确保响应式
model.value.trainTime = [
...model.value.trainTime.slice(0, index),
...model.value.trainTime.slice(index + 1),
]
// 删除后同步更新所有图表的 brush 区域
const areas = (model.value.trainTime || []).map(row => ({
brushType: 'lineX',
xAxisIndex: 0,
coordRange: [dayjs(row.st).valueOf(), dayjs(row.et).valueOf()],
}))
echartsRefs.value.forEach((chart) => {
if (chart) {
chart.dispatchAction({
type: 'brush',
areas,
})
isInitBrush.value = true
}
})
updateModelInfoDebounced()
}
async function clearModel() {
model.value.para = null
updateModelInfoDebounced()
await getHistory()
}
async function trainModel() {
const modelInfo = model.value
if (!modelInfo || !modelInfo.id) {
console.error('模型信息不完整,无法训练')
return
}
const pointInfo = modelInfo.pointInfo || []
if (pointInfo.length === 0) {
console.error('模型参数点信息为空,无法训练')
return
}
const params = {
conditon: modelInfo.alarmmodelset?.alarmcondition || '1==1',
Hyper_para: {
percent: modelInfo.rate,
},
Train_Data: {
points: pointInfo.map(item => item.pointId).join(','),
dead: pointInfo.map(item => (item.dead ? '1' : '0')).join(','),
limit: pointInfo.map(item => (item.limit ? '1' : '0')).join(','),
uplow: pointInfo
.map(
item =>
`${item.Upper ? item.Upper : null},${
item.Lower ? item.Lower : null
}`,
)
.join(';'),
interval: modelInfo.sampling * 1000,
time: modelInfo.trainTime
.map(item => `${item.st},${item.et}`)
.join(';'),
},
type: 'PCA',
smote_config: [],
smote: true,
}
spinning.value = true
try {
const response = await trainModelApi(params)
model.value.para = response
model.value.principal = response.Model_info.K
updateModelInfoDebounced()
getTestData()
createMessage.success('模型训练成功')
}
catch (error) {
console.error('模型训练失败:', error)
createMessage.error('模型训练失败')
}
spinning.value = false
}
const editForm = ref({
index: -1,
Upper: '',
Lower: '',
lowerBound: '',
upperBound: '',
dead: true,
limit: false,
})
const openEditPointModal = ref<boolean>(false)
let pointEditRecord = null
function editPoint() {
// 这里可以添加编辑点的逻辑
model.value.pointInfo[editForm.value.index] = {
...model.value.pointInfo[editForm.value.index],
Upper: editForm.value.Upper,
Lower: editForm.value.Lower,
lowerBound: editForm.value.lowerBound,
upperBound: editForm.value.upperBound,
dead: editForm.value.dead,
limit: editForm.value.limit,
}
updateModelInfoDebounced()
pointEditRecord.Upper = editForm.value.Upper
pointEditRecord.Lower = editForm.value.Lower
pointEditRecord.lowerBound = editForm.value.lowerBound
pointEditRecord.upperBound = editForm.value.upperBound
pointEditRecord.dead = editForm.value.dead
pointEditRecord.limit = editForm.value.limit
openEditPointModal.value = false
}
function openPointModal(index, record) {
// 当前页 index
const pageIndex = index
// 全局 index
const globalIndex = model.value.pointInfo.findIndex(
item => item.pointId === record.pointId,
)
openEditPointModal.value = true
pointEditRecord = record
editForm.value = {
index: globalIndex,
Upper: record?.Upper ?? '',
Lower: record?.Lower ?? '',
lowerBound: record?.lowerBound ?? '',
upperBound: record?.upperBound ?? '',
dead: !!record?.dead,
limit: !!record?.limit,
}
}
const mode = ref({
alarmcondition: '1==1',
alarmname: '全工况运行',
})
const openEditModeModal = ref<boolean>(false)
function openEditMode() {
openEditModeModal.value = true
mode.value = {
alarmcondition: model.value?.alarmmodelset?.alarmcondition || '1==1',
alarmname: model.value?.alarmmodelset?.alarmname || '全工况运行',
}
}
function closeEditMode() {
openEditModeModal.value = false
}
function handleEditMode() {
// 这里可以添加编辑模式的逻辑
model.value.alarmmodelset = mode.value
updateModelInfoDebounced()
closeEditMode()
}
const openEditModelModal = ref(false)
const editModelForm = ref({
sampling: 0,
rate: 0,
selectedKeys: [],
})
// 穿梭框数据源示例
const transferData = ref([]) // 穿梭框数据源
// 初始化时获取全部测点
async function fetchAllPoints(keyword = '') {
// 这里调用你的后端接口,传递 keyword
// const res = await fetchPointsApi({ keyword })
// 示例数据
const data = await pointListApi({ keyword })
console.log('Fetched points:', data)
transferData.value = transferData.value.filter(item => editModelForm.value.selectedKeys.includes(item.key))
.concat(data.map(item => ({
key: item.id,
title: item.name,
})))
console.log('Transfer data:', transferData.value)
}
onMounted(() => {
fetchAllPoints()
})
function handleTransferSearch(dir, value) {
if (dir === 'left') {
// 左侧:后端接口查询
fetchAllPoints(value)
}
else {
// 右侧:前端过滤
transferData.value = (model.value?.pointInfo || [])
.filter(item =>
editModelForm.value.selectedKeys.includes(`${item.description}|${item.pointId}|${item.unit}|${item.Lower}|${item.Upper}`)
&& (`${item.description || ''}(${item.pointId})`).includes(value),
)
.map(item => ({
key: `${item.description}|${item.pointId}|${item.unit}|${item.Lower}|${item.Upper}`,
title: `${item.description || ''} (${item.pointId})`,
}))
}
}
function openEditModel() {
editModelForm.value.sampling = model.value?.sampling || 0
editModelForm.value.rate = model.value?.rate || 0
editModelForm.value.selectedKeys = (model.value?.pointInfo || []).map(item => `${item.description}|${item.pointId}|${item.unit}|${item.Lower}|${item.Upper}`)
transferData.value = (model.value?.pointInfo || []).map(item => ({
key: `${item.description}|${item.pointId}|${item.unit}|${item.Lower}|${item.Upper}`,
title: `${item.description || ''} (${item.pointId})`,
}))
console.log('transferData:', transferData.value)
openEditModelModal.value = true
}
function handleEditModelOk() {
model.value.sampling = editModelForm.value.sampling
model.value.rate = editModelForm.value.rate
model.value.pointInfo = editModelForm.value.selectedKeys.map((key) => {
const [description, pointId, unit, Lower, Upper] = key.split('|')
return {
description,
pointId,
unit,
Lower,
Upper,
dead: true,
limit: false,
}
})
clearModel()
openEditModelModal.value = false
}
function handleEditModelCancel() {
openEditModelModal.value = false
}
async function bottomModel() {
if (!model.value.para) {
createMessage.error('模型未训练,无法下装')
return
}
try {
const response = await bottomModelApi(model.value.id)
model.value = response
createMessage.success('模型下装成功')
}
catch (error) {
console.error('底层模型获取失败:', error)
createMessage.error('底层模型获取失败')
}
}
return {
pointTable,
model,
activeKey,
historyTime,
historyList,
getHistory,
getOption,
setChartRef,
setEchartsRef,
onChartFinished,
echartsRefs,
onBrushSelected,
trainTimeTable,
handleDeleteTrainTime,
openEditPointModal,
editPoint,
openPointModal,
editForm,
trainModel,
openEditModeModal,
openEditMode,
closeEditMode,
handleEditMode,
mode,
spinning,
clearModel,
openEditModelModal,
editModelForm,
transferData,
handleTransferSearch,
openEditModel,
handleEditModelOk,
handleEditModelCancel,
bottomModel,
}
},
})
</script>
<template>
<PageWrapper content-background>
<div>
<a-card title="模型信息" :bordered="false">
<a-descriptions size="small" :column="4" bordered>
<a-descriptions-item label="模型名称">
{{ model?.name }}
</a-descriptions-item>
<a-descriptions-item label="描述">
{{ model?.description || "暂无描述" }}
</a-descriptions-item>
<a-descriptions-item label="版本">
{{ model?.Cur_Version || "v-test" }}
</a-descriptions-item>
<a-descriptions-item label="评估报告">
暂无
</a-descriptions-item>
<a-descriptions-item label="创建人">
{{ model?.founder }}
</a-descriptions-item>
<a-descriptions-item label="创建时间">
{{ model?.creatTime }}
</a-descriptions-item>
<a-descriptions-item label="最近修改人">
{{ model?.Modifier || "暂无" }}
</a-descriptions-item>
<a-descriptions-item label="最近修改时间">
{{ model?.modifiedTime || "暂无" }}
</a-descriptions-item>
</a-descriptions>
<a-divider />
<a-descriptions size="small" :column="4" bordered>
<a-descriptions-item label="算法">
{{ model?.algorithm || "PCA" }}
</a-descriptions-item>
<a-descriptions-item label="训练采样间隔">
{{ model?.sampling }}
</a-descriptions-item>
<a-descriptions-item label="参数个数">
{{ model?.pointInfo.length || "暂无" }}
</a-descriptions-item>
<a-descriptions-item label="最小主元贡献率">
{{ model?.rate }}
</a-descriptions-item>
<a-descriptions-item label="最小主元个数">
{{ model?.principal }}
</a-descriptions-item>
<a-descriptions-item label="模型性能">
{{ model?.precision }}
</a-descriptions-item>
<a-descriptions-item label="训练总时长(h)">
{{
(
model?.trainTime.reduce(
(total, item) => total + item.duration,
0,
) / 3600
).toFixed(2) || "暂无"
}}
</a-descriptions-item>
<a-descriptions-item label="有效样本数">
{{ model?.principal }}
</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card
title="模式"
:bordered="false"
style="margin-top: 16px; margin-bottom: -20px"
>
<a-button size="large" @click="openEditMode">
{{ model?.alarmmodelset.alarmname }}
</a-button>
</a-card>
<a-card :bordered="false">
<a-tabs v-model:active-key="activeKey">
<a-tab-pane key="1" tab="训练采样时间">
<BasicTable @register="trainTimeTable">
<template #action="{ record, index }">
<a-button
type="link"
danger
@click="handleDeleteTrainTime(index)"
>
删除
</a-button>
</template>
</BasicTable>
</a-tab-pane>
<a-tab-pane key="2" tab="测点参数">
<BasicTable @register="pointTable">
<template #action="{ record, index }">
<a-button type="primary" @click="openPointModal(index, record)">
编辑
</a-button>
</template>
</BasicTable>
</a-tab-pane>
</a-tabs>
</a-card>
<a-card title="智能训练" :bordered="false">
<div style="display: flex; align-items: center">
<a-form layout="inline" style="flex: 1">
<a-form-item label="模型预览时间范围">
<a-range-picker
v-model:value="historyTime"
show-time
@change="getHistory"
/>
</a-form-item>
</a-form>
<a-button
type="primary"
style="margin-left: auto"
@click="trainModel"
>
模型训练
</a-button>
<a-button
type="primary"
style="margin-left: 10px"
@click="clearModel"
>
清除训练结果
</a-button>
<a-button type="primary" style="margin-left: 6px" @click="openEditModel">
修改模型
</a-button>
<a-button
danger
style="margin-left: 10px"
@click="bottomModel"
>
下装
</a-button>
</div>
<a-divider />
<a-spin :spinning="spinning" size="large">
<div
v-for="(item, index) in historyList"
:key="index"
class="echart-box"
style="width: 100%"
>
<a-card :bordered="false" style="margin-bottom: 16px">
<template #title>
<span style="font-size: 20px">{{ item.name }}</span>
</template>
<VueECharts
:ref="(el) => setEchartsRef(el, index)"
:option="getOption(item)"
autoresize
style="width: 100%; height: 200px"
@finished="() => onChartFinished(index)"
@brush-selected="onBrushSelected"
/>
</a-card>
</div>
</a-spin>
</a-card>
</div>
<a-modal
v-model:open="openEditPointModal"
title="更改上下限"
@ok="editPoint"
>
<a-form
:model="editForm"
:label-col="{ span: 7 }"
:wrapper-col="{ span: 15 }"
>
<a-form-item label="上限">
<a-input-number
v-model:value="editForm.Upper"
placeholder="请输入上限"
/>
</a-form-item>
<a-form-item label="下限">
<a-input-number
v-model:value="editForm.Lower"
placeholder="请输入下限"
/>
</a-form-item>
<a-form-item label="残差上限">
<a-input-number
v-model:value="editForm.upperBound"
placeholder="请输入残差上限"
/>
</a-form-item>
<a-form-item label="残差下限">
<a-input-number
v-model:value="editForm.lowerBound"
placeholder="请输入残差下限"
/>
</a-form-item>
<a-form-item label="清洗方式" style="margin-top: 16px">
<a-checkbox v-model:checked="editForm.dead">
死区清洗
</a-checkbox>
<a-checkbox v-model:checked="editForm.limit">
限值清洗
</a-checkbox>
</a-form-item>
</a-form>
</a-modal>
<a-modal
v-model:open="openEditModeModal"
title="编辑模式"
@ok="handleEditMode"
@cancel="closeEditMode"
>
<a-form
:model="mode"
:label-col="{ span: 7 }"
:wrapper-col="{ span: 15 }"
>
<a-form-item label="模式名称">
<a-input
v-model:value="mode.alarmname"
placeholder="请输入模式名称"
/>
</a-form-item>
<a-form-item label="报警条件">
<a-input
v-model:value="mode.alarmcondition"
placeholder="请输入报警条件"
/>
</a-form-item>
</a-form>
</a-modal>
<a-modal
v-model:open="openEditModelModal"
title="编辑模型"
width="1200px"
@ok="handleEditModelOk"
@cancel="handleEditModelCancel"
>
<a-form
:model="editModelForm"
:label-col="{ span: 7 }"
:wrapper-col="{ span: 15 }"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="训练采样间隔" required>
<a-input-number
v-model:value="editModelForm.sampling"
placeholder="请输入训练采样间隔"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="最小主元贡献率" required>
<a-input-number
v-model:value="editModelForm.rate"
placeholder="请输入最小主元贡献率"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<div style="display: flex; justify-content: center; margin-top: 24px;margin-bottom: 10px;">
<a-transfer
v-model:target-keys="editModelForm.selectedKeys"
:data-source="transferData"
:render="item => item.title"
:row-key="(item) => item.key"
:pagination="false"
:show-search="true"
:filter="true"
search-placeholder="搜索测点"
:titles="['可选测点', '已选测点']"
:list-style="{
width: '500px',
height: '450px',
}"
@search="handleTransferSearch"
/>
</div>
</a-form>
</a-modal>
</PageWrapper>
</template>
<style>
.ant-card-head-title {
font-weight: bold;
}
.el-table .el-table__header th {
font-weight: bold;
}
</style>