列表显隐

This commit is contained in:
chenlin
2026-01-27 00:20:44 +08:00
parent 44fbc759fa
commit 5acb741497
3 changed files with 940 additions and 399 deletions

View File

@@ -0,0 +1,172 @@
<template>
<ElDrawer
v-model="drawerVisible"
title="列显隐"
direction="rtl"
size="560px"
:z-index="3000"
>
<div class="column-config-drawer">
<el-table
:data="columnConfigList"
border
stripe
max-height="calc(100vh - 200px)"
style="width: 100%"
>
<el-table-column prop="label" label="列名" width="200" fixed="left">
<template #default="{ row }">
<span>{{ row.label }}</span>
</template>
</el-table-column>
<el-table-column label="隐藏" width="80" align="center">
<template #default="{ row }">
<el-checkbox
v-model="row.hide"
@change="handleConfigChange"
/>
</template>
</el-table-column>
<el-table-column label="冻结" width="80" align="center">
<template #default="{ row }">
<el-checkbox
:model-value="row.fixed === 'left'"
@change="(val) => { row.fixed = val ? 'left' : false; handleConfigChange() }"
/>
</template>
</el-table-column>
<el-table-column label="过滤" width="80" align="center">
<template #default="{ row }">
<el-checkbox
v-model="row.filterable"
@change="handleConfigChange"
/>
</template>
</el-table-column>
<el-table-column label="排序" width="80" align="center">
<template #default="{ row }">
<el-checkbox
:model-value="row.sortable === 'custom'"
@change="(val) => { row.sortable = val ? 'custom' : false; handleConfigChange() }"
/>
</template>
</el-table-column>
</el-table>
</div>
</ElDrawer>
</template>
<script setup lang="ts">
import { debounce } from 'lodash-es'
defineOptions({ name: 'ColumnConfigDialog' })
interface ColumnConfig {
prop: string
label: string
hide: boolean
fixed: string | boolean
filterable: boolean
sortable: string | boolean
showColumn: boolean
}
interface Props {
modelValue: boolean
columns: Record<string, any>
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
'confirm': [config: Record<string, any>]
}>()
const drawerVisible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const columnConfigList = ref<ColumnConfig[]>([])
const initColumnConfig = () => {
if (!props.columns) return
columnConfigList.value = Object.keys(props.columns)
.filter(key => {
const column = props.columns[key]
return column.showColumn !== false && column.prop
})
.map(key => {
const column = props.columns[key]
return {
prop: column.prop || key,
label: column.label || key,
hide: column.hide || false,
fixed: column.fixed || false,
filterable: column.filterable || false,
sortable: column.sortable || false,
showColumn: column.showColumn !== false
}
})
.sort((a, b) => {
const labelA = a.label || ''
const labelB = b.label || ''
return labelA.localeCompare(labelB, 'zh-CN')
})
}
// 防抖保存配置
const saveConfig = debounce(() => {
const config: Record<string, any> = {}
columnConfigList.value.forEach(item => {
config[item.prop] = {
hide: item.hide,
fixed: item.fixed,
filterable: item.filterable,
sortable: item.sortable
}
})
emit('confirm', config)
}, 300)
const handleConfigChange = () => {
// 配置变化时直接触发保存(防抖处理)
saveConfig()
}
watch(
() => props.modelValue,
(val) => {
if (val) {
initColumnConfig()
}
},
{ immediate: true }
)
watch(
() => props.columns,
() => {
if (props.modelValue) {
initColumnConfig()
}
},
{ deep: true }
)
</script>
<style lang="scss" scoped>
.column-config-drawer {
:deep(.el-table) {
.el-table__header {
th {
background-color: var(--el-fill-color-light);
font-weight: 600;
}
}
}
}
</style>

View File

