|
|
<template> <div class="upload-minio"> <el-dialog title="文件上传列表" class="minio-file" :close-on-click-modal="false" :modal-append-to-body="false" append-to-body :visible.sync="uploadMinioVisible" :before-close="handleCloseDialog" width="600px" > <template #title> {{ uploadTitle }} <span style="color: red;font-size: 12px; ">单个文件不可超过10GB</span> </template>
<div class="uploader-drop" style="margin-bottom: 20px;"> <div class="uploader-btn" @click="triggerFileInput"> <p>{{ !isCatalogUpload ? '点击上传(可多文件上传)' : "点击上传ZIP包(只可单文件上传)" }}</p> <div style="margin: 20px 0 20px 0;"> <i class="iconfont icon-tianjiawenjian upload-icon" /> <input ref="fileInput" type="file" :accept="isCatalogUpload ? '.zip' : ''" :multiple="!isCatalogUpload" :disabled="!isCaValid || isCheckingCa" class="file-input" style="display: none;" @change="handleFileSelect" > </div> </div> <div class="el-upload__tip">上传限制文件大小:最大10GB/个</div> </div>
<div v-if="fileList.length !== 0" style="max-height: 400px; overflow: hidden; overflow-y: scroll;"> <div v-for="(fileItem, index) in fileList" :key="index" class="file-item"> <div class="file-name"> {{ fileItem.file.name }} <span class="file-size">{{ formatFileSize(fileItem.file.size) }}</span> <i class="iconfont icon-shanchu" @click="handleDeleteFile(index)" /> </div> <div v-if="fileItem.uploading" class="progress-wrapper"> <span class="progress-text">上传进度: {{ fileItem.progress }}%</span> <div class="progress-bar" :style="{ width: fileItem.progress + '%' }" /> </div> <div v-if="fileItem.merging" class="merge-loading "> <span>合并中...</span> </div> <p v-if="fileItem.errorMsg" class="error">{{ fileItem.errorMsg }}</p> <p v-if="fileItem.successMsg" class="success">{{ fileItem.successMsg }}</p> </div> </div> <div slot="footer" class="dialog-footer" style="margin-top: 0;"> <el-button type="text" @click="handleCloseDialog">取消</el-button> <el-button :disabled="fileList.some(item => item.uploading || item.merging) || fileList.length === 0" :loading="btnLoading" type="primary" @click="handleUploadConfirm">保存</el-button> </div> </el-dialog>
<!-- 判断是否有重复上传的文件 --> <el-dialog class="collectUpload-dialog" title="文件上传" :close-on-click-modal="false" :modal-append-to-body="false" append-to-body :visible.sync="repeatFileVisible"> <div class="setting-dialog"> <p style="color:#f00;margin-bottom: 20px;display:block">提示:以下所选文件在当前档案文件列表已存在</p> <div v-for="item in repeatFileData" :key="item.name" class="file-list" style="margin-bottom: 10px;"> <i class="iconfont icon-xiaowenjian" /> {{ item.name }} </div> </div> <div slot="footer" class="dialog-footer"> <el-button type="primary" @click="handleRepeatFile(0)">直接上传</el-button> <el-button style="width: 85px;" type="primary" @click="handleRepeatFile(1)">去重后上传</el-button> <el-button type="text" @click="repeatFileVisible = false">取消</el-button> </div> </el-dialog>
<!-- 目录上传报错 --> <el-dialog class="catalog-dialog" title="目录上传-失败列表" :close-on-click-modal="false" :modal-append-to-body="false" append-to-body :visible.sync="catalogErrorVisible"> <div class="setting-dialog"> <div style="margin-bottom: 20px; display:flex; justify-content: flex-start;"> <p style="margin-right: 20px;">总条数: <span style=" font-weight: bold;">{{ resultCatalog && resultCatalog.mountFile.total }} </span> 条</p> <p style="margin-right: 20px;">成功: <span style="color:#1AAE93; font-weight: bold;">{{ resultCatalog && resultCatalog.mountFile.successNum }} </span> 条</p> <p>失败: <span style="color:#f00; font-weight: bold; ">{{ resultCatalog && resultCatalog.mountFile.failNum }}</span> 条 </p> </div> <el-table class="archives-table" :data="catalogInfoData" style="min-width: 100%" height="calc(100vh - 676px)"> <el-table-column type="expand"> <template #default="{ row }"> <el-row style="padding-left: 20px;"> <el-col :span="24" style="line-height: 30px;"> <div v-for="(file, index) in row.children" :key="index"><i class="iconfont icon-xiaowenjian" />{{ file }}</div> </el-col> </el-row> </template> </el-table-column> <el-table-column prop="archives" label="档案/电子原文" /> <el-table-column prop="description" label="失败原因" /> </el-table> </div> <div slot="footer" class="dialog-footer"> <el-button type="primary" @click="catalogErrorVisible = false">确定</el-button> </div> </el-dialog> </div></template>
<script>import { FetchCheckCaValidity } from '@/api/system/auth2'import { FetchInitFileCategoryView } from '@/api/collect/collect'import { mapGetters } from 'vuex'import axios from 'axios'import SparkMD5 from 'spark-md5'import { getToken } from '@/utils/auth'import { getCurrentTime } from '@/utils/index'import { catalogUpload } from '@/utils/upload'
export default { name: 'MinioMultiChunkUpload', props: { selectedDocument: { type: Object, default: () => ({}) }, arcId: { type: String, default: '' }, selectedCategory: { type: Object, default: () => ({}) }, isBatchMount: { type: String, default: '' } },
computed: { ...mapGetters([ 'baseApi' ]) }, data() { return { uploadMinioVisible: false, // 弹框显示状态
fileList: [], // 多文件上传列表
CHUNK_SIZE: 5 * 1024 * 1024, // 分片大小 (5MB)
totalMergeStartTime: null, // 整体批量合并开始时间
totalMergeEndTime: null, // 整体批量合并结束时间
allChunksUploaded: false, // 所有文件分片是否上传完成
isCaValid: false, // CA证书是否有效
isCheckingCa: false, // 是否正在校验CA状态
// 新增目录上传相关
isCatalogUpload: false, uploadTitle: '文件上传', btnLoading: false,
// 重复文件相关
repeatFileVisible: false, repeatFileData: [], originFileData: [], tempSelectedFiles: null, // 临时存储选择的文件
// 目录上传报错相关
resultCatalog: { mountFile: {}, failArchives: [] }, catalogErrorVisible: false, catalogInfoData: [] } },
mounted() { this.getCheckCaValidity() // 初始化时自动校验CA证书
},
methods: { triggerFileInput() { this.$refs.fileInput.click() }, handleDeleteFile(index) { // 校验是否正在上传/合并,避免删除操作冲突
const fileItem = this.fileList[index] if (fileItem.uploading || fileItem.merging) { this.showMessage('当前文件正在上传/合并中,无法删除', 'warning') return }
// 删除对应索引的文件项
this.fileList.splice(index, 1) this.showMessage('文件已移除', 'success') }, // 切换上传类型(普通文件/目录ZIP包)
updateUploadOptions(uploadType) { this.isCatalogUpload = uploadType !== 1 this.uploadTitle = this.isCatalogUpload ? '原文目录上传' : '文件上传' this.fileList = [] this.uploadMinioVisible = true },
formatFileSize(bytes) { if (bytes === 0) return '0.00MB' const mb = bytes / (1024 * 1024) return mb.toFixed(2) + 'MB' },
/** * 工具方法:格式化时间戳为易读的本地时间(带毫秒) */ formatTime(timestamp) { if (!timestamp) return '无' return new Date(timestamp).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', millisecond: '3-digit' }) },
/** * 工具方法:计算两个时间戳的差值并格式化 */ getTimeDiff(start, end) { if (!start || !end) return '0ms' const diff = end - start if (diff < 1000) return `${diff}ms` if (diff < 60000) return `${(diff / 1000).toFixed(2)}s` return `${(diff / 60000).toFixed(2)}min` },
/** * 工具方法:获取图片分辨率 */ getImgPx(base64) { return new Promise((resolve) => { const img = new Image() img.onload = () => { resolve({ width: img.width, height: img.height }) } img.src = base64 }) },
/** * 工具方法:将文件转为base64 */ getBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader() reader.readAsDataURL(file) reader.onload = () => resolve(reader.result) reader.onerror = (err) => reject(err) }) },
/** * 工具方法:显示提示信息 */ showMessage(message, type) { this.$message({ message, type, offset: 8 }) },
/** * 重置上传状态 */ resetUploadState() { this.allChunksUploaded = false this.btnLoading = false },
/** * 校验CA证书有效性 */ async getCheckCaValidity() { try { this.isCheckingCa = true const res = await FetchCheckCaValidity() this.isCaValid = !!res if (!this.isCaValid) { this.showMessage('CA证书不在有效期内,无法进行分片上传', 'error') } } catch (err) { this.isCaValid = false this.showMessage('CA证书校验失败:' + err.message, 'error') console.error('CA校验接口异常:', err) } finally { this.isCheckingCa = false } },
/** * 获取已存在的文件列表(用于重复文件检测) */ async getFileList() { const params = { 'categoryId': this.selectedCategory.id, 'archivesId': this.arcId } const res = await FetchInitFileCategoryView(params) this.originFileData = res.returnlist || [] },
/** * 多文件选择处理函数:选择后打开弹框,检测重复文件 */ async handleFileSelect(e) { // 1. 校验CA状态(原有逻辑不变)
if (this.isCheckingCa) { this.showMessage('正在校验CA证书,请稍候...', 'warning') e.target.value = '' return } if (!this.isCaValid) { this.showMessage('CA证书不在有效期内,无法上传文件', 'error') e.target.value = '' return }
// 2. 获取选择的文件并初始化列表(原有逻辑不变)
const selectedFiles = Array.from(e.target.files) if (selectedFiles.length === 0) return
// 3. 重复文件检测(原有逻辑不变)
if (!this.isCatalogUpload) { await this.getFileList() const existingFileNames = this.originFileData.map(file => file.file_name) this.repeatFileData = selectedFiles.filter(file => existingFileNames.includes(file.name))
if (this.repeatFileData.length > 0) { this.repeatFileVisible = true this.tempSelectedFiles = selectedFiles e.target.value = '' return } }
// 4. 初始化文件列表(原有逻辑不变)
const newFileList = selectedFiles.map(file => ({ file, uploading: false, merging: false, progress: 0, errorMsg: '', successMsg: '', md5: '', md5StartTime: null, md5EndTime: null, chunkUploadStartTime: null, chunkUploadEndTime: null, mergeStartTime: null, mergeEndTime: null }))
this.fileList = [...this.fileList, ...newFileList] e.target.value = ''
// this.handleUploadConfirm()
},
/** * 处理重复文件选择 * @param {number} type 0-直接上传 1-去重后上传 */ handleRepeatFile(type) { this.repeatFileVisible = false
if (!this.tempSelectedFiles) return
let filesToUpload = []
if (type === 0) { // 直接上传所有文件
filesToUpload = this.tempSelectedFiles } else { // 去重后上传
const existingFileNames = this.originFileData.map(file => file.file_name) filesToUpload = this.tempSelectedFiles.filter(file => !existingFileNames.includes(file.name))
if (filesToUpload.length === 0) { this.showMessage('当前所选文件去重后无可上传的文件', 'error') this.$emit('close-dialog') return } }
// 初始化去重后的文件列表
const newFileList = filesToUpload.map(file => ({ file, uploading: false, merging: false, progress: 0, errorMsg: '', successMsg: '', md5: '', md5StartTime: null, md5EndTime: null, chunkUploadStartTime: null, chunkUploadEndTime: null, mergeStartTime: null, mergeEndTime: null }))
this.fileList = newFileList this.uploadMinioVisible = true this.tempSelectedFiles = null
// this.handleUploadConfirm()
},
/** * 计算文件MD5 */ calculateFileMd5(file, fileItem) { return new Promise((resolve, reject) => { fileItem.md5StartTime = new Date().getTime() console.log(`【${file.name}】MD5计算开始时间:${this.formatTime(fileItem.md5StartTime)}`)
const spark = new SparkMD5.ArrayBuffer() const fileReader = new FileReader() const chunkSize = 2 * 1024 * 1024 let offset = 0
const loadNextChunk = () => { const blob = file.slice(offset, offset + chunkSize) fileReader.readAsArrayBuffer(blob) }
fileReader.onload = (e) => { spark.append(e.target.result) offset += chunkSize if (offset < file.size) { loadNextChunk() } else { fileItem.md5EndTime = new Date().getTime() console.log(`【${file.name}】MD5计算结束时间:${this.formatTime(fileItem.md5EndTime)},耗时:${this.getTimeDiff(fileItem.md5StartTime, fileItem.md5EndTime)}`) resolve(spark.end()) } }
fileReader.onerror = (err) => reject(err) loadNextChunk() }) },
/** * 检查分片是否已存在 */ async checkChunkExists(fileMd5, chunkIndex) { const linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API const response = await axios.get(`${linkSrc}/api/collect/chunk`, { params: { fileMd5, chunkIndex }, headers: { 'Authorization': getToken() } })
if (response.data.code !== 200) { throw new Error('检查分片失败: ' + response.data.msg) }
return response.data.data },
/** * 上传单个分片 */ async uploadSingleChunk(file, fileMd5, chunkIndex) { const start = chunkIndex * this.CHUNK_SIZE const end = Math.min(start + this.CHUNK_SIZE, file.size) const chunkBlob = file.slice(start, end)
const formData = new FormData() formData.append('file', chunkBlob, `${fileMd5}_${chunkIndex}`) formData.append('fileMd5', fileMd5) formData.append('chunkIndex', chunkIndex)
const linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API const response = await axios.post(`${linkSrc}/api/collect/chunk`, formData, { headers: { 'Content-Type': 'multipart/form-data', 'Authorization': getToken() } })
if (response.data.code !== 200) { throw new Error(`分片${chunkIndex}上传失败: ` + response.data.msg) } },
/** * 校验所有分片是否存在 */ async checkAllChunksExist(fileMd5, totalChunks) { for (let i = 0; i < totalChunks; i++) { const result = await this.checkChunkExists(fileMd5, i) if (!result.exists) { console.warn(`【${fileMd5}】分片${i}未上传`) return false } } return true },
/** * 单个文件的分片上传流程 */ async uploadFileChunks(fileItem) { const file = fileItem.file fileItem.uploading = true fileItem.progress = 0 fileItem.errorMsg = '' fileItem.successMsg = ''
try { // 计算文件MD5
const fileMd5 = await this.calculateFileMd5(file, fileItem) fileItem.md5 = fileMd5 console.log(`【${file.name}】文件MD5:`, fileMd5)
// 计算总分片数
const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE) console.log(`【${file.name}】总分片数:`, totalChunks)
// 分片上传开始时间
fileItem.chunkUploadStartTime = new Date().getTime() console.log(`【${file.name}】分片上传开始时间:${this.formatTime(fileItem.chunkUploadStartTime)}`)
const uploadedChunks = [] for (let i = 0; i < totalChunks; i++) { // 检查分片是否已存在
const checkResult = await this.checkChunkExists(fileMd5, i) if (!checkResult.exists) { // 上传未存在的分片
await this.uploadSingleChunk(file, fileMd5, i) } uploadedChunks.push(i) fileItem.progress = Math.round((uploadedChunks.length / totalChunks) * 100) }
// 分片上传结束时间
fileItem.chunkUploadEndTime = new Date().getTime() console.log(`【${file.name}】分片上传结束时间:${this.formatTime(fileItem.chunkUploadEndTime)},耗时:${this.getTimeDiff(fileItem.chunkUploadStartTime, fileItem.chunkUploadEndTime)}`) fileItem.progress = 100 } catch (err) { const fileName = file.name if (fileItem.chunkUploadStartTime && !fileItem.chunkUploadEndTime) { fileItem.chunkUploadEndTime = new Date().getTime() console.log(`【${fileName}】分片上传异常结束时间:${this.formatTime(fileItem.chunkUploadEndTime)},耗时:${this.getTimeDiff(fileItem.chunkUploadStartTime, fileItem.chunkUploadEndTime)}`) } fileItem.errorMsg = '分片上传失败: ' + (err.message || '未知错误') console.error(`【${fileName}】分片上传流程异常:`, err.message) this.allChunksUploaded = false } finally { fileItem.uploading = false } },
/** * 批量上传并合并:整合目录上传逻辑 */ async handleUploadConfirm() { if (this.fileList.length === 0) { this.showMessage('请先选择要上传的文件!', 'warning') return }
// 过滤掉已上传/合并失败的文件
const pendingFiles = this.fileList.filter(item => !item.successMsg && !item.errorMsg) if (pendingFiles.length === 0) { this.showMessage('暂无待上传的文件!', 'warning') return }
this.btnLoading = true
// 1. 串行上传所有文件的分片
this.allChunksUploaded = true for (const fileItem of pendingFiles) { await this.uploadFileChunks(fileItem) if (fileItem.errorMsg) { this.allChunksUploaded = false } }
// 2. 分片上传完成后执行合并
if (!this.allChunksUploaded) { this.btnLoading = false this.showMessage('部分文件分片上传失败,无法执行合并', 'error') return }
const validFiles = this.fileList.filter(item => !item.errorMsg && item.progress === 100) if (validFiles.length === 0) { this.btnLoading = false this.showMessage('无有效分片上传完成的文件!', 'error') return }
this.totalMergeStartTime = new Date().getTime() console.log(`【整体合并】所有文件分片上传完成,开始合并,合并开始时间:${this.formatTime(this.totalMergeStartTime)}`) const nowDate = getCurrentTime()
validFiles.forEach(fileItem => { fileItem.merging = true fileItem.mergeStartTime = new Date().getTime() console.log(`【文件${fileItem.file.name}】合并开始时间:${this.formatTime(fileItem.mergeStartTime)}`) })
try { const processFiles = validFiles.map(async(fileItem) => { const file = fileItem.file const json = {} const jsonArray = [] const jsonString = {}
// 获取图片分辨率
if (file.type.startsWith('image')) { const fileBase64 = await this.getBase64(file) const imgRes = await this.getImgPx(fileBase64) jsonString.file_dpi = `${imgRes.width}px*${imgRes.height}px` } else { jsonString.file_dpi = '' }
// 构建文件信息
jsonString.file_name = file.name jsonString.file_size = file.size jsonString.file_type = file.name.split('.').pop() || '' jsonString.last_modified = file.lastModified jsonString.file_path = '' jsonString.sequence = null jsonString.archive_id = this.arcId jsonString.create_time = nowDate jsonString.id = null jsonString.file_thumbnail = '' jsonArray.push(jsonString)
// 校验分片完整性
const totalChunks = Math.ceil(file.size / this.CHUNK_SIZE) const chunksExist = await this.checkAllChunksExist(fileItem.md5, totalChunks) if (!chunksExist) { throw new Error(`【${file.name}】部分分片未上传完成,无法合并`) }
// 构建合并参数
json.categoryId = this.selectedCategory.id json.archivesId = this.arcId json.identifier = fileItem.md5 json.filename = file.name json.totalChunks = totalChunks json.totalSize = file.size json.fileJsonString = JSON.stringify(jsonArray)
return json })
const jsonArray = await Promise.all(processFiles)
// 调用合并接口
const linkSrc = process.env.NODE_ENV === 'production' ? window.g.ApiUrl : process.env.VUE_APP_BASE_API const response = await axios.post(`${linkSrc}/api/collect/merge`, jsonArray, { headers: { 'Authorization': getToken(), 'Content-Type': 'application/json' } })
this.totalMergeEndTime = new Date().getTime() const totalMergeDuration = this.getTimeDiff(this.totalMergeStartTime, this.totalMergeEndTime)
if (response.data.code === 200) { // 处理目录上传逻辑
if (this.isCatalogUpload && response.data.data.length === 1 && response.data.data[0] !== '') { // 调用目录上传接口
catalogUpload(`${linkSrc}/api/collect/catalogUpload`, response.data.data[0], this.selectedCategory.fondsId ).then(res => { this.btnLoading = false if (res.data.code === 200) { this.resultCatalog = res.data.data if (this.resultCatalog.mountFile.total === this.resultCatalog.mountFile.successNum) { this.showMessage('目录上传操作成功', 'success') this.uploadMinioVisible = false this.fileList = [] this.$emit('close-dialog') } else { this.catalogInfoData = [] this.resultCatalog.failArchives.forEach(item => { const parts = item.split(':') if (parts.length === 2) { const field = parts[0] let fileStr = parts[1] fileStr = fileStr.replace(/\[|\]/g, '') const fileArray = fileStr.split(',').map(file => file.trim()) const match = field.match(/^([\w-·]+)(.*)$/) let archives = '' let description = '' if (match) { archives = match[1] description = match[2] } this.catalogInfoData.push({ archives, description, children: fileArray }) } }) this.catalogErrorVisible = true this.$emit('close-dialog') } } else { this.showMessage(`目录上传失败: ${res.data.message}`, 'error') } }) } else { // 普通文件上传成功
this.showMessage('所有文件上传并合并成功', 'success') validFiles.forEach((fileItem) => { fileItem.mergeEndTime = new Date().getTime() const mergeDuration = this.getTimeDiff(fileItem.mergeStartTime, fileItem.mergeEndTime) console.log(`【文件${fileItem.file.name}】合并结束时间:${this.formatTime(fileItem.mergeEndTime)},合并耗时:${mergeDuration}`) fileItem.successMsg = '上传成功!' fileItem.merging = false }) console.log(`【整体合并】所有文件合并完成,整体合并耗时:${totalMergeDuration}`)
this.$emit('onUploadSuccess', response.data.data, validFiles.map(f => f.file.name), jsonArray) this.uploadMinioVisible = false this.fileList = [] this.$emit('close-dialog') } } else { throw new Error(response.data.msg || '合并失败') } } catch (err) { this.btnLoading = false this.totalMergeEndTime = new Date().getTime() const totalMergeDuration = this.getTimeDiff(this.totalMergeStartTime, this.totalMergeEndTime) console.log(`【整体合并】合并失败,耗时:${totalMergeDuration},异常信息:`, err) this.showMessage(`文件合并失败: ${err.message}`, 'error') validFiles.forEach(fileItem => { fileItem.merging = false fileItem.errorMsg = fileItem.errorMsg || `合并失败: ${err.message}` }) this.$emit('onUploadError', err) } finally { this.resetUploadState() } },
/** * 关闭弹框时清空文件列表 */ handleCloseDialog() { this.uploadMinioVisible = false this.fileList = [] this.resetUploadState() this.repeatFileData = [] this.tempSelectedFiles = null } }}</script>
<style scoped>.upload-minio { position: relative; margin-top: 10px;}
.uploader-drop{ display: flex; flex-direction: column; justify-content: center; text-align: center; height: 180px; border: none; background-color: #f5f5f5; .uploader-btn{ border: none; i{ font-size: 32px; color: #1F55EB; } &:hover{ background-color: transparent; } } .el-upload__tip{ font-size: 12px; color: #A6ADB6; }}
.file-input { position: absolute; left: 0; top: 0; width: 100px; height: 36px; padding: 5px; opacity: 0; cursor: pointer; z-index: 10;}
.file-item { width: 100%; border: 1px dashed #409eff; padding: 10px; border-radius: 4px; margin-bottom: 10px;}
.file-name { position: relative; font-weight: 500; color: #333; margin-bottom: 8px;}.icon-shanchu{ position: absolute; right: -7px; top: 0; font-size: 20px; color: #1F55EB; cursor: pointer; &:hover { color: #ff4444; transform: scale(1.1); transition: all 0.2s ease; }}.progress-wrapper { display: flex; flex-direction: column; gap: 5px;}
.progress-bar { height: 10px; background-color: #42b983; transition: width 0.3s ease; border-radius: 10px;}
.progress-text { font-size: 14px; color: #666;}
.merge-loading { color: #1890ff; font-size: 14px;}
.success { color: #00C851; font-size: 14px; margin: 5px 0 0 0;}
.error { color: #ff4444; font-size: 14px; margin: 5px 0 0 0;}
.minio-file{ .el-dialog{ .el-dialog__body{ padding: 15px 0 30px 0 !important; } }}.file-size { color: #666; font-weight: normal; font-size: 12px; margin-left: 8px;}
/* 重复文件弹窗样式 */.collectUpload-dialog .file-list { display: flex; align-items: center; gap: 8px;}
/* 目录上传失败弹窗样式 */.catalog-dialog .archives-table { --el-table-header-text-color: #666; --el-table-row-hover-bg-color: #f5f7fa;}</style>
|