2026-05-26 18:32:57 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<!--
|
|
|
|
|
|
page-table — CRUD 表格便捷组合体
|
|
|
|
|
|
================================
|
|
|
|
|
|
这是一个把「顶部按钮栏 + 数据表格 + 底部分页」打包在一起的高层组件。
|
|
|
|
|
|
适合 80% 的增删改查页面,你只需要传列定义、按钮定义、数据和分页参数即可。
|
|
|
|
|
|
|
|
|
|
|
|
依赖:element-ui 的 <el-table> <el-button> <el-pagination>
|
2026-05-27 18:37:37 +08:00
|
|
|
|
i18n:由调用方负责翻译,组件直接渲染传入的文本
|
2026-05-26 18:32:57 +08:00
|
|
|
|
|
|
|
|
|
|
@author 前端团队
|
|
|
|
|
|
@since 2026-05
|
|
|
|
|
|
-->
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref="root"
|
|
|
|
|
|
class="page-table"
|
|
|
|
|
|
:class="{ 'page-table--auto': autoHeight || height === 'auto' }"
|
|
|
|
|
|
v-loading="loading"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- ==================== 顶部:工具栏按钮区 ==================== -->
|
|
|
|
|
|
<!--
|
|
|
|
|
|
toolbarButtons 由 useTableButtons({ toolbar: [...] }) 生成
|
|
|
|
|
|
内部自动过滤 hasPermission === false 的按钮(无权限不显示)
|
|
|
|
|
|
按钮被 disabled 只在前端计算(如 needSelection 需要选中行),实际权限由后端校验
|
|
|
|
|
|
-->
|
|
|
|
|
|
<div ref="toolbar" class="page-table__toolbar" v-if="toolbarButtons.length || helpUrl">
|
|
|
|
|
|
<div class="page-table__toolbar-left">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-for="btn in visibleToolbarButtons"
|
|
|
|
|
|
:key="btn.key"
|
|
|
|
|
|
:type="btn.type"
|
|
|
|
|
|
:size="btn.size"
|
|
|
|
|
|
:icon="btn.icon"
|
|
|
|
|
|
:style="btn.cssStyle"
|
|
|
|
|
|
:disabled="btn.needSelection && !selectedCount"
|
|
|
|
|
|
@click="btn.onClick"
|
|
|
|
|
|
>
|
2026-05-28 11:30:08 +08:00
|
|
|
|
{{ $t(btn.label) }}
|
2026-05-26 18:32:57 +08:00
|
|
|
|
</el-button>
|
|
|
|
|
|
<!-- 自定义工具栏内容:如打印按钮、列筛选器等 -->
|
|
|
|
|
|
<slot name="toolbar-extra" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 帮助按钮:始终显示在工具栏最右侧,点击新窗口打开帮助文档 -->
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="helpUrl"
|
|
|
|
|
|
class="page-table__help-btn"
|
|
|
|
|
|
size="mini"
|
|
|
|
|
|
icon="el-icon-question"
|
|
|
|
|
|
@click="openHelp"
|
|
|
|
|
|
>
|
2026-05-27 18:37:37 +08:00
|
|
|
|
{{ helpText }}
|
2026-05-26 18:32:57 +08:00
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ==================== 中部:数据表格 ==================== -->
|
|
|
|
|
|
<!--
|
|
|
|
|
|
height 由外部传入或自动计算(autoHeight=true 时)
|
|
|
|
|
|
v-bind="tableAttrs" / v-on="tableListeners" 透传 el-table 原生属性和事件
|
|
|
|
|
|
-->
|
|
|
|
|
|
<div ref="tableWrapper" class="page-table__body">
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
ref="table"
|
2026-05-27 18:37:37 +08:00
|
|
|
|
:data="data"
|
2026-05-26 18:32:57 +08:00
|
|
|
|
:height="tableHeight"
|
|
|
|
|
|
:border="border"
|
|
|
|
|
|
:row-key="rowKey"
|
|
|
|
|
|
:header-cell-style="headerCellStyle"
|
|
|
|
|
|
v-bind="tableAttrs"
|
2026-06-24 14:38:43 +08:00
|
|
|
|
v-on="elTableListeners"
|
2026-05-26 18:32:57 +08:00
|
|
|
|
@selection-change="onSelectionChange"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!--
|
|
|
|
|
|
遍历 columns,有五种列类型:
|
|
|
|
|
|
1. type='selection' → 复选框列
|
|
|
|
|
|
2. type='index' → 序号列
|
|
|
|
|
|
3. slot='_actions' → 操作列(自动渲染 rowButtons)
|
|
|
|
|
|
4. slot/headerSlot → 自定义插槽列
|
|
|
|
|
|
5. 其他 → 普通数据列
|
|
|
|
|
|
-->
|
|
|
|
|
|
<template v-for="col in columns">
|
|
|
|
|
|
<!-- 1. 复选框列 -->
|
|
|
|
|
|
<el-table-column
|
|
|
|
|
|
v-if="col.type === 'selection'"
|
|
|
|
|
|
:key="'sel-' + col.idx"
|
|
|
|
|
|
type="selection"
|
|
|
|
|
|
:width="col.width"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<!-- 2. 序号列 -->
|
|
|
|
|
|
<el-table-column
|
|
|
|
|
|
v-else-if="col.type === 'index'"
|
|
|
|
|
|
:key="'idx-' + col.idx"
|
|
|
|
|
|
type="index"
|
|
|
|
|
|
:width="col.width"
|
2026-05-28 11:30:08 +08:00
|
|
|
|
:label="$t(col.label) || '#'"
|
2026-05-26 18:32:57 +08:00
|
|
|
|
/>
|
|
|
|
|
|
<!-- 3. 操作列:自动渲染 rowButtons(编辑/删除等行内按钮) -->
|
|
|
|
|
|
<!--
|
|
|
|
|
|
约定:columns 中 prop='_actions' 的列会被识别为操作列
|
|
|
|
|
|
rowButtons 由 useTableButtons({ row: [...] }) 生成
|
|
|
|
|
|
红色按钮通过 color='danger' 控制
|
|
|
|
|
|
-->
|
|
|
|
|
|
<el-table-column
|
|
|
|
|
|
v-else-if="col.slot === '_actions' && rowButtons.length"
|
|
|
|
|
|
:key="'act-' + col.idx"
|
|
|
|
|
|
v-bind="colAttrs(col)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #default="{ row, $index }">
|
2026-05-28 19:16:05 +08:00
|
|
|
|
<template v-for="btn in visibleRowButtons">
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-if="btn.visible(row)"
|
|
|
|
|
|
:key="btn.key"
|
|
|
|
|
|
:style="btn.cssStyle"
|
|
|
|
|
|
:class="{ 'action-btn--danger': btn.color === 'danger' }"
|
|
|
|
|
|
class="action-btn"
|
|
|
|
|
|
@click="btn.onClick(row, $index)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<i v-if="btn.icon" :class="btn.icon" />
|
|
|
|
|
|
{{ $t(btn.label) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</template>
|
2026-05-26 18:32:57 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<!-- 4. 自定义插槽列:列内容或表头可由父组件自定义 -->
|
|
|
|
|
|
<!--
|
|
|
|
|
|
使用方式:
|
|
|
|
|
|
- col.slot='status' → 父组件用 <template #col-status="{ row }">
|
|
|
|
|
|
- col.headerSlot='xyz' → 父组件用 <template #xyz> 自定义表头
|
|
|
|
|
|
-->
|
|
|
|
|
|
<el-table-column
|
|
|
|
|
|
v-else-if="col.slot || col.headerSlot"
|
|
|
|
|
|
:key="'slot-' + col.idx"
|
|
|
|
|
|
v-bind="colAttrs(col)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #header="{ column }">
|
|
|
|
|
|
<slot v-if="col.headerSlot" :name="col.headerSlot" :data="col" :column="column">
|
|
|
|
|
|
<span>{{ column.label }}</span>
|
|
|
|
|
|
</slot>
|
|
|
|
|
|
<span v-else>{{ column.label }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template #default="{ row, $index }">
|
|
|
|
|
|
<slot :name="'col-' + col.slot" :row="row" :index="$index">
|
|
|
|
|
|
<span>{{ row[col.prop] }}</span>
|
|
|
|
|
|
</slot>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<!-- 5. 普通数据列:直接按 prop 取 row 数据 -->
|
|
|
|
|
|
<el-table-column
|
|
|
|
|
|
v-else
|
|
|
|
|
|
:key="'col-' + col.idx"
|
|
|
|
|
|
v-bind="colAttrs(col)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<!-- 表格无数据时显示的占位内容 -->
|
|
|
|
|
|
<template #empty>
|
|
|
|
|
|
<slot name="empty" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<!-- 表格最后一行之后追加的内容 -->
|
|
|
|
|
|
<template #append>
|
|
|
|
|
|
<slot name="append" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<slot />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ==================== 底部:分页组件 ==================== -->
|
|
|
|
|
|
<!--
|
|
|
|
|
|
只传 pagination 对象({ current, size, total })才显示分页
|
|
|
|
|
|
page-change 事件回传 { current, size, total },父组件更新 pagination 后重新 fetchData
|
|
|
|
|
|
-->
|
|
|
|
|
|
<div ref="footer" class="page-table__footer" v-if="pagination">
|
|
|
|
|
|
<el-pagination
|
2026-06-25 00:49:52 +08:00
|
|
|
|
:current-page="paginationCurrent"
|
|
|
|
|
|
:page-size="paginationSize"
|
|
|
|
|
|
:total="paginationTotal"
|
2026-05-26 18:32:57 +08:00
|
|
|
|
:page-sizes="[10, 25, 50, 100, 250, 500]"
|
|
|
|
|
|
:disabled="loading"
|
|
|
|
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
|
@size-change="onSizeChange"
|
|
|
|
|
|
@current-change="onCurrentChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 自定义尾部内容 -->
|
|
|
|
|
|
<slot name="extra" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
/**
|
|
|
|
|
|
* PageTable — CRUD 表格便捷组合体
|
|
|
|
|
|
*
|
|
|
|
|
|
* 【核心职责】
|
|
|
|
|
|
* 1. 渲染顶部工具栏(增删改查等操作按钮,自动权限过滤)
|
|
|
|
|
|
* 2. 渲染数据表格(5 种列类型,自动 i18n 翻译)
|
|
|
|
|
|
* 3. 渲染底部分页
|
|
|
|
|
|
* 4. 表格高度自适应(可选)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 【典型用法】
|
|
|
|
|
|
* <page-table
|
|
|
|
|
|
* :columns="columns"
|
|
|
|
|
|
* :data="tableData"
|
|
|
|
|
|
* :loading="loading"
|
|
|
|
|
|
* :toolbar-buttons="toolbarButtons"
|
|
|
|
|
|
* :row-buttons="rowButtons"
|
|
|
|
|
|
* :pagination="pagination"
|
|
|
|
|
|
* auto-height
|
|
|
|
|
|
* @page-change="onPageChange"
|
|
|
|
|
|
* @selection-change="onSelect"
|
|
|
|
|
|
* />
|
|
|
|
|
|
*
|
|
|
|
|
|
* 【内部事件流】
|
|
|
|
|
|
* toolbarButtons[i].onClick() → 父组件方法 → fetchData → data 更新 → 表格刷新
|
|
|
|
|
|
* el-pagination @change → emit('page-change') → 父组件更新 pagination → fetchData
|
|
|
|
|
|
* el-table @selection-change → emit('selection-change') → 父组件获取选中行
|
|
|
|
|
|
*
|
|
|
|
|
|
* 【与 useTableColumns / useTableButtons 配合】
|
|
|
|
|
|
* 详见文档:docs/sct-base-table-refactor-design.md
|
|
|
|
|
|
*/
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'PageTable',
|
|
|
|
|
|
props: {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 列定义,由 useTableColumns() 生成
|
|
|
|
|
|
*
|
|
|
|
|
|
* 普通列:{ prop: 'code', label: '编码', minWidth: 120 }
|
|
|
|
|
|
* 操作列:{ prop: '_actions', label: '操作', width: 160, fixed: 'right' }
|
|
|
|
|
|
* 自定义列:{ prop: 'status', label: '状态', slot: true }
|
|
|
|
|
|
*
|
|
|
|
|
|
* 组件内部会按 type/slot/prop 自动分类渲染不同的 <el-table-column>
|
|
|
|
|
|
*/
|
|
|
|
|
|
columns: { type: Array, default: () => [] },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 表格行数据,直接传接口返回的数组
|
|
|
|
|
|
*/
|
|
|
|
|
|
data: { type: Array, default: () => [] },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 是否显示 loading 遮罩,通常在 fetchData 期间为 true
|
|
|
|
|
|
*/
|
|
|
|
|
|
loading: { type: Boolean, default: false },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 表格高度,不传则 el-table 按内容撑开
|
|
|
|
|
|
* 传 'auto' 或同时传 auto-height 启用自动计算(窗口高度 - 搜索区 - 工具栏 - 分页)
|
|
|
|
|
|
* 传数字/字符串(如 '500px')则固定高度
|
|
|
|
|
|
*/
|
|
|
|
|
|
height: { type: [String, Number], default: undefined },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 启用表格高度自适应:根据容器可视区域自动计算高度
|
|
|
|
|
|
* 启用后 height prop 被忽略,组件内部通过 ResizeObserver 持续计算
|
|
|
|
|
|
*/
|
|
|
|
|
|
autoHeight: { type: Boolean, default: false },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 是否显示表格边框,默认 true
|
|
|
|
|
|
*/
|
|
|
|
|
|
border: { type: Boolean, default: true },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 行数据的唯一 key,默认 'id',用于 el-table 的 row-key
|
|
|
|
|
|
* 树形表格或需要保留选中状态时必需
|
|
|
|
|
|
*/
|
|
|
|
|
|
rowKey: { type: String, default: 'id' },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 顶部工具栏按钮列表,由 useTableButtons({ toolbar: [...] }) 生成
|
|
|
|
|
|
*
|
|
|
|
|
|
* 每个按钮字段:
|
|
|
|
|
|
* key - 唯一标识(必填)
|
|
|
|
|
|
* label - 显示文本,支持 i18n key(组件自动 $t() 翻译)
|
|
|
|
|
|
* icon - element-ui 图标名('el-icon-plus' 等)
|
|
|
|
|
|
* type - 按钮样式类型('primary' | 'success' | 'warning' | 'danger')
|
|
|
|
|
|
* auth - 权限 key,传给 useTableButtons 的第二个参数判断
|
|
|
|
|
|
* cssStyle - 自定义样式对象
|
|
|
|
|
|
* needSelection - true 的话需要表格有选中行才能点击
|
|
|
|
|
|
* onClick - 点击回调(由父组件绑定方法)
|
|
|
|
|
|
* hasPermission - 自动注入,由 useTableButtons 计算(不要手动设)
|
|
|
|
|
|
*/
|
|
|
|
|
|
toolbarButtons: { type: Array, default: () => [] },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 行内操作按钮列表,由 useTableButtons({ row: [...] }) 生成
|
|
|
|
|
|
*
|
|
|
|
|
|
* 每个按钮字段:
|
|
|
|
|
|
* key / label / icon / auth / onClick — 同 toolbarButtons
|
|
|
|
|
|
* color - 'danger' 文字变红,用于删除等危险操作
|
|
|
|
|
|
* cssStyle - 默认 { marginRight: '10px', cursor: 'pointer' }
|
|
|
|
|
|
*/
|
|
|
|
|
|
rowButtons: { type: Array, default: () => [] },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 分页参数,传此 prop 才显示分页组件
|
|
|
|
|
|
*
|
|
|
|
|
|
* { current: 1, size: 10, total: 0 }
|
|
|
|
|
|
*
|
|
|
|
|
|
* total 通常在 getList 接口返回 res.count
|
|
|
|
|
|
*/
|
|
|
|
|
|
pagination: { type: Object, default: null },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 额外透传给 el-table 的原生属性,如 { stripe: true, size: 'medium' }
|
|
|
|
|
|
*/
|
|
|
|
|
|
tableAttrs: { type: Object, default: () => ({}) },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 额外透传给 el-table 的原生事件,如 { 'sort-change': handleSort }
|
|
|
|
|
|
*/
|
|
|
|
|
|
tableListeners: { type: Object, default: () => ({}) },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 帮助文档的跳转 URL。传了才显示工具栏右侧的问号按钮,点击新窗口打开
|
|
|
|
|
|
* 例:'/docs/factory-area-help.html'
|
|
|
|
|
|
*/
|
|
|
|
|
|
helpUrl: { type: String, default: '' },
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-05-27 18:37:37 +08:00
|
|
|
|
* 帮助按钮的文字,由调用方负责 i18n 翻译
|
2026-05-26 18:32:57 +08:00
|
|
|
|
* 默认 '帮助'
|
|
|
|
|
|
*/
|
|
|
|
|
|
helpText: { type: String, default: '帮助' }
|
|
|
|
|
|
},
|
|
|
|
|
|
data () {
|
|
|
|
|
|
return {
|
|
|
|
|
|
tableSelected: [], // 当前选中的行数据
|
|
|
|
|
|
computedHeight: null // autoHeight 时动态计算的高度
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 最终使用的表格高度:
|
|
|
|
|
|
* - 用户明确传了 height(非 'auto')→ 直接用
|
|
|
|
|
|
* - autoHeight=true 或 height='auto' → 用动态计算的 computedHeight
|
|
|
|
|
|
* - 否则 undefined(el-table 按内容自适应)
|
|
|
|
|
|
*/
|
|
|
|
|
|
tableHeight () {
|
|
|
|
|
|
if (this.height && this.height !== 'auto') return this.height
|
|
|
|
|
|
if (this.autoHeight || this.height === 'auto') return this.computedHeight || undefined
|
|
|
|
|
|
return undefined
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 过滤后的工具栏按钮(排除无权限的)
|
|
|
|
|
|
*/
|
|
|
|
|
|
visibleToolbarButtons () {
|
|
|
|
|
|
return this.toolbarButtons.filter(b => b.hasPermission !== false)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 过滤后的行内操作按钮(排除无权限的)
|
|
|
|
|
|
*/
|
|
|
|
|
|
visibleRowButtons () {
|
|
|
|
|
|
return this.rowButtons.filter(b => b.hasPermission !== false)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 选中的行数
|
|
|
|
|
|
*/
|
|
|
|
|
|
selectedCount () {
|
|
|
|
|
|
return this.tableSelected.length
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-25 00:49:52 +08:00
|
|
|
|
paginationCurrent () {
|
|
|
|
|
|
if (!this.pagination) return 1
|
|
|
|
|
|
return Number(this.pagination.current || this.pagination.currentPage || 1)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
paginationSize () {
|
|
|
|
|
|
if (!this.pagination) return 10
|
|
|
|
|
|
return Number(this.pagination.size || this.pagination.pageSize || 10)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
paginationTotal () {
|
|
|
|
|
|
if (!this.pagination) return 0
|
|
|
|
|
|
return Number(this.pagination.total || 0)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-26 18:32:57 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 表头样式:浅灰背景 + 黑色加粗文字
|
|
|
|
|
|
*/
|
|
|
|
|
|
headerCellStyle () {
|
|
|
|
|
|
return () => 'background-color: #F8F8F8; color: #000000; font-weight: 500;'
|
2026-06-24 14:38:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将父组件直接绑定在 page-table 上的 el-table 原生事件透传下去。
|
|
|
|
|
|
* selection-change 由当前组件统一维护选中行状态,避免父级监听被触发两次。
|
|
|
|
|
|
*/
|
|
|
|
|
|
elTableListeners () {
|
|
|
|
|
|
const listeners = {
|
|
|
|
|
|
...this.$listeners,
|
|
|
|
|
|
...this.tableListeners
|
|
|
|
|
|
}
|
|
|
|
|
|
delete listeners['selection-change']
|
|
|
|
|
|
delete listeners['page-change']
|
|
|
|
|
|
return listeners
|
2026-05-26 18:32:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
mounted () {
|
|
|
|
|
|
if (this.autoHeight || this.height === 'auto') {
|
|
|
|
|
|
this.startAutoHeight()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
beforeDestroy () {
|
|
|
|
|
|
this.stopAutoHeight()
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
/* ============ 列处理 ============ */
|
|
|
|
|
|
|
|
|
|
|
|
colAttrs (col) {
|
|
|
|
|
|
const attrs = { ...col }
|
|
|
|
|
|
delete attrs.idx
|
|
|
|
|
|
delete attrs.slot
|
|
|
|
|
|
delete attrs.headerSlot
|
2026-05-28 11:30:08 +08:00
|
|
|
|
if (attrs.label) {
|
|
|
|
|
|
attrs.label = this.$t(attrs.label)
|
|
|
|
|
|
}
|
2026-05-26 18:32:57 +08:00
|
|
|
|
return attrs
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* ============ 表格事件 ============ */
|
|
|
|
|
|
|
|
|
|
|
|
onSelectionChange (val) {
|
|
|
|
|
|
this.tableSelected = val
|
|
|
|
|
|
this.$emit('selection-change', val)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* ============ 帮助按钮 ============ */
|
|
|
|
|
|
|
|
|
|
|
|
openHelp () {
|
|
|
|
|
|
if (this.helpUrl) {
|
|
|
|
|
|
window.open(this.helpUrl, '_blank')
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* ============ 分页事件 ============ */
|
|
|
|
|
|
|
|
|
|
|
|
onSizeChange (size) {
|
|
|
|
|
|
this.$emit('page-change', {
|
|
|
|
|
|
current: 1,
|
2026-06-25 00:49:52 +08:00
|
|
|
|
currentPage: 1,
|
2026-05-26 18:32:57 +08:00
|
|
|
|
size,
|
2026-06-25 00:49:52 +08:00
|
|
|
|
pageSize: size,
|
|
|
|
|
|
total: this.paginationTotal
|
2026-05-26 18:32:57 +08:00
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onCurrentChange (current) {
|
|
|
|
|
|
this.$emit('page-change', {
|
|
|
|
|
|
current,
|
2026-06-25 00:49:52 +08:00
|
|
|
|
currentPage: current,
|
|
|
|
|
|
size: this.paginationSize,
|
|
|
|
|
|
pageSize: this.paginationSize,
|
|
|
|
|
|
total: this.paginationTotal
|
2026-05-26 18:32:57 +08:00
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* ============ 自适应高度 ============ */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 启动表格高度自适应
|
|
|
|
|
|
*
|
|
|
|
|
|
* 原理:
|
|
|
|
|
|
* 1. CSS flex 布局让 page-table 占满父容器 100% 高度
|
|
|
|
|
|
* 2. 工具栏和分页各占自然高度(flex-shrink: 0)
|
|
|
|
|
|
* 3. tableWrapper 中间区域自动填充剩余空间(flex: 1 + min-height: 0)
|
|
|
|
|
|
* 4. ResizeObserver 监听 tableWrapper 的实际渲染高度,赋给 el-table
|
|
|
|
|
|
*
|
|
|
|
|
|
* 这样 el-table 永远精确等于可用空间,不会产生外层滚动条
|
|
|
|
|
|
*/
|
|
|
|
|
|
startAutoHeight () {
|
|
|
|
|
|
this.stopAutoHeight()
|
|
|
|
|
|
|
|
|
|
|
|
this._resizeObserver = new ResizeObserver(() => {
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
const wrapper = this.$refs.tableWrapper
|
|
|
|
|
|
if (wrapper) {
|
|
|
|
|
|
this.computedHeight = wrapper.clientHeight
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (this.$refs.tableWrapper) {
|
|
|
|
|
|
this._resizeObserver.observe(this.$refs.tableWrapper)
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
const wrapper = this.$refs.tableWrapper
|
|
|
|
|
|
if (wrapper) {
|
|
|
|
|
|
this.computedHeight = wrapper.clientHeight
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
stopAutoHeight () {
|
|
|
|
|
|
if (this._resizeObserver) {
|
|
|
|
|
|
this._resizeObserver.disconnect()
|
|
|
|
|
|
this._resizeObserver = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.page-table {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 自适应高度模式:占满父容器 100%,禁止自身溢出 */
|
|
|
|
|
|
.page-table--auto {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.page-table__toolbar {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
}
|
|
|
|
|
|
.page-table__toolbar-left {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.page-table__help-btn {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
.page-table__body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.page-table__footer {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-btn {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
margin: 0 5px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-btn--danger {
|
|
|
|
|
|
color: #F56C6C;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|