tableColumnsMixin.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. import request from '@/utils/request';
  2. import _ from 'lodash';
  3. // 宽度由 el-table 自行计算的内置列类型:不给它们补 width,避免改坏默认布局
  4. const BUILTIN_COLUMN_TYPES = ['index', 'selection', 'expand'];
  5. export default {
  6. data() {
  7. return {
  8. newColumns: [],
  9. tabMixinsInit: true, //进入页面是否默认请求列配置
  10. // computed columns 的重算触发器:applyColumnWidths / handleColumnChange 通过 ++ 它强制重算。
  11. // 必须声明在这里才是响应式的——组件自己的 data 若也声明了会以组件的为准。
  12. // 注意:列宽恢复本身不依赖它(applyColumnWidth 读了响应式的 columnWidthMap),
  13. // 它保证的是"必填列被取消勾选后强制显示回来"这条路径能触发重算。
  14. columnsVersion: 0,
  15. // 列宽映射(prop/columnKey -> 宽度),用于 computed columns 应用拖拽/缓存后的宽度
  16. columnWidthMap: {},
  17. // 副表列宽映射:{ [scope]: { prop: width } },与主表配置隔离
  18. scopedWidthMaps: {},
  19. // 每页条数(页面可在自身 data 覆盖默认值)
  20. pageSize: 10
  21. };
  22. },
  23. created() {
  24. // 同步读取本地保存的每页条数,保证 ele-pro-table 首次加载即用正确值
  25. this.initPageSize();
  26. //从服务器获取缓存列表配置
  27. if (this.tabMixinsInit) {
  28. this.getTabColumns();
  29. }
  30. // 创建防抖函数并绑定this
  31. this.debouncedHandleColumnChange = this.debounce(
  32. this.handleColumnChangeImpl,
  33. 1000
  34. );
  35. // 列宽变化持久化防抖(与服务端列设置共用 saveColumns)
  36. this.debouncedSaveColumns = this.debounce(this.saveColumns, 1000);
  37. },
  38. methods: {
  39. // 实际的列变更处理逻辑
  40. handleColumnChangeImpl() {
  41. try {
  42. const list = this.getStorage(this.cacheKeyUrl + 'Cols');
  43. if (list) {
  44. this.saveColumns(list);
  45. }
  46. } catch (error) {
  47. console.error('处理列配置出错:', error);
  48. }
  49. },
  50. // 列表变化回调
  51. handleColumnChange() {
  52. const list = this.getStorage(this.cacheKeyUrl + 'Cols');
  53. if (list) {
  54. let requireList = [];
  55. let currentList = this.getColumns()
  56. .filter((item) => item.headerSlot)
  57. .map((item) => item.prop);
  58. list.forEach((item) => {
  59. if (currentList.includes(item.prop) && !item.checked) {
  60. requireList.push(item.label);
  61. item.checked = true;
  62. }
  63. });
  64. if (requireList.length) {
  65. this.setStorage(this.cacheKeyUrl + 'Cols', list);
  66. if ('columns' in this.$options.computed) {
  67. this.columnsVersion++;
  68. } else {
  69. this.columns = [...this.columns];
  70. }
  71. this.$message.warning(
  72. `${requireList.toString()} 为必填项,无法隐藏!`
  73. );
  74. return;
  75. }
  76. }
  77. this.debouncedHandleColumnChange();
  78. },
  79. // 获取table-column配置
  80. async getTabColumns() {
  81. const res = await this.getByTableId(this.cacheKeyUrl);
  82. // 从服务端配置恢复每页条数(优先服务端,同步本地缓存)
  83. if (res?.pageSize) {
  84. this.pageSize = res.pageSize;
  85. this.setStorage(this.cacheKeyUrl + 'PageSize', res.pageSize);
  86. }
  87. if (res?.columnConfig?.length > 0) {
  88. //对比接口返回和本地columns
  89. let { nlist, type } = this.columnsContrast(res.columnConfig);
  90. // 将nlist中的prop字段去重
  91. const uniqueList = _.uniqBy(nlist, 'prop');
  92. const uniqueLabelList = _.uniqBy(uniqueList, 'label');
  93. //有更新则更新服务缓存配置
  94. if (type) {
  95. this.saveColumns(uniqueLabelList);
  96. }
  97. this.setStorage(this.cacheKeyUrl + 'Cols', nlist);
  98. }
  99. // 一次性回填:旧版 'Widths' 存储({prop:width})迁移到新 'Cols' 存储
  100. this.migrateLegacyWidthsCache();
  101. // 无论服务端是否返回了列配置,都要从本地缓存恢复列宽:
  102. // 1. 服务端首次为空(用户刚调整过列宽,1s 防抖未触发或首次进入)时不能丢
  103. // 2. 多 tab 场景下本地可能有比服务端更新的宽度
  104. this.applyColumnWidthFromCache();
  105. this.applyColumnWidths();
  106. },
  107. // 一次性回填:旧版 'cacheKeyUrl + Widths' 存储格式 {prop: width}
  108. // 迁移到新 'cacheKeyUrl + Cols' 存储(数组)。只补不覆盖,回填完清空旧 key。
  109. migrateLegacyWidthsCache() {
  110. try {
  111. const colsKey = this.cacheKeyUrl + 'Cols';
  112. const widthsKey = this.cacheKeyUrl + 'Widths';
  113. const legacy = this.getStorage(widthsKey);
  114. if (!legacy || typeof legacy !== 'object') return;
  115. const existingCols = this.getStorage(colsKey) || [];
  116. const existingKeys = new Set(
  117. existingCols.map((c) => c.columnKey || c.prop || c.id)
  118. );
  119. let added = 0;
  120. Object.entries(legacy).forEach(([k, w]) => {
  121. if (k && w != null && !existingKeys.has(k)) {
  122. // 用本地列定义反查真实 label:columnsContrast 按 label 比对,
  123. // 写成 prop 名的话这些回填条目会被当成"已删除列"清掉
  124. const col = this.findColumnByKey(k);
  125. existingCols.push({
  126. id: k,
  127. prop: k,
  128. columnKey: k,
  129. label: (col && col.label) || k,
  130. checked: true,
  131. minWidth: w
  132. });
  133. added++;
  134. }
  135. });
  136. if (added > 0) {
  137. this.setStorage(colsKey, existingCols);
  138. }
  139. // 迁移完成(无论有无新增)都清掉旧 key,避免双份数据
  140. this.setStorage(widthsKey, {});
  141. } catch (e) {
  142. console.warn('[tabMixins] migrateLegacyWidthsCache 异常:', e);
  143. }
  144. },
  145. getColumns() {
  146. if (typeof this.columns == 'function') {
  147. return this.columns();
  148. } else {
  149. return this.columns;
  150. }
  151. },
  152. // 同步读取本地保存的每页条数(localStorage),用于首次加载
  153. initPageSize() {
  154. const local = this.getStorage(this.cacheKeyUrl + 'PageSize');
  155. if (local) {
  156. this.pageSize = local;
  157. }
  158. },
  159. // 每页条数变化处理:在 datasource({ limit }) 中调用,变化时保存
  160. handlePageSize(limit) {
  161. if (limit && limit !== this.pageSize) {
  162. this.pageSize = limit;
  163. this.savePageSize(limit);
  164. }
  165. },
  166. // 保存每页条数到 localStorage + 服务端 table-config(同时带上当前列配置)
  167. savePageSize(size) {
  168. this.setStorage(this.cacheKeyUrl + 'PageSize', size);
  169. // 服务端持久化:tableId + pageSize + columnConfig 三参数齐全
  170. const columnConfig = this.getStorage(this.cacheKeyUrl + 'Cols') || [];
  171. this.saveTableConfig({
  172. tableId: this.cacheKeyUrl,
  173. pageSize: size,
  174. columnConfig
  175. });
  176. },
  177. // 根据 prop / columnKey / label 查找本地列(支持 children 递归)
  178. findColumnByKey(key, label) {
  179. let result = null;
  180. const loop = (cols) => {
  181. for (const col of cols) {
  182. if (
  183. (key && (col.prop === key || col.columnKey === key)) ||
  184. (label && col.label === label)
  185. ) {
  186. result = col;
  187. return;
  188. }
  189. if (col.children && col.children.length) loop(col.children);
  190. if (result) return;
  191. }
  192. };
  193. loop(this.getColumns() || []);
  194. return result;
  195. },
  196. // 表头拖拽列宽变化回调:更新列宽映射并持久化
  197. handleHeaderDragend(newWidth, oldWidth, column) {
  198. if (!column || !newWidth) return;
  199. const key = column.property || column.columnKey;
  200. if (!key) return;
  201. // 仅持久化新宽度,不触发 columns 重算。
  202. // el-table 在拖拽过程中已实时更新列宽,此处重算会导致整表重渲染抖动;
  203. // 刷新后由 applyColumnWidthFromCache 从缓存恢复,无需拖拽时同步映射。
  204. this.persistColumnWidth(key, newWidth, column.label);
  205. },
  206. // 应用列宽并触发表格更新(自动区分 columns 是 computed 还是 data)
  207. applyColumnWidths() {
  208. try {
  209. const isComputed = !!(this._computedWatchers && this._computedWatchers.columns);
  210. if (isComputed) {
  211. // computed 场景:columns 内部已调用 applyColumnWidth,这里只需触发重算
  212. this.columnsVersion++;
  213. } else {
  214. // data 场景:直接应用宽度到 data 数组并触发更新
  215. // 防御:columns 可能是 computed 函数、null、undefined,或列定义里写成嵌套结构
  216. // 任何不可迭代形态都直接跳过,避免 "this.columns is not iterable"
  217. if (typeof this.columns === 'function') {
  218. // 把 columns 当成 computed 走
  219. this.columnsVersion = (this.columnsVersion || 0) + 1;
  220. return;
  221. }
  222. if (!Array.isArray(this.columns)) {
  223. console.warn(
  224. '[tabMixins] this.columns 不是数组,跳过 applyColumnWidths。当前类型:',
  225. typeof this.columns,
  226. this.columns
  227. );
  228. return;
  229. }
  230. this.applyColumnWidth(this.columns);
  231. this.columns = [...this.columns];
  232. }
  233. } catch (e) {
  234. // 任何意外都不要让 mixin 把整个页面崩掉
  235. console.warn('[tabMixins] applyColumnWidths 异常:', e);
  236. }
  237. },
  238. // 应用列宽映射到列数组(供 computed columns 生成列后调用,或 data columns 直接应用)
  239. applyColumnWidth(columns) {
  240. return this.applyColumnWidthWithMap(columns, this.columnWidthMap || {});
  241. },
  242. // 用指定宽度映射应用列宽(主表与副表共用同一套写入规则)
  243. applyColumnWidthWithMap(columns, map) {
  244. const keys = Object.keys(map || {});
  245. if (!keys.length || !columns) return columns;
  246. const loop = (cols) => {
  247. (cols || []).forEach((col) => {
  248. const k = col.prop || col.columnKey;
  249. if (k && map[k] != null) {
  250. // 列定义里 width 和 minWidth 是二选一关系:
  251. // - width:固定宽度(拖完会保持用户选的尺寸)
  252. // - minWidth:最小宽度(列可继续撑开/收缩)
  253. // 拖拽事件给的是"用户实际想要的列宽",要把它写到能控制渲染尺寸的字段:
  254. // 列原定义用 width → 覆盖 width;只用 minWidth → 覆盖 minWidth;两者都有 → 都覆盖
  255. const newW = map[k];
  256. if (col.width != null) {
  257. this.$set(col, 'width', newW);
  258. }
  259. if (col.minWidth != null) {
  260. this.$set(col, 'minWidth', newW);
  261. }
  262. // 列定义里既没 width 也没 minWidth:拖拽在 el-table 里实时生效了,
  263. // 但恢复时无处落脚 → 补一个 width 承载用户选的尺寸。
  264. // 只对真实数据列(有 prop)生效,且跳过 index/selection/expand 这类
  265. // 由 el-table 自己算宽度的内置列,避免改坏默认布局。
  266. // 注意:只有用户真的拖过这一列(map 里有值)才会走到这里,
  267. // 所以没拖过的列布局完全不变。
  268. if (
  269. col.width == null &&
  270. col.minWidth == null &&
  271. col.prop &&
  272. BUILTIN_COLUMN_TYPES.indexOf(col.type) === -1
  273. ) {
  274. this.$set(col, 'width', newW);
  275. }
  276. }
  277. if (col.children && col.children.length) loop(col.children);
  278. });
  279. };
  280. loop(columns);
  281. return columns;
  282. },
  283. // 持久化列宽到列设置(localStorage 与 ele-pro-table 共用 cacheKey+'Cols',并同步服务端)
  284. // label 必须写列的真实 label:columnsContrast 是按 label 比对的,
  285. // 若写成 prop 名,新建的条目会被判成"已删除列"在下一次进入页面时被清掉
  286. persistColumnWidth(key, width, label) {
  287. if (!key) return;
  288. const storageKey = this.cacheKeyUrl + 'Cols';
  289. const cached = this.getStorage(storageKey) || [];
  290. const setting = cached.find((c) => (c.columnKey || c.prop || c.id) === key);
  291. if (setting) {
  292. setting.minWidth = width;
  293. // 旧数据可能是用 key 兜底的 label,这里顺手纠正回真实 label
  294. if (label && !setting.label) {
  295. setting.label = label;
  296. }
  297. } else {
  298. cached.push({
  299. id: key,
  300. prop: key,
  301. columnKey: key,
  302. label: label || key,
  303. checked: true,
  304. minWidth: width
  305. });
  306. }
  307. this.setStorage(storageKey, cached);
  308. if (this.debouncedSaveColumns) {
  309. this.debouncedSaveColumns(cached);
  310. }
  311. },
  312. // 从缓存恢复列宽到 columnWidthMap(供 computed columns 应用)
  313. applyColumnWidthFromCache() {
  314. const cached = this.getStorage(this.cacheKeyUrl + 'Cols') || [];
  315. cached.forEach((c) => {
  316. const k = c.columnKey || c.prop || c.id;
  317. if (k && c.minWidth != null) {
  318. this.$set(this.columnWidthMap, k, c.minWidth);
  319. }
  320. });
  321. },
  322. // ============ 副表(同一组件内的第二/第三张表)列宽 ============
  323. // 一个组件里多张表共用 cacheKeyUrl 会互相污染:副表的列会被主表的
  324. // columnsContrast 当成"已删除列"清掉,导致存了宽度刷新也恢复不了。
  325. // 所以副表用 cacheKeyUrl + '_' + scope 派生独立 key,独立存独立取。
  326. tableColsKey(scope) {
  327. return this.cacheKeyUrl + (scope ? '_' + scope : '') + 'Cols';
  328. },
  329. // 读取某张副表的列宽缓存到响应式 map(组件 created 里同步调用即可,
  330. // $set 会通知依赖 scopedWidthMaps 的 computed 自动重算,无需手动刷新)
  331. loadScopedColumnWidth(scope) {
  332. try {
  333. const cached = this.getStorage(this.tableColsKey(scope)) || [];
  334. const map = {};
  335. cached.forEach((c) => {
  336. const k = c.columnKey || c.prop || c.id;
  337. if (k && c.minWidth != null) {
  338. map[k] = c.minWidth;
  339. }
  340. });
  341. this.$set(this.scopedWidthMaps, scope, map);
  342. } catch (e) {
  343. console.warn('[tabMixins] loadScopedColumnWidth 异常:', e);
  344. }
  345. },
  346. // 副表应用列宽:在副表的 computed columns 里调用
  347. // 用法:subColumns() { return this.applyScopedColumnWidth(this.rawCols, 'sub'); }
  348. applyScopedColumnWidth(columns, scope) {
  349. return this.applyColumnWidthWithMap(
  350. columns,
  351. this.scopedWidthMaps[scope] || {}
  352. );
  353. },
  354. // 副表拖拽列宽持久化:只写本地,不进服务端 table-config
  355. //(副表列不在主表 columnConfig 里,提交上去会被主表当脏数据清掉)
  356. persistScopedColumnWidth(scope, key, width) {
  357. if (!key || !width) return;
  358. try {
  359. const storageKey = this.tableColsKey(scope);
  360. const cached = this.getStorage(storageKey) || [];
  361. const setting = cached.find(
  362. (c) => (c.columnKey || c.prop || c.id) === key
  363. );
  364. if (setting) {
  365. setting.minWidth = width;
  366. } else {
  367. cached.push({
  368. id: key,
  369. prop: key,
  370. columnKey: key,
  371. label: key,
  372. checked: true,
  373. minWidth: width
  374. });
  375. }
  376. this.setStorage(storageKey, cached);
  377. } catch (e) {
  378. console.warn('[tabMixins] persistScopedColumnWidth 异常:', e);
  379. }
  380. },
  381. // 副表拖拽回调:在组件 methods 里包一层,透传给 persistScopedColumnWidth
  382. // 用法:onSubDragend(w, o, column) { this.persistScopedColumnWidth('sub', column && column.property, w); }
  383. onScopedColumnDragend(scope) {
  384. return function (newWidth, oldWidth, column) {
  385. if (!column) return;
  386. this.persistScopedColumnWidth(scope, column.property || column.columnKey, newWidth);
  387. }.bind(this);
  388. },
  389. //服务器和本地配置columns对比
  390. columnsContrast(list) {
  391. const key = 'label';
  392. var updateType = 0;
  393. let sList = list.filter((d, i, r) => {
  394. return d[key];
  395. });
  396. // 组件可能只有副表、根本没有主表 columns(如用列工厂的弹窗),
  397. // 此时 getColumns() 返回 undefined,兜底成空数组避免 .filter 抛错
  398. let devColumns = this.newColumns?.length
  399. ? this.newColumns
  400. : this.getColumns() || [];
  401. let dList = (Array.isArray(devColumns) ? devColumns : []).filter((d, i, r) => {
  402. return d[key] && d[key] !== '序号';
  403. });
  404. const keysA = new Set(sList.map((item) => item[key]));
  405. const keysB = new Set(dList.map((item) => item[key]));
  406. // 本地 比 缓存服务端 多的对象(新增)
  407. const added = dList.filter((item) => {
  408. return !keysA.has(item[key]) && (item.prop || item.label === '操作');
  409. });
  410. // 本地 比 缓存服务端 少的对象(删除)
  411. const removed = sList.filter((item) => !keysB.has(item[key]));
  412. const removedPropSet = new Set(removed.map((item) => item[key]));
  413. // 删除 缓存中 中被移除的对象
  414. const keptA = list.filter((item) => !removedPropSet.has(item[key]));
  415. added.forEach((item) => {
  416. //新增columns字段prop参数为必填
  417. if (item.prop) {
  418. item.id = item.prop;
  419. } else if (item.columnKey) {
  420. item.id = item.columnKey;
  421. }
  422. item.checked = true;
  423. });
  424. if (added.length > 0 || removed.length > 0) {
  425. updateType = 1;
  426. }
  427. // 更新项:key 存在但内容变化
  428. const dMap = new Map(dList.map((item) => [item[key], item]));
  429. const updated = keptA.map((sItem) => {
  430. const dItem = dMap.get(sItem[key]);
  431. if (dItem && dItem.prop && sItem.prop !== dItem.prop) {
  432. updateType = 1;
  433. // 记录旧值和新值
  434. const oldValue = sItem.prop;
  435. const newValue = dItem.prop;
  436. // 遍历所有属性,动态替换匹配旧值的字段
  437. const updatedItem = { ...sItem };
  438. Object.keys(updatedItem).forEach((k) => {
  439. if (updatedItem[k] === oldValue) {
  440. updatedItem[k] = newValue;
  441. }
  442. });
  443. return updatedItem;
  444. }
  445. return sItem;
  446. });
  447. // 合并保留的对象和新增的对象
  448. return { nlist: [...updated, ...added], type: updateType };
  449. },
  450. // 提交columns配置(tableId + pageSize + columnConfig 三参数齐全)
  451. async saveColumns(e) {
  452. const data = {
  453. tableId: this.cacheKeyUrl,
  454. pageSize: this.pageSize,
  455. columnConfig: e
  456. };
  457. const msg = await this.saveTableConfig(data);
  458. // console.log('列配置保存成功:', msg);
  459. return msg;
  460. },
  461. //获取localstorage缓存
  462. setStorage(key, value) {
  463. try {
  464. localStorage.setItem(key, JSON.stringify(value));
  465. } catch (e) {
  466. console.log('LocalStorage 存储错误:', e);
  467. if (e.name === 'QuotaExceededError') {
  468. this.clearCacheByPrefix(); //缓存不足,清除
  469. localStorage.setItem(key, JSON.stringify(value));
  470. }
  471. }
  472. },
  473. //缓存不足清除缓存
  474. clearCacheByPrefix() {
  475. const prefix = 'Cols'; // 标识后缀
  476. Object.keys(localStorage).forEach((key) => {
  477. if (key.endsWith(prefix)) {
  478. localStorage.removeItem(key);
  479. // console.log(`已清除缓存: ${key}`);
  480. }
  481. });
  482. // console.log('缓存清除完成');
  483. },
  484. //设置localstorage缓存
  485. getStorage(key) {
  486. try {
  487. const value = localStorage.getItem(key);
  488. return value ? JSON.parse(value) : null;
  489. } catch (e) {
  490. console.error('LocalStorage 解析错误:', e);
  491. return null;
  492. }
  493. },
  494. //防抖函数
  495. debounce(fn, delay) {
  496. let timer = null;
  497. return (...args) => {
  498. clearTimeout(timer);
  499. timer = setTimeout(() => {
  500. fn.apply(this, args);
  501. }, delay);
  502. };
  503. },
  504. //获取column记录接口
  505. async getByTableId(key) {
  506. try {
  507. const res = await request.get(
  508. `/sys/table-config/getByTableId/${key}`,
  509. {}
  510. );
  511. if (res.data.code == 0) {
  512. return res.data.data;
  513. }
  514. } catch (error) {
  515. console.error('获取列配置失败:', error);
  516. }
  517. },
  518. // 添加column记录接口
  519. async saveTableConfig(data) {
  520. try {
  521. const res = await request({
  522. url: '/sys/table-config/save',
  523. method: 'post',
  524. data
  525. });
  526. if (res.data.code == 0) {
  527. return res.data.data;
  528. }
  529. } catch (error) {
  530. console.error('保存列配置失败:', error);
  531. }
  532. },
  533. }
  534. };