Files
one-pipe-system/src/components/core/tables/ArtTable.vue
sexygoat 719db517f2
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m24s
fix: bug
2026-04-23 16:21:12 +08:00

531 lines
14 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.
<!-- 表格组件带分页默认分页大于一页时显示 -->
<template>
<div
class="art-table"
:class="{ 'header-background': showHeaderBackground }"
:style="{
marginTop: marginTop + 'px',
height: total ? 'calc(100% - 90px)' : 'calc(100% - 25px)'
}"
>
<div class="table-container">
<el-table
ref="tableRef"
v-loading="loading"
:data="tableData"
:row-key="rowKey"
:row-class-name="rowClassName"
:height="height"
:max-height="maxHeight"
:show-header="showHeader"
:highlight-current-row="highlightCurrentRow"
:size="tableSizeComputed"
:stripe="stripeComputed"
:border="borderComputed"
:tree-props="treeProps"
:default-expand-all="defaultExpandAll"
:header-cell-style="{
backgroundColor: showHeaderBackground ? 'var(--el-fill-color-lighter)' : '',
fontWeight: '500'
}"
@row-click="handleRowClick"
@row-contextmenu="handleRowContextmenu"
@selection-change="handleSelectionChange"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<!-- 序号列 -->
<el-table-column
v-if="index && tableData.length > 0"
type="index"
width="60"
label="序号"
align="center"
fixed="left"
/>
<!-- 动态列 -->
<slot v-if="tableData.length"></slot>
<!-- 操作列固定在右侧 -->
<el-table-column
v-if="actions && tableData.length > 0"
label="操作"
:width="actionsWidth"
fixed="right"
align="left"
>
<template #default="{ row }">
<div class="table-actions">
<component :is="renderActions(row)" />
</div>
</template>
</el-table-column>
<!-- 空数据 -->
<template #empty>
<el-empty :description="emptyText" v-show="!loading" />
</template>
</el-table>
</div>
<!-- 分页 -->
<div
v-if="pagination && tableData.length > 0"
class="table-pagination"
:class="paginationAlign"
>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="pageSizes"
:pager-count="isMobile ? 5 : 7"
:total="total"
:background="true"
:size="paginationSize"
:layout="paginationLayoutComputed"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:hide-on-single-page="hideOnSinglePage"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { ElButton, ElDropdown, ElDropdownMenu, ElDropdownItem, ElIcon } from 'element-plus'
import { ArrowDown } from '@element-plus/icons-vue'
import { useTableStore } from '@/store/modules/table'
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500)
interface TableAction {
label: string
handler: (row: any) => void
type?: 'primary' | 'danger'
permission?: string
}
interface TableProps {
/** 表格数据源 */
data?: any[]
/** 是否显示加载状态 */
loading?: boolean
/** 行数据的 Key用于标识每一行数据 */
rowKey?: string
/** 行的 className 的回调方法 */
rowClassName?: ((data: { row: any; rowIndex: number }) => string) | string
/** 是否显示边框 */
border?: boolean | null
/** 是否使用斑马纹样式 */
stripe?: boolean | null
/** 是否显示序号列 */
index?: boolean
/** 表格高度,可以是数字或 CSS 值 */
height?: string | number
/** 表格最大高度,可以是数字或 CSS 值 */
maxHeight?: string | number
/** 是否显示表头 */
showHeader?: boolean
/** 是否高亮当前行 */
highlightCurrentRow?: boolean
/** 空数据时显示的文本 */
emptyText?: string
/** 是否显示分页 */
pagination?: boolean
/** 总条目数 */
total?: number
/** 当前页码 */
currentPage?: number
/** 每页显示条目个数 */
pageSize?: number
/** 每页显示个数选择器的选项列表 */
pageSizes?: number[]
/** 只有一页时是否隐藏分页器 */
hideOnSinglePage?: boolean
/** 分页器的对齐方式 */
paginationAlign?: 'left' | 'center' | 'right'
/** 分页器的大小 */
paginationSize?: 'small' | 'default' | 'large'
/** 分页器的布局 */
paginationLayout?: string
/** 是否显示表头背景色 */
showHeaderBackground?: boolean | null
/** 表格距离顶部距离 */
marginTop?: number
/** 表格大小 */
size?: 'small' | 'default' | 'large'
/** 树形表格配置 */
treeProps?: { children?: string; hasChildren?: string }
/** 是否默认展开所有行 */
defaultExpandAll?: boolean
/** 操作列配置 */
actions?: ((row: any) => TableAction[]) | null
/** 操作列宽度 */
actionsWidth?: number
/** 直接显示的操作按钮数量默认2个 */
inlineActionsCount?: number
}
const props = withDefaults(defineProps<TableProps>(), {
data: () => [],
loading: false,
rowKey: 'id',
rowClassName: undefined,
border: null,
stripe: null,
index: false,
height: '100%',
showHeader: true,
highlightCurrentRow: false,
emptyText: '暂无数据',
pagination: true,
total: 0,
currentPage: 1,
pageSize: 20,
hideOnSinglePage: true,
pageSizes: () => [10, 20, 30, 50],
paginationAlign: 'center',
paginationSize: 'default',
paginationLayout: '',
showHeaderBackground: null,
marginTop: 20,
treeProps: undefined,
defaultExpandAll: false,
actions: null,
actionsWidth: 140,
inlineActionsCount: 1
})
/*
* 计算分页布局
* 移动端跟pc端使用两种不同的布局
*/
const paginationLayoutComputed = computed(() => {
if (props.paginationLayout) {
return props.paginationLayout
}
return isMobile.value
? 'prev, pager, next, jumper, sizes, total'
: 'total, sizes, prev, pager, next, jumper'
})
const emit = defineEmits([
'update:currentPage',
'update:pageSize',
'row-click',
'row-contextmenu',
'size-change',
'current-change',
'selection-change',
'cell-mouse-enter',
'cell-mouse-leave'
])
const tableStore = useTableStore()
const { tableSize } = storeToRefs(tableStore)
// 表格实例
const tableRef = ref()
// 提供给父组件的方法
defineExpose({
// 展开所有行
expandAll: () => {
const elTable = tableRef.value
if (!elTable) return
// 递归处理树形数据
const processRows = (rows: any[]) => {
rows.forEach((row) => {
const hasChildren = row.children?.length > 0
if (hasChildren) {
elTable.toggleRowExpansion(row, true)
processRows(row.children)
}
})
}
processRows(props.data)
},
// 收起所有行
collapseAll: () => {
const elTable = tableRef.value
if (!elTable) return
// 递归处理树形数据
const processRows = (rows: any[]) => {
rows.forEach((row) => {
const hasChildren = row.children?.length > 0
if (hasChildren) {
elTable.toggleRowExpansion(row, false)
processRows(row.children)
}
})
}
processRows(props.data)
}
})
// 表格大小 - props优先级高于store
const tableSizeComputed = computed(() => {
return props.size || tableSize.value
})
// 斑马纹
const stripeComputed = computed(() => {
return props.stripe === null ? tableStore.isZebra : props.stripe
})
// 边框
const borderComputed = computed(() => {
return props.border === null ? tableStore.isBorder : props.border
})
// 表头背景
const showHeaderBackground = computed(() => {
return props.showHeaderBackground === null
? tableStore.isHeaderBackground
: props.showHeaderBackground
})
// 表格数据
const tableData = computed(() => {
// 如果不显示分页或使用后端分页,直接返回原始数据
if (!props.pagination || props.total > props.data.length) return props.data
const start = (props.currentPage - 1) * props.pageSize
const end = start + props.pageSize
return props.data.slice(start, end)
})
// 当前页
const currentPage = computed({
get: () => props.currentPage,
set: (val) => emit('update:currentPage', val)
})
// 每页条数
const pageSize = computed({
get: () => props.pageSize,
set: (val) => emit('update:pageSize', val)
})
// 行点击事件
const handleRowClick = (row: any, column: any, event: any) => {
emit('row-click', row, column, event)
}
// 行右键事件
const handleRowContextmenu = (row: any, column: any, event: any) => {
emit('row-contextmenu', row, column, event)
}
// 单元格鼠标进入事件
const handleCellMouseEnter = (row: any, column: any, cell: any, event: any) => {
emit('cell-mouse-enter', row, column, cell, event)
}
// 单元格鼠标离开事件
const handleCellMouseLeave = (row: any, column: any, cell: any, event: any) => {
emit('cell-mouse-leave', row, column, cell, event)
}
// 选择变化事件
const handleSelectionChange = (selection: any) => {
emit('selection-change', selection)
}
// 每页条数改变
const handleSizeChange = (val: number) => {
emit('size-change', val)
}
// 当前页改变
const handleCurrentChange = (val: number) => {
emit('current-change', val)
scrollToTop()
}
// 表格滚动到顶部
const scrollToTop = () => {
nextTick(() => {
if (tableRef.value) {
tableRef.value.setScrollTop(0)
}
})
}
// 渲染操作列
const renderActions = (row: any) => {
if (!props.actions) return null
const actionList = props.actions(row)
// 如果没有操作按钮,显示提示
if (actionList.length === 0) {
return h(
'span',
{ style: 'color: var(--el-text-color-placeholder); font-size: 12px;' },
'暂无可用操作'
)
}
// 如果操作数量 <= inlineActionsCount直接显示所有按钮
if (actionList.length <= props.inlineActionsCount) {
return h(
'div',
{ style: 'display: flex; gap: 8px;' },
actionList.map((action) =>
h(
ElButton,
{
type: action.type === 'danger' ? 'danger' : 'primary',
link: true,
onClick: () => action.handler(row)
},
() => action.label
)
)
)
}
// 如果操作数量 > inlineActionsCount显示前 inlineActionsCount 个 + 更多下拉菜单
const inlineActions = actionList.slice(0, props.inlineActionsCount)
const moreActions = actionList.slice(props.inlineActionsCount)
return h('div', { style: 'display: flex; gap: 12px; align-items: center;' }, [
...inlineActions.map((action) =>
h(
ElButton,
{
type: action.type === 'danger' ? 'danger' : 'primary',
link: true,
onClick: () => action.handler(row)
},
() => action.label
)
),
h(
ElDropdown,
{
onCommand: (handler: (row: any) => void) => handler(row)
},
{
default: () =>
h(
'span',
{
style:
'display: flex; align-items: center; cursor: pointer; color: var(--el-color-primary);'
},
[h('span', '更多'), h(ElIcon, { style: 'margin-left: 4px;' }, () => h(ArrowDown))]
),
dropdown: () =>
h(ElDropdownMenu, {}, () =>
moreActions.map((action) =>
h(
ElDropdownItem,
{
command: () => action.handler(row),
style: action.type === 'danger' ? 'color: var(--el-color-danger);' : ''
},
() => action.label
)
)
)
}
)
])
}
</script>
<style lang="scss" scoped>
.art-table {
.table-container {
height: 100%;
}
.table-pagination {
display: flex;
margin-top: 16px;
// 分页对齐方式
&.left {
justify-content: flex-start;
}
&.center {
justify-content: center;
}
&.right {
justify-content: flex-end;
}
}
:deep(.el-table) {
th.el-table__cell {
font-weight: 600;
}
}
&.header-background {
:deep(.el-table) {
th.el-table__cell {
background-color: var(--el-fill-color-light);
}
}
}
// 解决el-image 和 el-table冲突层级冲突问题
// 只对包含图片的单元格应用,避免影响 fixed 列
::v-deep(.el-table__cell:has(.el-image)) {
position: static !important;
}
// 确保固定列能正常工作
:deep(.el-table__fixed-right) {
right: 0 !important;
}
}
// 移动端分页
@media (max-width: $device-phone) {
:deep(.el-pagination) {
display: flex;
flex-wrap: wrap;
gap: 15px 0;
align-items: center;
justify-content: center;
.el-pagination__sizes {
.el-select {
width: 100px !important;
.el-select__wrapper {
height: 30px !important;
}
}
}
.el-pager {
li {
margin-right: 2px;
}
}
.el-pagination__jump {
margin-left: 5px;
.el-input {
height: 32px !important;
}
}
}
}
</style>