import request from '@/utils/request'; import _ from 'lodash'; // 宽度由 el-table 自行计算的内置列类型:不给它们补 width,避免改坏默认布局 const BUILTIN_COLUMN_TYPES = ['index', 'selection', 'expand']; export default { data() { return { newColumns: [], tabMixinsInit: true, //进入页面是否默认请求列配置 // computed columns 的重算触发器:applyColumnWidths / handleColumnChange 通过 ++ 它强制重算。 // 必须声明在这里才是响应式的——组件自己的 data 若也声明了会以组件的为准。 // 注意:列宽恢复本身不依赖它(applyColumnWidth 读了响应式的 columnWidthMap), // 它保证的是"必填列被取消勾选后强制显示回来"这条路径能触发重算。 columnsVersion: 0, // 列宽映射(prop/columnKey -> 宽度),用于 computed columns 应用拖拽/缓存后的宽度 columnWidthMap: {}, // 副表列宽映射:{ [scope]: { prop: width } },与主表配置隔离 scopedWidthMaps: {}, // 每页条数(页面可在自身 data 覆盖默认值) pageSize: 10 }; }, created() { // 同步读取本地保存的每页条数,保证 ele-pro-table 首次加载即用正确值 this.initPageSize(); //从服务器获取缓存列表配置 if (this.tabMixinsInit) { this.getTabColumns(); } // 创建防抖函数并绑定this this.debouncedHandleColumnChange = this.debounce( this.handleColumnChangeImpl, 1000 ); // 列宽变化持久化防抖(与服务端列设置共用 saveColumns) this.debouncedSaveColumns = this.debounce(this.saveColumns, 1000); }, methods: { // 实际的列变更处理逻辑 handleColumnChangeImpl() { try { const list = this.getStorage(this.cacheKeyUrl + 'Cols'); if (list) { this.saveColumns(list); } } catch (error) { console.error('处理列配置出错:', error); } }, // 列表变化回调 handleColumnChange() { const list = this.getStorage(this.cacheKeyUrl + 'Cols'); if (list) { let requireList = []; let currentList = this.getColumns() .filter((item) => item.headerSlot) .map((item) => item.prop); list.forEach((item) => { if (currentList.includes(item.prop) && !item.checked) { requireList.push(item.label); item.checked = true; } }); if (requireList.length) { this.setStorage(this.cacheKeyUrl + 'Cols', list); if ('columns' in this.$options.computed) { this.columnsVersion++; } else { this.columns = [...this.columns]; } this.$message.warning( `${requireList.toString()} 为必填项,无法隐藏!` ); return; } } this.debouncedHandleColumnChange(); }, // 获取table-column配置 async getTabColumns() { const res = await this.getByTableId(this.cacheKeyUrl); // 从服务端配置恢复每页条数(优先服务端,同步本地缓存) if (res?.pageSize) { this.pageSize = res.pageSize; this.setStorage(this.cacheKeyUrl + 'PageSize', res.pageSize); } if (res?.columnConfig?.length > 0) { //对比接口返回和本地columns let { nlist, type } = this.columnsContrast(res.columnConfig); // 将nlist中的prop字段去重 const uniqueList = _.uniqBy(nlist, 'prop'); const uniqueLabelList = _.uniqBy(uniqueList, 'label'); //有更新则更新服务缓存配置 if (type) { this.saveColumns(uniqueLabelList); } this.setStorage(this.cacheKeyUrl + 'Cols', nlist); } // 一次性回填:旧版 'Widths' 存储({prop:width})迁移到新 'Cols' 存储 this.migrateLegacyWidthsCache(); // 无论服务端是否返回了列配置,都要从本地缓存恢复列宽: // 1. 服务端首次为空(用户刚调整过列宽,1s 防抖未触发或首次进入)时不能丢 // 2. 多 tab 场景下本地可能有比服务端更新的宽度 this.applyColumnWidthFromCache(); this.applyColumnWidths(); }, // 一次性回填:旧版 'cacheKeyUrl + Widths' 存储格式 {prop: width} // 迁移到新 'cacheKeyUrl + Cols' 存储(数组)。只补不覆盖,回填完清空旧 key。 migrateLegacyWidthsCache() { try { const colsKey = this.cacheKeyUrl + 'Cols'; const widthsKey = this.cacheKeyUrl + 'Widths'; const legacy = this.getStorage(widthsKey); if (!legacy || typeof legacy !== 'object') return; const existingCols = this.getStorage(colsKey) || []; const existingKeys = new Set( existingCols.map((c) => c.columnKey || c.prop || c.id) ); let added = 0; Object.entries(legacy).forEach(([k, w]) => { if (k && w != null && !existingKeys.has(k)) { // 用本地列定义反查真实 label:columnsContrast 按 label 比对, // 写成 prop 名的话这些回填条目会被当成"已删除列"清掉 const col = this.findColumnByKey(k); existingCols.push({ id: k, prop: k, columnKey: k, label: (col && col.label) || k, checked: true, minWidth: w }); added++; } }); if (added > 0) { this.setStorage(colsKey, existingCols); } // 迁移完成(无论有无新增)都清掉旧 key,避免双份数据 this.setStorage(widthsKey, {}); } catch (e) { console.warn('[tabMixins] migrateLegacyWidthsCache 异常:', e); } }, getColumns() { if (typeof this.columns == 'function') { return this.columns(); } else { return this.columns; } }, // 同步读取本地保存的每页条数(localStorage),用于首次加载 initPageSize() { const local = this.getStorage(this.cacheKeyUrl + 'PageSize'); if (local) { this.pageSize = local; } }, // 每页条数变化处理:在 datasource({ limit }) 中调用,变化时保存 handlePageSize(limit) { if (limit && limit !== this.pageSize) { this.pageSize = limit; this.savePageSize(limit); } }, // 保存每页条数到 localStorage + 服务端 table-config(同时带上当前列配置) savePageSize(size) { this.setStorage(this.cacheKeyUrl + 'PageSize', size); // 服务端持久化:tableId + pageSize + columnConfig 三参数齐全 const columnConfig = this.getStorage(this.cacheKeyUrl + 'Cols') || []; this.saveTableConfig({ tableId: this.cacheKeyUrl, pageSize: size, columnConfig }); }, // 根据 prop / columnKey / label 查找本地列(支持 children 递归) findColumnByKey(key, label) { let result = null; const loop = (cols) => { for (const col of cols) { if ( (key && (col.prop === key || col.columnKey === key)) || (label && col.label === label) ) { result = col; return; } if (col.children && col.children.length) loop(col.children); if (result) return; } }; loop(this.getColumns() || []); return result; }, // 表头拖拽列宽变化回调:更新列宽映射并持久化 handleHeaderDragend(newWidth, oldWidth, column) { if (!column || !newWidth) return; const key = column.property || column.columnKey; if (!key) return; // 仅持久化新宽度,不触发 columns 重算。 // el-table 在拖拽过程中已实时更新列宽,此处重算会导致整表重渲染抖动; // 刷新后由 applyColumnWidthFromCache 从缓存恢复,无需拖拽时同步映射。 this.persistColumnWidth(key, newWidth, column.label); }, // 应用列宽并触发表格更新(自动区分 columns 是 computed 还是 data) applyColumnWidths() { try { const isComputed = !!(this._computedWatchers && this._computedWatchers.columns); if (isComputed) { // computed 场景:columns 内部已调用 applyColumnWidth,这里只需触发重算 this.columnsVersion++; } else { // data 场景:直接应用宽度到 data 数组并触发更新 // 防御:columns 可能是 computed 函数、null、undefined,或列定义里写成嵌套结构 // 任何不可迭代形态都直接跳过,避免 "this.columns is not iterable" if (typeof this.columns === 'function') { // 把 columns 当成 computed 走 this.columnsVersion = (this.columnsVersion || 0) + 1; return; } if (!Array.isArray(this.columns)) { console.warn( '[tabMixins] this.columns 不是数组,跳过 applyColumnWidths。当前类型:', typeof this.columns, this.columns ); return; } this.applyColumnWidth(this.columns); this.columns = [...this.columns]; } } catch (e) { // 任何意外都不要让 mixin 把整个页面崩掉 console.warn('[tabMixins] applyColumnWidths 异常:', e); } }, // 应用列宽映射到列数组(供 computed columns 生成列后调用,或 data columns 直接应用) applyColumnWidth(columns) { return this.applyColumnWidthWithMap(columns, this.columnWidthMap || {}); }, // 用指定宽度映射应用列宽(主表与副表共用同一套写入规则) applyColumnWidthWithMap(columns, map) { const keys = Object.keys(map || {}); if (!keys.length || !columns) return columns; const loop = (cols) => { (cols || []).forEach((col) => { const k = col.prop || col.columnKey; if (k && map[k] != null) { // 列定义里 width 和 minWidth 是二选一关系: // - width:固定宽度(拖完会保持用户选的尺寸) // - minWidth:最小宽度(列可继续撑开/收缩) // 拖拽事件给的是"用户实际想要的列宽",要把它写到能控制渲染尺寸的字段: // 列原定义用 width → 覆盖 width;只用 minWidth → 覆盖 minWidth;两者都有 → 都覆盖 const newW = map[k]; if (col.width != null) { this.$set(col, 'width', newW); } if (col.minWidth != null) { this.$set(col, 'minWidth', newW); } // 列定义里既没 width 也没 minWidth:拖拽在 el-table 里实时生效了, // 但恢复时无处落脚 → 补一个 width 承载用户选的尺寸。 // 只对真实数据列(有 prop)生效,且跳过 index/selection/expand 这类 // 由 el-table 自己算宽度的内置列,避免改坏默认布局。 // 注意:只有用户真的拖过这一列(map 里有值)才会走到这里, // 所以没拖过的列布局完全不变。 if ( col.width == null && col.minWidth == null && col.prop && BUILTIN_COLUMN_TYPES.indexOf(col.type) === -1 ) { this.$set(col, 'width', newW); } } if (col.children && col.children.length) loop(col.children); }); }; loop(columns); return columns; }, // 持久化列宽到列设置(localStorage 与 ele-pro-table 共用 cacheKey+'Cols',并同步服务端) // label 必须写列的真实 label:columnsContrast 是按 label 比对的, // 若写成 prop 名,新建的条目会被判成"已删除列"在下一次进入页面时被清掉 persistColumnWidth(key, width, label) { if (!key) return; const storageKey = this.cacheKeyUrl + 'Cols'; const cached = this.getStorage(storageKey) || []; const setting = cached.find((c) => (c.columnKey || c.prop || c.id) === key); if (setting) { setting.minWidth = width; // 旧数据可能是用 key 兜底的 label,这里顺手纠正回真实 label if (label && !setting.label) { setting.label = label; } } else { cached.push({ id: key, prop: key, columnKey: key, label: label || key, checked: true, minWidth: width }); } this.setStorage(storageKey, cached); if (this.debouncedSaveColumns) { this.debouncedSaveColumns(cached); } }, // 从缓存恢复列宽到 columnWidthMap(供 computed columns 应用) applyColumnWidthFromCache() { const cached = this.getStorage(this.cacheKeyUrl + 'Cols') || []; cached.forEach((c) => { const k = c.columnKey || c.prop || c.id; if (k && c.minWidth != null) { this.$set(this.columnWidthMap, k, c.minWidth); } }); }, // ============ 副表(同一组件内的第二/第三张表)列宽 ============ // 一个组件里多张表共用 cacheKeyUrl 会互相污染:副表的列会被主表的 // columnsContrast 当成"已删除列"清掉,导致存了宽度刷新也恢复不了。 // 所以副表用 cacheKeyUrl + '_' + scope 派生独立 key,独立存独立取。 tableColsKey(scope) { return this.cacheKeyUrl + (scope ? '_' + scope : '') + 'Cols'; }, // 读取某张副表的列宽缓存到响应式 map(组件 created 里同步调用即可, // $set 会通知依赖 scopedWidthMaps 的 computed 自动重算,无需手动刷新) loadScopedColumnWidth(scope) { try { const cached = this.getStorage(this.tableColsKey(scope)) || []; const map = {}; cached.forEach((c) => { const k = c.columnKey || c.prop || c.id; if (k && c.minWidth != null) { map[k] = c.minWidth; } }); this.$set(this.scopedWidthMaps, scope, map); } catch (e) { console.warn('[tabMixins] loadScopedColumnWidth 异常:', e); } }, // 副表应用列宽:在副表的 computed columns 里调用 // 用法:subColumns() { return this.applyScopedColumnWidth(this.rawCols, 'sub'); } applyScopedColumnWidth(columns, scope) { return this.applyColumnWidthWithMap( columns, this.scopedWidthMaps[scope] || {} ); }, // 副表拖拽列宽持久化:只写本地,不进服务端 table-config //(副表列不在主表 columnConfig 里,提交上去会被主表当脏数据清掉) persistScopedColumnWidth(scope, key, width) { if (!key || !width) return; try { const storageKey = this.tableColsKey(scope); const cached = this.getStorage(storageKey) || []; const setting = cached.find( (c) => (c.columnKey || c.prop || c.id) === key ); if (setting) { setting.minWidth = width; } else { cached.push({ id: key, prop: key, columnKey: key, label: key, checked: true, minWidth: width }); } this.setStorage(storageKey, cached); } catch (e) { console.warn('[tabMixins] persistScopedColumnWidth 异常:', e); } }, // 副表拖拽回调:在组件 methods 里包一层,透传给 persistScopedColumnWidth // 用法:onSubDragend(w, o, column) { this.persistScopedColumnWidth('sub', column && column.property, w); } onScopedColumnDragend(scope) { return function (newWidth, oldWidth, column) { if (!column) return; this.persistScopedColumnWidth(scope, column.property || column.columnKey, newWidth); }.bind(this); }, //服务器和本地配置columns对比 columnsContrast(list) { const key = 'label'; var updateType = 0; let sList = list.filter((d, i, r) => { return d[key]; }); // 组件可能只有副表、根本没有主表 columns(如用列工厂的弹窗), // 此时 getColumns() 返回 undefined,兜底成空数组避免 .filter 抛错 let devColumns = this.newColumns?.length ? this.newColumns : this.getColumns() || []; let dList = (Array.isArray(devColumns) ? devColumns : []).filter((d, i, r) => { return d[key] && d[key] !== '序号'; }); const keysA = new Set(sList.map((item) => item[key])); const keysB = new Set(dList.map((item) => item[key])); // 本地 比 缓存服务端 多的对象(新增) const added = dList.filter((item) => { return !keysA.has(item[key]) && (item.prop || item.label === '操作'); }); // 本地 比 缓存服务端 少的对象(删除) const removed = sList.filter((item) => !keysB.has(item[key])); const removedPropSet = new Set(removed.map((item) => item[key])); // 删除 缓存中 中被移除的对象 const keptA = list.filter((item) => !removedPropSet.has(item[key])); added.forEach((item) => { //新增columns字段prop参数为必填 if (item.prop) { item.id = item.prop; } else if (item.columnKey) { item.id = item.columnKey; } item.checked = true; }); if (added.length > 0 || removed.length > 0) { updateType = 1; } // 更新项:key 存在但内容变化 const dMap = new Map(dList.map((item) => [item[key], item])); const updated = keptA.map((sItem) => { const dItem = dMap.get(sItem[key]); if (dItem && dItem.prop && sItem.prop !== dItem.prop) { updateType = 1; // 记录旧值和新值 const oldValue = sItem.prop; const newValue = dItem.prop; // 遍历所有属性,动态替换匹配旧值的字段 const updatedItem = { ...sItem }; Object.keys(updatedItem).forEach((k) => { if (updatedItem[k] === oldValue) { updatedItem[k] = newValue; } }); return updatedItem; } return sItem; }); // 合并保留的对象和新增的对象 return { nlist: [...updated, ...added], type: updateType }; }, // 提交columns配置(tableId + pageSize + columnConfig 三参数齐全) async saveColumns(e) { const data = { tableId: this.cacheKeyUrl, pageSize: this.pageSize, columnConfig: e }; const msg = await this.saveTableConfig(data); // console.log('列配置保存成功:', msg); return msg; }, //获取localstorage缓存 setStorage(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { console.log('LocalStorage 存储错误:', e); if (e.name === 'QuotaExceededError') { this.clearCacheByPrefix(); //缓存不足,清除 localStorage.setItem(key, JSON.stringify(value)); } } }, //缓存不足清除缓存 clearCacheByPrefix() { const prefix = 'Cols'; // 标识后缀 Object.keys(localStorage).forEach((key) => { if (key.endsWith(prefix)) { localStorage.removeItem(key); // console.log(`已清除缓存: ${key}`); } }); // console.log('缓存清除完成'); }, //设置localstorage缓存 getStorage(key) { try { const value = localStorage.getItem(key); return value ? JSON.parse(value) : null; } catch (e) { console.error('LocalStorage 解析错误:', e); return null; } }, //防抖函数 debounce(fn, delay) { let timer = null; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }, //获取column记录接口 async getByTableId(key) { try { const res = await request.get( `/sys/table-config/getByTableId/${key}`, {} ); if (res.data.code == 0) { return res.data.data; } } catch (error) { console.error('获取列配置失败:', error); } }, // 添加column记录接口 async saveTableConfig(data) { try { const res = await request({ url: '/sys/table-config/save', method: 'post', data }); if (res.data.code == 0) { return res.data.data; } } catch (error) { console.error('保存列配置失败:', error); } }, } };