优化计费逻辑
This commit is contained in:
@@ -40,8 +40,31 @@
|
||||
/>
|
||||
</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"
|
||||
@@ -58,7 +81,7 @@
|
||||
@sort-change="(c) => $emit('sort-change', c)"
|
||||
>
|
||||
<!-- 多选列 -->
|
||||
<el-table-column v-if="selection" type="selection" width="55" align="center" />
|
||||
<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" />
|
||||
@@ -116,6 +139,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElCheckbox, ElTooltip, TableV2Alignment, TableV2FixedDir } from 'element-plus'
|
||||
/**
|
||||
* 公共表格组件 BasicTable
|
||||
* 集成搜索栏 + 工具栏 + 表格 + 分页,基于 columns/searchColumns 配置驱动,内置复用 BasicForm、RightToolbar、Pagination、DictTag。
|
||||
@@ -283,6 +307,11 @@ const props = defineProps({
|
||||
autoLoad: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// Render only visible rows. Needs a bounded height; falls back to the viewport.
|
||||
virtual: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -327,6 +356,7 @@ watch(
|
||||
|
||||
// 实际渲染的列(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(() =>
|
||||
@@ -375,6 +405,7 @@ async function getList() {
|
||||
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)
|
||||
// 查询失败时清空旧结果,避免用户误以为旧列表是本次查询结果。
|
||||
@@ -426,9 +457,17 @@ 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() {
|
||||
@@ -463,13 +502,257 @@ defineExpose({
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
return h('div', { class: 'basic-table-vcell' }, slotFn({ row: rowData, $index: rowIndex }))
|
||||
}
|
||||
const text = renderVirtualText(col, rowData, rowIndex)
|
||||
if (!text) return h('div', { class: 'basic-table-vcell' }, '')
|
||||
return h(ElTooltip, {
|
||||
content: text,
|
||||
placement: 'top',
|
||||
showAfter: 300,
|
||||
enterable: false
|
||||
}, {
|
||||
default: () => h('div', { class: 'basic-table-vcell' }, text)
|
||||
})
|
||||
}
|
||||
}
|
||||
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-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>
|
||||
|
||||
Reference in New Issue
Block a user