841 lines
24 KiB
Vue
841 lines
24 KiB
Vue
<template>
|
||
<div class="basic-table" v-text-drag-copy="props.textDragCopy">
|
||
<!-- 搜索栏 -->
|
||
<basic-form
|
||
v-if="searchColumns.length"
|
||
ref="searchFormRef"
|
||
:model-value="queryParams"
|
||
:schemas="searchColumns"
|
||
:rules="searchRules"
|
||
:label-width="searchLabelWidth"
|
||
:action-align="searchActionAlign"
|
||
:per-row="searchPerRow"
|
||
:size="size"
|
||
inline
|
||
v-show="showSearch"
|
||
>
|
||
<template v-for="name in searchSlotNames" :key="name" #[toFormSlot(name)]>
|
||
<slot
|
||
:name="name"
|
||
:model="queryParams"
|
||
:item="props.searchColumns.find((item) => item.prop === name.slice('search-'.length))"
|
||
/>
|
||
</template>
|
||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||
<slot name="search-extra" />
|
||
</basic-form>
|
||
|
||
<!-- 工具栏 -->
|
||
<el-row v-if="showToolbar" :gutter="10" class="mb8">
|
||
<slot name="toolbar" :selection="selectionList" :ids="selectionIds" />
|
||
<el-col :span="1.5" v-if="!$slots.toolbar">
|
||
<el-button type="primary" plain icon="Plus" @click="$emit('add')" v-if="showAdd">新增</el-button>
|
||
</el-col>
|
||
<right-toolbar
|
||
v-model:showSearch="showSearchInternal"
|
||
:columns="toolbarColumns"
|
||
:search="searchColumns.length > 0"
|
||
@query-table="getList"
|
||
/>
|
||
</el-row>
|
||
|
||
<!-- 虚拟滚动:只渲染可视行 -->
|
||
<div
|
||
v-if="virtual"
|
||
ref="virtualHostRef"
|
||
class="basic-table-virtual"
|
||
v-loading="loading"
|
||
>
|
||
<el-table-v2
|
||
v-if="virtualWidth > 0 && tableData.length"
|
||
:columns="virtualColumns"
|
||
:data="tableData"
|
||
:width="virtualWidth"
|
||
:height="virtualHeight"
|
||
:row-height="32"
|
||
:header-height="32"
|
||
:row-key="virtualRowKey"
|
||
fixed
|
||
:row-event-handlers="virtualRowHandlers"
|
||
/>
|
||
<el-empty v-else-if="!loading" />
|
||
</div>
|
||
|
||
<!-- 表格 -->
|
||
<el-table
|
||
v-else
|
||
ref="tableRef"
|
||
v-loading="loading"
|
||
:data="tableData"
|
||
:row-key="rowKey"
|
||
:border="border"
|
||
:stripe="stripe"
|
||
:size="size"
|
||
:height="height"
|
||
:max-height="maxHeight"
|
||
:default-sort="defaultSort"
|
||
:span-method="spanMethod"
|
||
@selection-change="handleSelectionChange"
|
||
@row-click="(row, col, e) => $emit('row-click', row, col, e)"
|
||
@sort-change="(c) => $emit('sort-change', c)"
|
||
>
|
||
<!-- 多选列 -->
|
||
<el-table-column v-if="selection" type="selection" width="55" align="center" :fixed="leftFixed ? 'left' : false" />
|
||
|
||
<!-- 序号列 -->
|
||
<el-table-column v-if="showIndex" type="index" label="序号" width="55" align="center" />
|
||
|
||
<!-- 动态列 -->
|
||
<el-table-column
|
||
v-for="col in visibleColumns"
|
||
:key="col.key"
|
||
:prop="col.prop"
|
||
:label="col.label"
|
||
:width="col.width"
|
||
:min-width="col.minWidth"
|
||
:align="col.align || 'center'"
|
||
:fixed="col.fixed"
|
||
:sortable="col.sortable"
|
||
:show-overflow-tooltip="col.showOverflowTooltip !== false"
|
||
>
|
||
<template #header="scope">
|
||
<slot :name="`header-${col.prop}`" v-bind="scope">{{ col.label }}</slot>
|
||
</template>
|
||
<template #default="scope">
|
||
<slot :name="`col-${col.prop}`" v-bind="scope" :row="scope.row">
|
||
<!-- 字典列 -->
|
||
<dict-tag
|
||
v-if="col.dict"
|
||
:options="getDictOptions(col.dict)"
|
||
:value="scope.row[col.prop]"
|
||
/>
|
||
<!-- 时间格式化 -->
|
||
<span v-else-if="col.parseTime">{{ parseTime(scope.row[col.prop], col.parseTime === true ? undefined : col.parseTime) }}</span>
|
||
<!-- 自定义格式化 -->
|
||
<span v-else-if="col.formatter">{{ col.formatter(scope.row, col, scope.row[col.prop], scope.$index) }}</span>
|
||
<!-- 默认显示 -->
|
||
<span v-else>{{ scope.row[col.prop] }}</span>
|
||
</slot>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<!-- 空数据 -->
|
||
<template #empty>
|
||
<slot name="empty"><el-empty /></slot>
|
||
</template>
|
||
</el-table>
|
||
|
||
<!-- 分页;可选的分页前置内容用于展示汇总等信息。 -->
|
||
<div class="basic-table-pagination" :class="{ 'basic-table-pagination--with-extra': $slots['pagination-extra'] }">
|
||
<slot name="pagination-extra" />
|
||
<pagination
|
||
v-show="total > 0"
|
||
:total="total"
|
||
v-model:page="queryParams.pageNum"
|
||
v-model:limit="queryParams.pageSize"
|
||
:page-sizes="pageSizes"
|
||
@pagination="getList"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ElCheckbox, ElTooltip, TableV2Alignment, TableV2FixedDir } from 'element-plus'
|
||
/**
|
||
* 公共表格组件 BasicTable
|
||
* 集成搜索栏 + 工具栏 + 表格 + 分页,基于 columns/searchColumns 配置驱动,内置复用 BasicForm、RightToolbar、Pagination、DictTag。
|
||
*
|
||
* 用法示例:
|
||
* <basic-table
|
||
* ref="tableRef"
|
||
* :columns="columns"
|
||
* :search-columns="searchColumns"
|
||
* :query="listPost"
|
||
* :text-drag-copy="false" // 可关闭表格文字拖选复制
|
||
* @add="handleAdd"
|
||
* >
|
||
* <template #toolbar="{ selection, ids }">
|
||
* <el-button type="primary" icon="Plus" @click="handleAdd">新增</el-button>
|
||
* </template>
|
||
* <template #col-status="{ row }">
|
||
* <dict-tag :options="sys_normal_disable" :value="row.status" />
|
||
* </template>
|
||
* </basic-table>
|
||
*
|
||
* column 配置:
|
||
* {
|
||
* label: '岗位名称', // 列标题
|
||
* prop: 'postName', // 字段名
|
||
* width: 120, // 列宽
|
||
* minWidth: 120,
|
||
* align: 'center', // 对齐(默认 center)
|
||
* fixed: 'left'|'right',
|
||
* sortable: true,
|
||
* showOverflowTooltip: true, // 默认 true
|
||
* dict: 'sys_normal_disable', // 用字典标签渲染
|
||
* parseTime: true | 'YYYY-MM-DD', // 时间格式化(true 用默认格式)
|
||
* formatter: (row,col,val,index)=> ..., // 自定义格式化
|
||
* show: true, // 是否显示(参与显隐列)
|
||
* showInToolbar: true, // 是否在显隐列工具栏中显示(默认 true)
|
||
* visible: true // 初始是否可见(显隐列控制)
|
||
* }
|
||
*
|
||
* searchColumns 配置:同 BasicForm 的 schemas。
|
||
*/
|
||
|
||
defineOptions({ name: 'BasicTable' })
|
||
|
||
const props = defineProps({
|
||
// 列配置数组
|
||
columns: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
// 搜索表单配置(同 BasicForm schemas)
|
||
searchColumns: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
// 搜索表单校验规则
|
||
searchRules: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
// 搜索表单标签宽度
|
||
searchLabelWidth: {
|
||
type: [String, Number],
|
||
default: 'auto'
|
||
},
|
||
// 搜索按钮区对齐:left / center / right
|
||
searchActionAlign: {
|
||
type: String,
|
||
default: 'left'
|
||
},
|
||
// 搜索区固定每行项数(>0 时启用栅格布局,多余项自动换行;0 为流式换行)
|
||
searchPerRow: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
// 列表加载函数:(params) => Promise<{rows, total}> | {rows, total}
|
||
query: {
|
||
type: Function,
|
||
required: true
|
||
},
|
||
// 额外固定查询参数(每次请求合并传入,不参与搜索表单重置)
|
||
extraParams: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
// 初始查询参数(搜索字段的默认值)
|
||
defaultParams: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
// 是否显示多选列
|
||
selection: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 最多勾选条数,0 表示不限制
|
||
selectionMax: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
// 是否显示序号列
|
||
showIndex: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 是否显示工具栏
|
||
showToolbar: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 是否支持在同一列中拖选文字并复制(特殊交互表格可关闭)
|
||
textDragCopy: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 是否显示默认新增按钮(无 toolbar 插槽时生效)
|
||
showAdd: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 是否显示搜索栏(可双向控制)
|
||
showSearch: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 行 key
|
||
rowKey: {
|
||
type: [String, Function],
|
||
default: undefined
|
||
},
|
||
// 边框
|
||
border: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 斑马纹
|
||
stripe: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 表格/表单尺寸(large/default/small)
|
||
size: {
|
||
type: String,
|
||
default: undefined
|
||
},
|
||
// 表格高度
|
||
height: { type: [String, Number], default: undefined },
|
||
maxHeight: { type: [String, Number], default: undefined },
|
||
// 默认排序(透传 el-table default-sort,如 { prop: 'createTime', order: 'descending' })
|
||
defaultSort: {
|
||
type: Object,
|
||
default: undefined
|
||
},
|
||
// 单元格合并方法(透传 el-table span-method),签名同 el-table:({ row, column, rowIndex, columnIndex }) => { rowspan, colspan }
|
||
spanMethod: {
|
||
type: Function,
|
||
default: null
|
||
},
|
||
// 每页条数可选项
|
||
pageSizes: {
|
||
type: Array,
|
||
default: () => [10, 20, 30, 50]
|
||
},
|
||
// 默认每页条数
|
||
pageSize: {
|
||
type: Number,
|
||
default: 10
|
||
},
|
||
// 是否在挂载后自动加载
|
||
autoLoad: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// Render only visible rows. Needs a bounded height; falls back to the viewport.
|
||
virtual: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
})
|
||
|
||
const emit = defineEmits(['add', 'update:showSearch', 'row-click', 'sort-change', 'selection-change'])
|
||
|
||
const { proxy } = getCurrentInstance()
|
||
const tableRef = ref()
|
||
const searchFormRef = ref()
|
||
|
||
const loading = ref(false)
|
||
const tableData = ref([])
|
||
const total = ref(0)
|
||
const selectionList = ref([])
|
||
const showSearchInternal = ref(props.showSearch)
|
||
|
||
// 查询参数:搜索字段 + 分页
|
||
const queryParams = reactive({
|
||
pageNum: 1,
|
||
pageSize: props.pageSize,
|
||
...props.defaultParams
|
||
})
|
||
|
||
watch(() => props.showSearch, (v) => (showSearchInternal.value = v))
|
||
watch(showSearchInternal, (v) => emit('update:showSearch', v))
|
||
|
||
// ====== 列显隐状态 ======
|
||
// 内部维护列状态(带 key/visible),供 RightToolbar 控制显隐
|
||
const tableColumns = ref([])
|
||
function initColumns() {
|
||
tableColumns.value = props.columns.map((col, index) => ({
|
||
...col,
|
||
key: col.key || col.prop || index,
|
||
visible: col.visible !== false && col.show !== false
|
||
}))
|
||
}
|
||
initColumns()
|
||
watch(
|
||
() => props.columns,
|
||
() => initColumns(),
|
||
{ deep: false }
|
||
)
|
||
|
||
// 实际渲染的列(visible !== false)
|
||
const visibleColumns = computed(() => tableColumns.value.filter((c) => c.visible !== false))
|
||
const leftFixed = computed(() => visibleColumns.value.some((c) => c.fixed === 'left' || c.fixed === true))
|
||
|
||
// 参与显隐工具栏的列(有 label 且未禁用工具栏显示)
|
||
const toolbarColumns = computed(() =>
|
||
tableColumns.value.filter((c) => c.label && c.showInToolbar !== false)
|
||
)
|
||
|
||
// ====== 字典加载(用于表格列 dict 渲染) ======
|
||
const dictMap = reactive({})
|
||
const dictTypes = computed(() => {
|
||
const set = new Set()
|
||
props.columns.forEach((c) => c.dict && set.add(c.dict))
|
||
props.searchColumns.forEach((c) => c.dict && set.add(c.dict))
|
||
return [...set]
|
||
})
|
||
watch(
|
||
dictTypes,
|
||
(types) => {
|
||
if (!types.length) return
|
||
const dictRefs = proxy.useDict(...types)
|
||
types.forEach((t) => {
|
||
const d = dictRefs[t]
|
||
dictMap[t] = isRef(d) ? d : ref(d)
|
||
})
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
function getDictOptions(type) {
|
||
const d = dictMap[type]
|
||
return isRef(d) ? d.value : d || []
|
||
}
|
||
|
||
// ====== 搜索栏插槽透传:search-xxx -> form-xxx ======
|
||
const slots = useSlots()
|
||
const searchSlotNames = computed(() =>
|
||
Object.keys(slots).filter((n) => n.startsWith('search-') && n !== 'search-extra')
|
||
)
|
||
function toFormSlot(name) {
|
||
return 'form-' + name.slice('search-'.length)
|
||
}
|
||
|
||
// ====== 数据加载 ======
|
||
async function getList() {
|
||
loading.value = true
|
||
try {
|
||
const params = { ...queryParams, ...props.extraParams }
|
||
const res = await props.query(params)
|
||
tableData.value = res?.rows || []
|
||
total.value = res?.total || 0
|
||
if (props.virtual) selectionList.value = []
|
||
} catch (e) {
|
||
console.error('BasicTable 加载失败', e)
|
||
// 查询失败时清空旧结果,避免用户误以为旧列表是本次查询结果。
|
||
tableData.value = []
|
||
total.value = 0
|
||
selectionList.value = []
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 搜索
|
||
function handleQuery() {
|
||
queryParams.pageNum = 1
|
||
getList()
|
||
}
|
||
|
||
// 重置搜索
|
||
function resetQuery() {
|
||
searchFormRef.value?.resetFields()
|
||
Object.keys(props.defaultParams || {}).forEach((k) => {
|
||
queryParams[k] = props.defaultParams[k]
|
||
})
|
||
queryParams.pageNum = 1
|
||
getList()
|
||
}
|
||
|
||
// 选中变化。超过上限时只保留先勾到的行,并同步虚拟表和普通表的勾选状态。
|
||
let cappingSelection = false
|
||
function handleSelectionChange(val) {
|
||
if (cappingSelection) return
|
||
const rows = Array.isArray(val) ? val.slice() : []
|
||
const max = Number(props.selectionMax) || 0
|
||
if (max > 0 && rows.length > max) {
|
||
const kept = rows.slice(0, max)
|
||
cappingSelection = true
|
||
selectionList.value = kept
|
||
emit('selection-change', kept)
|
||
proxy.$modal.msgWarning(`单次最多勾选 ${max} 条`)
|
||
nextTick(() => {
|
||
const table = tableRef.value
|
||
if (table?.toggleRowSelection) {
|
||
const keys = new Set(kept.map((row) => virtualRowId(row)))
|
||
tableData.value.forEach((row) => table.toggleRowSelection(row, keys.has(virtualRowId(row))))
|
||
}
|
||
cappingSelection = false
|
||
})
|
||
return
|
||
}
|
||
selectionList.value = rows
|
||
emit('selection-change', rows)
|
||
}
|
||
const selectionIds = computed(() =>
|
||
selectionList.value.map((r) => (props.rowKey ? r[props.rowKey] : r.id)).filter((v) => v !== undefined)
|
||
)
|
||
|
||
// ====== 暴露方法 ======
|
||
function refresh() {
|
||
getList()
|
||
}
|
||
function reload() {
|
||
queryParams.pageNum = 1
|
||
getList()
|
||
}
|
||
function getSelection() {
|
||
return selectionList.value
|
||
}
|
||
function getSelectionIds() {
|
||
return selectionIds.value
|
||
}
|
||
function clearSelection() {
|
||
if (props.virtual) {
|
||
handleSelectionChange([])
|
||
return
|
||
}
|
||
tableRef.value?.clearSelection()
|
||
}
|
||
function toggleRowSelection(row, selected) {
|
||
if (props.virtual) {
|
||
toggleVirtualRow(row, selected !== false)
|
||
return
|
||
}
|
||
tableRef.value?.toggleRowSelection(row, selected)
|
||
}
|
||
function getTableRef() {
|
||
return tableRef.value
|
||
}
|
||
function getSearchFormRef() {
|
||
return searchFormRef.value
|
||
}
|
||
function getQueryParams() {
|
||
return { ...queryParams, ...props.extraParams }
|
||
}
|
||
function setQueryParams(p) {
|
||
Object.assign(queryParams, p)
|
||
}
|
||
|
||
defineExpose({
|
||
refresh,
|
||
reload,
|
||
getList,
|
||
handleQuery,
|
||
resetQuery,
|
||
getSelection,
|
||
getSelectionIds,
|
||
clearSelection,
|
||
toggleRowSelection,
|
||
getTableRef,
|
||
getSearchFormRef,
|
||
getQueryParams,
|
||
setQueryParams,
|
||
getData: () => tableData.value,
|
||
/** 当前页数据(响应式,父组件 span-method 可直接订阅) */
|
||
data: tableData
|
||
})
|
||
|
||
const virtualHostRef = ref()
|
||
const virtualWidth = ref(0)
|
||
const virtualHeight = ref(480)
|
||
let virtualResizeObserver = null
|
||
|
||
function measureVirtual() {
|
||
const host = virtualHostRef.value
|
||
if (!host) return
|
||
virtualWidth.value = Math.max(0, Math.floor(host.clientWidth))
|
||
const passed = Number(props.height)
|
||
if (Number.isFinite(passed) && passed > 0) {
|
||
virtualHeight.value = passed
|
||
return
|
||
}
|
||
const top = host.getBoundingClientRect().top
|
||
virtualHeight.value = Math.max(240, Math.floor(window.innerHeight - top - 70))
|
||
}
|
||
|
||
function virtualRowId(row) {
|
||
if (!row) return undefined
|
||
if (typeof props.rowKey === 'function') return props.rowKey(row)
|
||
if (props.rowKey) return row[props.rowKey]
|
||
return row.id
|
||
}
|
||
|
||
const virtualRowKey = computed(() => (typeof props.rowKey === 'string' && props.rowKey) ? props.rowKey : 'id')
|
||
|
||
function isRowSelected(row) {
|
||
const key = virtualRowId(row)
|
||
return selectionList.value.some((item) => virtualRowId(item) === key)
|
||
}
|
||
|
||
function allRowsSelected() {
|
||
return tableData.value.length > 0 && tableData.value.every((row) => isRowSelected(row))
|
||
}
|
||
|
||
function someRowsSelected() {
|
||
return selectionList.value.length > 0 && !allRowsSelected()
|
||
}
|
||
|
||
function toggleVirtualRow(row, checked) {
|
||
const key = virtualRowId(row)
|
||
const next = selectionList.value.filter((item) => virtualRowId(item) !== key)
|
||
if (checked) next.push(row)
|
||
handleSelectionChange(next)
|
||
}
|
||
|
||
function toggleVirtualAll(checked) {
|
||
handleSelectionChange(checked ? tableData.value.slice() : [])
|
||
}
|
||
|
||
function renderVirtualCheckbox(checked, indeterminate, onChange) {
|
||
return h(ElCheckbox, {
|
||
modelValue: checked,
|
||
indeterminate,
|
||
'onUpdate:modelValue': onChange,
|
||
onClick: (event) => event.stopPropagation()
|
||
})
|
||
}
|
||
|
||
function columnWidth(col) {
|
||
const width = Number(col.width || col.minWidth)
|
||
return Number.isFinite(width) && width > 0 ? width : 140
|
||
}
|
||
|
||
function columnAlign() {
|
||
return TableV2Alignment.LEFT
|
||
}
|
||
|
||
function columnFixed(col) {
|
||
if (col.fixed === 'left' || col.fixed === true) return TableV2FixedDir.LEFT
|
||
if (col.fixed === 'right') return TableV2FixedDir.RIGHT
|
||
return undefined
|
||
}
|
||
|
||
function renderVirtualText(col, row, rowIndex) {
|
||
let text = row ? row[col.prop] : ''
|
||
if (col.parseTime) {
|
||
text = proxy.parseTime(text, col.parseTime === true ? undefined : col.parseTime)
|
||
} else if (col.formatter) {
|
||
text = col.formatter(row, col, text, rowIndex)
|
||
}
|
||
if (text == null || text === '') return ''
|
||
return String(text)
|
||
}
|
||
|
||
/** Flatten slot VNode text for virtual-table overflow tooltip. */
|
||
function extractSlotText(nodes) {
|
||
if (nodes == null || nodes === false) return ''
|
||
if (typeof nodes === 'string' || typeof nodes === 'number') return String(nodes)
|
||
if (Array.isArray(nodes)) return nodes.map(extractSlotText).join('')
|
||
if (typeof nodes === 'object') {
|
||
if (typeof nodes.children === 'string' || typeof nodes.children === 'number') {
|
||
return String(nodes.children)
|
||
}
|
||
if (Array.isArray(nodes.children)) return extractSlotText(nodes.children)
|
||
}
|
||
return ''
|
||
}
|
||
|
||
/** True when slot tree contains a component (button/tag/...), skip auto tooltip. */
|
||
function slotHasComponent(nodes) {
|
||
if (nodes == null || nodes === false) return false
|
||
if (Array.isArray(nodes)) return nodes.some(slotHasComponent)
|
||
if (typeof nodes !== 'object') return false
|
||
if (typeof nodes.type === 'object' || typeof nodes.type === 'function') return true
|
||
if (Array.isArray(nodes.children)) return slotHasComponent(nodes.children)
|
||
return false
|
||
}
|
||
|
||
function resolveVirtualTooltip(col, rowData, rowIndex, slotNodes) {
|
||
if (col.showOverflowTooltip === false) return ''
|
||
if (typeof col.tooltipFormatter === 'function') {
|
||
const tip = col.tooltipFormatter(rowData, col, rowData ? rowData[col.prop] : undefined, rowIndex)
|
||
return tip == null || tip === '' ? '' : String(tip)
|
||
}
|
||
if (slotNodes != null) {
|
||
if (slotHasComponent(slotNodes)) return ''
|
||
return extractSlotText(slotNodes).trim()
|
||
}
|
||
return renderVirtualText(col, rowData, rowIndex)
|
||
}
|
||
|
||
function wrapVirtualTooltip(tip, cell) {
|
||
if (!tip) return cell
|
||
return h(ElTooltip, {
|
||
content: tip,
|
||
placement: 'top',
|
||
showAfter: 300,
|
||
enterable: false
|
||
}, {
|
||
default: () => cell
|
||
})
|
||
}
|
||
|
||
const virtualColumns = computed(() => {
|
||
const cols = []
|
||
if (props.selection) {
|
||
cols.push({
|
||
key: '__selection',
|
||
dataKey: '__selection',
|
||
width: 55,
|
||
align: TableV2Alignment.CENTER,
|
||
fixed: leftFixed.value ? TableV2FixedDir.LEFT : undefined,
|
||
headerCellRenderer: () => renderVirtualCheckbox(allRowsSelected(), someRowsSelected(), toggleVirtualAll),
|
||
cellRenderer: ({ rowData }) => renderVirtualCheckbox(isRowSelected(rowData), false, (checked) => toggleVirtualRow(rowData, checked))
|
||
})
|
||
}
|
||
if (props.showIndex) {
|
||
cols.push({
|
||
key: '__index',
|
||
dataKey: '__index',
|
||
title: '#',
|
||
width: 55,
|
||
align: TableV2Alignment.CENTER,
|
||
cellRenderer: ({ rowIndex }) => String((queryParams.pageNum - 1) * queryParams.pageSize + rowIndex + 1)
|
||
})
|
||
}
|
||
visibleColumns.value.forEach((col) => {
|
||
const slotFn = slots[`col-${col.prop}`]
|
||
const headerSlot = slots[`header-${col.prop}`]
|
||
const column = {
|
||
key: String(col.key || col.prop),
|
||
dataKey: col.prop,
|
||
title: col.label,
|
||
width: columnWidth(col),
|
||
align: columnAlign(col),
|
||
fixed: columnFixed(col),
|
||
cellRenderer: ({ rowData, rowIndex }) => {
|
||
if (col.dict) {
|
||
return h(resolveComponent('DictTag'), {
|
||
options: getDictOptions(col.dict),
|
||
value: rowData[col.prop]
|
||
})
|
||
}
|
||
if (slotFn) {
|
||
const nodes = slotFn({ row: rowData, $index: rowIndex })
|
||
const cell = h('div', { class: 'basic-table-vcell' }, nodes)
|
||
return wrapVirtualTooltip(resolveVirtualTooltip(col, rowData, rowIndex, nodes), cell)
|
||
}
|
||
const text = renderVirtualText(col, rowData, rowIndex)
|
||
const cell = h('div', { class: 'basic-table-vcell' }, text || '')
|
||
return wrapVirtualTooltip(resolveVirtualTooltip(col, rowData, rowIndex), cell)
|
||
}
|
||
}
|
||
if (headerSlot) column.headerCellRenderer = () => headerSlot({ column: col })
|
||
cols.push(column)
|
||
})
|
||
return cols
|
||
})
|
||
|
||
const virtualRowHandlers = {
|
||
onClick({ rowData, event }) {
|
||
emit('row-click', rowData, null, event)
|
||
}
|
||
}
|
||
|
||
watch(() => props.height, () => nextTick(measureVirtual))
|
||
|
||
if (props.autoLoad) {
|
||
onMounted(getList)
|
||
}
|
||
|
||
onMounted(() => {
|
||
if (!props.virtual) return
|
||
nextTick(() => {
|
||
measureVirtual()
|
||
if (virtualHostRef.value && typeof ResizeObserver !== 'undefined') {
|
||
virtualResizeObserver = new ResizeObserver(() => measureVirtual())
|
||
virtualResizeObserver.observe(virtualHostRef.value)
|
||
}
|
||
window.addEventListener('resize', measureVirtual)
|
||
})
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
if (virtualResizeObserver) virtualResizeObserver.disconnect()
|
||
window.removeEventListener('resize', measureVirtual)
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.basic-table {
|
||
width: 100%;
|
||
}
|
||
.basic-table-pagination--with-extra {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.basic-table-pagination--with-extra :deep(.pagination-container) {
|
||
margin-left: auto;
|
||
}
|
||
.basic-table-virtual {
|
||
width: 100%;
|
||
min-height: 240px;
|
||
border: 1px solid #ebeef5;
|
||
background: #fff;
|
||
}
|
||
.basic-table-virtual :deep(.el-table-v2) {
|
||
--el-table-header-bg-color: #f8f8f9;
|
||
--el-table-header-text-color: #515a6e;
|
||
--el-table-border-color: #ebeef5;
|
||
--el-table-row-hover-bg-color: #f5f7fa;
|
||
--el-table-text-color: #606266;
|
||
font-size: 13px;
|
||
color: #606266;
|
||
}
|
||
.basic-table-virtual :deep(.el-table-v2__header-row) {
|
||
background: #f8f8f9;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
.basic-table-virtual :deep(.el-table-v2__header-cell) {
|
||
background: #f8f8f9;
|
||
color: #515a6e;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
border-right: 1px solid #ebeef5;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
justify-content: flex-start !important;
|
||
text-align: left;
|
||
}
|
||
.basic-table-virtual :deep(.el-table-v2__header-cell-text) {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
text-align: left;
|
||
}
|
||
.basic-table-virtual :deep(.el-table-v2__header-cell:last-child),
|
||
.basic-table-virtual :deep(.el-table-v2__row-cell:last-child) {
|
||
border-right: none;
|
||
}
|
||
.basic-table-virtual :deep(.el-table-v2__row) {
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
.basic-table-virtual :deep(.el-table-v2__row:hover) {
|
||
background: #f5f7fa;
|
||
}
|
||
.basic-table-virtual :deep(.el-table-v2__row-cell) {
|
||
color: #606266;
|
||
font-size: 13px;
|
||
border-right: 1px solid #ebeef5;
|
||
padding: 0 8px;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
justify-content: flex-start;
|
||
text-align: left;
|
||
min-width: 0;
|
||
}
|
||
.basic-table-virtual :deep(.el-table-v2__row-cell .el-tooltip__trigger) {
|
||
display: block;
|
||
max-width: 100%;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
}
|
||
.basic-table-vcell {
|
||
display: block;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
max-width: 100%;
|
||
min-width: 0;
|
||
line-height: 20px;
|
||
}
|
||
</style>
|