| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553 |
- 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);
- }
- },
-
- }
- };
|