This commit is contained in:
caijun
2026-01-27 10:14:30 +08:00
4 changed files with 973 additions and 403 deletions

View File

@@ -0,0 +1,179 @@
<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
:model-value="row.sortable === 'custom'"
@change="(val) => { row.sortable = val ? 'custom' : false; handleConfigChange() }"
/>
</template>
</el-table-column>
<!-- 导出 -->
<el-table-column label="导出" width="80" align="center">
<template #default="{ row }">
<el-checkbox
:model-value="row.isExport === 'Y'"
@change="(val) => { row.isExport = val ? 'Y' : 'N'; 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
sortable: string | boolean
showColumn: boolean
sortNum?: number
isExport?: string // 'Y' 表示导出,'N' 表示不导出
}
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)
.map(key => {
const column = props.columns[key]
return {
prop: column.prop || key,
label: column.label || key,
hide: column.hide || false,
fixed: column.fixed || false,
sortable: column.sortable || false,
showColumn: column.showColumn !== false,
sortNum: column.sortNum,
// 默认全部勾选导出,如果已有配置则使用配置值
isExport: column.isExport !== undefined ? column.isExport : 'Y'
}
})
.sort((a, b) => {
// 优先按 sortNum 排序(从小到大)
if (a.sortNum !== undefined && b.sortNum !== undefined) {
return a.sortNum - b.sortNum
}
if (a.sortNum !== undefined) return -1
if (b.sortNum !== undefined) return 1
// 如果都没有 sortNum则按 label 排序
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,
sortable: item.sortable,
isExport: item.isExport || 'Y' // 默认导出
}
})
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> <template>
<div <div class="low-table relative" :class="[
class="low-table relative"
:class="[
`low-table__${tableId}`, `low-table__${tableId}`,
{ {
summary: tableOption.showSummary, summary: tableOption.showSummary,
'low-table-grid': tableOption.grid, 'low-table-grid': tableOption.grid,
'no-menu': !tableOption.menu 'no-menu': !tableOption.menu
} }
]" ]">
>
<span prop="delegateUserId" style="display:none"> <span prop="delegateUserId" style="display:none">
<userSelect <userSelect id="costomUserSelect" v-model="formData" v-bind="userVBind" class="w-100%"></userSelect>
id="costomUserSelect"
v-model="formData"
v-bind="userVBind"
class="w-100%"
></userSelect>
</span> </span>
<span prop="delegateDictId" style="display:none"> <span prop="delegateDictId" style="display:none">
<DicTableSelect <DicTableSelect id="costomDictSelect" :column="distSelectColumn" size="default" type="add" prop="fields_7897245"
id="costomDictSelect" :scope="dictSelectScope" @set-form-data="handleSetFormData"></DicTableSelect>
:column="distSelectColumn"
size="default"
type="add"
prop="fields_7897245"
:scope="dictSelectScope"
@set-form-data="handleSetFormData"
></DicTableSelect>
</span> </span>
<!-- 顶部统计 --> <!-- 顶部统计 -->
<div <div v-if="summaryTop.show" class="low-table-summary absolute left-0 top-0 w-100% h-auto z-999"
v-if="summaryTop.show" v-hasResize="onSummaryTopResize">
class="low-table-summary absolute left-0 top-0 w-100% h-auto z-999" <SummaryTop ref="summaryTopRef" v-if="tableSummary.topList?.length" :summaryList="tableSummary.topList">
v-hasResize="onSummaryTopResize" </SummaryTop>
>
<SummaryTop
ref="summaryTopRef"
v-if="tableSummary.topList?.length"
:summaryList="tableSummary.topList"
></SummaryTop>
</div> </div>
<div <div class="low-table-content gap-x-10px w-100%" :class="{
class="low-table-content gap-x-10px w-100%"
:class="{
show_fixed_bar: isShowFixedBar, show_fixed_bar: isShowFixedBar,
[`low-table-grid__${tableInfo.singleCardSpan}`]: tableOption.grid [`low-table-grid__${tableInfo.singleCardSpan}`]: tableOption.grid
}" }" :style="{ paddingTop: summaryTop.height + 'px' }">
:style="{ paddingTop: summaryTop.height + 'px' }"
>
<!-- 左树右表树表 --> <!-- 左树右表树表 -->
<div <div class="left-tree-box flex-basis-200px flex-shrink-0" v-if="tableInfo.tableType == 'treeAround'">
class="left-tree-box flex-basis-200px flex-shrink-0" <avue-tree ref="treeRef" :option="treeAroundOption" :data="treeAroundData" @node-click="treeAroundNodeClick">
v-if="tableInfo.tableType == 'treeAround'"
>
<avue-tree
ref="treeRef"
:option="treeAroundOption"
:data="treeAroundData"
@node-click="treeAroundNodeClick"
>
<template #default="{ data }"> <template #default="{ data }">
<span <span class="el-tree-node__label" :class="{
class="el-tree-node__label"
:class="{
active: active:
data[treeAroundOption.props.value] == treeAroundRow[treeAroundOption.props.value] data[treeAroundOption.props.value] == treeAroundRow[treeAroundOption.props.value]
}" }">
>
{{ data[treeAroundOption.props.label] }} {{ data[treeAroundOption.props.label] }}
</span> </span>
</template> </template>
</avue-tree> </avue-tree>
</div> </div>
<div <!-- 列配置抽屉 -->
class="flex-1 w-100%" <ColumnConfigDialog v-if="props.model === 'default'" v-model="showColumnConfigDrawer"
:class="{ 'table-content': tableInfo.tableType == 'treeAround' }" :columns="tableOption.column" @confirm="handleColumnConfigConfirm" />
v-if="isInit" <div class="flex-1 w-100%" :class="{ 'table-content': tableInfo.tableType == 'treeAround' }" v-if="isInit">
>
<!-- 主体表格 --> <!-- 主体表格 -->
<avue-crud <avue-crud ref="crudRef" v-model="tableForm" v-model:search="tableSearch" v-bind="crudBind"
ref="crudRef" :table-loading="loading" :data="tableData" :option="tableOption" :before-open="beforeOpen"
v-model="tableForm" :before-close="beforeClose" :row-style="tableDefaultFun.rowStyle" :cell-style="tableDefaultFun.cellStyle"
v-model:search="tableSearch" :summary-method="tableDefaultFun.summaryMethod" :span-method="tableDefaultFun.spanMethod"
v-bind="crudBind" @search-change="searchChange" @search-reset="resetChange" @row-save="rowSave" @row-update="rowUpdate"
:table-loading="loading" @row-del="rowDel" @refresh-change="refreshChange" @size-change="sizeChange" @current-change="currentChange"
:data="tableData" @selection-change="selectionChange" @sort-change="sortChange" @select-all="selectAll"
:option="tableOption" @row-click="tableDefaultFun.rowClick" @row-dblclick="tableDefaultFun.rowDblclick"
:before-open="beforeOpen" @cell-click="tableDefaultFun.cellClick" @cell-dblclick="tableDefaultFun.cellDblclick" @tree-load="treeLoad"
:before-close="beforeClose" @expand-change="expandChanges" :upload-before="uploadBefore" :upload-exceed="uploadExceed"
:row-style="tableDefaultFun.rowStyle" :upload-sized="uploadSized" :upload-preview="uploadPreview">
: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 }"> <template #menu-left="{ size }">
<TableButton <TableButton v-show="menuLeftShow" type="header" :size="size" :buttonObj="buttonObj"
v-show="menuLeftShow" :selectLength="tableSelect.length" @menu-left-handle="menuLeftHandle"></TableButton>
type="header"
:size="size"
:buttonObj="buttonObj"
:selectLength="tableSelect.length"
@menu-left-handle="menuLeftHandle"
></TableButton>
</template> </template>
<!-- 自定义操作列 --> <!-- 自定义操作列 -->
<template #menu="{ size, row, index }"> <template #menu="{ size, row, index }">
<TableButton <TableButton :type="tableInfo.menuStyle == 'more' ? 'more' : 'menu'" :max-num="tableInfo.maxMenuNum"
:type="tableInfo.menuStyle == 'more' ? 'more' : 'menu'" :size="size" :buttonObj="buttonObj" :row="row" :index="index" @menu-handle="menuHandle"></TableButton>
:max-num="tableInfo.maxMenuNum"
:size="size"
:buttonObj="buttonObj"
:row="row"
:index="index"
@menu-handle="menuHandle"
></TableButton>
</template> </template>
<!-- 自定义多选提示 --> <!-- 自定义多选提示 -->
<template #tip> <template #tip>
<span <span class="inline-block pl-10px c-#999" v-if="model == 'dicTable' && dicMaxLimit" type="danger">
class="inline-block pl-10px c-#999"
v-if="model == 'dicTable' && dicMaxLimit"
type="danger"
>
{{ t('Avue.crud.selectMaxPrepend') }} {{ t('Avue.crud.selectMaxPrepend') }}
{{ dicMaxLimit }} {{ dicMaxLimit }}
{{ t('Avue.crud.selectMaxAppend') }} {{ t('Avue.crud.selectMaxAppend') }}
@@ -154,42 +75,22 @@
</template> </template>
<!-- 单选 --> <!-- 单选 -->
<template #lowSelectRadio="{ row, index }"> <template #lowSelectRadio="{ row, index }">
<el-radio <el-radio class="low-select-radio" v-model="radioValue" :label="row[tableOption.rowKey || 'id']"
class="low-select-radio" :disabled="!tableOption.selectable(row, index)" @click.stop="radioClick(row, index)" />
v-model="radioValue"
:label="row[tableOption.rowKey || 'id']"
:disabled="!tableOption.selectable(row, index)"
@click.stop="radioClick(row, index)"
/>
</template> </template>
<!-- 自定义表头 --> <!-- 自定义表头 -->
<template v-for="prop in inlineSearch" :key="prop" #[`${prop}-header`]="{ column }"> <template v-for="prop in inlineSearch" :key="prop" #[`${prop}-header`]="{ column }">
<InlineSearch <InlineSearch v-model="tableSearch[prop]" :prop="prop" :column="column" :crudRef="crudRef"
v-model="tableSearch[prop]" @execute-search="searchChange"></InlineSearch>
:prop="prop"
:column="column"
:crudRef="crudRef"
@execute-search="searchChange"
></InlineSearch>
</template> </template>
<!-- 自定义表单 --> <!-- 自定义表单 -->
<template v-for="c in slotData.form" :key="c.prop" #[`${c.prop}-form`]="scope"> <template v-for="c in slotData.form" :key="c.prop" #[`${c.prop}-form`]="scope">
<!-- <div>{{scope}}</div> --> <!-- <div>{{scope}}</div> -->
<AvueSlot <AvueSlot slotType="form" :scope="scope" :control="c" v-model="tableForm[c.prop]"></AvueSlot>
slotType="form"
:scope="scope"
:control="c"
v-model="tableForm[c.prop]"
></AvueSlot>
</template> </template>
<!-- 自定义搜索 --> <!-- 自定义搜索 -->
<template v-for="c in slotData.search" :key="c.prop" #[`${c.prop}-search`]="scope"> <template v-for="c in slotData.search" :key="c.prop" #[`${c.prop}-search`]="scope">
<AvueSlot <AvueSlot slotType="search" :scope="scope" :control="c" v-model="tableSearch[c.prop]"></AvueSlot>
slotType="search"
:scope="scope"
:control="c"
v-model="tableSearch[c.prop]"
></AvueSlot>
</template> </template>
<!-- 自定义列 --> <!-- 自定义列 -->
<template v-for="c in slotData.list" :key="c.prop" #[c.prop]="scope"> <template v-for="c in slotData.list" :key="c.prop" #[c.prop]="scope">
@@ -197,36 +98,18 @@
</template> </template>
<!-- 自定义附表表单 --> <!-- 自定义附表表单 -->
<template #lowCustomSubTable-form="{ type, disabled, column }"> <template #lowCustomSubTable-form="{ type, disabled, column }">
<avue-tabs <avue-tabs ref="subTabsRef" :option="column.tabsOption" @change="(tab) => (subTabsValue = tab)"></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-for="sub in column.tabsOption.column" :key="sub.prop">
<template v-if="sub.subType == 'many'"> <template v-if="sub.subType == 'many'">
<SubTable <SubTable :ref="(el) => (subTableRef[sub.prop] = el)" v-model="tableForm[sub.prop]"
:ref="(el) => (subTableRef[sub.prop] = el)" v-show="sub.prop == subTabsValue.prop" :prop="sub.prop" :tableId="sub.tableId"
v-model="tableForm[sub.prop]" :optionData="subTableObj[sub.tableId]" :type="type" :disabled="disabled"
v-show="sub.prop == subTabsValue.prop" @execute-custom-btn-enhance="executeCustomBtnEnhance"></SubTable>
:prop="sub.prop"
:tableId="sub.tableId"
:optionData="subTableObj[sub.tableId]"
:type="type"
:disabled="disabled"
@execute-custom-btn-enhance="executeCustomBtnEnhance"
></SubTable>
</template> </template>
<template v-if="sub.subType == 'one'"> <template v-if="sub.subType == 'one'">
<SubForm <SubForm :ref="(el) => (subFormRef[sub.prop] = el)" v-model="tableForm[sub.prop]"
:ref="(el) => (subFormRef[sub.prop] = el)" v-show="sub.prop == subTabsValue.prop" :prop="sub.prop" :tableId="sub.tableId"
v-model="tableForm[sub.prop]" :optionData="subTableObj[sub.tableId]" :type="type" :disabled="disabled"></SubForm>
v-show="sub.prop == subTabsValue.prop"
:prop="sub.prop"
:tableId="sub.tableId"
:optionData="subTableObj[sub.tableId]"
:type="type"
:disabled="disabled"
></SubForm>
</template> </template>
</template> </template>
</template> </template>
@@ -234,18 +117,11 @@
<!-- 主附表内嵌 --> <!-- 主附表内嵌 -->
<template v-if="tableInfo.subTemplate == 'innerTable'"> <template v-if="tableInfo.subTemplate == 'innerTable'">
<div class="p-20px pt-0 pb-10px" v-if="tableOption.expandRowKeys.includes(row.id)"> <div class="p-20px pt-0 pb-10px" v-if="tableOption.expandRowKeys.includes(row.id)">
<avue-tabs <avue-tabs :option="innerTabsOption" @change="(tab) => (innerTabsValue = tab)"></avue-tabs>
:option="innerTabsOption"
@change="(tab) => (innerTabsValue = tab)"
></avue-tabs>
<template v-for="sub in innerTabsOption.column" :key="sub.prop"> <template v-for="sub in innerTabsOption.column" :key="sub.prop">
<div class="w-100%" v-show="sub.prop == innerTabsValue.prop"> <div class="w-100%" v-show="sub.prop == innerTabsValue.prop">
<LowTable <LowTable :ref="(el) => (innerTableRef[sub.prop] = el)" :tableId="sub.tableId" v-bind="sub.vBind"
:ref="(el) => (innerTableRef[sub.prop] = el)" :fixedSearch="{ ...(innerSubSearch[row.id]?.[sub.prop] || {}) }"></LowTable>
:tableId="sub.tableId"
v-bind="sub.vBind"
:fixedSearch="{ ...(innerSubSearch[row.id]?.[sub.prop] || {}) }"
></LowTable>
</div> </div>
</template> </template>
</div> </div>
@@ -254,11 +130,7 @@
<template v-if="tableInfo.singleStyle == 'expand'"> <template v-if="tableInfo.singleStyle == 'expand'">
<div class="expand-table-box px-20px py-10px"> <div class="expand-table-box px-20px py-10px">
<el-row> <el-row>
<el-form-item <el-form-item v-for="prop in expandProp" :key="prop" :label="tableOption.column[prop].label + ''">
v-for="prop in expandProp"
:key="prop"
:label="tableOption.column[prop].label + ''"
>
{{ row[`$${prop}`] || row[prop] }} {{ row[`$${prop}`] || row[prop] }}
</el-form-item> </el-form-item>
</el-row> </el-row>
@@ -271,11 +143,8 @@
<avue-tabs :option="erpTabsOption" @change="(tab) => (subTabsValue = tab)"></avue-tabs> <avue-tabs :option="erpTabsOption" @change="(tab) => (subTabsValue = tab)"></avue-tabs>
<template v-for="sub in erpTabsOption.column" :key="sub.prop"> <template v-for="sub in erpTabsOption.column" :key="sub.prop">
<div class="w-100%" v-show="sub.prop == subTabsValue.prop"> <div class="w-100%" v-show="sub.prop == subTabsValue.prop">
<LowTable <LowTable :ref="(el) => (erpTableRef[sub.prop] = el)" :tableId="sub.tableId" v-bind="sub.vBind">
:ref="(el) => (erpTableRef[sub.prop] = el)" </LowTable>
:tableId="sub.tableId"
v-bind="sub.vBind"
></LowTable>
</div> </div>
</template> </template>
</div> </div>
@@ -284,67 +153,32 @@
</div> </div>
</div> </div>
<!-- 代码编辑器 --> <!-- 代码编辑器 -->
<DesignPopup <DesignPopup v-if="popShowObj.mEditor" v-model="MEDialog.value" v-bind="MEDialog.params" :isBodyFull="true">
v-if="popShowObj.mEditor"
v-model="MEDialog.value"
v-bind="MEDialog.params"
:isBodyFull="true"
>
<template #default> <template #default>
<MonacoEditor v-model="MEData.value" v-bind="MEData.params"></MonacoEditor> <MonacoEditor v-model="MEData.value" v-bind="MEData.params"></MonacoEditor>
</template> </template>
</DesignPopup> </DesignPopup>
<!-- 导入 --> <!-- 导入 -->
<DesignPopup <DesignPopup v-if="popShowObj.import" v-model="importDialog" :title="t('Avue.crud.importTitle')" width="80%"
v-if="popShowObj.import" :dialog-params="{ alignCenter: true }" :handleClose="importRef?.handleClose">
v-model="importDialog"
:title="t('Avue.crud.importTitle')"
width="80%"
:dialog-params="{ alignCenter: true }"
:handleClose="importRef?.handleClose"
>
<template #default="{ isFull }"> <template #default="{ isFull }">
<ImportData <ImportData ref="importRef" :importId="tableId" :columns="tableOption.column"
ref="importRef" :tableDescribe="tableInfo.tableDescribe" :show="importDialog" :isFull="isFull"
:importId="tableId" @close-popup="importDialog = false" @reset-change="resetData"></ImportData>
:columns="tableOption.column"
:tableDescribe="tableInfo.tableDescribe"
:show="importDialog"
:isFull="isFull"
@close-popup="importDialog = false"
@reset-change="resetData"
></ImportData>
</template> </template>
</DesignPopup> </DesignPopup>
<!-- 自定义表单 --> <!-- 自定义表单 -->
<DesignPopup <DesignPopup v-if="popShowObj.form" v-model="customForm.open" :controlType="tableOption.dialogType || 'dialog'"
v-if="popShowObj.form" :title="tableOption[`${customForm.formType}Title`] || customForm.title" :width="tableOption.dialogWidth || '60%'"
v-model="customForm.open" :footer-btn="customForm.footerBtn" :fullscreen="true">
:controlType="tableOption.dialogType || 'dialog'" <LowForm v-if="customForm.open" ref="customFormRef" :formType="customForm.formType"
:title="tableOption[`${customForm.formType}Title`] || customForm.title" :formOption="customForm.formOption" :defaultData="customForm.defaultData" :formId="tableInfo.formId"
:width="tableOption.dialogWidth || '60%'" handleType="returnData" :beforeClose="customFormClose"></LowForm>
: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> </DesignPopup>
<!-- 增强注册的控件 --> <!-- 增强注册的控件 -->
<template v-for="item in rendControlData" :key="item.key"> <template v-for="item in rendControlData" :key="item.key">
<component <component :ref="(el) => (componentRef[item.key] = el)" :is="componentObj[item.random]" v-bind="item.params || {}"
:ref="(el) => (componentRef[item.key] = el)" v-model="item.show"></component>
:is="componentObj[item.random]"
v-bind="item.params || {}"
v-model="item.show"
></component>
</template> </template>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -377,6 +211,8 @@ import * as Vue from 'vue'
import { useAppStore } from '@/store/modules/app' import { useAppStore } from '@/store/modules/app'
import { Value } from 'sass' import { Value } from 'sass'
import { object } from 'vue-types' import { object } from 'vue-types'
import ColumnConfigDialog from './components/ColumnConfigDialog.vue'
import { saveColumnConfig, getColumnConfig, type ColumnConfig } from '@/utils/indexedDB'
defineOptions({ name: 'LowTable' }) defineOptions({ name: 'LowTable' })
let funcInteface = (value1, value2) => { } let funcInteface = (value1, value2) => { }
@@ -642,6 +478,10 @@ const crudRef = ref()
const componentRef = ref({}) const componentRef = ref({})
const summaryTopRef = ref() const summaryTopRef = ref()
// 列配置相关
const showColumnConfigDrawer = ref(false)
const fieldListRef = ref<any[]>([]) // 保存 fieldList 的引用,用于修改 webEntity.isShowColumn
const { uploadBefore, uploadExceed, uploadSized, uploadPreview } = useAvueUpload(jsEnhanceObj) const { uploadBefore, uploadExceed, uploadSized, uploadPreview } = useAvueUpload(jsEnhanceObj)
const isShowFixedBar = computed(() => { const isShowFixedBar = computed(() => {
@@ -732,7 +572,7 @@ const fixed_bar_left = computed(() => {
const initTable = async () => { const initTable = async () => {
isInit.value = false isInit.value = false
loading.value = true loading.value = true
let data = {} let data: any = {}
if (props.model == 'default') { if (props.model == 'default') {
data = await TableApi.getWebConfig(props.tableId) data = await TableApi.getWebConfig(props.tableId)
} else if (props.model == 'erpTable' || props.model == 'innerTable') { } else if (props.model == 'erpTable' || props.model == 'innerTable') {
@@ -740,6 +580,15 @@ const initTable = async () => {
} else if (isDicTable.value) { } else if (isDicTable.value) {
data = await getDicTableConfig(props.tableId, props.dicConfigStr) 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, { const optionData = initTableOption(data, {
tableId: props.tableId, tableId: props.tableId,
calcHeight: props.calcHeight, calcHeight: props.calcHeight,
@@ -855,10 +704,24 @@ const initTable = async () => {
initInlineSearch() initInlineSearch()
initExpandTable() initExpandTable()
isInit.value = true isInit.value = true
// 从 IndexedDB 加载列配置并应用到 crudRef此时 tableOption 已初始化,需要同步到 crudRef
if (props.model === 'default') {
await loadColumnConfigToCrudRef()
}
if (optionData.tableInfo.isGetData || isDicTable.value) { if (optionData.tableInfo.isGetData || isDicTable.value) {
getTableData(true, { type: 'init', isGetSummary: true }) getTableData(true, { type: 'init', isGetSummary: true })
} else loading.value = false } else loading.value = false
initTableLayout() initTableLayout()
// 初始化完成后,尝试替换列显隐按钮
if (props.model === 'default') {
await nextTick()
setTimeout(() => {
replaceColumnButton()
}, 300)
}
if (optionData.tableInfo.subTable?.length) { if (optionData.tableInfo.subTable?.length) {
optionData.tableInfo.subTable.forEach( optionData.tableInfo.subTable.forEach(
(id, index) => (subTableObj.value[id] = { tableId: id, index }) (id, index) => (subTableObj.value[id] = { tableId: id, index })
@@ -1532,6 +1395,273 @@ const refreshChange = () => {
getTableData(true, { isGetSummary: true }) 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) => {
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.sortable !== undefined) {
tableOption.value.column[prop].sortable = colConfig.sortable
}
if (colConfig.isExport !== undefined) {
tableOption.value.column[prop].isExport = colConfig.isExport
}
}
})
// 同步到 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.sortable !== undefined) {
column.sortable = colConfig.sortable
}
if (colConfig.isExport !== undefined) {
column.isExport = colConfig.isExport
}
}
})
// 强制更新表格布局和配置
await nextTick()
if (crudRef.value && crudRef.value.$refs && crudRef.value.$refs.table) {
// 强制更新表格布局
crudRef.value.$refs.table.doLayout()
// 强制 Vue 重新渲染列配置
await nextTick()
}
}
// 保存到 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'
isExport: config[prop].isExport || '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)
if (savedConfig) {
// 修改 fieldList 中的 webEntity.isShowColumn
fieldListRef.value.forEach((field: any) => {
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.sortable !== undefined) {
tableOption.value.column[prop].sortable = colConfig.sortable
}
if (colConfig.isExport !== undefined) {
tableOption.value.column[prop].isExport = colConfig.isExport
}
}
})
// 等待 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.sortable !== undefined) {
column.sortable = colConfig.sortable
}
if (colConfig.isExport !== undefined) {
column.isExport = colConfig.isExport
}
}
})
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) => { const subDataFormatting = (data, type) => {
if (tabsColumn.value) { if (tabsColumn.value) {
tabsColumn.value.forEach((item) => { tabsColumn.value.forEach((item) => {
@@ -1858,15 +1988,34 @@ const expandChanges = (row, expendList) => {
} }
} }
// 计算需要导出的列字段(英文字段名)
const exportFields = computed(() => {
if (!tableOption.value.column) return []
const fields: string[] = []
Object.keys(tableOption.value.column).forEach(prop => {
const column = tableOption.value.column[prop]
// 如果 isExport 为 'Y' 或者未设置(默认导出),则添加到导出列表
if (column.isExport === 'Y' || (column.isExport === undefined && !column.hide)) {
fields.push(prop)
}
})
return fields
})
const exportTableData = async () => { const exportTableData = async () => {
const exportBtn = buttonObj.value.header.exportBtn const exportBtn = buttonObj.value.header.exportBtn
const isSelect = tableSelect.value.length const isSelect = tableSelect.value.length
await message.confirm(isSelect ? t('Avue.crud.exportTip') : t('Avue.crud.exportAllTip')) await message.confirm(isSelect ? t('Avue.crud.exportTip') : t('Avue.crud.exportAllTip'))
exportBtn.params.loading = true exportBtn.params.loading = true
const searchObj = isSelect const searchObj: any = isSelect
? { jeeLowCode_export_ids: selectIds.value } ? { jeeLowCode_export_ids: selectIds.value }
: await getSearchData('search') : await getSearchData('search')
TableApi.exportExcelData(props.tableId, searchObj) TableApi.exportExcelData(props.tableId, {
...searchObj,
jeeLowCode_export_fields: exportFields.value
})
.then((data) => download.excel(data, tableInfo.value.tableDescribe, 'xlsx')) .then((data) => download.excel(data, tableInfo.value.tableDescribe, 'xlsx'))
.finally(() => (exportBtn.params.loading = false)) .finally(() => (exportBtn.params.loading = false))
} }
@@ -2137,6 +2286,11 @@ const beforeUnload = (event) => {
onMounted(async () => { onMounted(async () => {
window.addEventListener('beforeunload', beforeUnload) window.addEventListener('beforeunload', beforeUnload)
// 监听并替换列显隐按钮
if (props.model === 'default') {
replaceColumnButton()
}
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
@@ -2181,15 +2335,7 @@ defineExpose({
max-width: calc(100% - 210px); max-width: calc(100% - 210px);
} }
&.show_fixed_bar &.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 {
> 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 { .el-scrollbar__bar.is-horizontal {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
@@ -2304,6 +2450,7 @@ defineExpose({
.el-form--default { .el-form--default {
.el-table--border { .el-table--border {
&::before, &::before,
&::after { &::after {
width: 0; width: 0;

View File

@@ -116,7 +116,7 @@ const initColumn = (data, componentData, columnParams) => {
const summaryBottom = {} const summaryBottom = {}
const tableDic = {} const tableDic = {}
data.forEach(item => { data.forEach(item => {
const { dictEntity, webEntity, queryEntity, exportEntity, summaryEntity, fieldCode, fieldName, fieldType, fieldLen, fieldPointLen, fieldDefaultVal } = item const { dictEntity, webEntity, queryEntity, exportEntity, summaryEntity, fieldCode, fieldName, fieldType, fieldLen, fieldPointLen, fieldDefaultVal, sortNum } = item
const { cellWidthType, cellWidth, controlsConfig, verifyConfig, isShowForm, isShowList, isDbSelect, isShowColumn, isShowSort, isRequired } = webEntity const { cellWidthType, cellWidth, controlsConfig, verifyConfig, isShowForm, isShowList, isDbSelect, isShowColumn, isShowSort, isRequired } = webEntity
let controlType = webEntity.controlType || 'input' let controlType = webEntity.controlType || 'input'
const { queryIsWeb, queryMode, queryConfig, queryDefaultVal } = queryEntity const { queryIsWeb, queryMode, queryConfig, queryDefaultVal } = queryEntity
@@ -140,7 +140,8 @@ const initColumn = (data, componentData, columnParams) => {
dataType: ['Integer', 'BigInt', 'BigDecimal'].includes(fieldType) || controlType == 'number' ? 'number' : 'string', dataType: ['Integer', 'BigInt', 'BigDecimal'].includes(fieldType) || controlType == 'number' ? 'number' : 'string',
overHidden: isCardTable ? false : true, overHidden: isCardTable ? false : true,
className: `low-field__${fieldCode} control-${controlType}`, className: `low-field__${fieldCode} control-${controlType}`,
labelClassName: `low-header__${fieldCode}` labelClassName: `low-header__${fieldCode}`,
sortNum: sortNum
} }
//租户字段的列表、表单权限控制 //租户字段的列表、表单权限控制

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
sortable?: string | boolean
isShowColumn?: string // 'Y' 表示显示,'N' 表示隐藏
isExport?: string // 'Y' 表示导出,'N' 表示不导出
}
/**
* 保存列配置
* @param key 唯一标识(通常是 route.params.id
* @param config 列配置对象,格式:{ prop: { hide, fixed, sortable, isExport } }
*/
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, sortable, isExport } }
*/
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
}
}