vue3+element-plus动态与静态表格数据渲染
一、表格组件:
<template>
<el-table
ref="myTable"
:data="tableData"
:header-cell-style="headerCellStyle"
header-row-class-name="my-table-header"
cell-class-name="my-td-cell"
:row-style="rowStyle"
:cell-style="cellStyle"
:span-method="dynamic ? objectSpanMethod : handleSpanMethod"
@sort-change="tableSort"
@selection-change="handleSelectionChange"
class="stock-table"
border
:max-height="height"
row-key="securityID"
:show-overflow-tooltip="dynamic || !showOverflowTooltip ? false : true"
stripe
>
<slot name="selection"></slot>
<el-table-column v-if="indexShow" label="序号" width="50" align="center">
<template #default="scope">
<span>{{ (currentPage - 1) * pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<!-- 表头渲染 -->
<template v-for="(item, index) in tableColums" :key="index">
<el-table-column
v-if="!item.hidden"
:fixed="item.fixed"
:width="item.width"
:min-width="item.minWidth || ''"
:prop="item.prop"
:sortable="item.sortable"
header-align="center"
:align="item.align || 'center'"
:resizable="false"
>
<template #header>
<div class="heard-text static-heard">
<div class="heard-name" v-html="item.label"></div>
</div>
</template>
<!-- 静态表格表内容渲染 -->
<!-- 使用 scoped slot 将列的内容交给父组件自定义 -->
<template #default="scope">
<slot :name="`column-${item.prop}`" :row="scope.row" :column="item">
<!-- 处理监管处罚第一个表格链接跳转问题 -->
<el-link
type="primary"
:underline="false"
@click="openLink(scope.row.link)"
class="link-ellipsis"
v-if="tableId === 'yearTable' && item.prop === 'title'"
>{{ scope.row[item.prop] }}</el-link
>
<!-- 触及ST指标的次数 悬浮查看年份 -->
<div v-else-if="item.isST">
<el-tooltip
effect="dark"
:content="[...new Set(scope.row[item.prop])].join('、')"
placement="top"
v-if="
scope.row[item.prop] &&
Array.isArray(scope.row[item.prop]) &&
scope.row[item.prop].length > 0
"
>
<span class="red">{{ scope.row[item.prop].length }}</span>
</el-tooltip>
<span v-else>{{
Array.isArray(scope.row[item.prop])
? scope.row[item.prop].length
: scope.row[item.prop]
}}</span>
</div>
<!-- 触及ST指标的次数为拼接的字段 悬浮查看年份 -->
<div
v-else-if="item.isJointST"
:style="{
textAlign:
scope.row[item.prop] && scope.row[item.prop].length === 0
? 'center'
: item.align
}"
>
<template
v-if="
scope.row[item.prop] &&
scope.row[item.prop].length > 0 &&
!item.isNumber
"
>
<span v-for="(i, idx) in scope.row[item.prop]" :key="idx">
<!-- 辖区募投异常变更项目字段根据结构特殊处理 -->
<span v-if="item.noShowYear"
><span class="dot">·</span>{{ i.name
}}<span v-if="idx < scope.row[item.prop].length - 1"
><br /></span
></span>
<span v-else>
<span
>{{ i.name }}
<el-tooltip
effect="dark"
:content="i.years"
placement="top"
>
<span class="red">{{ i.count }}</span>
</el-tooltip></span
>
<!-- 只有当不是最后一个元素时才显示顿号 -->
<span v-if="idx < scope.row[item.prop].length - 1">、</span>
</span>
</span>
</template>
<!-- 如果是延期回复次数根据后端返回结构单独处理 -->
<template v-else-if="item.isNumber">
<el-tooltip
effect="dark"
:content="scope.row[item.prop] && scope.row[item.prop].years"
placement="top"
v-if="
scope.row[item.prop] && scope.row[item.prop].count !== 0
"
>
<span class="red">{{ scope.row[item.prop].count }}</span>
</el-tooltip>
<span v-else>{{
scope.row[item.prop] && scope.row[item.prop].count
}}</span>
</template>
<template v-else>
<span>/</span>
</template>
</div>
<!-- 默认内容(文本为是,则字体颜色标红) -->
<div
:style="{
color:
scope.row[item.prop] === '是'
? '#ff0000'
: scope.row[item.prop] !== '/' &&
scope.row.announceUrl !== null
? item.color
: '',
cursor:
scope.row[item.prop] !== '/' && scope.row.announceUrl !== null
? item.cursor
: '',
textAlign: scope.row[item.prop] === '/' ? 'center' : item.align
}"
@click="
item.cursor && scope.row.announceUrl !== null
? toPage(scope.row)
: ''
"
:class="tableId === 'warnDetail2' ? '' : 'table-content'"
v-else
>
{{
(item.prop === 'declareDate' &&
scope.row[item.prop] === null) ||
(item.prop === 'guaranteeOverdueNum' &&
scope.row[item.prop] === null)
? '/'
: item.prop === 'cancelDate' && scope.row[item.prop] === null
? '未撤销'
: scope.row[item.prop]
}}
</div>
</slot>
</template>
</el-table-column>
</template>
<!-- 动态表格动态表头表内容渲染 -->
<template v-if="dynamic">
<el-table-column
v-for="(item, index) in tableData && tableData[0]?.data"
:key="index"
:min-width="200"
:prop="item.sortColumnCode || ''"
:sortable="item.sortColumnCode ? 'custom' : false"
header-align="center"
>
<template #header>
<div class="heard-text">
<div class="heard-name">{{ item.dataName }}</div>
<div class="change-text" v-if="item.headFlag === 1">更</div>
</div>
</template>
<template #default="scope">
<!-- 默认内容(valFlag后端返回1的字段,则字体颜色标红) -->
<div
:style="{
color:
scope.row.data &&
scope.row.data[index] &&
scope.row.data[index].valFlag === 1
? '#FF0000'
: '',
textAlign:
scope.row.data &&
scope.row.data[index] &&
scope.row.data[index].textAlign === 'left' &&
scope.row.data[index].dataVal !== '/'
? 'left'
: 'center'
}"
>
{{
scope.row.data &&
scope.row.data[index] &&
scope.row.data[index].dataVal
}}
</div></template
>
</el-table-column>
</template>
</el-table>
<!-- 分页组件 -->
<paging
v-if="paginationShow"
:currentPage="currentPage"
:pageSize="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:pageSizes="[10, 20, 50, 100]"
:total="total"
:background="true"
@handleCurrentChange="changeCurrentPage"
@handleSizeChange="changePageSize"
/>
</template>
<script setup lang="ts">
import Paging from '@/views/stock-index/components/paging.vue'
import { ElMessage } from 'element-plus'
import { exportFileUrl } from '@/pages/monitor/monitorApi/stockMonitor'
import { useReportStore } from '@/store/modules/report'
// 定义合并规则的类型
interface MergeRule {
rowIndex: number // 行索引
columnIndex: number // 列索引
rowspan: number // 合并的行数
colspan: number // 合并的列数
}
const props = defineProps({
showOverflowTooltip: {
// 表格是否展示浮框
type: Boolean,
default: true
},
dynamic: {
// 是否动态表格
type: Boolean,
default: false
},
paginationShow: {
// 是否显示分页
type: Boolean,
default: false
},
currentPage: {
// 当前页
type: Number,
default: 1
},
pageSize: {
// 一页展示条数
type: Number,
default: 10
},
total: {
// 总条数
type: Number,
default: 0
},
height: {
type: String,
default: '400'
},
rowHeight: {
type: Number,
default: 40 // 默认行高
},
// 静态表格合并规则
mergeRules: {
type: Array as any,
default: () => []
},
// 动态表格合并字段
mergeKey: {
type: String,
default: ''
},
// 表格id
tableId: {
type: String,
default: ''
},
// 表头数据
tableColums: {
type: Array as () => any[],
default: () => []
},
// 表内容数据
tableData: {
type: Array as () => any[],
default: () => []
},
//是否显示列表序号
indexShow: {
type: Boolean,
default: false
}
})
const emit = defineEmits([
'tableSort',
'handleSelectionChange',
'changeCurrentPage',
'changePageSize'
])
const myTable = ref()
// 设置表头样式
const headerCellStyle = () => {
return {
height: '34px',
background: '#F9FBFD',
color: '#333333',
fontSize: '14px',
borderRight: '1px solid #ebeef8',
padding: '1px 0',
fontWeight: 'normal'
}
}
// 动态设置行高
const rowStyle = () => {
return {
height: `${props.rowHeight}px`,
lineHeight: `${props.rowHeight}px`
}
}
// 设置单元格样式
const cellStyle = ({
row,
column,
rowIndex,
columnIndex
}: {
row: any
column: any
rowIndex: number
columnIndex: number
}) => {
// 设置监管处罚科目(行业排名)单元格为/时居中显示,否则居左显示
if (
column.property === 'subject' &&
row.subject === '/' &&
columnIndex === 1
) {
return {
fontSize: '14px',
color: '#666666',
textAlign: 'center'
}
} else {
return {
fontSize: '14px',
color: '#666666'
}
}
}
//监听全局参数搜索
watch(
() => useReportStore().topSelectObj,
(newVal) => {
myTable.value.clearSort() // 清除表格排序
}
)
const openedWindows = new Map()
// 公告跳转
const toPage = (row: any) => {
if (row.announceUrl && row.announceUrl !== null) {
exportFileUrl(row.announceUrl).then((res: any) => {
if (res.code === 200) {
// 检查是否已经有一个窗口打开了该链接
if (openedWindows.has(res.data)) {
const windowRef = openedWindows.get(res.data)
if (!windowRef.closed) {
windowRef.focus() // 聚焦到已打开的窗口
return
}
}
// 如果没有打开的窗口,则打开一个新窗口
const newWindow = window.open(res.data, '_blank')
openedWindows.set(res.data, newWindow) // 存储窗口引用
} else {
ElMessage({
type: 'error',
message: res.message
})
}
})
}
}
// 监管处罚链接跳转
const openLink = (url: any) => {
if (url === '') return // 如果链接为空,直接返回
// 如果没有打开的窗口,则打开一个新窗口
const newWindow = window.open(url, '_blank', 'noreferrer,noopener')
openedWindows.set(url, newWindow) // 存储窗口引用
}
// 动态表格合并规则
const objectSpanMethod = ({
row,
column,
rowIndex,
columnIndex
}: {
row: any
column: any
rowIndex: number
columnIndex: number
}) => {
if (columnIndex !== 0) {
return { rowspan: 1, colspan: 1 }
}
if (!props.mergeKey) {
return { rowspan: 1, colspan: 1 }
}
if (
rowIndex === 0 ||
row[props.mergeKey] !== props.tableData[rowIndex - 1][props.mergeKey]
) {
let rowspan = 1
for (let i = rowIndex + 1; i < props.tableData.length; i++) {
if (props.tableData[i][props.mergeKey] === row[props.mergeKey]) {
rowspan++
} else {
break
}
}
return { rowspan, colspan: 1 }
} else {
return { rowspan: 0, colspan: 0 }
}
}
// 定义静态表格合并参数字段
type TableId = 'warnDetail' | 'warnDetail1' | 'warnDetail2' | 'warnDetail3' // 定义可能的 tableId 类型
type MergeConfig = Record<TableId, Record<number, string>> // 定义 mergeConfig 的类型
const mergeConfig: MergeConfig = {
warnDetail: { 0: 'year' },
warnDetail1: { 0: 'name', 1: 'declareDate' },
warnDetail2: {
0: 'correctionAccouPeri',
1: 'iserror',
2: 'declareDate',
3: 'announce'
},
warnDetail3: {
0: 'declareDate'
}
}
// 静态表格合并规则
const handleSpanMethod = ({
row,
column,
rowIndex,
columnIndex
}: {
row: any
column: any
rowIndex: number
columnIndex: number
}) => {
const tableId = props.tableId as TableId // 使用类型断言确保 tableId 是 TableId 类型
const config = mergeConfig[tableId] // 现在 TypeScript 知道 config 的类型
if (config && config[columnIndex]) {
return mergeRows({ row, rowIndex, key: config[columnIndex] })
} else {
return handleMergeRules({ rowIndex, columnIndex })
}
}
// 静态表格统一合并方法
const mergeRows = ({
row,
rowIndex,
key
}: {
row: any
rowIndex: number
key: string
}) => {
if (rowIndex === 0 || row[key] !== props.tableData[rowIndex - 1][key]) {
let rowspan = 1
for (let i = rowIndex + 1; i < props.tableData.length; i++) {
if (props.tableData[i][key] === row[key]) {
rowspan++
} else {
break
}
}
return { rowspan, colspan: 1 }
} else {
return { rowspan: 0, colspan: 0 }
}
}
// 外部传进来的表格合并方法
const handleMergeRules = ({
rowIndex,
columnIndex
}: {
rowIndex: number
columnIndex: number
}) => {
const rule = props.mergeRules.find(
(rule: MergeRule) =>
rule.rowIndex === rowIndex && rule.columnIndex === columnIndex
)
if (rule) {
return { rowspan: rule.rowspan, colspan: rule.colspan }
}
for (const r of props.mergeRules) {
if (
rowIndex > r.rowIndex &&
rowIndex < r.rowIndex + r.rowspan &&
columnIndex >= r.columnIndex &&
columnIndex < r.columnIndex + r.colspan
) {
return { rowspan: 0, colspan: 0 }
}
}
return { rowspan: 1, colspan: 1 }
}
// 表格排序
const tableSort = (dataObj: any) => {
const col: any = props.tableColums?.filter((v: any) => {
return v.prop === dataObj.prop
})[0]
let prop = ''
let order = dataObj.order
if (col) {
prop = col.prop
} else {
prop = dataObj.prop
}
emit('tableSort', [order ? prop : '', order])
}
// 表格多选
const handleSelectionChange = (selection: any) => {
emit('handleSelectionChange', selection)
}
// 点击改变当前页重新渲染表格数据
const changeCurrentPage = (current: number) => {
emit('changeCurrentPage', current)
}
//size改变
const changePageSize = (size: number) => {
emit('changePageSize', size)
}
//清空选中
const clearSelection = () => {
myTable.value.clearSelection()
}
const clearTableSort = () => {
myTable.value && myTable.value.clearSort()
}
defineExpose({
// 暴露子组件方法给父组件调用
clearSelection,
clearTableSort
})
</script>
<style lang="less" scoped>
.dot {
font-size: 40px;
margin-right: 5px;
vertical-align: middle;
}
.link-ellipsis {
&:hover {
color: #3a5bb7;
}
}
.red {
color: @text-red;
}
.table-content {
white-space: nowrap; /* 强制文本在一行显示 */
overflow: hidden; /* 隐藏超出容器的内容 */
text-overflow: ellipsis; /* 超出部分显示省略号 */
}
:deep(.my-table-header .cell) {
font-size: 13px;
padding: 6px 4px;
font-weight: 600;
}
:deep(.my-selection .cell) {
display: flex;
justify-content: center;
align-items: center;
}
.heard-text {
// display: flex;
// justify-content: center;
display: inline-block;
vertical-align: middle;
&.static-heard {
max-width: calc(100% - 15px);
}
.heard-name {
display: inline-block;
}
.change-text {
width: 14px;
height: 14px;
background: #fbfdfe;
border-radius: 2px;
border: 1px solid #eb9c35;
font-size: 12px;
color: #eb9c35;
// display: flex;
// align-items: center;
// justify-content: center;
display: inline-block;
line-height: 14px;
text-align: center;
position: relative;
top: -1px;
margin-left: 4px;
// margin-top: 2px;
}
& + :deep(.caret-wrapper) {
top: 1px;
}
}
</style>
二、使用表格组件:
<template>
<!-- 静态表格使用 -->
<div class="table-box">
<TableList
:table-colums="colums"
:table-data="requestParams.tableData"
pagination-show
index-show
:show-overflow-tooltip="false"
:page-size="requestParams.limit"
:current-page="requestParams.page"
:total="requestParams.total"
@change-page-size="($event) => changePageSize($event)"
@change-current-page="($event) => changeCurrentPage($event)"
@table-sort="($event) => tableDataSort($event)"
height="450"
>
<template v-slot:column-symbol="{ row }">
<div class="name-symbol">
<p>{{ row.shortName }}</p>
<p>({{ row.symbol }})</p>
</div>
</template>
</TableList>
<!-- 动态表格使用(mergeKey为表格合并字段) -->
<TableList
:table-colums="colums"
:table-data="tableData"
dynamic
height="450"
mergeKey="indicatorType"
></TableList>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import TableList from './TableList.vue'
import { queryOtherUnitsData } from '@/pages/monitor/monitorApi/jurisdiction/index'
const requestParams = ref({
page: 1,
limit: 10,
total: 0,
order: '',
sort: '',
abnormalCount: 0
tableData: [], // 表格数据
loading: false // 加载状态
})
const colums = ref([
{
prop: 'symbol',
label: '公司(代码)',
sortable: true,
width: '120'
},
{
prop: 'violationTypeList',
label: '违规类型',
isJointST: true,
align: 'left'
},
{
prop: 'supervisorList',
label: '处罚单位',
isJointST: true,
align: 'left'
}
])
//排序点击
const tableDataSort = (arr: any) => {
requestParams.value.page = 1
requestParams.value.sort = arr[0]
requestParams.value.order = arr[1]
getOtherUnitsData()
}
//页码更改
const changePageSize = (val: number) => {
requestParams.value.limit = val
getOtherUnitsData()
}
//分页
const changeCurrentPage = (val: number) => {
requestParams.value.page = val
getOtherUnitsData()
}
// 查询交易所所有表格数据
const getOtherUnitsData = () => {
requestParams.value.loading = true
requestParams.value.tableData = []
const params = {
limit: requestParams.value.limit,
page: requestParams.value.page,
sort: requestParams.value.sort,
order: requestParams.value.order
}
queryOtherUnitsData(params).then((res: any) => {
if (res.code === 200) {
requestParams.value.tableData = res.data.data
requestParams.value.total = res.data.total
} else {
ElMessage({
type: 'error',
message: res.message
})
}
requestParams.value.loading = false
})
}
onMounted(() => {
getOtherUnitsData()
})
</script>
<style scoped lang="less">
.item {
border: 1px solid #ebeef8;
border-top: none;
.title {
display: flex;
align-items: center;
padding-left: 26px;
height: 34px;
background: #f4f7fc;
border: 1px solid #ebeef8;
border-left: none;
border-right: none;
font-weight: bold;
font-size: 14px;
color: #333333;
position: relative;
&::before {
content: '';
position: absolute;
left: 15px;
top: 50%;
margin-top: -5px;
width: 4px;
height: 10px;
background: #3a5bb7;
border-radius: 0px 2px 2px 0px;
}
.more {
margin-left: auto;
margin-right: 15px;
font-size: 12px;
color: #666666;
font-weight: normal !important;
.times {
color: @text-red;
}
}
}
.table-box {
padding: 15px;
.name-symbol {
color: @text-blue;
cursor: pointer;
}
}
}
</style>