@@ -1,152 +1,73 @@
<template>
<div
class="low-table relative"
:class="[
<div class="low-table relative" :class="[
`low-table__${tableId}`,
{
summary: tableOption.showSummary,
'low-table-grid': tableOption.grid,
'no-menu': !tableOption.menu
}
]"
>
]">
<span prop="delegateUserId" style="display:none">
<userSelect
id="costomUserSelect"
v-model="formData"
v-bind="userVBind"
class="w-100%"
></userSelect>
<userSelect id="costomUserSelect" v-model="formData" v-bind="userVBind" class="w-100%"></userSelect>
</span>
<span prop="delegateDictId" style="display:none">
<DicTableSelect
id="costomDictSelect"
:column="distSelectColumn"
size="default"
type="add"
prop="fields_7897245"
:scope="dictSelectScope"
@set-form-data="handleSetFormData"
></DicTableSelect>
<DicTableSelect id="costomDictSelect" :column="distSelectColumn" size="default" type="add" prop="fields_7897245"
:scope="dictSelectScope" @set-form-data="handleSetFormData"></DicTableSelect>
</span>
<!-- 顶部统计 -->
<div
v-if="summaryTop.show"
class="low-table-summary absolute left-0 top-0 w-100% h-auto z-999"
v-hasResize="onSummaryTopResize"
>
<SummaryTop
ref="summaryTopRef"
v-if="tableSummary.topList?.length"
:summaryList="tableSummary.topList"
></SummaryTop>
<div v-if="summaryTop.show" class="low-table-summary absolute left-0 top-0 w-100% h-auto z-999"
v-hasResize="onSummaryTopResize">
<SummaryTop ref="summaryTopRef" v-if="tableSummary.topList?.length" :summaryList="tableSummary.topList">
</SummaryTop>
</div>
<div
class="low-table-content gap-x-10px w-100%"
:class="{
<div class="low-table-content gap-x-10px w-100%" :class="{
show_fixed_bar: isShowFixedBar,
[`low-table-grid__${tableInfo.singleCardSpan}`]: tableOption.grid
}"
:style="{ paddingTop: summaryTop.height + 'px' }"
>
}" :style="{ paddingTop: summaryTop.height + 'px' }">
<!-- 左树右表树表 -->
<div
class="left-tree-box flex-basis-200px flex-shrink-0"
v-if="tableInfo.tableType == 'treeAround'"
>
<avue-tree
ref="treeRef"
:option="treeAroundOption"
:data="treeAroundData"
@node-click="treeAroundNodeClick"
>
<div class="left-tree-box flex-basis-200px flex-shrink-0" v-if="tableInfo.tableType == 'treeAround'">
<avue-tree ref="treeRef" :option="treeAroundOption" :data="treeAroundData" @node-click="treeAroundNodeClick">
<template #default="{ data }">
<span
class="el-tree-node__label"
:class="{
<span class="el-tree-node__label" :class="{
active:
data[treeAroundOption.props.value] == treeAroundRow[treeAroundOption.props.value]
}"
>
}">
{{ data[treeAroundOption.props.label] }}
</span>
</template>
</avue-tree>
</div>
<div
class="flex-1 w-100%"
:class="{ 'table-content': tableInfo.tableType == 'treeAround' }"
v-if="isInit"
>
<!-- 列配置抽屉 -->
<ColumnConfigDialog v-if="props.model === 'default'" v-model="showColumnConfigDrawer"
:columns="tableOption.column" @confirm="handleColumnConfigConfirm" />
<div class="flex-1 w-100%" :class="{ 'table-content': tableInfo.tableType == 'treeAround' }" v-if="isInit">
<!-- 主体表格 -->
<avue-crud
ref="crudRef"
v-model="tableForm"
v-model:search="tableSearch"
v-bind="crudBind"
:table-loading="loading"
:data="tableData"
:option="tableOption"
:before-open="beforeOpen"
:before-close="beforeClose"
:row-style="tableDefaultFun.rowStyle"
:cell-style="tableDefaultFun.cellStyle"
:summary-method="tableDefaultFun.summaryMethod"
:span-method="tableDefaultFun.spanMethod"
@search-change="searchChange"
@search-reset="resetChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
@refresh-change="refreshChange"
@size-change="sizeChange"
@current-change="currentChange"
@selection-change="selectionChange"
@sort-change="sortChange"
@select-all="selectAll"
@row-click="tableDefaultFun.rowClick"
@row-dblclick="tableDefaultFun.rowDblclick"
@cell-click="tableDefaultFun.cellClick"
@cell-dblclick="tableDefaultFun.cellDblclick"
@tree-load="treeLoad"
@expand-change="expandChanges"
:upload-before="uploadBefore"
:upload-exceed="uploadExceed"
:upload-sized="uploadSized"
:upload-preview="uploadPreview"
>
<avue-crud ref="crudRef" v-model="tableForm" v-model:search="tableSearch" v-bind="crudBind"
:table-loading="loading" :data="tableData" :option="tableOption" :before-open="beforeOpen"
:before-close="beforeClose" :row-style="tableDefaultFun.rowStyle" :cell-style="tableDefaultFun.cellStyle"
:summary-method="tableDefaultFun.summaryMethod" :span-method="tableDefaultFun.spanMethod"
@search-change="searchChange" @search-reset="resetChange" @row-save="rowSave" @row-update="rowUpdate"
@row-del="rowDel" @refresh-change="refreshChange" @size-change="sizeChange" @current-change="currentChange"
@selection-change="selectionChange" @sort-change="sortChange" @select-all="selectAll"
@row-click="tableDefaultFun.rowClick" @row-dblclick="tableDefaultFun.rowDblclick"
@cell-click="tableDefaultFun.cellClick" @cell-dblclick="tableDefaultFun.cellDblclick" @tree-load="treeLoad"
@expand-change="expandChanges" :upload-before="uploadBefore" :upload-exceed="uploadExceed"
:upload-sized="uploadSized" :upload-preview="uploadPreview">
<!-- 自定义表格头部操作 -->
<template #menu-left="{ size }">
<TableButton
v-show="menuLeftShow"
type="header"
:size="size"
:buttonObj="buttonObj"
:selectLength="tableSelect.length"
@menu-left-handle="menuLeftHandle"
></TableButton>
<TableButton v-show="menuLeftShow" type="header" :size="size" :buttonObj="buttonObj"
:selectLength="tableSelect.length" @menu-left-handle="menuLeftHandle"></TableButton>
</template>
<!-- 自定义操作列 -->
<template #menu="{ size, row, index }">
<TableButton
:type="tableInfo.menuStyle == 'more' ? 'more' : 'menu'"
:max-num="tableInfo.maxMenuNum"
:size="size"
:buttonObj="buttonObj"
:row="row"
:index="index"
@menu-handle="menuHandle"
></TableButton>
<TableButton :type="tableInfo.menuStyle == 'more' ? 'more' : 'menu'" :max-num="tableInfo.maxMenuNum"
:size="size" :buttonObj="buttonObj" :row="row" :index="index" @menu-handle="menuHandle"></TableButton>
</template>
<!-- 自定义多选提示 -->
<template #tip>
<span
class="inline-block pl-10px c-#999"
v-if="model == 'dicTable' && dicMaxLimit"
type="danger"
>
<span class="inline-block pl-10px c-#999" v-if="model == 'dicTable' && dicMaxLimit" type="danger">
{{ t('Avue.crud.selectMaxPrepend') }}
{{ dicMaxLimit }}
{{ t('Avue.crud.selectMaxAppend') }}
@@ -154,42 +75,22 @@
</template>
<!-- 单选 -->
<template #lowSelectRadio="{ row, index }">
<el-radio
class="low-select-radio"
v-model="radioValue"
:label="row[tableOption.rowKey || 'id']"
:disabled="!tableOption.selectable(row, index)"
@click.stop="radioClick(row, index)"
/>
<el-radio class="low-select-radio" v-model="radioValue" :label="row[tableOption.rowKey || 'id']"
:disabled="!tableOption.selectable(row, index)" @click.stop="radioClick(row, index)" />
</template>
<!-- 自定义表头 -->
<template v-for="prop in inlineSearch" :key="prop" #[`${prop}-header`]="{ column }">
<InlineSearch
v-model="tableSearch[prop]"
:prop="prop"
:column="column"
:crudRef="crudRef"
@execute-search="searchChange"
></InlineSearch>
<InlineSearch v-model="tableSearch[prop]" :prop="prop" :column="column" :crudRef="crudRef"
@execute-search="searchChange"></InlineSearch>
</template>
<!-- 自定义表单 -->
<template v-for="c in slotData.form" :key="c.prop" #[`${c.prop}-form`]="scope">
<!-- <div>{{scope}}</div> -->
<AvueSlot
slotType="form"
:scope="scope"
:control="c"
v-model="tableForm[c.prop]"
></AvueSlot>
<AvueSlot slotType="form" :scope="scope" :control="c" v-model="tableForm[c.prop]"></AvueSlot>
</template>
<!-- 自定义搜索 -->
<template v-for="c in slotData.search" :key="c.prop" #[`${c.prop}-search`]="scope">
<AvueSlot
slotType="search"
:scope="scope"
:control="c"
v-model="tableSearch[c.prop]"
></AvueSlot>
<AvueSlot slotType="search" :scope="scope" :control="c" v-model="tableSearch[c.prop]"></AvueSlot>
</template>
<!-- 自定义列 -->
<template v-for="c in slotData.list" :key="c.prop" #[c.prop]="scope">
@@ -197,36 +98,18 @@
</template>
<!-- 自定义附表表单 -->
<template #lowCustomSubTable-form="{ type, disabled, column }">
<avue-tabs
ref="subTabsRef"
:option="column.tabsOption"
@change="(tab) => (subTabsValue = tab)"
></avue-tabs>
<avue-tabs ref="subTabsRef" :option="column.tabsOption" @change="(tab) => (subTabsValue = tab)"></avue-tabs>
<template v-for="sub in column.tabsOption.column" :key="sub.prop">
<template v-if="sub.subType == 'many'">
<SubTable
:ref="(el) => (subTableRef[sub.prop] = el)"
v-model="tableForm[sub.prop]"
v-show="sub.prop == subTabsValue.prop"
:prop="sub.prop"
:tableId="sub.tableId"
:optionData="subTableObj[sub.tableId]"
:type="type"
:disabled="disabled"
@execute-custom-btn-enhance="executeCustomBtnEnhance"
></SubTable>
<SubTable :ref="(el) => (subTableRef[sub.prop] = el)" v-model="tableForm[sub.prop]"
v-show="sub.prop == subTabsValue.prop" :prop="sub.prop" :tableId="sub.tableId"
:optionData="subTableObj[sub.tableId]" :type="type" :disabled="disabled"
@execute-custom-btn-enhance="executeCustomBtnEnhance"></SubTable>
</template>
<template v-if="sub.subType == 'one'">
<SubForm
:ref="(el) => (subFormRef[sub.prop] = el)"
v-model="tableForm[sub.prop]"
v-show="sub.prop == subTabsValue.prop"
:prop="sub.prop"
:tableId="sub.tableId"
:optionData="subTableObj[sub.tableId]"
:type="type"
:disabled="disabled"
></SubForm>
<SubForm :ref="(el) => (subFormRef[sub.prop] = el)" v-model="tableForm[sub.prop]"
v-show="sub.prop == subTabsValue.prop" :prop="sub.prop" :tableId="sub.tableId"
:optionData="subTableObj[sub.tableId]" :type="type" :disabled="disabled"></SubForm>
</template>
</template>
</template>
@@ -234,18 +117,11 @@
<!-- 主附表内嵌 -->
<template v-if="tableInfo.subTemplate == 'innerTable'">
<div class="p-20px pt-0 pb-10px" v-if="tableOption.expandRowKeys.includes(row.id)">
<avue-tabs
:option="innerTabsOption"
@change="(tab) => (innerTabsValue = tab)"
></avue-tabs>
<avue-tabs :option="innerTabsOption" @change="(tab) => (innerTabsValue = tab)"></avue-tabs>
<template v-for="sub in innerTabsOption.column" :key="sub.prop">
<div class="w-100%" v-show="sub.prop == innerTabsValue.prop">
<LowTable
:ref="(el) => (innerTableRef[sub.prop] = el)"
:tableId="sub.tableId"
v-bind="sub.vBind"
:fixedSearch="{ ...(innerSubSearch[row.id]?.[sub.prop] || {}) }"
></LowTable>
<LowTable :ref="(el) => (innerTableRef[sub.prop] = el)" :tableId="sub.tableId" v-bind="sub.vBind"
:fixedSearch="{ ...(innerSubSearch[row.id]?.[sub.prop] || {}) }"></LowTable>
</div>
</template>
</div>
@@ -254,11 +130,7 @@
<template v-if="tableInfo.singleStyle == 'expand'">
<div class="expand-table-box px-20px py-10px">
<el-row>
<el-form-item
v-for="prop in expandProp"
:key="prop"
:label="tableOption.column[prop].label + ''"
>
<el-form-item v-for="prop in expandProp" :key="prop" :label="tableOption.column[prop].label + ''">
{{ row[`$${prop}`] || row[prop] }}
</el-form-item>
</el-row>
@@ -271,11 +143,8 @@
<avue-tabs :option="erpTabsOption" @change="(tab) => (subTabsValue = tab)"></avue-tabs>
<template v-for="sub in erpTabsOption.column" :key="sub.prop">
<div class="w-100%" v-show="sub.prop == subTabsValue.prop">
<LowTable
:ref="(el) => (erpTableRef[sub.prop] = el)"
:tableId="sub.tableId"
v-bind="sub.vBind"
></LowTable>
<LowTable :ref="(el) => (erpTableRef[sub.prop] = el)" :tableId="sub.tableId" v-bind="sub.vBind">
</LowTable>
</div>
</template>
</div>
@@ -284,67 +153,32 @@
</div>
</div>
<!-- 代码编辑器 -->
<DesignPopup
v-if="popShowObj.mEditor"
v-model="MEDialog.value"
v-bind="MEDialog.params"
:isBodyFull="true"
>
<DesignPopup v-if="popShowObj.mEditor" v-model="MEDialog.value" v-bind="MEDialog.params" :isBodyFull="true">
<template #default>
<MonacoEditor v-model="MEData.value" v-bind="MEData.params"></MonacoEditor>
</template>
</DesignPopup>
<!-- 导入 -->
<DesignPopup
v-if="popShowObj.import"
v-model="importDialog"
:title="t('Avue.crud.importTitle')"
width="80%"
:dialog-params="{ alignCenter: true }"
:handleClose="importRef?.handleClose"
>
<DesignPopup v-if="popShowObj.import" v-model="importDialog" :title="t('Avue.crud.importTitle')" width="80%"
:dialog-params="{ alignCenter: true }" :handleClose="importRef?.handleClose">
<template #default="{ isFull }">
<ImportData
ref="importRef"
:importId="tableId"
:columns="tableOption.column"
:tableDescribe="tableInfo.tableDescribe"
:show="importDialog"
:isFull="isFull"
@close-popup="importDialog = false"
@reset-change="resetData"
></ImportData>
<ImportData ref="importRef" :importId="tableId" :columns="tableOption.column"
:tableDescribe="tableInfo.tableDescribe" :show="importDialog" :isFull="isFull"
@close-popup="importDialog = false" @reset-change="resetData"></ImportData>
</template>
</DesignPopup>
<!-- 自定义表单 -->
<DesignPopup
v-if="popShowObj.form"
v-model="customForm.open"
:controlType="tableOption.dialogType || 'dialog'"
:title="tableOption[`${customForm.formType}Title`] || customForm.title"
:width="tableOption.dialogWidth || '60%'"
:footer-btn="customForm.footerBtn"
:fullscreen = "true"
>
<LowForm
v-if="customForm.open"
ref="customFormRef"
:formType="customForm.formType"
:formOption="customForm.formOption"
:defaultData="customForm.defaultData"
:formId="tableInfo.formId"
handleType="returnData"
:beforeClose="customFormClose"
></LowForm>
<DesignPopup v-if="popShowObj.form" v-model="customForm.open" :controlType="tableOption.dialogType || 'dialog'"
:title="tableOption[`${customForm.formType}Title`] || customForm.title" :width="tableOption.dialogWidth || '60%'"
:footer-btn="customForm.footerBtn" :fullscreen="true">
<LowForm v-if="customForm.open" ref="customFormRef" :formType="customForm.formType"
:formOption="customForm.formOption" :defaultData="customForm.defaultData" :formId="tableInfo.formId"
handleType="returnData" :beforeClose="customFormClose"></LowForm>
</DesignPopup>
<!-- 增强注册的控件 -->
<template v-for="item in rendControlData" :key="item.key">
<component
:ref="(el) => (componentRef[item.key] = el)"
:is="componentObj[item.random]"
v-bind="item.params || {}"
v-model="item.show"
></component>
<component :ref="(el) => (componentRef[item.key] = el)" :is="componentObj[item.random]" v-bind="item.params || {}"
v-model="item.show"></component>
</template>
</template>
<script lang="ts" setup>
@@ -377,6 +211,8 @@ import * as Vue from 'vue'
import { useAppStore } from '@/store/modules/app'
import { Value } from 'sass'
import { object } from 'vue-types'
import ColumnConfigDialog from './components/ColumnConfigDialog.vue'
import { saveColumnConfig, getColumnConfig, type ColumnConfig } from '@/utils/indexedDB'
defineOptions({ name: 'LowTable' })
let funcInteface = (value1, value2) => { }
@@ -642,6 +478,10 @@ const crudRef = ref()
const componentRef = ref({})
const summaryTopRef = ref()
// 列配置相关
const showColumnConfigDrawer = ref(false)
const fieldListRef = ref<any[]>([]) // 保存 fieldList 的引用,用于修改 webEntity.isShowColumn
const { uploadBefore, uploadExceed, uploadSized, uploadPreview } = useAvueUpload(jsEnhanceObj)
const isShowFixedBar = computed(() => {
@@ -732,7 +572,7 @@ const fixed_bar_left = computed(() => {
const initTable = async () => {
isInit.value = false
loading.value = true
let data = {}
let data: any = {}
if (props.model == 'default') {
data = await TableApi.getWebConfig(props.tableId)
} else if (props.model == 'erpTable' || props.model == 'innerTable') {
@@ -740,6 +580,15 @@ const initTable = async () => {
} else if (isDicTable.value) {
data = await getDicTableConfig(props.tableId, props.dicConfigStr)
}
// 保存 fieldList 的引用,用于列配置时修改 webEntity.isShowColumn
if (props.model === 'default' && data.fieldList) {
fieldListRef.value = data.fieldList
// 在初始化 tableOption 之前,先从 IndexedDB 加载列配置并应用到 fieldList
await loadColumnConfigToFieldList()
}
const optionData = initTableOption(data, {
tableId: props.tableId,
calcHeight: props.calcHeight,
@@ -855,10 +704,24 @@ const initTable = async () => {
initInlineSearch()
initExpandTable()
isInit.value = true
// 从 IndexedDB 加载列配置并应用到 crudRef此时 tableOption 已初始化,需要同步到 crudRef
if (props.model === 'default') {
await loadColumnConfigToCrudRef()
}
if (optionData.tableInfo.isGetData || isDicTable.value) {
getTableData(true, { type: 'init', isGetSummary: true })
} else loading.value = false
initTableLayout()
// 初始化完成后,尝试替换列显隐按钮
if (props.model === 'default') {
await nextTick()
setTimeout(() => {
replaceColumnButton()
}, 300)
}
if (optionData.tableInfo.subTable?.length) {
optionData.tableInfo.subTable.forEach(
(id, index) => (subTableObj.value[id] = { tableId: id, index })
@@ -1532,6 +1395,271 @@ const refreshChange = () => {
getTableData(true, { isGetSummary: true })
}
// 替换列显隐按钮
let buttonReplaced = false
let observerInstance: MutationObserver | null = null
const replaceColumnButton = () => {
if (buttonReplaced || props.model !== 'default') return
const tryReplace = () => {
const originalButton = document.querySelector('.avue-crud__columnBtn:not([data-custom-replaced])')
if (originalButton && !buttonReplaced) {
buttonReplaced = true
// 创建新按钮
const newButton = originalButton.cloneNode(true) as HTMLElement
newButton.setAttribute('data-custom-replaced', 'true')
// 清除原按钮的所有事件监听器
const newButtonClone = newButton.cloneNode(true) as HTMLElement
newButtonClone.setAttribute('data-custom-replaced', 'true')
// 添加新的事件监听器
newButtonClone.addEventListener('click', (e) => {
debugger
e.preventDefault()
e.stopPropagation()
showColumnConfigDrawer.value = true
})
// 替换原按钮
if (originalButton.parentNode) {
originalButton.parentNode.replaceChild(newButtonClone, originalButton)
}
// 停止观察
if (observerInstance) {
observerInstance.disconnect()
observerInstance = null
}
}
}
// 立即尝试一次
tryReplace()
// 如果还没替换成功,使用观察者
if (!buttonReplaced) {
observerInstance = new MutationObserver(() => {
if (!buttonReplaced) {
tryReplace()
}
})
// 只观察 crudRef 所在的区域,避免观察整个 body
const crudElement = crudRef.value?.$el
if (crudElement) {
observerInstance.observe(crudElement, {
childList: true,
subtree: true
})
} else {
// 如果 crudRef 还没准备好,观察 body 但限制范围
observerInstance.observe(document.body, {
childList: true,
subtree: true
})
}
// 设置超时,避免无限观察
setTimeout(() => {
if (observerInstance) {
observerInstance.disconnect()
observerInstance = null
}
}, 10000) // 10秒后停止观察
}
}
// 列配置确认处理
const handleColumnConfigConfirm = async (config: Record<string, ColumnConfig>) => {
if (props.model !== 'default') return
// 修改 fieldList 中的 webEntity.isShowColumn
if (fieldListRef.value && fieldListRef.value.length > 0) {
fieldListRef.value.forEach((field: any) => {
if (field.webEntity && field.fieldCode && config[field.fieldCode] !== undefined) {
const colConfig = config[field.fieldCode]
// hide === true 表示 checkbox 选中,对应 isShowColumn = 'N'
// hide === false 表示 checkbox 不选中,对应 isShowColumn = 'Y'
if (colConfig.hide !== undefined) {
field.webEntity.isShowColumn = colConfig.hide ? 'N' : 'Y'
}
}
})
}
// 应用列配置到 tableOption
Object.keys(config).forEach(prop => {
if (tableOption.value.column[prop]) {
const colConfig = config[prop]
if (colConfig.hide !== undefined) {
tableOption.value.column[prop].hide = colConfig.hide
}
if (colConfig.fixed !== undefined) {
tableOption.value.column[prop].fixed = colConfig.fixed
}
if (colConfig.filterable !== undefined) {
tableOption.value.column[prop].filterable = colConfig.filterable
}
if (colConfig.sortable !== undefined) {
tableOption.value.column[prop].sortable = colConfig.sortable
}
}
})
// 同步到 crudRef
await nextTick()
if (crudRef.value && crudRef.value.columnOption) {
crudRef.value.columnOption.forEach((column: any) => {
if (config[column.prop]) {
const colConfig = config[column.prop]
if (colConfig.hide !== undefined) {
column.hide = colConfig.hide
}
if (colConfig.fixed !== undefined) {
column.fixed = colConfig.fixed
}
if (colConfig.filterable !== undefined) {
column.filterable = colConfig.filterable
}
if (colConfig.sortable !== undefined) {
column.sortable = colConfig.sortable
}
}
})
// 强制更新表格布局
if (crudRef.value.$refs && crudRef.value.$refs.table) {
crudRef.value.$refs.table.doLayout()
}
}
// 保存到 IndexedDB保存 isShowColumn 的值)
const pageId = (route.params.id as string) || props.tableId
const saveConfig: Record<string, ColumnConfig> = {}
Object.keys(config).forEach(prop => {
saveConfig[prop] = {
...config[prop],
isShowColumn: config[prop].hide ? 'N' : 'Y' // hide=true checkbox选中→'N'hide=false checkbox不选中→'Y'
}
})
await saveColumnConfig(pageId, saveConfig)
}
// 从 IndexedDB 加载列配置并应用到 fieldList在 initTableOption 之前调用)
const loadColumnConfigToFieldList = async () => {
if (props.model !== 'default' || !fieldListRef.value || fieldListRef.value.length === 0) return
try {
const pageId = (route.params.id as string) || props.tableId
const savedConfig = await getColumnConfig(pageId)
debugger
if (savedConfig) {
// 修改 fieldList 中的 webEntity.isShowColumn
fieldListRef.value.forEach((field: any) => {
debugger
if (field.webEntity && field.fieldCode && savedConfig[field.fieldCode] !== undefined) {
const colConfig = savedConfig[field.fieldCode]
// 优先使用 isShowColumn如果没有则使用 hide 转换
if (colConfig.isShowColumn !== undefined) {
field.webEntity.isShowColumn = colConfig.isShowColumn
} else if (colConfig.hide !== undefined) {
// hide=true checkbox选中→'N'hide=false checkbox不选中→'Y'
field.webEntity.isShowColumn = colConfig.hide ? 'N' : 'Y'
}
}
})
}
} catch (error) {
console.error('加载列配置到 fieldList 失败:', error)
}
}
// 从 IndexedDB 加载列配置并应用到 crudRef在 tableOption 初始化之后调用)
const loadColumnConfigToCrudRef = async () => {
if (props.model !== 'default' || !tableOption.value.column) return
try {
const pageId = (route.params.id as string) || props.tableId
const savedConfig = await getColumnConfig(pageId)
if (savedConfig && tableOption.value.column) {
// 应用配置到 tableOption.column
Object.keys(savedConfig).forEach(prop => {
if (tableOption.value.column[prop]) {
const colConfig = savedConfig[prop]
// 优先使用 isShowColumn 转换,如果没有则使用 hide
// isShowColumn='Y' 表示 checkbox 未选中,所以 hide=false
// isShowColumn='N' 表示 checkbox 选中,所以 hide=true
if (colConfig.isShowColumn !== undefined) {
tableOption.value.column[prop].hide = colConfig.isShowColumn === 'N'
} else if (colConfig.hide !== undefined) {
tableOption.value.column[prop].hide = colConfig.hide
}
if (colConfig.fixed !== undefined) {
tableOption.value.column[prop].fixed = colConfig.fixed
}
if (colConfig.filterable !== undefined) {
tableOption.value.column[prop].filterable = colConfig.filterable
}
if (colConfig.sortable !== undefined) {
tableOption.value.column[prop].sortable = colConfig.sortable
}
}
})
// 等待 DOM 更新后同步到 crudRef
await nextTick()
await nextTick()
// 使用重试机制确保 crudRef 准备好
let retryCount = 0
const maxRetries = 10
const tryApplyToCrudRef = () => {
if (crudRef.value && crudRef.value.columnOption) {
crudRef.value.columnOption.forEach((column: any) => {
if (savedConfig[column.prop]) {
const colConfig = savedConfig[column.prop]
// isShowColumn='Y' 表示 checkbox 未选中,所以 hide=false
// isShowColumn='N' 表示 checkbox 选中,所以 hide=true
if (colConfig.isShowColumn !== undefined) {
column.hide = colConfig.isShowColumn === 'N'
} else if (colConfig.hide !== undefined) {
column.hide = colConfig.hide
}
if (colConfig.fixed !== undefined) {
column.fixed = colConfig.fixed
}
if (colConfig.filterable !== undefined) {
column.filterable = colConfig.filterable
}
if (colConfig.sortable !== undefined) {
column.sortable = colConfig.sortable
}
}
})
if (crudRef.value.$refs && crudRef.value.$refs.table) {
crudRef.value.$refs.table.doLayout()
}
return true
}
return false
}
while (retryCount < maxRetries && !tryApplyToCrudRef()) {
retryCount++
await new Promise(resolve => setTimeout(resolve, 100))
}
}
} catch (error) {
console.error('加载列配置到 crudRef 失败:', error)
}
}
const subDataFormatting = (data, type) => {
if (tabsColumn.value) {
tabsColumn.value.forEach((item) => {
@@ -2137,6 +2265,11 @@ const beforeUnload = (event) => {
onMounted(async () => {
window.addEventListener('beforeunload', beforeUnload)
// 监听并替换列显隐按钮
if (props.model === 'default') {
replaceColumnButton()
}
})
onBeforeUnmount(() => {
@@ -2181,15 +2314,7 @@ defineExpose({
max-width: calc(100% - 210px);
}
&.show_fixed_bar
> div
> ::v-deep(.avue-crud)
> .avue-crud__body
> .el-card__body
> .el-form
> div
> .el-table__inner-wrapper
> .el-table__body-wrapper {
&.show_fixed_bar>div> ::v-deep(.avue-crud)>.avue-crud__body>.el-card__body>.el-form>div>.el-table__inner-wrapper>.el-table__body-wrapper {
.el-scrollbar__bar.is-horizontal {
position: fixed;
bottom: 0;
@@ -2304,6 +2429,7 @@ defineExpose({
.el-form--default {
.el-table--border {
&::before,
&::after {
width: 0;

243
src/utils/indexedDB.ts Normal file
View File

@@ -0,0 +1,243 @@
/**
* IndexedDB 工具类
* 用于存储和读取表格列配置(显隐/冻结/过滤/排序)
*/
const DB_NAME = 'lc_frontend_db'
const DB_VERSION = 1
const STORE_NAME = 'table_column_config'
let dbInstance: IDBDatabase | null = null
/**
* 打开数据库并确保对象存储存在
*/
const openDBWithStore = (version: number): Promise<IDBDatabase> => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, version)
request.onerror = () => {
reject(request.error)
}
request.onsuccess = () => {
const db = request.result
// 检查对象存储是否存在
if (!db.objectStoreNames.contains(STORE_NAME)) {
// 对象存储不存在,需要升级数据库
db.close()
// 用更高版本打开以触发升级
const upgradeRequest = indexedDB.open(DB_NAME, db.version + 1)
upgradeRequest.onerror = () => {
reject(upgradeRequest.error)
}
upgradeRequest.onupgradeneeded = (event) => {
const upgradeDb = (event.target as IDBOpenDBRequest).result
// 如果对象存储不存在,创建它
if (!upgradeDb.objectStoreNames.contains(STORE_NAME)) {
upgradeDb.createObjectStore(STORE_NAME, { keyPath: 'key' })
}
}
upgradeRequest.onsuccess = () => {
resolve(upgradeRequest.result)
}
} else {
resolve(db)
}
}
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: 'key' })
}
}
})
}
/**
* 初始化 IndexedDB
*/
const initDB = (): Promise<IDBDatabase> => {
return new Promise((resolve, reject) => {
if (dbInstance) {
// 检查对象存储是否存在
if (dbInstance.objectStoreNames.contains(STORE_NAME)) {
resolve(dbInstance)
return
} else {
// 对象存储不存在,需要重新初始化
dbInstance.close()
dbInstance = null
}
}
// 先尝试获取当前数据库版本(不指定版本号打开)
const checkRequest = indexedDB.open(DB_NAME)
checkRequest.onsuccess = () => {
const checkDb = checkRequest.result
const currentVersion = checkDb.version
checkDb.close()
// 使用当前版本或更高版本打开
const targetVersion = Math.max(currentVersion, DB_VERSION)
openDBWithStore(targetVersion)
.then((db) => {
dbInstance = db
resolve(dbInstance)
})
.catch(reject)
}
checkRequest.onerror = () => {
// 如果检查失败,直接使用默认版本尝试打开
openDBWithStore(DB_VERSION)
.then((db) => {
dbInstance = db
resolve(dbInstance)
})
.catch(reject)
}
})
}
/**
* 列配置接口
*/
export interface ColumnConfig {
hide?: boolean
fixed?: string | boolean
filterable?: boolean
sortable?: string | boolean
isShowColumn?: string // 'Y' 表示显示,'N' 表示隐藏
}
/**
* 保存列配置
* @param key 唯一标识(通常是 route.params.id
* @param config 列配置对象,格式:{ prop: { hide, fixed, filterable, sortable } }
*/
export const saveColumnConfig = async (key: string, config: Record<string, ColumnConfig>): Promise<void> => {
try {
const db = await initDB()
const transaction = db.transaction([STORE_NAME], 'readwrite')
const store = transaction.objectStore(STORE_NAME)
await new Promise<void>((resolve, reject) => {
const request = store.put({ key, config, updateTime: Date.now() })
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
} catch (error) {
console.error('保存列配置失败:', error)
}
}
/**
* 读取列配置
* @param key 唯一标识(通常是 route.params.id
* @returns 列配置对象,格式:{ prop: { hide, fixed, filterable, sortable } }
*/
export const getColumnConfig = async (key: string): Promise<Record<string, ColumnConfig> | null> => {
try {
const db = await initDB()
const transaction = db.transaction([STORE_NAME], 'readonly')
const store = transaction.objectStore(STORE_NAME)
return new Promise<Record<string, ColumnConfig> | null>((resolve, reject) => {
const request = store.get(key)
request.onsuccess = () => {
const result = request.result
resolve(result ? result.config : null)
}
request.onerror = () => reject(request.error)
})
} catch (error) {
console.error('读取列配置失败:', error)
return null
}
}
/**
* 删除列配置
* @param key 唯一标识(通常是 route.params.id
*/
export const deleteColumnConfig = async (key: string): Promise<void> => {
try {
const db = await initDB()
const transaction = db.transaction([STORE_NAME], 'readwrite')
const store = transaction.objectStore(STORE_NAME)
await new Promise<void>((resolve, reject) => {
const request = store.delete(key)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
} catch (error) {
console.error('删除列配置失败:', error)
}
}
/**
* 保存搜索区域显示状态
* @param key 唯一标识(通常是 route.params.id
* @param visible 是否显示
*/
export const saveSearchVisible = async (key: string, visible: boolean): Promise<void> => {
try {
const db = await initDB()
const transaction = db.transaction([STORE_NAME], 'readwrite')
const store = transaction.objectStore(STORE_NAME)
const data = await new Promise<any>((resolve, reject) => {
const request = store.get(key)
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
const config = data?.config || {}
await new Promise<void>((resolve, reject) => {
const request = store.put({
key,
config,
searchVisible: visible,
updateTime: Date.now()
})
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
} catch (error) {
console.error('保存搜索区域显示状态失败:', error)
}
}
/**
* 读取搜索区域显示状态
* @param key 唯一标识(通常是 route.params.id
* @returns 是否显示
*/
export const getSearchVisible = async (key: string): Promise<boolean | null> => {
try {
const db = await initDB()
const transaction = db.transaction([STORE_NAME], 'readonly')
const store = transaction.objectStore(STORE_NAME)
return new Promise<boolean | null>((resolve, reject) => {
const request = store.get(key)
request.onsuccess = () => {
const result = request.result
resolve(result && result.searchVisible !== undefined ? result.searchVisible : null)
}
request.onerror = () => reject(request.error)
})
} catch (error) {
console.error('读取搜索区域显示状态失败:', error)
return null
}
}