优化计费逻辑

This commit is contained in:
wuweihua
2026-10-17 01:37:33 +08:00
parent 52f714c760
commit db6f5e882a
29 changed files with 1460 additions and 145 deletions
+284 -1
View File
@@ -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>