@idooel/components 0.0.3-beta.2 → 0.0.3-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1665,8 +1665,12 @@ var script$D = {
1665
1665
  return !this.dataSource.length;
1666
1666
  },
1667
1667
  getScrollHeightByHeight: function getScrollHeightByHeight() {
1668
- // 始终返回可用的剩余高度,让表格内容不足时也能占满容器
1669
- return this.height - this.tableHeaderHeight - this.paginationHeight;
1668
+ // 始终基于稳定的表头/分页高度计算可用区域,避免 fallback 造成内容压到分页区
1669
+ var headerHeight = this.tableHeaderHeight || 64;
1670
+ var paginationHeight = this.paginationHeight || 64;
1671
+ var rawHeight = this.height - headerHeight - paginationHeight;
1672
+ // 规避小数像素导致的 1px 裁切,给表体预留 1px 安全边距
1673
+ return Math.max(Math.floor(rawHeight) - 1, 0);
1670
1674
  },
1671
1675
  isFlexColumn: function isFlexColumn() {
1672
1676
  return this.columns.every(function (col) {
@@ -1690,12 +1694,45 @@ var script$D = {
1690
1694
  }
1691
1695
  return total;
1692
1696
  },
1697
+ estimatedMinContentWidth: function estimatedMinContentWidth() {
1698
+ var cols = this.innerColumns || [];
1699
+ var specifiedWidthSum = 0;
1700
+ var unspecifiedCount = 0;
1701
+ cols.forEach(function (col) {
1702
+ var w = col && col.width;
1703
+ if (typeof w === 'number') {
1704
+ specifiedWidthSum += w;
1705
+ } else {
1706
+ unspecifiedCount += 1;
1707
+ }
1708
+ });
1709
+ var selectionWidth = this.rowSelection ? 60 : 0;
1710
+ var operationWidth = this.operations && typeof this.operations.width === 'number' ? this.operations.width : 0;
1711
+ // 未配置宽度列给更保守的最小宽度,贴近运行时真实占宽,
1712
+ // 避免“容器已明显缩小但横向滚动仍不触发”。
1713
+ var unspecifiedMinWidth = unspecifiedCount * 320;
1714
+ return specifiedWidthSum + unspecifiedMinWidth + selectionWidth + operationWidth;
1715
+ },
1716
+ hasIncompleteColumnWidth: function hasIncompleteColumnWidth() {
1717
+ var cols = this.innerColumns || [];
1718
+ if (!cols.length) return false;
1719
+ return cols.some(function (col) {
1720
+ return typeof (col && col.width) !== 'number';
1721
+ });
1722
+ },
1693
1723
  /**
1694
1724
  * 是否需要横向滚动:容器宽度 < 列总宽度
1695
1725
  */
1696
1726
  needHorizontalScroll: function needHorizontalScroll() {
1697
1727
  // 未获取容器宽度前,先假定需要滚动(保守策略)
1698
1728
  if (!this.containerWidth) return true;
1729
+ // 存在未配置 width 的列时,totalColumnsWidth 会低估真实内容宽度;
1730
+ // 此时改用保守阈值判定,避免误判导致列被压缩变形。
1731
+ if (this.hasIncompleteColumnWidth) {
1732
+ var baseFallbackX = typeof this.x === 'number' ? this.x : 1200;
1733
+ var fallbackX = Math.max(baseFallbackX, this.estimatedMinContentWidth);
1734
+ return this.containerWidth < fallbackX - 5;
1735
+ }
1699
1736
  // 加一点容差
1700
1737
  return this.containerWidth < this.totalColumnsWidth - 5;
1701
1738
  },
@@ -1726,9 +1763,11 @@ var script$D = {
1726
1763
  */
1727
1764
  smartScroll: function smartScroll() {
1728
1765
  if (!this.needHorizontalScroll) {
1729
- // 不需要横向滚动,只保留 y 方向(如果需要)
1730
- if (this.height && this.needScrollY) {
1731
- var availableHeight = this.tableHeaderHeight && this.paginationHeight ? this.getScrollHeightByHeight : this.height - 100;
1766
+ // 不需要横向滚动时,只要存在高度约束就下发 y
1767
+ // 旧逻辑依赖估算行高 needScrollY,遇到多行文本/变高行时会误判,
1768
+ // 导致 body 自然撑高(overflow: visible),内容超出可视区却没有纵向滚动。
1769
+ if (this.height) {
1770
+ var availableHeight = this.getScrollHeightByHeight;
1732
1771
  if (availableHeight > 50) {
1733
1772
  return {
1734
1773
  y: availableHeight
@@ -1737,8 +1776,20 @@ var script$D = {
1737
1776
  }
1738
1777
  return {};
1739
1778
  }
1740
- // 需要横向滚动,使用原有逻辑
1741
- return this.getScroll;
1779
+ // 需要横向滚动时,若未设置 y,会导致横向滚动条落在超高表体底部,
1780
+ // 外层容器 overflow:hidden 场景下用户看不到滚动条。
1781
+ // 这里在有高度约束时强制注入 y,保证横向滚动条始终在可视区域内。
1782
+ var baseScroll = this.getScroll || {};
1783
+ if (this.height && (baseScroll.y === undefined || baseScroll.y === null)) {
1784
+ var _availableHeight = this.getScrollHeightByHeight;
1785
+ if (_availableHeight > 50) {
1786
+ return _objectSpread2(_objectSpread2({}, baseScroll), {}, {
1787
+ y: _availableHeight
1788
+ });
1789
+ }
1790
+ }
1791
+ // 需要横向滚动,其他情况使用原有逻辑
1792
+ return baseScroll;
1742
1793
  },
1743
1794
  /**
1744
1795
  * 智能 rowSelection 配置:
@@ -1778,14 +1829,22 @@ var script$D = {
1778
1829
  // rowSelection 的 checkbox/radio 列是 antd 自动加的,给一个经验宽度避免误差
1779
1830
  var selectionWidth = this.rowSelection ? 60 : 0;
1780
1831
 
1781
- // 只有当所有列都明确给了宽度(total > 0)时才 clamp
1782
- if (total > 0) {
1832
+ // 只有所有列都明确给了 width 时才 clamp,避免低估导致表格被挤压
1833
+ var allColsHaveWidth = cols.length > 0 && cols.every(function (col) {
1834
+ return typeof (col && col.width) === 'number';
1835
+ });
1836
+ if (allColsHaveWidth && total > 0) {
1783
1837
  var minX = total + selectionWidth;
1784
1838
  if (baseX > minX) baseX = minX;
1785
1839
  }
1840
+ // 存在未配置宽度列时,x 至少取估算最小内容宽度,
1841
+ // 避免出现 needHorizontalScroll=true 但 scrollWidth==clientWidth 的“无滚动条”状态。
1842
+ if (this.hasIncompleteColumnWidth) {
1843
+ baseX = Math.max(baseX, this.estimatedMinContentWidth);
1844
+ }
1786
1845
  }
1787
1846
  if (this.height && this.needScrollY) {
1788
- var availableHeight = this.tableHeaderHeight && this.paginationHeight ? this.getScrollHeightByHeight : this.height - 100;
1847
+ var availableHeight = this.getScrollHeightByHeight;
1789
1848
  if (availableHeight > 50) {
1790
1849
  return {
1791
1850
  x: baseX,
@@ -1842,7 +1901,7 @@ var script$D = {
1842
1901
  setTableHeaderHeight: function setTableHeaderHeight() {
1843
1902
  var _this3 = this;
1844
1903
  this.$nextTick(function () {
1845
- var el = _this3.$el.querySelector('.ant-table-header');
1904
+ var el = _this3.$el.querySelector('.ant-table-header') || _this3.$el.querySelector('.ant-table-thead');
1846
1905
  if (!el) return;
1847
1906
  var _el$getBoundingClient2 = el.getBoundingClientRect(),
1848
1907
  height = _el$getBoundingClient2.height;
@@ -1974,13 +2033,16 @@ var script$D = {
1974
2033
  var _iterator = _createForOfIteratorHelper(entries),
1975
2034
  _step;
1976
2035
  try {
1977
- var _loop = function _loop() {
2036
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
1978
2037
  var entry = _step.value;
1979
2038
  var newWidth = entry.contentRect.width;
2039
+ var delta = Math.abs(newWidth - _this8.containerWidth);
2040
+ var oldNeedScroll = _this8.needHorizontalScroll;
2041
+ // 始终更新容器宽度,避免临界区被阈值过滤后状态滞留。
2042
+ _this8.containerWidth = newWidth;
2043
+ var newNeedScroll = _this8.needHorizontalScroll;
1980
2044
  // 只有宽度变化超过阈值才触发更新,避免微小变化导致频繁重渲染
1981
- if (Math.abs(newWidth - _this8.containerWidth) > 10) {
1982
- var oldNeedScroll = _this8.needHorizontalScroll;
1983
- _this8.containerWidth = newWidth;
2045
+ if (delta > 10 || newNeedScroll !== oldNeedScroll) {
1984
2046
  // 容器宽度变化会触发 needHorizontalScroll 的重新计算
1985
2047
  // needHorizontalScroll 的 watcher 会处理表格重新渲染和列宽同步
1986
2048
  // 这里不需要直接调用 syncFixedColumns,避免与 watcher 的执行时机冲突
@@ -1994,23 +2056,18 @@ var script$D = {
1994
2056
  });
1995
2057
 
1996
2058
  // 如果 needHorizontalScroll 状态没有变化,说明只是列宽需要调整,直接同步
1997
- // 如果状态变化了,watcher 会处理重新渲染
1998
- if (_this8.needHorizontalScroll === oldNeedScroll) {
1999
- // 防抖:延迟同步固定列,避免频繁调用
2000
- if (_this8._resizeDebounceTimer) {
2001
- clearTimeout(_this8._resizeDebounceTimer);
2002
- }
2003
- _this8._resizeDebounceTimer = setTimeout(function () {
2004
- _this8.syncFixedColumns();
2005
- _this8.syncHeaderTableWidth();
2006
- _this8.bindScrollSync();
2007
- }, 150);
2059
+ // 状态变化时 watcher 会重渲染,但这里仍补一次同步,避免出现
2060
+ // “缩小后没滚动,刷新后才出现”的时序窗口。
2061
+ if (_this8._resizeDebounceTimer) {
2062
+ clearTimeout(_this8._resizeDebounceTimer);
2008
2063
  }
2064
+ _this8._resizeDebounceTimer = setTimeout(function () {
2065
+ _this8.syncFixedColumns();
2066
+ _this8.syncHeaderTableWidth();
2067
+ _this8.bindScrollSync();
2068
+ }, 150);
2009
2069
  });
2010
2070
  }
2011
- };
2012
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
2013
- _loop();
2014
2071
  }
2015
2072
  } catch (err) {
2016
2073
  _iterator.e(err);
@@ -2087,7 +2144,12 @@ var __vue_render__$D = function __vue_render__() {
2087
2144
  return _c("div", {
2088
2145
  ref: "tableWrapper",
2089
2146
  staticClass: "g-table__wrapper",
2147
+ class: {
2148
+ "g-table__wrapper--need-h-scroll": _vm.needHorizontalScroll
2149
+ },
2090
2150
  style: _vm.wrapperStyle
2151
+ }, [_c("div", {
2152
+ staticClass: "g-table__main"
2091
2153
  }, [_c("a-table", {
2092
2154
  key: _vm.tableRenderKey,
2093
2155
  class: [_vm.isNoData && "g-table__no-data"],
@@ -2113,7 +2175,7 @@ var __vue_render__$D = function __vue_render__() {
2113
2175
  }, _vm.$listeners))];
2114
2176
  }
2115
2177
  }])
2116
- }), _vm._v(" "), _c("div", {
2178
+ })], 1), _vm._v(" "), _c("div", {
2117
2179
  staticClass: "g-table__pagination"
2118
2180
  }, [_vm.mode === "default" ? _c("a-pagination", {
2119
2181
  attrs: {
@@ -2142,7 +2204,7 @@ var __vue_render__$D = function __vue_render__() {
2142
2204
  change: _vm.onChangePagination,
2143
2205
  showSizeChange: _vm.onShowSizeChange
2144
2206
  }
2145
- })], 1)], 1);
2207
+ })], 1)]);
2146
2208
  };
2147
2209
  var __vue_staticRenderFns__$D = [];
2148
2210
  __vue_render__$D._withStripped = true;
@@ -2150,21 +2212,21 @@ __vue_render__$D._withStripped = true;
2150
2212
  /* style */
2151
2213
  var __vue_inject_styles__$D = function __vue_inject_styles__(inject) {
2152
2214
  if (!inject) return;
2153
- inject("data-v-59975dac_0", {
2154
- source: "@charset \"UTF-8\";\n.g-table__wrapper[data-v-59975dac] {\n /**\n * 修复\"宽屏下表格两侧出现空白\"问题:\n * 当视口宽度大于表格内容宽度时,主表(ant-table-scroll)不会自动拉伸,\n * 而固定列(fixed-left/fixed-right)是 position:absolute 定位在容器边缘,中间就出现空白。\n * 解决方案:让主表的 table 元素 min-width:100%,使其始终填满滚动容器。\n */\n}\n.g-table__wrapper[data-v-59975dac] .ant-table-scroll .ant-table-header table,\n.g-table__wrapper[data-v-59975dac] .ant-table-scroll .ant-table-body table {\n min-width: 100%;\n}\n.g-table__wrapper[data-v-59975dac] {\n /**\n * 修复\"固定列 + scroll.x\"场景下,主表(ant-table-scroll)里会渲染一份 fixed 列的占位 header/cell。\n * 这份占位本来只用于计算宽度,但在某些布局下会被看见,表现为\"操作列前多了一大块空白/空列\"。\n * 这里用 visibility:hidden 隐藏占位(不影响占位宽度与 fixed 计算),避免视觉空白。\n */\n}\n.g-table__wrapper[data-v-59975dac] .ant-table-scroll .ant-table-header thead > tr > th.ant-table-fixed-columns-in-body.ant-table-row-cell-last,\n.g-table__wrapper[data-v-59975dac] .ant-table-scroll .ant-table-body tbody > tr > td.ant-table-fixed-columns-in-body.ant-table-row-cell-last {\n visibility: hidden;\n}\n.g-table__wrapper[data-v-59975dac] {\n /* 强制统一行高,确保主表和固定列对齐 */\n}\n.g-table__wrapper[data-v-59975dac] .ant-table-tbody > tr > td {\n height: 54px;\n padding: 8px 16px;\n vertical-align: middle;\n box-sizing: border-box;\n line-height: 38px;\n}\n.g-table__wrapper[data-v-59975dac] {\n /* 表头也统一高度和样式 */\n}\n.g-table__wrapper[data-v-59975dac] .ant-table-thead > tr > th {\n height: 54px;\n padding: 8px 16px;\n vertical-align: middle;\n box-sizing: border-box;\n line-height: 38px;\n}\n.g-table__wrapper[data-v-59975dac] {\n /* 分页区域固定在底部 */\n}\n.g-table__wrapper .g-table__pagination[data-v-59975dac] {\n display: flex;\n flex-direction: row;\n justify-content: end;\n border-top: unset;\n padding-top: 8px;\n padding-bottom: 8px;\n background: #fff;\n}\n.g-table__wrapper[data-v-59975dac] {\n /* 空数据状态顶部显示 */\n}\n.g-table__wrapper .g-table__no-data[data-v-59975dac] {\n position: relative;\n}\n.g-table__wrapper .g-table__no-data[data-v-59975dac] .ant-table-placeholder {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -40%);\n width: 100%;\n height: 100%;\n text-align: center;\n color: #999;\n font-size: 14px;\n font-weight: normal;\n line-height: 20px;\n overflow: hidden;\n border: unset;\n}\n\n/*# sourceMappingURL=index.vue.map */",
2215
+ inject("data-v-6cef17bc_0", {
2216
+ source: "@charset \"UTF-8\";\n.g-table__wrapper[data-v-6cef17bc] {\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.g-table__wrapper .g-table__main[data-v-6cef17bc] {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n.g-table__wrapper[data-v-6cef17bc] {\n /**\n * 修复\"宽屏下表格两侧出现空白\"问题:\n * 当视口宽度大于表格内容宽度时,主表(ant-table-scroll)不会自动拉伸,\n * 而固定列(fixed-left/fixed-right)是 position:absolute 定位在容器边缘,中间就出现空白。\n * 解决方案:让主表的 table 元素 min-width:100%,使其始终填满滚动容器。\n */\n}\n.g-table__wrapper[data-v-6cef17bc]:not(.g-table__wrapper--need-h-scroll) .ant-table-scroll .ant-table-header table,\n.g-table__wrapper[data-v-6cef17bc]:not(.g-table__wrapper--need-h-scroll) .ant-table-scroll .ant-table-body table {\n min-width: 100%;\n}\n.g-table__wrapper[data-v-6cef17bc] {\n /**\n * 修复\"固定列 + scroll.x\"场景下,主表(ant-table-scroll)里会渲染一份 fixed 列的占位 header/cell。\n * 这份占位本来只用于计算宽度,但在某些布局下会被看见,表现为\"操作列前多了一大块空白/空列\"。\n * 这里用 visibility:hidden 隐藏占位(不影响占位宽度与 fixed 计算),避免视觉空白。\n */\n}\n.g-table__wrapper[data-v-6cef17bc] .ant-table-scroll .ant-table-header thead > tr > th.ant-table-fixed-columns-in-body.ant-table-row-cell-last,\n.g-table__wrapper[data-v-6cef17bc] .ant-table-scroll .ant-table-body tbody > tr > td.ant-table-fixed-columns-in-body.ant-table-row-cell-last {\n visibility: hidden;\n}\n.g-table__wrapper[data-v-6cef17bc] {\n /* 强制统一行高,确保主表和固定列对齐 */\n}\n.g-table__wrapper[data-v-6cef17bc] .ant-table-tbody > tr > td {\n height: 54px;\n padding: 8px 16px;\n vertical-align: middle;\n box-sizing: border-box;\n line-height: 38px;\n}\n.g-table__wrapper[data-v-6cef17bc] {\n /* 表头也统一高度和样式 */\n}\n.g-table__wrapper[data-v-6cef17bc] .ant-table-thead > tr > th {\n height: 54px;\n padding: 8px 16px;\n vertical-align: middle;\n box-sizing: border-box;\n line-height: 38px;\n}\n.g-table__wrapper[data-v-6cef17bc] {\n /* 分页区域固定在底部 */\n}\n.g-table__wrapper .g-table__pagination[data-v-6cef17bc] {\n display: flex;\n flex-direction: row;\n justify-content: end;\n border-top: unset;\n padding-top: 8px;\n padding-bottom: 8px;\n background: #fff;\n}\n.g-table__wrapper[data-v-6cef17bc] {\n /* 空数据状态顶部显示 */\n}\n.g-table__wrapper .g-table__no-data[data-v-6cef17bc] {\n position: relative;\n}\n.g-table__wrapper .g-table__no-data[data-v-6cef17bc] .ant-table-placeholder > td {\n height: auto !important;\n line-height: normal !important;\n padding-top: 24px !important;\n padding-bottom: 24px !important;\n}\n.g-table__wrapper .g-table__no-data[data-v-6cef17bc] .ant-table-placeholder {\n position: static;\n width: auto;\n height: auto;\n text-align: center;\n color: #999;\n font-size: 14px;\n font-weight: normal;\n line-height: 20px;\n overflow: visible;\n border: unset;\n}\n\n/*# sourceMappingURL=index.vue.map */",
2155
2217
  map: {
2156
2218
  "version": 3,
2157
2219
  "sources": ["index.vue", "E:\\code\\OnlineStudy-Base\\base-elearning-frontend-model\\packages\\components\\packages\\table\\src\\index.vue"],
2158
2220
  "names": [],
2159
- "mappings": "AAAA,gBAAgB;ACqhBhB;EACA;;;;;IAAA;AD9gBA;ACohBA;;EAEA,eAAA;ADlhBA;ACygBA;EAYA;;;;IAAA;AD9gBA;ACmhBA;;EAEA,kBAAA;ADjhBA;AC8fA;EAsBA,sBAAA;ADjhBA;ACkhBA;EACA,YAAA;EACA,iBAAA;EACA,sBAAA;EACA,sBAAA;EACA,iBAAA;ADhhBA;ACofA;EA+BA,eAAA;ADhhBA;ACihBA;EACA,YAAA;EACA,iBAAA;EACA,sBAAA;EACA,sBAAA;EACA,iBAAA;AD/gBA;AC0eA;EAwCA,cAAA;AD/gBA;ACghBA;EACA,aAAA;EACA,mBAAA;EACA,oBAAA;EACA,iBAAA;EACA,gBAAA;EACA,mBAAA;EACA,gBAAA;AD9gBA;AC8dA;EAmDA,cAAA;AD9gBA;AC+gBA;EACA,kBAAA;AD7gBA;AC8gBA;EACA,kBAAA;EACA,QAAA;EACA,SAAA;EACA,gCAAA;EACA,WAAA;EACA,YAAA;EACA,kBAAA;EACA,WAAA;EACA,eAAA;EACA,mBAAA;EACA,iBAAA;EACA,gBAAA;EACA,aAAA;AD5gBA;;AAEA,oCAAoC",
2221
+ "mappings": "AAAA,gBAAgB;ACwkBhB;EACA,aAAA;EACA,sBAAA;EACA,aAAA;ADtkBA;ACwkBA;EACA,OAAA;EACA,aAAA;EACA,gBAAA;ADtkBA;AC8jBA;EAWA;;;;;IAAA;ADjkBA;ACwkBA;;EAEA,eAAA;ADtkBA;ACkjBA;EAwBA;;;;IAAA;ADnkBA;ACwkBA;;EAEA,kBAAA;ADtkBA;ACuiBA;EAkCA,sBAAA;ADtkBA;ACukBA;EACA,YAAA;EACA,iBAAA;EACA,sBAAA;EACA,sBAAA;EACA,iBAAA;ADrkBA;AC6hBA;EA2CA,eAAA;ADrkBA;ACskBA;EACA,YAAA;EACA,iBAAA;EACA,sBAAA;EACA,sBAAA;EACA,iBAAA;ADpkBA;ACmhBA;EAoDA,cAAA;ADpkBA;ACqkBA;EACA,aAAA;EACA,mBAAA;EACA,oBAAA;EACA,iBAAA;EACA,gBAAA;EACA,mBAAA;EACA,gBAAA;ADnkBA;ACugBA;EA+DA,cAAA;ADnkBA;ACokBA;EACA,kBAAA;ADlkBA;ACmkBA;EACA,uBAAA;EACA,8BAAA;EACA,4BAAA;EACA,+BAAA;ADjkBA;ACmkBA;EACA,gBAAA;EACA,WAAA;EACA,YAAA;EACA,kBAAA;EACA,WAAA;EACA,eAAA;EACA,mBAAA;EACA,iBAAA;EACA,iBAAA;EACA,aAAA;ADjkBA;;AAEA,oCAAoC",
2160
2222
  "file": "index.vue",
2161
- "sourcesContent": ["@charset \"UTF-8\";\n.g-table__wrapper {\n /**\n * 修复\"宽屏下表格两侧出现空白\"问题:\n * 当视口宽度大于表格内容宽度时,主表(ant-table-scroll)不会自动拉伸,\n * 而固定列(fixed-left/fixed-right)是 position:absolute 定位在容器边缘,中间就出现空白。\n * 解决方案:让主表的 table 元素 min-width:100%,使其始终填满滚动容器。\n */\n}\n.g-table__wrapper ::v-deep .ant-table-scroll .ant-table-header table,\n.g-table__wrapper ::v-deep .ant-table-scroll .ant-table-body table {\n min-width: 100%;\n}\n.g-table__wrapper {\n /**\n * 修复\"固定列 + scroll.x\"场景下,主表(ant-table-scroll)里会渲染一份 fixed 列的占位 header/cell。\n * 这份占位本来只用于计算宽度,但在某些布局下会被看见,表现为\"操作列前多了一大块空白/空列\"。\n * 这里用 visibility:hidden 隐藏占位(不影响占位宽度与 fixed 计算),避免视觉空白。\n */\n}\n.g-table__wrapper ::v-deep .ant-table-scroll .ant-table-header thead > tr > th.ant-table-fixed-columns-in-body.ant-table-row-cell-last,\n.g-table__wrapper ::v-deep .ant-table-scroll .ant-table-body tbody > tr > td.ant-table-fixed-columns-in-body.ant-table-row-cell-last {\n visibility: hidden;\n}\n.g-table__wrapper {\n /* 强制统一行高,确保主表和固定列对齐 */\n}\n.g-table__wrapper ::v-deep .ant-table-tbody > tr > td {\n height: 54px;\n padding: 8px 16px;\n vertical-align: middle;\n box-sizing: border-box;\n line-height: 38px;\n}\n.g-table__wrapper {\n /* 表头也统一高度和样式 */\n}\n.g-table__wrapper ::v-deep .ant-table-thead > tr > th {\n height: 54px;\n padding: 8px 16px;\n vertical-align: middle;\n box-sizing: border-box;\n line-height: 38px;\n}\n.g-table__wrapper {\n /* 分页区域固定在底部 */\n}\n.g-table__wrapper .g-table__pagination {\n display: flex;\n flex-direction: row;\n justify-content: end;\n border-top: unset;\n padding-top: 8px;\n padding-bottom: 8px;\n background: #fff;\n}\n.g-table__wrapper {\n /* 空数据状态顶部显示 */\n}\n.g-table__wrapper .g-table__no-data {\n position: relative;\n}\n.g-table__wrapper .g-table__no-data ::v-deep .ant-table-placeholder {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -40%);\n width: 100%;\n height: 100%;\n text-align: center;\n color: #999;\n font-size: 14px;\n font-weight: normal;\n line-height: 20px;\n overflow: hidden;\n border: unset;\n}\n\n/*# sourceMappingURL=index.vue.map */", "<template>\r\n <div class=\"g-table__wrapper\" ref=\"tableWrapper\" :style=\"wrapperStyle\">\r\n <a-table\r\n :key=\"tableRenderKey\"\r\n :bordered=\"bordered\"\r\n :class=\"[isNoData && 'g-table__no-data']\"\r\n :pagination=\"false\"\r\n :loading=\"loading\"\r\n size=\"middle\"\r\n :columns=\"smartColumns\"\r\n :row-selection=\"smartRowSelection\"\r\n :row-class-name=\"setRowClassName\"\r\n :data-source=\"dataSource\"\r\n :scroll=\"smartScroll\">\r\n <template slot=\"action\" slot-scope=\"record\">\r\n <Actions v-on=\"$listeners\" :data-source=\"actions\" :record=\"record\"></Actions>\r\n </template>\r\n </a-table>\r\n <div class=\"g-table__pagination\">\r\n <a-pagination\r\n :show-total=\"all => `共 ${all} 条数据`\"\r\n v-if=\"mode === 'default'\"\r\n show-size-changer \r\n show-quick-jumper\r\n :pageSize=\"innerPageSize\"\r\n :current=\"innerCurrentPage\"\r\n :pageSizeOptions=\"pageSizeOptions\"\r\n @change=\"onChangePagination\"\r\n @showSizeChange=\"onShowSizeChange\"\r\n :total=\"total\">\r\n </a-pagination>\r\n <ele-pagination\r\n v-else\r\n :pageSize=\"innerPageSize\"\r\n :current=\"innerCurrentPage\"\r\n :pageSizeOptions=\"pageSizeOptions\"\r\n @change=\"onChangePagination\"\r\n @showSizeChange=\"onShowSizeChange\"\r\n :total=\"total\"\r\n ></ele-pagination>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script>\r\nimport Actions from './action.vue'\r\nexport default {\r\n name: 'ele-table',\r\n components: {\r\n Actions\r\n },\r\n props: {\r\n mode: {\r\n type: String,\r\n default: 'default',\r\n validator: (value) => {\r\n return ['default', 'next-cursor'].includes(value)\r\n }\r\n },\r\n // ant table wrapper\r\n height: {\r\n type: Number\r\n },\r\n width: {\r\n type: Number\r\n },\r\n x: {\r\n // ant-design-vue: scroll.x 支持 number | string(如 'max-content')\r\n type: [Number, String],\r\n default: 1200\r\n },\r\n y: {\r\n type: Number,\r\n default: 200\r\n },\r\n scroll: {\r\n type: Object\r\n },\r\n rowSelection: {\r\n type: Object\r\n },\r\n actions: {\r\n type: Array,\r\n default: () => []\r\n },\r\n total: {\r\n type: Number,\r\n default: 0\r\n },\r\n loading: {\r\n type: Boolean,\r\n default: false\r\n },\r\n columns: {\r\n type: Array,\r\n default: () => []\r\n },\r\n dataSource: {\r\n type: Array,\r\n default: () => []\r\n },\r\n pageSize: {\r\n type: [Number, String],\r\n default: 10\r\n },\r\n pageSizeOptions: {\r\n type: Array,\r\n default: () => ['10', '20', '30', '40']\r\n },\r\n bordered: {\r\n type: Boolean,\r\n default: true\r\n }\r\n },\r\n data() {\r\n return {\r\n tableHeaderHeight: 0,\r\n paginationHeight: 0,\r\n innerPageSize: 10,\r\n innerCurrentPage: 1,\r\n tableContentHeight: 0,\r\n obs: [],\r\n // 容器宽度,用于智能判断是否需要 fixed 列\r\n containerWidth: 0,\r\n // 用于强制重新渲染表格(当 fixed 列状态切换时)\r\n tableRenderKey: 0\r\n }\r\n },\r\n computed: {\r\n wrapperStyle () {\r\n // 外层容器样式\r\n if (!this.height) return {}\r\n return { height: `${this.height}px` }\r\n },\r\n needScrollY () {\r\n // 判断是否需要 y 轴滚动:基于数据行数与可用高度预估\r\n if (!this.height) return false\r\n \r\n const availableHeight = this.getScrollHeightByHeight\r\n if (availableHeight <= 0) return false\r\n \r\n // 预估每行高度(包含边框),antd 默认约 54px\r\n const estimatedRowHeight = 54\r\n const estimatedTableHeight = this.dataSource.length * estimatedRowHeight\r\n \r\n return estimatedTableHeight > availableHeight\r\n },\r\n innerColumns () {\r\n return this.columns.filter(col => !Object.keys(col).includes('multiple'))\r\n },\r\n isNoData () {\r\n return !this.dataSource.length\r\n },\r\n getScrollHeightByHeight () {\r\n // 始终返回可用的剩余高度,让表格内容不足时也能占满容器\r\n return this.height - this.tableHeaderHeight - this.paginationHeight\r\n },\r\n isFlexColumn () {\r\n return this.columns.every(col => !col.width)\r\n },\r\n /**\r\n * 计算所有列的总宽度(包括 rowSelection 的 checkbox 列和操作列)\r\n */\r\n totalColumnsWidth () {\r\n const cols = this.innerColumns || []\r\n let total = cols.reduce((sum, col) => {\r\n const w = col && col.width\r\n return sum + (typeof w === 'number' ? w : 0)\r\n }, 0)\r\n // rowSelection 的 checkbox/radio 列,antd 默认约 60px\r\n if (this.rowSelection) total += 60\r\n // 操作列(operations)的宽度\r\n if (this.operations && this.operations.width && typeof this.operations.width === 'number') {\r\n total += this.operations.width\r\n }\r\n return total\r\n },\r\n /**\r\n * 是否需要横向滚动:容器宽度 < 列总宽度\r\n */\r\n needHorizontalScroll () {\r\n // 未获取容器宽度前,先假定需要滚动(保守策略)\r\n if (!this.containerWidth) return true\r\n // 加一点容差\r\n return this.containerWidth < this.totalColumnsWidth - 5\r\n },\r\n /**\r\n * 智能列配置:\r\n * - 当需要横向滚动时,保留原始 fixed 属性\r\n * - 当容器足够宽时,移除 fixed 属性,让表格自动铺满\r\n */\r\n smartColumns () {\r\n if (this.needHorizontalScroll) {\r\n // 需要滚动,保留原始配置\r\n return this.innerColumns\r\n }\r\n // 不需要滚动,移除所有 fixed 属性\r\n return this.innerColumns.map(col => {\r\n if (col.fixed) {\r\n const { fixed, ...rest } = col\r\n return rest\r\n }\r\n return col\r\n })\r\n },\r\n /**\r\n * 智能 scroll 配置:\r\n * - 当需要横向滚动时,设置 scroll.x\r\n * - 当容器足够宽时,不设置 scroll.x,避免产生空白区域\r\n */\r\n smartScroll () {\r\n if (!this.needHorizontalScroll) {\r\n // 不需要横向滚动,只保留 y 方向(如果需要)\r\n if (this.height && this.needScrollY) {\r\n const availableHeight = this.tableHeaderHeight && this.paginationHeight \r\n ? this.getScrollHeightByHeight \r\n : this.height - 100\r\n if (availableHeight > 50) {\r\n return { y: availableHeight }\r\n }\r\n }\r\n return {}\r\n }\r\n // 需要横向滚动,使用原有逻辑\r\n return this.getScroll\r\n },\r\n /**\r\n * 智能 rowSelection 配置:\r\n * - 当需要横向滚动时,保留原始 fixed 属性\r\n * - 当容器足够宽时,移除 fixed 属性\r\n */\r\n smartRowSelection () {\r\n if (!this.rowSelection) return null\r\n if (this.needHorizontalScroll) {\r\n return this.rowSelection\r\n }\r\n // 不需要滚动,移除 fixed 属性\r\n if (this.rowSelection.fixed) {\r\n const { fixed, ...rest } = this.rowSelection\r\n return rest\r\n }\r\n return this.rowSelection\r\n },\r\n getScroll () {\r\n if (this.scroll) {\r\n return this.scroll\r\n } else {\r\n // 固定列需要 scroll.x 才能正确同步行高,始终设置一个有效值\r\n let baseX = (this.x === '' || this.x === null || this.x === undefined) ? 1200 : this.x\r\n\r\n // 解决“x 给太大导致操作列前出现大块空白”的问题:\r\n // 当所有列都给了明确 width 时,scroll.x 取列宽总和最合理;大于总和会产生多余区域。\r\n if (typeof baseX === 'number') {\r\n const cols = this.innerColumns || []\r\n const total = cols.reduce((sum, col) => {\r\n const w = col && col.width\r\n return sum + (typeof w === 'number' ? w : 0)\r\n }, 0)\r\n\r\n // rowSelection 的 checkbox/radio 列是 antd 自动加的,给一个经验宽度避免误差\r\n const selectionWidth = this.rowSelection ? 60 : 0\r\n\r\n // 只有当所有列都明确给了宽度(total > 0)时才 clamp\r\n if (total > 0) {\r\n const minX = total + selectionWidth\r\n if (baseX > minX) baseX = minX\r\n }\r\n }\r\n \r\n if (this.height && this.needScrollY) {\r\n const availableHeight = this.tableHeaderHeight && this.paginationHeight \r\n ? this.getScrollHeightByHeight \r\n : this.height - 100\r\n \r\n if (availableHeight > 50) {\r\n return { x: baseX, y: availableHeight }\r\n }\r\n }\r\n return { x: baseX }\r\n }\r\n }\r\n },\r\n watch: {\r\n pageSize: {\r\n handler (pageSize) {\r\n this.innerPageSize = pageSize\r\n },\r\n immediate: true\r\n },\r\n /**\r\n * 监听 needHorizontalScroll 变化,强制重新渲染表格\r\n * 当从\"需要固定列\"切换到\"不需要固定列\"或反之时,antd 的 a-table 需要完全重新渲染\r\n */\r\n needHorizontalScroll (newVal, oldVal) {\r\n if (newVal !== oldVal) {\r\n // 更新 key 强制 Vue 销毁并重建 a-table 组件\r\n this.tableRenderKey++\r\n // 使用重试机制确保固定列完全渲染\r\n this.$nextTick(() => {\r\n this.retrySyncFixedColumns(newVal)\r\n })\r\n }\r\n }\r\n },\r\n methods: {\r\n onShowSizeChange (current, pageSize) {\r\n this.innerCurrentPage = current\r\n this.innerPageSize = pageSize\r\n this.$emit('change-page', current, pageSize)\r\n },\r\n setPaginationHeight () {\r\n this.$nextTick(() => {\r\n const el = this.$el.querySelector('.g-table__pagination')\r\n if (el) {\r\n const { height } = el.getBoundingClientRect()\r\n this.paginationHeight = height\r\n }\r\n })\r\n },\r\n setTableHeaderHeight () {\r\n this.$nextTick(() => {\r\n const el = this.$el.querySelector('.ant-table-header')\r\n if (!el) return\r\n const { height } = el.getBoundingClientRect()\r\n this.tableHeaderHeight = height\r\n })\r\n },\r\n setTableTbodyHeight () {\r\n this.$nextTick(() => {\r\n this.setTableHeaderHeight()\r\n })\r\n },\r\n setRowClassName (record, idx) {\r\n return idx % 2 === 0 ? 'g-table__row--even' : 'g-table__row--odd'\r\n },\r\n onChangePagination (page, pageSize) {\r\n this.innerCurrentPage = page\r\n this.innerPageSize = pageSize\r\n this.$emit('change-page', page, pageSize)\r\n },\r\n syncFixedColumns () {\r\n // 强制 ant-design-vue 重新计算固定列的宽度\r\n this.$nextTick(() => {\r\n const tableEl = this.$el.querySelector('.ant-table')\r\n if (tableEl) {\r\n // 触发窗口 resize 事件,让 ant-design-vue 重新计算\r\n window.dispatchEvent(new Event('resize'))\r\n }\r\n })\r\n },\r\n /**\r\n * 带重试的固定列同步机制\r\n * antd 的 a-table 在 key 变化后重新渲染固定列需要一定时间,\r\n * 这里通过重试确保固定列渲染完成后再同步。\r\n * @param {boolean} needFixed - 是否需要固定列\r\n * @param {number} retries - 当前重试次数\r\n */\r\n retrySyncFixedColumns (needFixed, retries = 0) {\r\n const MAX_RETRIES = 5\r\n const DELAY = 100 // 每次延迟 100ms\r\n\r\n setTimeout(() => {\r\n this.syncFixedColumns()\r\n this.syncHeaderTableWidth()\r\n this.bindScrollSync()\r\n\r\n // 如果需要固定列,检查是否已渲染\r\n if (needFixed && retries < MAX_RETRIES) {\r\n const hasFixedLeft = this.$el.querySelector('.ant-table-fixed-left .ant-table-body tbody tr')\r\n const hasFixedRight = this.$el.querySelector('.ant-table-fixed-right .ant-table-body tbody tr')\r\n \r\n // 如果有固定列配置但还没渲染出来,继续重试\r\n const hasFixedConfig = this.innerColumns.some(c => c.fixed) || (this.rowSelection && this.rowSelection.fixed)\r\n if (hasFixedConfig && !hasFixedLeft && !hasFixedRight) {\r\n this.retrySyncFixedColumns(needFixed, retries + 1)\r\n }\r\n }\r\n }, DELAY)\r\n },\r\n /**\r\n * 修复 x: 'max-content' 场景下表头不跟着横向滚动的问题。\r\n * 原因:ant-design-vue 的 header table 和 body table 各自算 max-content,\r\n * header 按表头文字算、body 按实际数据算,二者宽度不同时 header 就\"滚不动\"。\r\n * 方案:数据渲染后把 header table 的 width 强制设成和 body table 一样。\r\n */\r\n syncHeaderTableWidth () {\r\n this.$nextTick(() => {\r\n const headerTable = this.$el.querySelector('.ant-table-scroll .ant-table-header table')\r\n const bodyTable = this.$el.querySelector('.ant-table-scroll .ant-table-body table')\r\n if (!headerTable || !bodyTable) return\r\n\r\n const bodyW = bodyTable.getBoundingClientRect().width\r\n const headerW = headerTable.getBoundingClientRect().width\r\n\r\n // 始终同步表头宽度到表体宽度,确保窗口变大/变小时都能正确响应\r\n // 只有当宽度差异超过 2px 时才更新,避免频繁设置样式\r\n if (Math.abs(bodyW - headerW) > 2) {\r\n headerTable.style.width = `${bodyW}px`\r\n headerTable.style.minWidth = `${bodyW}px`\r\n }\r\n })\r\n },\r\n /**\r\n * 监听表体横向滚动,同步到表头(防止 antd 自带同步失效)\r\n */\r\n bindScrollSync () {\r\n const body = this.$el.querySelector('.ant-table-scroll .ant-table-body')\r\n const header = this.$el.querySelector('.ant-table-scroll .ant-table-header')\r\n if (!body || !header) return\r\n\r\n if (this._scrollHandler) return // 已绑定\r\n this._scrollHandler = () => {\r\n header.scrollLeft = body.scrollLeft\r\n }\r\n body.addEventListener('scroll', this._scrollHandler, { passive: true })\r\n },\r\n unbindScrollSync () {\r\n const body = this.$el.querySelector('.ant-table-scroll .ant-table-body')\r\n if (body && this._scrollHandler) {\r\n body.removeEventListener('scroll', this._scrollHandler)\r\n this._scrollHandler = null\r\n }\r\n },\r\n /**\r\n * 测量容器宽度\r\n */\r\n measureContainerWidth () {\r\n const wrapper = this.$refs.tableWrapper\r\n if (wrapper) {\r\n this.containerWidth = wrapper.clientWidth\r\n }\r\n },\r\n /**\r\n * 使用 ResizeObserver 监听容器宽度变化\r\n */\r\n observeContainerWidth () {\r\n const wrapper = this.$refs.tableWrapper\r\n if (!wrapper || typeof ResizeObserver === 'undefined') return\r\n\r\n if (this._containerResizeObserver) return // 已绑定\r\n\r\n this._containerResizeObserver = new ResizeObserver((entries) => {\r\n for (const entry of entries) {\r\n const newWidth = entry.contentRect.width\r\n // 只有宽度变化超过阈值才触发更新,避免微小变化导致频繁重渲染\r\n if (Math.abs(newWidth - this.containerWidth) > 10) {\r\n const oldNeedScroll = this.needHorizontalScroll\r\n this.containerWidth = newWidth\r\n // 容器宽度变化会触发 needHorizontalScroll 的重新计算\r\n // needHorizontalScroll 的 watcher 会处理表格重新渲染和列宽同步\r\n // 这里不需要直接调用 syncFixedColumns,避免与 watcher 的执行时机冲突\r\n // 无论 needHorizontalScroll 是否变化,都立即同步表头宽度\r\n // 确保窗口变大/变小时表头都能及时响应\r\n this.$nextTick(() => {\r\n // 使用 requestAnimationFrame 确保在浏览器重绘后同步表头宽度\r\n // 这样能确保表体宽度已经更新完成\r\n requestAnimationFrame(() => {\r\n this.syncHeaderTableWidth()\r\n })\r\n \r\n // 如果 needHorizontalScroll 状态没有变化,说明只是列宽需要调整,直接同步\r\n // 如果状态变化了,watcher 会处理重新渲染\r\n if (this.needHorizontalScroll === oldNeedScroll) {\r\n // 防抖:延迟同步固定列,避免频繁调用\r\n if (this._resizeDebounceTimer) {\r\n clearTimeout(this._resizeDebounceTimer)\r\n }\r\n this._resizeDebounceTimer = setTimeout(() => {\r\n this.syncFixedColumns()\r\n this.syncHeaderTableWidth()\r\n this.bindScrollSync()\r\n }, 150)\r\n }\r\n })\r\n }\r\n }\r\n })\r\n this._containerResizeObserver.observe(wrapper)\r\n },\r\n /**\r\n * 断开容器宽度监听\r\n */\r\n unobserveContainerWidth () {\r\n if (this._containerResizeObserver) {\r\n this._containerResizeObserver.disconnect()\r\n this._containerResizeObserver = null\r\n }\r\n // 清理防抖定时器\r\n if (this._resizeDebounceTimer) {\r\n clearTimeout(this._resizeDebounceTimer)\r\n this._resizeDebounceTimer = null\r\n }\r\n }\r\n },\r\n mounted() {\r\n this.$nextTick(() => {\r\n // 先测量容器宽度,用于智能判断是否需要 fixed 列\r\n this.measureContainerWidth()\r\n this.observeContainerWidth()\r\n\r\n this.setPaginationHeight()\r\n setTimeout(() => {\r\n this.setTableTbodyHeight()\r\n this.setPaginationHeight()\r\n // 强制同步固定列和主表的列宽\r\n this.syncFixedColumns()\r\n // 同步表头 table 宽度(修复 max-content 场景)\r\n this.syncHeaderTableWidth()\r\n // 绑定横向滚动同步\r\n this.bindScrollSync()\r\n }, 200)\r\n })\r\n \r\n // 监听数据变化,重新同步列宽\r\n this.$watch('dataSource', () => {\r\n this.$nextTick(() => {\r\n setTimeout(() => {\r\n this.syncFixedColumns()\r\n this.syncHeaderTableWidth()\r\n this.bindScrollSync()\r\n }, 100)\r\n })\r\n }, { deep: true })\r\n },\r\n destroyed () {\r\n this.obs.forEach(ob => ob.disconnect())\r\n this.unbindScrollSync()\r\n this.unobserveContainerWidth()\r\n }\r\n}\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n.g-table__wrapper {\r\n /**\r\n * 修复\"宽屏下表格两侧出现空白\"问题:\r\n * 当视口宽度大于表格内容宽度时,主表(ant-table-scroll)不会自动拉伸,\r\n * 而固定列(fixed-left/fixed-right)是 position:absolute 定位在容器边缘,中间就出现空白。\r\n * 解决方案:让主表的 table 元素 min-width:100%,使其始终填满滚动容器。\r\n */\r\n ::v-deep .ant-table-scroll .ant-table-header table,\r\n ::v-deep .ant-table-scroll .ant-table-body table {\r\n min-width: 100%;\r\n }\r\n\r\n /**\r\n * 修复\"固定列 + scroll.x\"场景下,主表(ant-table-scroll)里会渲染一份 fixed 列的占位 header/cell。\r\n * 这份占位本来只用于计算宽度,但在某些布局下会被看见,表现为\"操作列前多了一大块空白/空列\"。\r\n * 这里用 visibility:hidden 隐藏占位(不影响占位宽度与 fixed 计算),避免视觉空白。\r\n */\r\n ::v-deep .ant-table-scroll .ant-table-header thead > tr > th.ant-table-fixed-columns-in-body.ant-table-row-cell-last,\r\n ::v-deep .ant-table-scroll .ant-table-body tbody > tr > td.ant-table-fixed-columns-in-body.ant-table-row-cell-last {\r\n visibility: hidden;\r\n }\r\n\r\n /* 强制统一行高,确保主表和固定列对齐 */\r\n ::v-deep .ant-table-tbody > tr > td {\r\n height: 54px;\r\n padding: 8px 16px;\r\n vertical-align: middle;\r\n box-sizing: border-box;\r\n line-height: 38px;\r\n }\r\n\r\n /* 表头也统一高度和样式 */\r\n ::v-deep .ant-table-thead > tr > th {\r\n height: 54px;\r\n padding: 8px 16px;\r\n vertical-align: middle;\r\n box-sizing: border-box;\r\n line-height: 38px;\r\n }\r\n\r\n /* 分页区域固定在底部 */\r\n .g-table__pagination {\r\n display: flex;\r\n flex-direction: row;\r\n justify-content: end;\r\n border-top: unset;\r\n padding-top: 8px;\r\n padding-bottom: 8px;\r\n background: #fff;\r\n }\r\n\r\n /* 空数据状态顶部显示 */\r\n .g-table__no-data {\r\n position: relative;\r\n ::v-deep .ant-table-placeholder {\r\n position: absolute;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -40%);\r\n width: 100%;\r\n height: 100%;\r\n text-align: center;\r\n color: #999;\r\n font-size: 14px;\r\n font-weight: normal;\r\n line-height: 20px;\r\n overflow: hidden;\r\n border: unset;\r\n }\r\n }\r\n}\r\n</style>"]
2223
+ "sourcesContent": ["@charset \"UTF-8\";\n.g-table__wrapper {\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.g-table__wrapper .g-table__main {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n.g-table__wrapper {\n /**\n * 修复\"宽屏下表格两侧出现空白\"问题:\n * 当视口宽度大于表格内容宽度时,主表(ant-table-scroll)不会自动拉伸,\n * 而固定列(fixed-left/fixed-right)是 position:absolute 定位在容器边缘,中间就出现空白。\n * 解决方案:让主表的 table 元素 min-width:100%,使其始终填满滚动容器。\n */\n}\n.g-table__wrapper:not(.g-table__wrapper--need-h-scroll) ::v-deep .ant-table-scroll .ant-table-header table,\n.g-table__wrapper:not(.g-table__wrapper--need-h-scroll) ::v-deep .ant-table-scroll .ant-table-body table {\n min-width: 100%;\n}\n.g-table__wrapper {\n /**\n * 修复\"固定列 + scroll.x\"场景下,主表(ant-table-scroll)里会渲染一份 fixed 列的占位 header/cell。\n * 这份占位本来只用于计算宽度,但在某些布局下会被看见,表现为\"操作列前多了一大块空白/空列\"。\n * 这里用 visibility:hidden 隐藏占位(不影响占位宽度与 fixed 计算),避免视觉空白。\n */\n}\n.g-table__wrapper ::v-deep .ant-table-scroll .ant-table-header thead > tr > th.ant-table-fixed-columns-in-body.ant-table-row-cell-last,\n.g-table__wrapper ::v-deep .ant-table-scroll .ant-table-body tbody > tr > td.ant-table-fixed-columns-in-body.ant-table-row-cell-last {\n visibility: hidden;\n}\n.g-table__wrapper {\n /* 强制统一行高,确保主表和固定列对齐 */\n}\n.g-table__wrapper ::v-deep .ant-table-tbody > tr > td {\n height: 54px;\n padding: 8px 16px;\n vertical-align: middle;\n box-sizing: border-box;\n line-height: 38px;\n}\n.g-table__wrapper {\n /* 表头也统一高度和样式 */\n}\n.g-table__wrapper ::v-deep .ant-table-thead > tr > th {\n height: 54px;\n padding: 8px 16px;\n vertical-align: middle;\n box-sizing: border-box;\n line-height: 38px;\n}\n.g-table__wrapper {\n /* 分页区域固定在底部 */\n}\n.g-table__wrapper .g-table__pagination {\n display: flex;\n flex-direction: row;\n justify-content: end;\n border-top: unset;\n padding-top: 8px;\n padding-bottom: 8px;\n background: #fff;\n}\n.g-table__wrapper {\n /* 空数据状态顶部显示 */\n}\n.g-table__wrapper .g-table__no-data {\n position: relative;\n}\n.g-table__wrapper .g-table__no-data ::v-deep .ant-table-placeholder > td {\n height: auto !important;\n line-height: normal !important;\n padding-top: 24px !important;\n padding-bottom: 24px !important;\n}\n.g-table__wrapper .g-table__no-data ::v-deep .ant-table-placeholder {\n position: static;\n width: auto;\n height: auto;\n text-align: center;\n color: #999;\n font-size: 14px;\n font-weight: normal;\n line-height: 20px;\n overflow: visible;\n border: unset;\n}\n\n/*# sourceMappingURL=index.vue.map */", "<template>\r\n <div class=\"g-table__wrapper\" :class=\"{ 'g-table__wrapper--need-h-scroll': needHorizontalScroll }\" ref=\"tableWrapper\" :style=\"wrapperStyle\">\r\n <div class=\"g-table__main\">\r\n <a-table\r\n :key=\"tableRenderKey\"\r\n :bordered=\"bordered\"\r\n :class=\"[isNoData && 'g-table__no-data']\"\r\n :pagination=\"false\"\r\n :loading=\"loading\"\r\n size=\"middle\"\r\n :columns=\"smartColumns\"\r\n :row-selection=\"smartRowSelection\"\r\n :row-class-name=\"setRowClassName\"\r\n :data-source=\"dataSource\"\r\n :scroll=\"smartScroll\">\r\n <template slot=\"action\" slot-scope=\"record\">\r\n <Actions v-on=\"$listeners\" :data-source=\"actions\" :record=\"record\"></Actions>\r\n </template>\r\n </a-table>\r\n </div>\r\n <div class=\"g-table__pagination\">\r\n <a-pagination\r\n :show-total=\"all => `共 ${all} 条数据`\"\r\n v-if=\"mode === 'default'\"\r\n show-size-changer \r\n show-quick-jumper\r\n :pageSize=\"innerPageSize\"\r\n :current=\"innerCurrentPage\"\r\n :pageSizeOptions=\"pageSizeOptions\"\r\n @change=\"onChangePagination\"\r\n @showSizeChange=\"onShowSizeChange\"\r\n :total=\"total\">\r\n </a-pagination>\r\n <ele-pagination\r\n v-else\r\n :pageSize=\"innerPageSize\"\r\n :current=\"innerCurrentPage\"\r\n :pageSizeOptions=\"pageSizeOptions\"\r\n @change=\"onChangePagination\"\r\n @showSizeChange=\"onShowSizeChange\"\r\n :total=\"total\"\r\n ></ele-pagination>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script>\r\nimport Actions from './action.vue'\r\nexport default {\r\n name: 'ele-table',\r\n components: {\r\n Actions\r\n },\r\n props: {\r\n mode: {\r\n type: String,\r\n default: 'default',\r\n validator: (value) => {\r\n return ['default', 'next-cursor'].includes(value)\r\n }\r\n },\r\n // ant table wrapper\r\n height: {\r\n type: Number\r\n },\r\n width: {\r\n type: Number\r\n },\r\n x: {\r\n // ant-design-vue: scroll.x 支持 number | string(如 'max-content')\r\n type: [Number, String],\r\n default: 1200\r\n },\r\n y: {\r\n type: Number,\r\n default: 200\r\n },\r\n scroll: {\r\n type: Object\r\n },\r\n rowSelection: {\r\n type: Object\r\n },\r\n actions: {\r\n type: Array,\r\n default: () => []\r\n },\r\n total: {\r\n type: Number,\r\n default: 0\r\n },\r\n loading: {\r\n type: Boolean,\r\n default: false\r\n },\r\n columns: {\r\n type: Array,\r\n default: () => []\r\n },\r\n dataSource: {\r\n type: Array,\r\n default: () => []\r\n },\r\n pageSize: {\r\n type: [Number, String],\r\n default: 10\r\n },\r\n pageSizeOptions: {\r\n type: Array,\r\n default: () => ['10', '20', '30', '40']\r\n },\r\n bordered: {\r\n type: Boolean,\r\n default: true\r\n }\r\n },\r\n data() {\r\n return {\r\n tableHeaderHeight: 0,\r\n paginationHeight: 0,\r\n innerPageSize: 10,\r\n innerCurrentPage: 1,\r\n tableContentHeight: 0,\r\n obs: [],\r\n // 容器宽度,用于智能判断是否需要 fixed 列\r\n containerWidth: 0,\r\n // 用于强制重新渲染表格(当 fixed 列状态切换时)\r\n tableRenderKey: 0\r\n }\r\n },\r\n computed: {\r\n wrapperStyle () {\r\n // 外层容器样式\r\n if (!this.height) return {}\r\n return { height: `${this.height}px` }\r\n },\r\n needScrollY () {\r\n // 判断是否需要 y 轴滚动:基于数据行数与可用高度预估\r\n if (!this.height) return false\r\n \r\n const availableHeight = this.getScrollHeightByHeight\r\n if (availableHeight <= 0) return false\r\n \r\n // 预估每行高度(包含边框),antd 默认约 54px\r\n const estimatedRowHeight = 54\r\n const estimatedTableHeight = this.dataSource.length * estimatedRowHeight\r\n \r\n return estimatedTableHeight > availableHeight\r\n },\r\n innerColumns () {\r\n return this.columns.filter(col => !Object.keys(col).includes('multiple'))\r\n },\r\n isNoData () {\r\n return !this.dataSource.length\r\n },\r\n getScrollHeightByHeight () {\r\n // 始终基于稳定的表头/分页高度计算可用区域,避免 fallback 造成内容压到分页区\r\n const headerHeight = this.tableHeaderHeight || 64\r\n const paginationHeight = this.paginationHeight || 64\r\n const rawHeight = this.height - headerHeight - paginationHeight\r\n // 规避小数像素导致的 1px 裁切,给表体预留 1px 安全边距\r\n return Math.max(Math.floor(rawHeight) - 1, 0)\r\n },\r\n isFlexColumn () {\r\n return this.columns.every(col => !col.width)\r\n },\r\n /**\r\n * 计算所有列的总宽度(包括 rowSelection 的 checkbox 列和操作列)\r\n */\r\n totalColumnsWidth () {\r\n const cols = this.innerColumns || []\r\n let total = cols.reduce((sum, col) => {\r\n const w = col && col.width\r\n return sum + (typeof w === 'number' ? w : 0)\r\n }, 0)\r\n // rowSelection 的 checkbox/radio 列,antd 默认约 60px\r\n if (this.rowSelection) total += 60\r\n // 操作列(operations)的宽度\r\n if (this.operations && this.operations.width && typeof this.operations.width === 'number') {\r\n total += this.operations.width\r\n }\r\n return total\r\n },\r\n estimatedMinContentWidth () {\r\n const cols = this.innerColumns || []\r\n let specifiedWidthSum = 0\r\n let unspecifiedCount = 0\r\n cols.forEach(col => {\r\n const w = col && col.width\r\n if (typeof w === 'number') {\r\n specifiedWidthSum += w\r\n } else {\r\n unspecifiedCount += 1\r\n }\r\n })\r\n const selectionWidth = this.rowSelection ? 60 : 0\r\n const operationWidth = (this.operations && typeof this.operations.width === 'number') ? this.operations.width : 0\r\n // 未配置宽度列给更保守的最小宽度,贴近运行时真实占宽,\r\n // 避免“容器已明显缩小但横向滚动仍不触发”。\r\n const unspecifiedMinWidth = unspecifiedCount * 320\r\n return specifiedWidthSum + unspecifiedMinWidth + selectionWidth + operationWidth\r\n },\r\n hasIncompleteColumnWidth () {\r\n const cols = this.innerColumns || []\r\n if (!cols.length) return false\r\n return cols.some(col => typeof (col && col.width) !== 'number')\r\n },\r\n /**\r\n * 是否需要横向滚动:容器宽度 < 列总宽度\r\n */\r\n needHorizontalScroll () {\r\n // 未获取容器宽度前,先假定需要滚动(保守策略)\r\n if (!this.containerWidth) return true\r\n // 存在未配置 width 的列时,totalColumnsWidth 会低估真实内容宽度;\r\n // 此时改用保守阈值判定,避免误判导致列被压缩变形。\r\n if (this.hasIncompleteColumnWidth) {\r\n const baseFallbackX = typeof this.x === 'number' ? this.x : 1200\r\n const fallbackX = Math.max(baseFallbackX, this.estimatedMinContentWidth)\r\n return this.containerWidth < fallbackX - 5\r\n }\r\n // 加一点容差\r\n return this.containerWidth < this.totalColumnsWidth - 5\r\n },\r\n /**\r\n * 智能列配置:\r\n * - 当需要横向滚动时,保留原始 fixed 属性\r\n * - 当容器足够宽时,移除 fixed 属性,让表格自动铺满\r\n */\r\n smartColumns () {\r\n if (this.needHorizontalScroll) {\r\n // 需要滚动,保留原始配置\r\n return this.innerColumns\r\n }\r\n // 不需要滚动,移除所有 fixed 属性\r\n return this.innerColumns.map(col => {\r\n if (col.fixed) {\r\n const { fixed, ...rest } = col\r\n return rest\r\n }\r\n return col\r\n })\r\n },\r\n /**\r\n * 智能 scroll 配置:\r\n * - 当需要横向滚动时,设置 scroll.x\r\n * - 当容器足够宽时,不设置 scroll.x,避免产生空白区域\r\n */\r\n smartScroll () {\r\n if (!this.needHorizontalScroll) {\r\n // 不需要横向滚动时,只要存在高度约束就下发 y。\r\n // 旧逻辑依赖估算行高 needScrollY,遇到多行文本/变高行时会误判,\r\n // 导致 body 自然撑高(overflow: visible),内容超出可视区却没有纵向滚动。\r\n if (this.height) {\r\n const availableHeight = this.getScrollHeightByHeight\r\n if (availableHeight > 50) {\r\n return { y: availableHeight }\r\n }\r\n }\r\n return {}\r\n }\r\n // 需要横向滚动时,若未设置 y,会导致横向滚动条落在超高表体底部,\r\n // 外层容器 overflow:hidden 场景下用户看不到滚动条。\r\n // 这里在有高度约束时强制注入 y,保证横向滚动条始终在可视区域内。\r\n const baseScroll = this.getScroll || {}\r\n if (this.height && (baseScroll.y === undefined || baseScroll.y === null)) {\r\n const availableHeight = this.getScrollHeightByHeight\r\n if (availableHeight > 50) {\r\n return { ...baseScroll, y: availableHeight }\r\n }\r\n }\r\n // 需要横向滚动,其他情况使用原有逻辑\r\n return baseScroll\r\n },\r\n /**\r\n * 智能 rowSelection 配置:\r\n * - 当需要横向滚动时,保留原始 fixed 属性\r\n * - 当容器足够宽时,移除 fixed 属性\r\n */\r\n smartRowSelection () {\r\n if (!this.rowSelection) return null\r\n if (this.needHorizontalScroll) {\r\n return this.rowSelection\r\n }\r\n // 不需要滚动,移除 fixed 属性\r\n if (this.rowSelection.fixed) {\r\n const { fixed, ...rest } = this.rowSelection\r\n return rest\r\n }\r\n return this.rowSelection\r\n },\r\n getScroll () {\r\n if (this.scroll) {\r\n return this.scroll\r\n } else {\r\n // 固定列需要 scroll.x 才能正确同步行高,始终设置一个有效值\r\n let baseX = (this.x === '' || this.x === null || this.x === undefined) ? 1200 : this.x\r\n\r\n // 解决“x 给太大导致操作列前出现大块空白”的问题:\r\n // 当所有列都给了明确 width 时,scroll.x 取列宽总和最合理;大于总和会产生多余区域。\r\n if (typeof baseX === 'number') {\r\n const cols = this.innerColumns || []\r\n const total = cols.reduce((sum, col) => {\r\n const w = col && col.width\r\n return sum + (typeof w === 'number' ? w : 0)\r\n }, 0)\r\n\r\n // rowSelection 的 checkbox/radio 列是 antd 自动加的,给一个经验宽度避免误差\r\n const selectionWidth = this.rowSelection ? 60 : 0\r\n\r\n // 只有所有列都明确给了 width 时才 clamp,避免低估导致表格被挤压\r\n const allColsHaveWidth = cols.length > 0 && cols.every(col => typeof (col && col.width) === 'number')\r\n if (allColsHaveWidth && total > 0) {\r\n const minX = total + selectionWidth\r\n if (baseX > minX) baseX = minX\r\n }\r\n // 存在未配置宽度列时,x 至少取估算最小内容宽度,\r\n // 避免出现 needHorizontalScroll=true 但 scrollWidth==clientWidth 的“无滚动条”状态。\r\n if (this.hasIncompleteColumnWidth) {\r\n baseX = Math.max(baseX, this.estimatedMinContentWidth)\r\n }\r\n }\r\n if (this.height && this.needScrollY) {\r\n const availableHeight = this.getScrollHeightByHeight\r\n \r\n if (availableHeight > 50) {\r\n return { x: baseX, y: availableHeight }\r\n }\r\n }\r\n return { x: baseX }\r\n }\r\n }\r\n },\r\n watch: {\r\n pageSize: {\r\n handler (pageSize) {\r\n this.innerPageSize = pageSize\r\n },\r\n immediate: true\r\n },\r\n /**\r\n * 监听 needHorizontalScroll 变化,强制重新渲染表格\r\n * 当从\"需要固定列\"切换到\"不需要固定列\"或反之时,antd 的 a-table 需要完全重新渲染\r\n */\r\n needHorizontalScroll (newVal, oldVal) {\r\n if (newVal !== oldVal) {\r\n // 更新 key 强制 Vue 销毁并重建 a-table 组件\r\n this.tableRenderKey++\r\n // 使用重试机制确保固定列完全渲染\r\n this.$nextTick(() => {\r\n this.retrySyncFixedColumns(newVal)\r\n })\r\n }\r\n }\r\n },\r\n methods: {\r\n onShowSizeChange (current, pageSize) {\r\n this.innerCurrentPage = current\r\n this.innerPageSize = pageSize\r\n this.$emit('change-page', current, pageSize)\r\n },\r\n setPaginationHeight () {\r\n this.$nextTick(() => {\r\n const el = this.$el.querySelector('.g-table__pagination')\r\n if (el) {\r\n const { height } = el.getBoundingClientRect()\r\n this.paginationHeight = height\r\n }\r\n })\r\n },\r\n setTableHeaderHeight () {\r\n this.$nextTick(() => {\r\n const el = this.$el.querySelector('.ant-table-header') || this.$el.querySelector('.ant-table-thead')\r\n if (!el) return\r\n const { height } = el.getBoundingClientRect()\r\n this.tableHeaderHeight = height\r\n })\r\n },\r\n setTableTbodyHeight () {\r\n this.$nextTick(() => {\r\n this.setTableHeaderHeight()\r\n })\r\n },\r\n setRowClassName (record, idx) {\r\n return idx % 2 === 0 ? 'g-table__row--even' : 'g-table__row--odd'\r\n },\r\n onChangePagination (page, pageSize) {\r\n this.innerCurrentPage = page\r\n this.innerPageSize = pageSize\r\n this.$emit('change-page', page, pageSize)\r\n },\r\n syncFixedColumns () {\r\n // 强制 ant-design-vue 重新计算固定列的宽度\r\n this.$nextTick(() => {\r\n const tableEl = this.$el.querySelector('.ant-table')\r\n if (tableEl) {\r\n // 触发窗口 resize 事件,让 ant-design-vue 重新计算\r\n window.dispatchEvent(new Event('resize'))\r\n }\r\n })\r\n },\r\n /**\r\n * 带重试的固定列同步机制\r\n * antd 的 a-table 在 key 变化后重新渲染固定列需要一定时间,\r\n * 这里通过重试确保固定列渲染完成后再同步。\r\n * @param {boolean} needFixed - 是否需要固定列\r\n * @param {number} retries - 当前重试次数\r\n */\r\n retrySyncFixedColumns (needFixed, retries = 0) {\r\n const MAX_RETRIES = 5\r\n const DELAY = 100 // 每次延迟 100ms\r\n\r\n setTimeout(() => {\r\n this.syncFixedColumns()\r\n this.syncHeaderTableWidth()\r\n this.bindScrollSync()\r\n\r\n // 如果需要固定列,检查是否已渲染\r\n if (needFixed && retries < MAX_RETRIES) {\r\n const hasFixedLeft = this.$el.querySelector('.ant-table-fixed-left .ant-table-body tbody tr')\r\n const hasFixedRight = this.$el.querySelector('.ant-table-fixed-right .ant-table-body tbody tr')\r\n \r\n // 如果有固定列配置但还没渲染出来,继续重试\r\n const hasFixedConfig = this.innerColumns.some(c => c.fixed) || (this.rowSelection && this.rowSelection.fixed)\r\n if (hasFixedConfig && !hasFixedLeft && !hasFixedRight) {\r\n this.retrySyncFixedColumns(needFixed, retries + 1)\r\n }\r\n }\r\n }, DELAY)\r\n },\r\n /**\r\n * 修复 x: 'max-content' 场景下表头不跟着横向滚动的问题。\r\n * 原因:ant-design-vue 的 header table 和 body table 各自算 max-content,\r\n * header 按表头文字算、body 按实际数据算,二者宽度不同时 header 就\"滚不动\"。\r\n * 方案:数据渲染后把 header table 的 width 强制设成和 body table 一样。\r\n */\r\n syncHeaderTableWidth () {\r\n this.$nextTick(() => {\r\n const headerTable = this.$el.querySelector('.ant-table-scroll .ant-table-header table')\r\n const bodyTable = this.$el.querySelector('.ant-table-scroll .ant-table-body table')\r\n if (!headerTable || !bodyTable) return\r\n\r\n const bodyW = bodyTable.getBoundingClientRect().width\r\n const headerW = headerTable.getBoundingClientRect().width\r\n\r\n // 始终同步表头宽度到表体宽度,确保窗口变大/变小时都能正确响应\r\n // 只有当宽度差异超过 2px 时才更新,避免频繁设置样式\r\n if (Math.abs(bodyW - headerW) > 2) {\r\n headerTable.style.width = `${bodyW}px`\r\n headerTable.style.minWidth = `${bodyW}px`\r\n }\r\n })\r\n },\r\n /**\r\n * 监听表体横向滚动,同步到表头(防止 antd 自带同步失效)\r\n */\r\n bindScrollSync () {\r\n const body = this.$el.querySelector('.ant-table-scroll .ant-table-body')\r\n const header = this.$el.querySelector('.ant-table-scroll .ant-table-header')\r\n if (!body || !header) return\r\n\r\n if (this._scrollHandler) return // 已绑定\r\n this._scrollHandler = () => {\r\n header.scrollLeft = body.scrollLeft\r\n }\r\n body.addEventListener('scroll', this._scrollHandler, { passive: true })\r\n },\r\n unbindScrollSync () {\r\n const body = this.$el.querySelector('.ant-table-scroll .ant-table-body')\r\n if (body && this._scrollHandler) {\r\n body.removeEventListener('scroll', this._scrollHandler)\r\n this._scrollHandler = null\r\n }\r\n },\r\n /**\r\n * 测量容器宽度\r\n */\r\n measureContainerWidth () {\r\n const wrapper = this.$refs.tableWrapper\r\n if (wrapper) {\r\n this.containerWidth = wrapper.clientWidth\r\n }\r\n },\r\n /**\r\n * 使用 ResizeObserver 监听容器宽度变化\r\n */\r\n observeContainerWidth () {\r\n const wrapper = this.$refs.tableWrapper\r\n if (!wrapper || typeof ResizeObserver === 'undefined') return\r\n\r\n if (this._containerResizeObserver) return // 已绑定\r\n\r\n this._containerResizeObserver = new ResizeObserver((entries) => {\r\n for (const entry of entries) {\r\n const newWidth = entry.contentRect.width\r\n const delta = Math.abs(newWidth - this.containerWidth)\r\n const oldNeedScroll = this.needHorizontalScroll\r\n // 始终更新容器宽度,避免临界区被阈值过滤后状态滞留。\r\n this.containerWidth = newWidth\r\n const newNeedScroll = this.needHorizontalScroll\r\n // 只有宽度变化超过阈值才触发更新,避免微小变化导致频繁重渲染\r\n if (delta > 10 || newNeedScroll !== oldNeedScroll) {\r\n // 容器宽度变化会触发 needHorizontalScroll 的重新计算\r\n // needHorizontalScroll 的 watcher 会处理表格重新渲染和列宽同步\r\n // 这里不需要直接调用 syncFixedColumns,避免与 watcher 的执行时机冲突\r\n // 无论 needHorizontalScroll 是否变化,都立即同步表头宽度\r\n // 确保窗口变大/变小时表头都能及时响应\r\n this.$nextTick(() => {\r\n // 使用 requestAnimationFrame 确保在浏览器重绘后同步表头宽度\r\n // 这样能确保表体宽度已经更新完成\r\n requestAnimationFrame(() => {\r\n this.syncHeaderTableWidth()\r\n })\r\n \r\n // 如果 needHorizontalScroll 状态没有变化,说明只是列宽需要调整,直接同步\r\n // 状态变化时 watcher 会重渲染,但这里仍补一次同步,避免出现\r\n // “缩小后没滚动,刷新后才出现”的时序窗口。\r\n if (this._resizeDebounceTimer) {\r\n clearTimeout(this._resizeDebounceTimer)\r\n }\r\n this._resizeDebounceTimer = setTimeout(() => {\r\n this.syncFixedColumns()\r\n this.syncHeaderTableWidth()\r\n this.bindScrollSync()\r\n }, 150)\r\n })\r\n }\r\n }\r\n })\r\n this._containerResizeObserver.observe(wrapper)\r\n },\r\n /**\r\n * 断开容器宽度监听\r\n */\r\n unobserveContainerWidth () {\r\n if (this._containerResizeObserver) {\r\n this._containerResizeObserver.disconnect()\r\n this._containerResizeObserver = null\r\n }\r\n // 清理防抖定时器\r\n if (this._resizeDebounceTimer) {\r\n clearTimeout(this._resizeDebounceTimer)\r\n this._resizeDebounceTimer = null\r\n }\r\n }\r\n },\r\n mounted() {\r\n this.$nextTick(() => {\r\n // 先测量容器宽度,用于智能判断是否需要 fixed 列\r\n this.measureContainerWidth()\r\n this.observeContainerWidth()\r\n\r\n this.setPaginationHeight()\r\n setTimeout(() => {\r\n this.setTableTbodyHeight()\r\n this.setPaginationHeight()\r\n // 强制同步固定列和主表的列宽\r\n this.syncFixedColumns()\r\n // 同步表头 table 宽度(修复 max-content 场景)\r\n this.syncHeaderTableWidth()\r\n // 绑定横向滚动同步\r\n this.bindScrollSync()\r\n }, 200)\r\n })\r\n \r\n // 监听数据变化,重新同步列宽\r\n this.$watch('dataSource', () => {\r\n this.$nextTick(() => {\r\n setTimeout(() => {\r\n this.syncFixedColumns()\r\n this.syncHeaderTableWidth()\r\n this.bindScrollSync()\r\n }, 100)\r\n })\r\n }, { deep: true })\r\n },\r\n destroyed () {\r\n this.obs.forEach(ob => ob.disconnect())\r\n this.unbindScrollSync()\r\n this.unobserveContainerWidth()\r\n }\r\n}\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n.g-table__wrapper {\r\n display: flex;\r\n flex-direction: column;\r\n min-height: 0;\r\n\r\n .g-table__main {\r\n flex: 1;\r\n min-height: 0;\r\n overflow: hidden;\r\n }\r\n\r\n /**\r\n * 修复\"宽屏下表格两侧出现空白\"问题:\r\n * 当视口宽度大于表格内容宽度时,主表(ant-table-scroll)不会自动拉伸,\r\n * 而固定列(fixed-left/fixed-right)是 position:absolute 定位在容器边缘,中间就出现空白。\r\n * 解决方案:让主表的 table 元素 min-width:100%,使其始终填满滚动容器。\r\n */\r\n &:not(.g-table__wrapper--need-h-scroll) {\r\n ::v-deep .ant-table-scroll .ant-table-header table,\r\n ::v-deep .ant-table-scroll .ant-table-body table {\r\n min-width: 100%;\r\n }\r\n }\r\n\r\n /**\r\n * 修复\"固定列 + scroll.x\"场景下,主表(ant-table-scroll)里会渲染一份 fixed 列的占位 header/cell。\r\n * 这份占位本来只用于计算宽度,但在某些布局下会被看见,表现为\"操作列前多了一大块空白/空列\"。\r\n * 这里用 visibility:hidden 隐藏占位(不影响占位宽度与 fixed 计算),避免视觉空白。\r\n */\r\n ::v-deep .ant-table-scroll .ant-table-header thead > tr > th.ant-table-fixed-columns-in-body.ant-table-row-cell-last,\r\n ::v-deep .ant-table-scroll .ant-table-body tbody > tr > td.ant-table-fixed-columns-in-body.ant-table-row-cell-last {\r\n visibility: hidden;\r\n }\r\n\r\n /* 强制统一行高,确保主表和固定列对齐 */\r\n ::v-deep .ant-table-tbody > tr > td {\r\n height: 54px;\r\n padding: 8px 16px;\r\n vertical-align: middle;\r\n box-sizing: border-box;\r\n line-height: 38px;\r\n }\r\n\r\n /* 表头也统一高度和样式 */\r\n ::v-deep .ant-table-thead > tr > th {\r\n height: 54px;\r\n padding: 8px 16px;\r\n vertical-align: middle;\r\n box-sizing: border-box;\r\n line-height: 38px;\r\n }\r\n\r\n /* 分页区域固定在底部 */\r\n .g-table__pagination {\r\n display: flex;\r\n flex-direction: row;\r\n justify-content: end;\r\n border-top: unset;\r\n padding-top: 8px;\r\n padding-bottom: 8px;\r\n background: #fff;\r\n }\r\n\r\n /* 空数据状态顶部显示 */\r\n .g-table__no-data {\r\n position: relative;\r\n ::v-deep .ant-table-placeholder > td {\r\n height: auto !important;\r\n line-height: normal !important;\r\n padding-top: 24px !important;\r\n padding-bottom: 24px !important;\r\n }\r\n ::v-deep .ant-table-placeholder {\r\n position: static;\r\n width: auto;\r\n height: auto;\r\n text-align: center;\r\n color: #999;\r\n font-size: 14px;\r\n font-weight: normal;\r\n line-height: 20px;\r\n overflow: visible;\r\n border: unset;\r\n }\r\n }\r\n}\r\n</style>"]
2162
2224
  },
2163
2225
  media: undefined
2164
2226
  });
2165
2227
  };
2166
2228
  /* scoped */
2167
- var __vue_scope_id__$D = "data-v-59975dac";
2229
+ var __vue_scope_id__$D = "data-v-6cef17bc";
2168
2230
  /* module identifier */
2169
2231
  var __vue_module_identifier__$D = undefined;
2170
2232
  /* functional template */
@@ -2463,6 +2525,7 @@ var script$B = {
2463
2525
  tableQuerys: {},
2464
2526
  resizeObserverModelTableWrapper: null,
2465
2527
  resizeObserverModelTableContainer: null,
2528
+ resizeObserverSearchArea: null,
2466
2529
  modelTableWrapperHeight: 0,
2467
2530
  currentTreeNodeData: {},
2468
2531
  currentRowData: {},
@@ -3504,7 +3567,7 @@ var script$B = {
3504
3567
  }
3505
3568
  }, _callee8);
3506
3569
  }))();
3507
- }), _defineProperty(_defineProperty(_defineProperty(_defineProperty(_methods$1, "requestTableData", function requestTableData() {
3570
+ }), _defineProperty(_defineProperty(_defineProperty(_defineProperty(_defineProperty(_methods$1, "requestTableData", function requestTableData() {
3508
3571
  var _arguments = arguments,
3509
3572
  _this19 = this;
3510
3573
  return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9() {
@@ -3601,18 +3664,8 @@ var script$B = {
3601
3664
  width = _tableRef$$el$getBoun.width;
3602
3665
  this.tableWidth = width;
3603
3666
 
3604
- // 获取分页组件的高度(如果存在)
3605
- var paginationHeight = 0;
3606
- var paginationEl = tableRef.$el.querySelector('.g-table__pagination');
3607
- if (paginationEl) {
3608
- paginationHeight = paginationEl.getBoundingClientRect().height || 50;
3609
- } else {
3610
- // 如果分页组件还未渲染,使用默认高度
3611
- paginationHeight = 50;
3612
- }
3613
-
3614
- // 计算表格高度:视口高度 - 表格顶部距离 - 分页高度 - 额外高度
3615
- var calculatedHeight = currentViewportHeight - tableToTop - paginationHeight - this.overHeight - 20;
3667
+ // 计算表格高度:外层容器高度应覆盖到视口底部;分页高度由 ele-table 内部自行扣减
3668
+ var calculatedHeight = currentViewportHeight - tableToTop - this.overHeight;
3616
3669
  // 确保最小高度,避免表格过小
3617
3670
  this.tableHeight = Math.max(calculatedHeight, 200);
3618
3671
  }), "calculateTreeHeight", function calculateTreeHeight() {
@@ -3630,45 +3683,70 @@ var script$B = {
3630
3683
  var titleHeight = titleEl.getBoundingClientRect().height;
3631
3684
  this.treeWrapperHeight = height - titleHeight;
3632
3685
  }
3633
- }), "keepAliveRefresh", function keepAliveRefresh() {
3686
+ }), "observeSearchAreaHeight", function observeSearchAreaHeight() {
3634
3687
  var _this20 = this;
3688
+ var searchAreaRef = this.$refs[this.searchArea];
3689
+ var searchAreaEl = searchAreaRef && searchAreaRef.$el;
3690
+ if (!searchAreaEl || typeof ResizeObserver === 'undefined') return;
3691
+ this.resizeObserverSearchArea = new ResizeObserver(function (entries) {
3692
+ var _iterator = _createForOfIteratorHelper(entries),
3693
+ _step;
3694
+ try {
3695
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
3696
+ var entry = _step.value;
3697
+ requestAnimationFrame(function () {
3698
+ _this20.calculateTableHeight();
3699
+ if (_this20.showTree) {
3700
+ _this20.calculateTreeHeight();
3701
+ }
3702
+ });
3703
+ }
3704
+ } catch (err) {
3705
+ _iterator.e(err);
3706
+ } finally {
3707
+ _iterator.f();
3708
+ }
3709
+ });
3710
+ this.resizeObserverSearchArea.observe(searchAreaEl);
3711
+ }), "keepAliveRefresh", function keepAliveRefresh() {
3712
+ var _this21 = this;
3635
3713
  return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee1() {
3636
3714
  return _regenerator().w(function (_context1) {
3637
3715
  while (1) switch (_context1.n) {
3638
3716
  case 0:
3639
- return _context1.a(2, _this20.withPerformanceMonitoring('keepAliveRefresh', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0() {
3640
- var _this20$tableMeta$ove, overrideInit;
3717
+ return _context1.a(2, _this21.withPerformanceMonitoring('keepAliveRefresh', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0() {
3718
+ var _this21$tableMeta$ove, overrideInit;
3641
3719
  return _regenerator().w(function (_context0) {
3642
3720
  while (1) switch (_context0.n) {
3643
3721
  case 0:
3644
3722
  // 重新计算表格高度(应对窗口大小变化)
3645
- _this20.$nextTick(function () {
3723
+ _this21.$nextTick(function () {
3646
3724
  setTimeout(function () {
3647
- _this20.calculateTableHeight();
3648
- if (_this20.showTree) {
3649
- _this20.calculateTreeHeight();
3725
+ _this21.calculateTableHeight();
3726
+ if (_this21.showTree) {
3727
+ _this21.calculateTreeHeight();
3650
3728
  }
3651
3729
  }, 200);
3652
3730
  });
3653
3731
 
3654
3732
  // 刷新列表数据
3655
- _this20$tableMeta$ove = _this20.tableMeta.overrideInit, overrideInit = _this20$tableMeta$ove === void 0 ? false : _this20$tableMeta$ove;
3733
+ _this21$tableMeta$ove = _this21.tableMeta.overrideInit, overrideInit = _this21$tableMeta$ove === void 0 ? false : _this21$tableMeta$ove;
3656
3734
  if (!overrideInit) {
3657
3735
  _context0.n = 1;
3658
3736
  break;
3659
3737
  }
3660
3738
  // 如果使用自定义初始化模式,触发 INIT 事件
3661
- _this20.$emit(RESERVE_EVENT_NAMES.INIT, _objectSpread2({}, _this20.exposed));
3739
+ _this21.$emit(RESERVE_EVENT_NAMES.INIT, _objectSpread2({}, _this21.exposed));
3662
3740
  _context0.n = 2;
3663
3741
  break;
3664
3742
  case 1:
3665
3743
  _context0.n = 2;
3666
- return _this20.requestTableData(_this20.tableQuerys);
3744
+ return _this21.requestTableData(_this21.tableQuerys);
3667
3745
  case 2:
3668
- _this20.$emit('x:refresh-completed', {
3669
- tableData: _this20.tableData,
3670
- treeData: _this20.treeData,
3671
- currentRowData: _this20.currentRowData
3746
+ _this21.$emit('x:refresh-completed', {
3747
+ tableData: _this21.tableData,
3748
+ treeData: _this21.treeData,
3749
+ currentRowData: _this21.currentRowData
3672
3750
  });
3673
3751
  case 3:
3674
3752
  return _context0.a(2);
@@ -3680,7 +3758,7 @@ var script$B = {
3680
3758
  }))();
3681
3759
  })),
3682
3760
  mounted: function mounted() {
3683
- var _this21 = this;
3761
+ var _this22 = this;
3684
3762
  // 初始化时先设置一个默认高度,避免布局混乱
3685
3763
  this.tableHeight = 400;
3686
3764
  if (this.showTree) {
@@ -3690,32 +3768,33 @@ var script$B = {
3690
3768
  // 延迟计算,确保所有组件都已渲染
3691
3769
  this.$nextTick(function () {
3692
3770
  setTimeout(function () {
3693
- _this21.calculateTableHeight();
3694
- _this21.calculateTreeHeight();
3771
+ _this22.calculateTableHeight();
3772
+ _this22.calculateTreeHeight();
3773
+ _this22.observeSearchAreaHeight();
3695
3774
  }, 200);
3696
3775
  });
3697
3776
 
3698
3777
  // 使用 ResizeObserver 监听容器大小变化
3699
3778
  this.resizeObserverModelTableWrapper = new ResizeObserver(function (entries) {
3700
- var _iterator = _createForOfIteratorHelper(entries),
3701
- _step;
3779
+ var _iterator2 = _createForOfIteratorHelper(entries),
3780
+ _step2;
3702
3781
  try {
3703
- for (_iterator.s(); !(_step = _iterator.n()).done;) {
3704
- var _ = _step.value;
3782
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
3783
+ var _ = _step2.value;
3705
3784
  requestAnimationFrame(function () {
3706
3785
  // 延迟重新计算,确保分页组件高度已更新
3707
3786
  setTimeout(function () {
3708
- _this21.calculateTableHeight();
3709
- if (_this21.showTree) {
3710
- _this21.calculateTreeHeight();
3787
+ _this22.calculateTableHeight();
3788
+ if (_this22.showTree) {
3789
+ _this22.calculateTreeHeight();
3711
3790
  }
3712
3791
  }, 100);
3713
3792
  });
3714
3793
  }
3715
3794
  } catch (err) {
3716
- _iterator.e(err);
3795
+ _iterator2.e(err);
3717
3796
  } finally {
3718
- _iterator.f();
3797
+ _iterator2.f();
3719
3798
  }
3720
3799
  });
3721
3800
  if (this.$refs[this.modelTableWrapper]) {
@@ -3725,19 +3804,19 @@ var script$B = {
3725
3804
  // 监听表格容器大小变化(用于同步树高度)
3726
3805
  if (this.showTree && this.$refs[this.modelTableContainerRef]) {
3727
3806
  this.resizeObserverModelTableContainer = new ResizeObserver(function (entries) {
3728
- var _iterator2 = _createForOfIteratorHelper(entries),
3729
- _step2;
3807
+ var _iterator3 = _createForOfIteratorHelper(entries),
3808
+ _step3;
3730
3809
  try {
3731
- for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
3732
- var _ = _step2.value;
3810
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
3811
+ var _ = _step3.value;
3733
3812
  requestAnimationFrame(function () {
3734
- _this21.calculateTreeHeight();
3813
+ _this22.calculateTreeHeight();
3735
3814
  });
3736
3815
  }
3737
3816
  } catch (err) {
3738
- _iterator2.e(err);
3817
+ _iterator3.e(err);
3739
3818
  } finally {
3740
- _iterator2.f();
3819
+ _iterator3.f();
3741
3820
  }
3742
3821
  });
3743
3822
  this.resizeObserverModelTableContainer.observe(this.$refs[this.modelTableContainerRef]);
@@ -3745,11 +3824,11 @@ var script$B = {
3745
3824
 
3746
3825
  // 监听窗口大小变化
3747
3826
  this.handleResize = function () {
3748
- _this21.$nextTick(function () {
3827
+ _this22.$nextTick(function () {
3749
3828
  setTimeout(function () {
3750
- _this21.calculateTableHeight();
3751
- if (_this21.showTree) {
3752
- _this21.calculateTreeHeight();
3829
+ _this22.calculateTableHeight();
3830
+ if (_this22.showTree) {
3831
+ _this22.calculateTreeHeight();
3753
3832
  }
3754
3833
  }, 100);
3755
3834
  });
@@ -3763,6 +3842,9 @@ var script$B = {
3763
3842
  if (this.resizeObserverModelTableContainer) {
3764
3843
  this.resizeObserverModelTableContainer.disconnect();
3765
3844
  }
3845
+ if (this.resizeObserverSearchArea) {
3846
+ this.resizeObserverSearchArea.disconnect();
3847
+ }
3766
3848
  if (this.handleResize) {
3767
3849
  window.removeEventListener('resize', this.handleResize);
3768
3850
  }
@@ -3774,13 +3856,13 @@ var script$B = {
3774
3856
  }
3775
3857
  },
3776
3858
  activated: function activated() {
3777
- var _this22 = this;
3859
+ var _this23 = this;
3778
3860
  return _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee10() {
3779
3861
  return _regenerator().w(function (_context10) {
3780
3862
  while (1) switch (_context10.n) {
3781
3863
  case 0:
3782
3864
  _context10.n = 1;
3783
- return _this22.keepAliveRefresh();
3865
+ return _this23.keepAliveRefresh();
3784
3866
  case 1:
3785
3867
  return _context10.a(2);
3786
3868
  }
@@ -3916,21 +3998,21 @@ __vue_render__$B._withStripped = true;
3916
3998
  /* style */
3917
3999
  var __vue_inject_styles__$B = function __vue_inject_styles__(inject) {
3918
4000
  if (!inject) return;
3919
- inject("data-v-98a9f3fc_0", {
3920
- source: ".ele.model__tree-table[data-v-98a9f3fc] {\n background: transparent;\n display: flex;\n flex-direction: row;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n.ele.model__tree-table .model__tree-table--container[data-v-98a9f3fc] {\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n.ele.model__tree-table .model__tree-table--container .model__tree--wrapper[data-v-98a9f3fc] {\n width: 240px;\n background: #fff;\n flex-shrink: 0;\n padding: 16px;\n box-sizing: border-box;\n margin-right: 16px;\n overflow-y: auto;\n overflow-x: hidden;\n}\n.ele.model__tree-table .model__table--container[data-v-98a9f3fc] {\n width: 100%;\n min-width: 0;\n background: #fff;\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--bar[data-v-98a9f3fc] {\n width: 100%;\n height: 8px;\n background: var(--idooel-primary-color);\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--text[data-v-98a9f3fc] {\n text-align: left;\n padding: 16px;\n font-size: 16px;\n font-weight: bold;\n background: #fff;\n border-bottom: 1px solid;\n border-color: var(--idoole-black-016);\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper[data-v-98a9f3fc] {\n background: #fff;\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .button-row__area[data-v-98a9f3fc] {\n width: 100%;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n padding-top: 16px;\n padding-bottom: 8px;\n padding-right: 16px;\n flex-shrink: 0;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .g-table__wrapper[data-v-98a9f3fc] {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .g-table__wrapper .fsm[data-v-98a9f3fc] {\n cursor: pointer;\n color: var(--idooel-primary-color);\n}\n\n/*# sourceMappingURL=index.vue.map */",
4001
+ inject("data-v-08876ae3_0", {
4002
+ source: ".ele.model__tree-table[data-v-08876ae3] {\n background: transparent;\n display: flex;\n flex-direction: row;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n.ele.model__tree-table .model__tree-table--container[data-v-08876ae3] {\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n.ele.model__tree-table .model__tree-table--container .model__tree--wrapper[data-v-08876ae3] {\n width: 240px;\n background: #fff;\n flex-shrink: 0;\n padding: 16px;\n box-sizing: border-box;\n margin-right: 16px;\n overflow-y: auto;\n overflow-x: hidden;\n}\n.ele.model__tree-table .model__table--container[data-v-08876ae3] {\n width: 100%;\n min-width: 0;\n background: #fff;\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--bar[data-v-08876ae3] {\n width: 100%;\n height: 8px;\n background: var(--idooel-primary-color);\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--text[data-v-08876ae3] {\n text-align: left;\n padding: 16px;\n font-size: 16px;\n font-weight: bold;\n background: #fff;\n border-bottom: 1px solid;\n border-color: var(--idoole-black-016);\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper[data-v-08876ae3] {\n background: #fff;\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .button-row__area[data-v-08876ae3] {\n width: 100%;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n padding-top: 16px;\n padding-bottom: 8px;\n padding-right: 16px;\n flex-shrink: 0;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .g-table__wrapper[data-v-08876ae3] {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .g-table__wrapper .fsm[data-v-08876ae3] {\n cursor: pointer;\n color: var(--idooel-primary-color);\n}\n\n/*# sourceMappingURL=index.vue.map */",
3921
4003
  map: {
3922
4004
  "version": 3,
3923
4005
  "sources": ["E:\\code\\OnlineStudy-Base\\base-elearning-frontend-model\\packages\\components\\packages\\models\\tree-table-model\\src\\index.vue", "index.vue"],
3924
4006
  "names": [],
3925
- "mappings": "AAixCA;EACA,uBAAA;EACA,aAAA;EACA,mBAAA;EACA,WAAA;EACA,YAAA;EACA,gBAAA;AChxCA;ADixCA;EACA,aAAA;EACA,sBAAA;EACA,YAAA;AC/wCA;ADgxCA;EACA,YAAA;EACA,gBAAA;EACA,cAAA;EACA,aAAA;EACA,sBAAA;EACA,kBAAA;EACA,gBAAA;EACA,kBAAA;AC9wCA;ADixCA;EACA,WAAA;EACA,YAAA;EACA,gBAAA;EACA,aAAA;EACA,sBAAA;EACA,YAAA;EACA,gBAAA;AC/wCA;ADixCA;EACA,WAAA;EACA,WAAA;EACA,uCAAA;EACA,2BAAA;EACA,4BAAA;AC/wCA;ADixCA;EACA,gBAAA;EACA,aAAA;EACA,eAAA;EACA,iBAAA;EACA,gBAAA;EACA,wBAAA;EACA,qCAAA;AC/wCA;ADkxCA;EACA,gBAAA;EACA,aAAA;EACA,sBAAA;EACA,YAAA;EACA,gBAAA;AChxCA;ADixCA;EACA,WAAA;EACA,aAAA;EACA,mBAAA;EACA,mBAAA;EACA,8BAAA;EACA,iBAAA;EACA,mBAAA;EACA,mBAAA;EACA,cAAA;AC/wCA;ADixCA;EACA,OAAA;EACA,aAAA;EACA,gBAAA;AC/wCA;ADgxCA;EACA,eAAA;EACA,kCAAA;AC9wCA;;AAEA,oCAAoC",
4007
+ "mappings": "AA6xCA;EACA,uBAAA;EACA,aAAA;EACA,mBAAA;EACA,WAAA;EACA,YAAA;EACA,gBAAA;AC5xCA;AD6xCA;EACA,aAAA;EACA,sBAAA;EACA,YAAA;AC3xCA;AD4xCA;EACA,YAAA;EACA,gBAAA;EACA,cAAA;EACA,aAAA;EACA,sBAAA;EACA,kBAAA;EACA,gBAAA;EACA,kBAAA;AC1xCA;AD6xCA;EACA,WAAA;EACA,YAAA;EACA,gBAAA;EACA,aAAA;EACA,sBAAA;EACA,YAAA;EACA,gBAAA;AC3xCA;AD6xCA;EACA,WAAA;EACA,WAAA;EACA,uCAAA;EACA,2BAAA;EACA,4BAAA;AC3xCA;AD6xCA;EACA,gBAAA;EACA,aAAA;EACA,eAAA;EACA,iBAAA;EACA,gBAAA;EACA,wBAAA;EACA,qCAAA;AC3xCA;AD8xCA;EACA,gBAAA;EACA,aAAA;EACA,sBAAA;EACA,YAAA;EACA,gBAAA;AC5xCA;AD6xCA;EACA,WAAA;EACA,aAAA;EACA,mBAAA;EACA,mBAAA;EACA,8BAAA;EACA,iBAAA;EACA,mBAAA;EACA,mBAAA;EACA,cAAA;AC3xCA;AD6xCA;EACA,OAAA;EACA,aAAA;EACA,gBAAA;AC3xCA;AD4xCA;EACA,eAAA;EACA,kCAAA;AC1xCA;;AAEA,oCAAoC",
3926
4008
  "file": "index.vue",
3927
- "sourcesContent": ["<template>\r\n <section class=\"ele model__tree-table\">\r\n <section class=\"model__tree-table--container\" v-if=\"showTree\">\r\n <div class=\"model__tree--title\"></div>\r\n <section :ref=\"modelTreeWrapper\" class=\"model__tree--wrapper\" :style=\"{height: `${treeWrapperHeight}px`}\">\r\n <ele-tree\r\n :tree-data=\"treeData\"\r\n :defaultExpandedKeys=\"defaultExpandedKeys\"\r\n :defaultSelectedKeys=\"defaultSelectedKeys\"\r\n @select=\"selectTreeNode\"\r\n :replace-fields=\"mapFields\">\r\n </ele-tree>\r\n </section>\r\n </section>\r\n <section class=\"model__table--container\" :ref=\"modelTableContainerRef\">\r\n <div class=\"model__table--title\" v-if=\"title\">\r\n <template v-if=\"titleMode\">\r\n <div :class=\"[`model__table-title--${titleMode}`]\"></div>\r\n </template>\r\n <template v-else>\r\n <div class=\"model__table-title--text\">{{ title }}</div>\r\n </template>\r\n </div>\r\n <section :ref=\"modelTableWrapper\" class=\"model__table--wrapper\">\r\n <ele-search-area :ref=\"searchArea\" @search=\"onSearch\" :data-source=\"searchMeta.elements\"></ele-search-area>\r\n <div class=\"button-row__area\">\r\n <ele-button-group class=\"model-table__button-group\" v-on=\"overrideButtonGroupEvent\" :ref=\"buttonGroup\" @click=\"handleClickButtonGroup\" :data-source=\"getButtonGroupElements\"></ele-button-group>\r\n <slot name=\"tags\"></slot>\r\n <slot v-if=\"$slots['sub-center']\" name=\"sub-center\"></slot>\r\n </div>\r\n <ele-table\r\n v-on=\"overrideTableEvent\"\r\n :ref=\"tableRef\"\r\n :row-selection=\"rowSelection\"\r\n :loading=\"loading\" \r\n :columns=\"columns\"\r\n :total=\"total\"\r\n :x=\"x\"\r\n :y=\"y\"\r\n :bordered=\"setBorder\"\r\n :height=\"tableHeight\"\r\n :width=\"tableWidth\"\r\n :actions=\"actions\"\r\n :pageSize=\"pageSize\"\r\n :pageSizeOptions=\"pageSizeOptions\"\r\n :data-source=\"tableData\"\r\n :mode=\"mode\"\r\n @change-page=\"onChangePage\"\r\n ></ele-table>\r\n </section>\r\n </section>\r\n <ele-modal-form v-model=\"modalFormValue\" v-on=\"overrideModalFormEvent\" :meta=\"modalFormMeta\"></ele-modal-form>\r\n <ele-modal-fsm v-model=\"showFsmModal\" :contextProp=\"fsmContextProp\" :meta=\"fsmMeta\" @cancel=\"handleCloseFsmModal\"></ele-modal-fsm>\r\n <ele-modal-table\r\n :meta=\"modalTableMeta\"\r\n v-model=\"modalTableValue\"\r\n v-on=\"overrideModalTableEvent\"\r\n ></ele-modal-table>\r\n </section>\r\n</template>\r\n\r\n<script>\r\nimport { type, net } from '@idooel/shared'\r\nimport { v4 as uuidv4 } from 'uuid'\r\nimport { BUILT_IN_EVENT_NAMES, RESERVE_EVENT_NAMES, BUILT_IN_TRIGGER, CONTEXT } from '../../../utils'\r\nimport { createTreeTableModel } from '@idooel/runtime-context'\r\nexport default {\r\n name: 'ele-tree-table-model',\r\n props: {\r\n title: {\r\n type: [Object, String]\r\n },\r\n overHeight: {\r\n type: Number,\r\n default: 0\r\n },\r\n treeMeta: {\r\n type: Object,\r\n default: () => ({})\r\n },\r\n searchMeta: {\r\n type: Object,\r\n default: () => ({})\r\n },\r\n buttonGroupMeta: {\r\n typeof: Object,\r\n default: () => ({})\r\n },\r\n tableMeta: {\r\n type: Object,\r\n default: () => ({})\r\n },\r\n createMeta: {\r\n type: Object\r\n },\r\n editMeta: {\r\n type: Object\r\n }\r\n },\r\n inject: {\r\n parentScopeId: {\r\n default: () => (() => null)\r\n },\r\n parentEvalExpression: {\r\n from: 'evalExpression',\r\n default: () => (() => undefined)\r\n }\r\n },\r\n provide () {\r\n return {\r\n evalExpression: this.parseExpression,\r\n requestTreeData: this.requestTreeData,\r\n requestTableData: this.requestTableData,\r\n keepAliveRefresh: this.keepAliveRefresh,\r\n parentScopeId: () => this.model?.scopeId,\r\n [CONTEXT]: () => {\r\n return {\r\n exposed: this.exposed\r\n }\r\n }\r\n }\r\n },\r\n data () {\r\n return {\r\n tableHeight: 0,\r\n tableWidth: 0,\r\n modalFormMeta: {},\r\n modalFormValue: false,\r\n treeData: [],\r\n tableData: [],\r\n defaultExpandedKeys: [],\r\n defaultSelectedKeys: [],\r\n replaceFields: {\r\n title: 'title',\r\n children: 'children',\r\n key: 'id'\r\n },\r\n loading: false,\r\n total: 0,\r\n tableQuerys: {},\r\n resizeObserverModelTableWrapper: null,\r\n resizeObserverModelTableContainer: null,\r\n modelTableWrapperHeight: 0,\r\n currentTreeNodeData: {},\r\n currentRowData: {},\r\n treeWrapperHeight: 0,\r\n currentTableSelection: this.currentTableMode == 'radio' ? {} : [],\r\n showFsmModal: false,\r\n fsmMeta: {},\r\n fsmContextProp: {},\r\n modalTableValue: false,\r\n modalTableMeta: {}\r\n }\r\n },\r\n computed: {\r\n setBorder () {\r\n return this.tableMeta.bordered === false ? false : true\r\n },\r\n rowSelection () {\r\n if (!this.currentTableMode) return void 0\r\n return {\r\n columnTitle: this.currentSelectionColumn.columnTitle,\r\n fixed: true,\r\n type: this.currentTableMode,\r\n onChange: this.onChangeTableSelection\r\n }\r\n },\r\n currentSelectionColumn () {\r\n const { multiple } = this.tableMeta\r\n const target = this.columns.find(item => Object.keys(item).includes('multiple'))\r\n const isGlobalExistMultiple = Object.keys(this.tableMeta).includes('multiple')\r\n if (target) {\r\n return target\r\n } else if (isGlobalExistMultiple) {\r\n return { multiple }\r\n }\r\n return void 0\r\n },\r\n x () {\r\n const { x } = this.tableMeta\r\n return x\r\n },\r\n y () {\r\n const { y } = this.tableMeta\r\n return y\r\n },\r\n currentTableMode () {\r\n if (!this.currentSelectionColumn) return void 0\r\n const { multiple } = this.currentSelectionColumn\r\n if (type.isBool(multiple)) {\r\n if (multiple) {\r\n return 'checkbox'\r\n } else {\r\n return 'radio'\r\n }\r\n } else {\r\n return void 0\r\n }\r\n },\r\n modelTableContainerRef () {\r\n return uuidv4()\r\n },\r\n titleMode () {\r\n if (type.isObject(this.title)) {\r\n const { mode = '' } = this.title\r\n return mode\r\n }\r\n return void 0\r\n },\r\n tableRef () {\r\n return uuidv4()\r\n },\r\n exposed () {\r\n return {\r\n showModalForm: this.showModalForm,\r\n closeModalForm: this.closeModalForm,\r\n showModalTable: this.showModalTable,\r\n closeModalTable: this.closeModalTable,\r\n currentTableSelection: this.currentTableSelection,\r\n currentTreeNode: this.currentTreeNodeData,\r\n requestTableData: this.requestTableData,\r\n keepAliveRefresh: this.keepAliveRefresh,\r\n refreshTreeData: this.refreshTreeData,\r\n querys: this.tableQuerys,\r\n currentRowData: this.getCurrentRowData(),\r\n getCurrentRowData: this.getCurrentRowData,\r\n setCurrentRowData: this.setCurrentRowData,\r\n setCurrentTableSelection: this.setCurrentTableSelection,\r\n getCurrentTableSelection: this.getCurrentTableSelection,\r\n cleanCurrentModelEffect: this.cleanCurrentModelEffect,\r\n route: this.$route,\r\n _route: this.$route.query,\r\n _routeMeta: this.$route.meta\r\n }\r\n },\r\n overrideTableEvent () {\r\n const events = this.actions.reduce((ret, action) => {\r\n ret[action.eventName || action.key] = (e) => {\r\n this.setCurrentRowData(e.exposed.currentRowData)\r\n const { target } = action\r\n const targetMeta = this.findMetaByKey(target)\r\n const { mode } = targetMeta\r\n mode && this.dispatchTrigger({ mode, record: e.exposed.currentRowData, modeMeta: targetMeta })\r\n this.$emit(action.eventName || action.key, { ...e, currentTreeNode: this.currentTreeNodeData, exposed: { ...this.exposed, ...e.exposed } })\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...this.$listeners,\r\n ...events,\r\n [BUILT_IN_EVENT_NAMES.EDIT]: this[BUILT_IN_EVENT_NAMES.EDIT],\r\n [BUILT_IN_EVENT_NAMES.SUBMIT]: this[BUILT_IN_EVENT_NAMES.SUBMIT]\r\n }\r\n },\r\n overrideModalFormEvent () {\r\n const { footerMeta } = this.modalFormMeta\r\n const { elements = [] } = footerMeta || {}\r\n const eles = type.isFunction(elements) ? elements.call(this) : elements\r\n const events = eles.reduce((ret, ele) => {\r\n ret[ele.eventName] = (e = {}) => {\r\n if (ele.eventName === 'cancel') {\r\n this.closeModalForm()\r\n } else {\r\n const { exposed = {} } = e\r\n this.$emit(`${ele.eventName || ele.key}`, { ...e, currentTreeNode: this.currentTreeNodeData, exposed: Object.assign({}, exposed )})\r\n }\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...events\r\n }\r\n },\r\n overrideModalTableEvent () {\r\n const { footerMeta } = this.modalTableMeta\r\n const { elements = [] } = footerMeta || {}\r\n const eles = type.isFunction(elements) ? elements.call(this) : elements\r\n const events = eles.reduce((ret, ele) => {\r\n ret[ele.eventName] = (e = {}) => {\r\n if (ele.eventName === 'cancel') {\r\n this.closeModalTable()\r\n } else {\r\n const { exposed = {} } = e\r\n this.$emit(`${ele.eventName || ele.key}`, { ...e, currentTreeNode: this.currentTreeNodeData, exposed: Object.assign({}, exposed )})\r\n }\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...events,\r\n exposed: this.exposed\r\n }\r\n },\r\n overrideButtonGroupEvent () {\r\n const events = this.getButtonGroupElements.reduce((ret, ele) => {\r\n ret[ele.eventName] = (e) => {\r\n this.$emit(ele.eventName || 'click', { ...e, currentTreeNode: this.currentTreeNodeData, exposed: Object.assign({}, e.exposed || {}, this.exposed)})\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...this.$listeners,\r\n ...events,\r\n [BUILT_IN_EVENT_NAMES.CREATE]: this[BUILT_IN_EVENT_NAMES.CREATE],\r\n exposed: this.exposed\r\n }\r\n },\r\n showTree () {\r\n return !!Object.keys(this.treeMeta).length\r\n },\r\n buttonGroup () {\r\n return uuidv4()\r\n },\r\n searchArea () {\r\n return uuidv4()\r\n },\r\n modelTreeWrapper () {\r\n return uuidv4()\r\n },\r\n modelTableWrapper () {\r\n return uuidv4()\r\n },\r\n actions () {\r\n const { operations } = this.tableMeta\r\n if (operations) {\r\n return operations.elements\r\n } else {\r\n return []\r\n }\r\n },\r\n pageSize () {\r\n const { page = {} } = this.tableMeta\r\n return page.pageSize || 10\r\n },\r\n mode () {\r\n const { page = {} } = this.tableMeta\r\n return page.mode\r\n },\r\n pageSizeOptions () {\r\n const { page = {} } = this.tableMeta\r\n return page.pageSizeOptions || ['10', '20', '30', '40']\r\n },\r\n columns () {\r\n const { columns, operations } = this.tableMeta\r\n if (type.get(columns) === 'array') {\r\n const columnsOptions = columns.map(item => {\r\n const { mode = 'text' } = item\r\n if (item.render) {\r\n return {\r\n ...item,\r\n customRender: (text, record, index) => {\r\n const { $createElement } = this\r\n return item.render.call(this, \r\n { h: $createElement, ctx: this },\r\n text ? typeof text == 'object' ? text[item.dataIndex] : text : '', \r\n record, index)\r\n }\r\n }\r\n } else if (mode !== BUILT_IN_TRIGGER.TEXT) {\r\n const { [`${mode}Meta`]: modeMeta } = item\r\n return {\r\n ...item,\r\n customRender: (text, record, index) => {\r\n return <span onClick={() => this.dispatchTrigger({ mode, record, modeMeta, index })} class={ mode }>{ text }</span>\r\n }\r\n }\r\n }\r\n return {\r\n ...item\r\n }\r\n })\r\n if (operations) {\r\n return [\r\n ...columnsOptions,\r\n {\r\n title: '操作',\r\n width: operations.width,\r\n key: 'action',\r\n fixed: 'right',\r\n scopedSlots: { customRender: 'action' }\r\n }\r\n ]\r\n }\r\n return columnsOptions\r\n } else {\r\n console.error('Error: columns is invalid, please check it')\r\n return []\r\n }\r\n },\r\n getButtonGroupElements () {\r\n const { elements } = this.buttonGroupMeta\r\n if (type.get(elements) === 'function') {\r\n return elements.call(this)\r\n } else if (type.get(elements) === 'array') {\r\n return elements\r\n } else {\r\n return []\r\n }\r\n },\r\n mapFields () {\r\n const { replaceFields = {} } = this.treeMeta\r\n const mapFields = type.isEmpty(replaceFields) ? this.replaceFields : replaceFields\r\n return mapFields\r\n }\r\n },\r\n async created () {\r\n // onSearch会初始化请求表格数据,所以不需要在这里请求表格数据\r\n // 初始化数据池管理器(使用新的 Mutix-based runtime-context)\r\n try {\r\n // 使用组件名 + UUID 作为调试名称,方便在 DevTools 中区分多个实例\r\n // 这里的 Symbol description 将被 mutix devtools plugin 提取显示\r\n const debugName = `TreeTableModel_${this.tableRef || uuidv4().slice(0, 8)}`\r\n // 获取父作用域 ID\r\n const parentId = this.parentScopeId?.() || null\r\n // 创建模型时传入父作用域\r\n this.model = createTreeTableModel(Symbol(debugName), undefined, parentId)\r\n \r\n if (!this.model) {\r\n throw new Error('Failed to create tree table model')\r\n }\r\n } catch (error) {\r\n console.error('Error creating tree table model:', error)\r\n this.model = null\r\n return\r\n }\r\n \r\n // 初始化 currentRowData\r\n this.initializeCurrentRowData()\r\n \r\n // 订阅数据池变化,实现精确的状态同步\r\n this.setupModelSubscriptions()\r\n \r\n if (this.showTree) {\r\n this.treeData = await this.requestTreeData()\r\n const [defaultTreeNode = {}] = this.treeData\r\n this.defaultExpandedKeys = [defaultTreeNode[this.mapFields.key]]\r\n this.defaultSelectedKeys = [defaultTreeNode[this.mapFields.key]]\r\n \r\n // 1. 先更新模型(Single Source of Truth)\r\n if (this.model) {\r\n this.model.setSelectedNode(defaultTreeNode)\r\n }\r\n \r\n // 2. 组件层数据通过订阅自动更新,无需手动设置\r\n // this.currentTreeNodeData = defaultTreeNode // 不再需要\r\n \r\n const { params = {}, fieldMap = {}, overrideInit = false } = this.tableMeta\r\n \r\n // 使用最新的 currentRowData 和统一的表达式上下文\r\n const currentRowData = this.getCurrentRowData()\r\n const extraContext = { \r\n currentRowData: currentRowData\r\n }\r\n \r\n const initQuerys = Object.assign({}, params, this.parseFieldMap(fieldMap, extraContext))\r\n if (overrideInit) {\r\n this.$emit(RESERVE_EVENT_NAMES.INIT, { ...this.exposed })\r\n } else {\r\n this.tableData = await this.requestTableData(initQuerys)\r\n }\r\n } else {\r\n const { params = {}, fieldMap = {} } = this.tableMeta\r\n const currentRowData = this.getCurrentRowData()\r\n const extraContext = { \r\n currentRowData: currentRowData\r\n }\r\n this.tableQuerys = Object.assign({}, params, this.parseFieldMap(fieldMap, extraContext))\r\n }\r\n },\r\n methods: {\r\n /**\r\n * 构建标准化的表达式数据上下文\r\n * @returns {Object} 表达式求值上下文\r\n */\r\n buildExpressionContext () {\r\n let parentSelectedNode = {}\r\n let parentCurrentRowData = {}\r\n \r\n if (this.parentEvalExpression) {\r\n try {\r\n parentSelectedNode = this.parentEvalExpression('selectedNode') || {}\r\n parentCurrentRowData = this.parentEvalExpression('currentRowData') || {}\r\n } catch (e) {\r\n // ignore\r\n }\r\n }\r\n\r\n const currentTreeNode = this.model ? this.model.getSelectedNode() : this.currentTreeNodeData\r\n const currentRowData = this.getCurrentRowData()\r\n\r\n return {\r\n // 路由数据\r\n _route: this.$route.query,\r\n _routeMeta: this.$route.meta,\r\n \r\n // 树节点数据 - 合并父级数据以支持字段级 fallback\r\n currentTreeNode: { ...parentSelectedNode, ...currentTreeNode },\r\n selectedNode: { ...parentSelectedNode, ...currentTreeNode },\r\n \r\n // 表格数据 - 合并父级数据以支持字段级 fallback\r\n currentRowData: { ...parentCurrentRowData, ...currentRowData },\r\n currentTableSelection: this.getCurrentTableSelection(),\r\n \r\n // 分页数据\r\n pagination: {\r\n currentPage: this.tableQuerys.currentPage || 1,\r\n pageSize: this.tableQuerys.pageSize || this.pageSize,\r\n total: this.total\r\n },\r\n \r\n // 查询参数\r\n queryParams: this.tableQuerys,\r\n \r\n // 表格元数据\r\n tableMeta: this.tableMeta,\r\n treeMeta: this.treeMeta,\r\n \r\n // 暴露的方法和数据\r\n exposed: this.exposed\r\n }\r\n },\r\n \r\n /**\r\n * 统一表达式解析方法\r\n * @param {string} expression - 表达式字符串\r\n * @param {Object} extraContext - 额外上下文数据\r\n * @returns {any} 表达式求值结果\r\n */\r\n parseExpression (expression, extraContext = {}) {\r\n if (!this.model) {\r\n console.warn('[TreeTableModel] Model not initialized, cannot parse expression')\r\n return undefined\r\n }\r\n \r\n try {\r\n const context = {\r\n ...this.buildExpressionContext(),\r\n ...extraContext\r\n }\r\n const val = this.model.eval(expression, context)\r\n if ((val === undefined || val === null) && this.parentEvalExpression) {\r\n return this.parentEvalExpression(expression, extraContext)\r\n }\r\n return val\r\n } catch (error) {\r\n console.error(`[TreeTableModel] Expression parsing failed: \"${expression}\"`, error)\r\n return undefined\r\n }\r\n },\r\n \r\n /**\r\n * 统一字段映射解析方法\r\n * @param {Object} fieldMap - 字段映射配置\r\n * @param {Object} extraContext - 额外上下文数据\r\n * @returns {Object} 解析后的参数字段\r\n */\r\n parseFieldMap (fieldMap = {}, extraContext = {}) {\r\n if (!fieldMap || Object.keys(fieldMap).length === 0) {\r\n return {}\r\n }\r\n \r\n const ret = {}\r\n Object.keys(fieldMap).forEach((expression) => {\r\n const targetKey = fieldMap[expression]\r\n // Use this.parseExpression which internally uses model.eval for better context access\r\n ret[targetKey] = this.parseExpression(expression, extraContext)\r\n })\r\n \r\n return ret\r\n },\r\n \r\n /**\r\n * 设置模型订阅,实现精确的状态同步\r\n */\r\n setupModelSubscriptions () {\r\n if (!this.model) {\r\n console.warn('[TreeTableModel] Model not initialized, cannot setup subscriptions')\r\n return\r\n }\r\n \r\n // 订阅当前行数据变化\r\n this.unsubscribeRowData = this.model.subscribe('currentRowData', (newValue, oldValue) => {\r\n // 模型已经确保只有真正变化时才会调用这里\r\n this.currentRowData = newValue || {}\r\n // 触发视图更新,但避免过度使用$forceUpdate\r\n this.$nextTick(() => {\r\n this.$emit('x:current-row-changed', { \r\n currentRowData: this.currentRowData,\r\n previousRowData: oldValue \r\n })\r\n })\r\n })\r\n \r\n // 订阅表格数据变化\r\n this.unsubscribeTableData = this.model.subscribe('tableData', (newValue) => {\r\n // 模型已经确保只有真正变化时才会调用这里\r\n this.tableData = newValue || []\r\n this.$emit('x:table-data-changed', { tableData: this.tableData })\r\n })\r\n \r\n // 订阅树数据变化\r\n this.unsubscribeTreeData = this.model.subscribe('treeData', (newValue) => {\r\n // 模型已经确保只有真正变化时才会调用这里\r\n this.treeData = newValue || []\r\n this.$emit('x:tree-data-changed', { treeData: this.treeData })\r\n })\r\n \r\n // 订阅加载状态变化\r\n this.unsubscribeLoading = this.model.subscribe('loading', (newValue) => {\r\n if (this.loading !== newValue) {\r\n this.loading = newValue\r\n this.$emit('x:loading-changed', { loading: this.loading })\r\n }\r\n })\r\n \r\n // 订阅总记录数变化\r\n this.unsubscribeTotal = this.model.subscribe('total', (newValue) => {\r\n if (this.total !== newValue) {\r\n this.total = newValue\r\n this.$emit('x:total-changed', { total: this.total })\r\n }\r\n })\r\n \r\n // 订阅共享数据变化(用于弹窗表格等场景)\r\n this.unsubscribeShared = this.model.subscribe('_sharedData', (newValue) => {\r\n if (newValue && Object.keys(newValue).length > 0) {\r\n this.setCurrentRowData(newValue)\r\n this.$emit('x:shared-data-changed', { sharedData: newValue })\r\n }\r\n })\r\n \r\n // 订阅选择数据变化\r\n this.unsubscribeSelection = this.model.subscribe('currentTableSelection', (newValue, oldValue) => {\r\n // 模型已经确保只有真正变化时才会调用这里\r\n this.currentTableSelection = newValue || {}\r\n // 触发视图更新和事件通知\r\n this.$nextTick(() => {\r\n this.$emit('x:current-table-selection-changed', { \r\n currentTableSelection: this.currentTableSelection,\r\n previousCurrentTableSelection: oldValue \r\n })\r\n })\r\n })\r\n \r\n // 订阅选中的树节点变化\r\n this.unsubscribeSelectedNode = this.model.subscribe('selectedNode', (newValue, oldValue) => {\r\n // 模型已经确保只有真正变化时才会调用这里\r\n this.currentTreeNodeData = newValue || {}\r\n // 触发视图更新和事件通知\r\n this.$nextTick(() => {\r\n this.$emit('x:selected-node-changed', { \r\n selectedNode: this.currentTreeNodeData,\r\n previousSelectedNode: oldValue \r\n })\r\n })\r\n })\r\n \r\n console.log('[TreeTableModel] Model subscriptions setup completed')\r\n },\r\n \r\n /**\r\n * 性能监控工具方法\r\n * @param {string} operation - 操作名称\r\n * @param {Function} fn - 要监控的函数\r\n * @returns {Promise<any>} 函数执行结果\r\n */\r\n async withPerformanceMonitoring (operation, fn) {\r\n const startTime = performance.now()\r\n try {\r\n const result = await fn()\r\n const duration = performance.now() - startTime\r\n console.log(`[TreeTableModel] ${operation} took ${duration.toFixed(2)}ms`)\r\n return result\r\n } catch (error) {\r\n const duration = performance.now() - startTime\r\n console.error(`[TreeTableModel] ${operation} failed after ${duration.toFixed(2)}ms:`, error)\r\n throw error\r\n }\r\n },\r\n \r\n /**\r\n * 错误处理工具方法\r\n * @param {string} context - 错误上下文\r\n * @param {Function} fn - 要执行的函数\r\n * @param {any} fallbackValue - 错误时的默认值\r\n * @returns {any} 函数执行结果或默认值\r\n */\r\n withErrorHandling (context, fn, fallbackValue = undefined) {\r\n try {\r\n return fn()\r\n } catch (error) {\r\n console.error(`[TreeTableModel] Error in ${context}:`, error)\r\n return fallbackValue\r\n }\r\n },\r\n \r\n /**\r\n * 清理所有模型订阅\r\n */\r\n cleanupModelSubscriptions () {\r\n const subscriptions = [\r\n 'unsubscribeRowData',\r\n 'unsubscribeTableData', \r\n 'unsubscribeTreeData',\r\n 'unsubscribeLoading',\r\n 'unsubscribeTotal',\r\n 'unsubscribeShared',\r\n 'unsubscribeSelection',\r\n 'unsubscribeSelectedNode'\r\n ]\r\n \r\n subscriptions.forEach(subscriptionName => {\r\n if (this[subscriptionName] && typeof this[subscriptionName] === 'function') {\r\n try {\r\n this[subscriptionName]()\r\n this[subscriptionName] = null\r\n } catch (error) {\r\n console.warn(`[TreeTableModel] Failed to cleanup subscription: ${subscriptionName}`, error)\r\n }\r\n }\r\n })\r\n \r\n console.log('[TreeTableModel] Model subscriptions cleanup completed')\r\n },\r\n \r\n initializeCurrentRowData () {\r\n if (!this.model) return\r\n \r\n let initialData = {}\r\n\r\n // 1. 优先检查共享数据(来自父级)\r\n const sharedData = this.model.getSharedData()\r\n if (sharedData && Object.keys(sharedData).length > 0) {\r\n initialData = sharedData\r\n } else {\r\n // 2. 其次检查 props 或路由参数 (示例逻辑)\r\n // const { rowId } = this.$route.query\r\n // if (rowId) { ... }\r\n }\r\n\r\n // 3. 设置到 Model,这是唯一的数据源初始化入口\r\n this.model.setCurrentRowData(initialData)\r\n \r\n // 4. 同步回本地(虽然 subscribe 会处理,但为了立即能用,这里也设置一下)\r\n this.currentRowData = initialData\r\n },\r\n async refreshTreeData () {\r\n return this.withPerformanceMonitoring('refreshTreeData', async () => {\r\n this.treeData = await this.requestTreeData()\r\n const [defaultTreeNode = {}] = this.treeData\r\n this.defaultExpandedKeys = [defaultTreeNode[this.mapFields.key]]\r\n this.defaultSelectedKeys = [defaultTreeNode[this.mapFields.key]]\r\n \r\n // 1. 先更新模型(Single Source of Truth)\r\n if (this.model) {\r\n this.model.setSelectedNode(defaultTreeNode)\r\n }\r\n \r\n // 2. 组件层数据通过订阅自动更新,无需手动设置\r\n // this.currentTreeNodeData = defaultTreeNode // 不再需要\r\n \r\n this.$emit('x:tree-refreshed', { \r\n treeData: this.treeData,\r\n currentTreeNode: defaultTreeNode \r\n })\r\n \r\n return this.treeData\r\n })\r\n },\r\n dispatchTrigger ({ mode, record = {}, modeMeta = { } }) {\r\n switch (mode) {\r\n case BUILT_IN_TRIGGER.FSM:\r\n this[`${BUILT_IN_TRIGGER.FSM}Trigger`](record, modeMeta = type.isEmpty(modeMeta) ? { \r\n url: 'api-fsm/workbench/fsm/auditFlow',\r\n requestType: 'GET',\r\n fieldMap: {\r\n modelCode: 'modelCode',\r\n businessId: 'businessId'\r\n }\r\n } : modeMeta)\r\n break\r\n case BUILT_IN_TRIGGER.ELE_MODAL_FORM:\r\n this.modalFormMeta = modeMeta\r\n this.showModalForm(modeMeta)\r\n break\r\n case BUILT_IN_TRIGGER.ELE_MODAL_TABLE:\r\n this.modalTableMeta = modeMeta\r\n // 将当前行的 record 数据传递给 modal table\r\n this.showModalTable(modeMeta, record)\r\n break\r\n default:\r\n break\r\n }\r\n },\r\n handleCloseFsmModal () {\r\n this.showFsmModal = false\r\n },\r\n [`${BUILT_IN_TRIGGER.FSM}Trigger`] (record, meta) {\r\n this.fsmMeta = meta\r\n this.fsmContextProp = record\r\n this.showFsmModal = true\r\n },\r\n onChangeTableSelection (_, selectedRows = []) {\r\n // 1. 先更新模型(Single Source of Truth)\r\n if (this.model) {\r\n this.model.setCurrentTableSelection(selectedRows)\r\n }\r\n \r\n // 2. 组件层数据通过订阅自动更新,无需手动设置\r\n // this.setCurrentTableSelection(selectedRows) // 不再需要\r\n \r\n // 3. 继续后续逻辑\r\n this.$emit('on-change-table-selection', this.currentTableSelection)\r\n this.$emit('x:refresh-exposed', { exposed: this.exposed })\r\n },\r\n setCurrentTableSelection (props = {}) {\r\n return this.withErrorHandling('setCurrentTableSelection', () => {\r\n // 处理数据格式\r\n let processedData\r\n if (this.currentTableMode === 'radio') {\r\n processedData = (type.isArray(props) && props.length > 0) ? props[0] : type.isObject(props) ? props : {}\r\n } else {\r\n processedData = type.isArray(props) ? props : []\r\n }\r\n \r\n // 1. 始终先更新 Model (Single Source of Truth)\r\n if (this.model) {\r\n this.model.setCurrentTableSelection(processedData)\r\n }\r\n \r\n // 2. 组件层数据通过订阅自动更新,无需手动设置\r\n // this.$set(this, 'currentTableSelection', processedData) // 不再需要\r\n \r\n // 3. 对外广播\r\n this.$emit('x:current-table-selection-set', { currentTableSelection: processedData })\r\n \r\n return processedData\r\n }, {})\r\n },\r\n getCurrentTableSelection () {\r\n return this.withErrorHandling('getCurrentTableSelection', () => {\r\n if (this.model) {\r\n return this.model.getCurrentTableSelection()\r\n }\r\n console.warn('[TreeTableModel] Model not initialized, getCurrentTableSelection returning local data')\r\n return this.currentTableSelection || {}\r\n }, {})\r\n },\r\n setCurrentRowData (props = {}) {\r\n return this.withErrorHandling('setCurrentRowData', () => {\r\n // 关键:通过浅克隆断开与 Vue 2 Observer 的引用,保证数据纯净\r\n const newData = props ? { ...props } : {}\r\n \r\n // 1. 始终先更新 Model (Single Source of Truth)\r\n if (this.model) {\r\n this.model.setCurrentRowData(newData)\r\n } else {\r\n console.warn('[TreeTableModel] Model not initialized, cannot setCurrentRowData in model')\r\n }\r\n \r\n // 2. 更新本地视图 (双重保证,避免订阅延迟导致的闪烁)\r\n this.currentRowData = newData\r\n \r\n // 3. 对外广播\r\n // 注意:不再需要手动 emit 事件给外部来同步状态,因为外部如果想监听,应该监听 model 变化\r\n // 但为了兼容旧代码,保持一些必要的 emit 可能是好的,比如 'row-change'\r\n this.$emit('x:current-row-data-set', { currentRowData: this.currentRowData })\r\n \r\n return this.currentRowData\r\n }, {})\r\n },\r\n getCurrentRowData () {\r\n return this.withErrorHandling('getCurrentRowData', () => {\r\n if (this.model) {\r\n return this.model.getCurrentRowData()\r\n }\r\n console.warn('[TreeTableModel] Model not initialized, getCurrentRowData returning local data')\r\n return this.currentRowData || {}\r\n }, {})\r\n },\r\n cleanCurrentModelEffect (clearRowData = true) {\r\n const action = () => {\r\n this.setCurrentTableSelection()\r\n if (clearRowData) {\r\n this.setCurrentRowData({})\r\n }\r\n }\r\n \r\n if (this.model) {\r\n this.model.batch(action)\r\n } else {\r\n action()\r\n }\r\n },\r\n [BUILT_IN_EVENT_NAMES.SUBMIT] (props = {}) {\r\n this.cleanCurrentModelEffect()\r\n this.requestTableData()\r\n },\r\n [BUILT_IN_EVENT_NAMES.EDIT] (props = {}) {\r\n const { record = {} } = props\r\n this.setCurrentRowData(record)\r\n this.modalFormMeta = this.editMeta\r\n this.modalFormValue = true\r\n },\r\n [BUILT_IN_EVENT_NAMES.CREATE] () {\r\n this.modalFormMeta = this.createMeta\r\n this.modalFormValue = true\r\n },\r\n showModalForm (modeMeta = {}) {\r\n if (type.isStr(modeMeta)) {\r\n const targetMeta = this.findMetaByKey(modeMeta)\r\n this.modalFormMeta = targetMeta\r\n } else {\r\n this.modalFormMeta = modeMeta\r\n }\r\n this.modalFormValue = true\r\n },\r\n showModalTable (modeMeta = {}, record = null) {\r\n // 获取当前行数据并设置到共享命名空间\r\n const currentRowData = record || this.getCurrentRowData()\r\n \r\n // 🔍 关键调试日志 - 开始\r\n console.log('[DEBUG] showModalTable - currentRowData:', currentRowData)\r\n console.log('[DEBUG] showModalTable - currentTreeNodeData:', this.currentTreeNodeData)\r\n console.log('[DEBUG] showModalTable - 是否有model:', !!this.model)\r\n \r\n if (this.model) {\r\n this.model.setSharedData(currentRowData)\r\n } else {\r\n console.warn('Model not initialized, cannot setSharedData')\r\n }\r\n \r\n let targetMeta = modeMeta\r\n if (type.isStr(modeMeta)) {\r\n targetMeta = this.findMetaByKey(modeMeta)\r\n }\r\n \r\n // 🔍 关键调试日志 - fieldMap处理\r\n console.log('[DEBUG] showModalTable - targetMeta:', targetMeta)\r\n console.log('[DEBUG] showModalTable - 是否有fieldMap:', !!(targetMeta && targetMeta.fieldMap))\r\n \r\n // 解析 fieldMap 参数,使用完整的上下文包括 currentRowData 和父级数据\r\n if (targetMeta && targetMeta.tableMeta.fieldMap) {\r\n const { fieldMap, params = {} } = targetMeta.tableMeta\r\n \r\n console.log('[DEBUG] showModalTable - fieldMap:', fieldMap)\r\n \r\n // 关键:构建完整的上下文,保留原有数据并添加父级节点\r\n const extraContext = {}\r\n \r\n console.log('[DEBUG] showModalTable - extraContext:', extraContext)\r\n \r\n const parsedParams = this.parseFieldMap(fieldMap, extraContext)\r\n \r\n console.log('[DEBUG] showModalTable - parsedParams:', parsedParams)\r\n \r\n // 将解析后的参数传递给 modal table\r\n targetMeta.tableMeta = {\r\n ...targetMeta.tableMeta,\r\n params: {\r\n ...params,\r\n ...parsedParams\r\n }\r\n }\r\n \r\n console.log('[DEBUG] showModalTable - 最终targetMeta.params:', targetMeta.params)\r\n }\r\n \r\n this.modalTableMeta = targetMeta\r\n this.modalTableValue = true\r\n \r\n console.log('[DEBUG] showModalTable - 最终modalTableMeta:', this.modalTableMeta)\r\n },\r\n closeModalForm () {\r\n this.modalFormValue = false\r\n },\r\n closeModalTable () {\r\n this.modalTableValue = false\r\n },\r\n findMetaByKey (key) {\r\n return this.$attrs[key] || {}\r\n },\r\n handleClickButtonGroup (props) {\r\n const { eventName, target } = props\r\n const targetMeta = this.findMetaByKey(target)\r\n const { mode } = targetMeta\r\n mode && this.dispatchTrigger({ mode, modeMeta: targetMeta })\r\n this.$emit(eventName || 'click', { currentTreeNode: this.currentTreeNodeData })\r\n },\r\n async onSearch (props) {\r\n const { overrideInit = false } = this.tableMeta\r\n this.tableQuerys = Object.assign(this.tableQuerys, props)\r\n if (overrideInit) {\r\n this.$emit(RESERVE_EVENT_NAMES.TREE_CHANGE, { ...this.exposed })\r\n } else {\r\n const { initSearch = false } = props\r\n if (this.showTree && initSearch) return\r\n this.tableData = await this.requestTableData()\r\n }\r\n },\r\n async selectTreeNode (selectedKeys, e) {\r\n const { fieldMap } = this.tableMeta\r\n const selectedNodeData = e.node.$vnode.data.props.dataRef || {}\r\n \r\n // 1. 先更新模型(Single Source of Truth)\r\n if (this.model) {\r\n this.model.setSelectedNode(selectedNodeData)\r\n }\r\n\r\n // 2. 组件层数据通过订阅自动更新,无需手动设置\r\n // this.currentTreeNodeData = selectedNodeData // 不再需要\r\n \r\n // 3. 继续后续逻辑\r\n // 使用统一的字段映射解析方法,自动包含所需的上下文\r\n const execFieldMapRet = this.parseFieldMap(fieldMap)\r\n const { overrideInit = false } = this.tableMeta\r\n if (overrideInit) {\r\n this.$emit(RESERVE_EVENT_NAMES.TREE_CHANGE, { ...this.exposed })\r\n } else {\r\n this.tableData = await this.requestTableData(execFieldMapRet)\r\n }\r\n },\r\n async requestTreeData () {\r\n const { url, requestType = 'GET', params = {}, fieldMap = {} } = this.treeMeta\r\n \r\n // 使用统一的字段映射解析方法\r\n const fieldMapRet = this.parseFieldMap(fieldMap)\r\n \r\n try {\r\n const startTime = performance.now()\r\n const ret = await net[requestType.toLowerCase()](\r\n url,\r\n { ...params, ...fieldMapRet }\r\n ).then(resp => {\r\n const { data } = resp || {}\r\n return data\r\n })\r\n \r\n // 性能监控\r\n const duration = performance.now() - startTime\r\n console.log(`[TreeTableModel] Tree data request took ${duration.toFixed(2)}ms`)\r\n \r\n // 通过模型设置树数据\r\n if (this.model) {\r\n this.model.setTreeData(ret)\r\n }\r\n \r\n return ret\r\n } catch (error) {\r\n console.error('[TreeTableModel] Failed to request tree data:', error)\r\n // 错误时设置空数据\r\n if (this.model) {\r\n this.model.setTreeData([])\r\n }\r\n return []\r\n }\r\n },\r\n async onChangePage (page, pageSize) {\r\n this.tableData = await this.requestTableData({ currentPage: page, pageSize })\r\n },\r\n async requestTableData (props = {}) {\r\n const { url, requestType = 'GET', page = {} } = this.tableMeta\r\n \r\n // 更新查询参数\r\n this.tableQuerys = Object.assign(this.tableQuerys, { \r\n currentPage: this.tableQuerys.currentPage || 1, \r\n pageSize: this.tableQuerys.pageSize || page.pageSize || 10 \r\n }, props)\r\n \r\n // 通过模型设置加载状态\r\n if (this.model) {\r\n this.model.setLoading(true)\r\n }\r\n \r\n this.$emit(RESERVE_EVENT_NAMES.WATCH, { ...this.exposed })\r\n \r\n try {\r\n const startTime = performance.now()\r\n const ret = await net[requestType.toLowerCase()](\r\n url,\r\n this.tableQuerys\r\n ).then(resp => {\r\n const { data = [], count } = resp || {}\r\n \r\n // 通过模型设置数据\r\n if (this.model) {\r\n this.model.batch(() => {\r\n this.model.setTableData(data || [])\r\n this.model.setTotal(count || 0)\r\n this.model.setPagination(\r\n this.tableQuerys.currentPage || 1,\r\n this.tableQuerys.pageSize || page.pageSize || 10\r\n )\r\n })\r\n }\r\n \r\n // 性能监控\r\n const duration = performance.now() - startTime\r\n console.log(`[TreeTableModel] Table data request took ${duration.toFixed(2)}ms, returned ${(data || []).length} items`)\r\n \r\n return (data || []).map(item => {\r\n delete item.children\r\n return {\r\n key: uuidv4(),\r\n ...item\r\n }\r\n })\r\n })\r\n \r\n // 更新本地数据(保持兼容)\r\n this.tableData = ret\r\n this.total = this.model ? this.model.getTotal() : 0\r\n \r\n return ret\r\n } catch (error) {\r\n console.error('[TreeTableModel] Failed to request table data:', error)\r\n \r\n // 错误时设置空数据\r\n if (this.model) {\r\n this.model.setTableData([])\r\n this.model.setTotal(0)\r\n }\r\n \r\n this.tableData = []\r\n this.total = 0\r\n return []\r\n } finally {\r\n const finalize = () => {\r\n if (this.model) {\r\n this.model.setLoading(false)\r\n }\r\n this.loading = false\r\n // 清理当前模型效果(不清空currentRowData)\r\n this.cleanCurrentModelEffect(false)\r\n }\r\n\r\n // 使用 batch 确保 loading 状态改变和清理操作原子化\r\n if (this.model) {\r\n this.model.batch(finalize)\r\n } else {\r\n finalize()\r\n }\r\n }\r\n },\r\n calculateTableHeight () {\r\n const currentViewportHeight = window.innerHeight\r\n const tableRef = this.$refs[this.tableRef]\r\n if (!tableRef || !tableRef.$el) return\r\n \r\n const { top: tableToTop, width } = tableRef.$el.getBoundingClientRect()\r\n this.tableWidth = width\r\n \r\n // 获取分页组件的高度(如果存在)\r\n let paginationHeight = 0\r\n const paginationEl = tableRef.$el.querySelector('.g-table__pagination')\r\n if (paginationEl) {\r\n paginationHeight = paginationEl.getBoundingClientRect().height || 50\r\n } else {\r\n // 如果分页组件还未渲染,使用默认高度\r\n paginationHeight = 50\r\n }\r\n \r\n // 计算表格高度:视口高度 - 表格顶部距离 - 分页高度 - 额外高度\r\n const calculatedHeight = currentViewportHeight - tableToTop - paginationHeight - this.overHeight - 20\r\n // 确保最小高度,避免表格过小\r\n this.tableHeight = Math.max(calculatedHeight, 200)\r\n },\r\n calculateTreeHeight () {\r\n if (!this.showTree) return\r\n const modelTableContainerRef = this.$refs[this.modelTableContainerRef]\r\n if (!modelTableContainerRef) return\r\n \r\n const { height } = modelTableContainerRef.getBoundingClientRect()\r\n // 确保树的高度和表格容器高度一致\r\n this.treeWrapperHeight = height\r\n \r\n // 如果表格容器有标题,需要减去标题高度\r\n const titleEl = modelTableContainerRef.querySelector('.model__table--title')\r\n if (titleEl) {\r\n const titleHeight = titleEl.getBoundingClientRect().height\r\n this.treeWrapperHeight = height - titleHeight\r\n }\r\n },\r\n async keepAliveRefresh () {\r\n return this.withPerformanceMonitoring('keepAliveRefresh', async () => {\r\n // 重新计算表格高度(应对窗口大小变化)\r\n this.$nextTick(() => {\r\n setTimeout(() => {\r\n this.calculateTableHeight()\r\n if (this.showTree) {\r\n this.calculateTreeHeight()\r\n }\r\n }, 200)\r\n })\r\n \r\n // 刷新列表数据\r\n const { overrideInit = false } = this.tableMeta\r\n if (overrideInit) {\r\n // 如果使用自定义初始化模式,触发 INIT 事件\r\n this.$emit(RESERVE_EVENT_NAMES.INIT, { ...this.exposed })\r\n } else {\r\n // 使用当前查询参数刷新表格数据\r\n await this.requestTableData(this.tableQuerys)\r\n }\r\n \r\n this.$emit('x:refresh-completed', { \r\n tableData: this.tableData,\r\n treeData: this.treeData,\r\n currentRowData: this.currentRowData\r\n })\r\n })\r\n }\r\n },\r\n mounted () {\r\n // 初始化时先设置一个默认高度,避免布局混乱\r\n this.tableHeight = 400\r\n if (this.showTree) {\r\n this.treeWrapperHeight = 400\r\n }\r\n \r\n // 延迟计算,确保所有组件都已渲染\r\n this.$nextTick(() => {\r\n setTimeout(() => {\r\n this.calculateTableHeight()\r\n this.calculateTreeHeight()\r\n }, 200)\r\n })\r\n \r\n // 使用 ResizeObserver 监听容器大小变化\r\n this.resizeObserverModelTableWrapper = new ResizeObserver(entries => {\r\n for (const _ of entries) {\r\n requestAnimationFrame(() => {\r\n // 延迟重新计算,确保分页组件高度已更新\r\n setTimeout(() => {\r\n this.calculateTableHeight()\r\n if (this.showTree) {\r\n this.calculateTreeHeight()\r\n }\r\n }, 100)\r\n })\r\n }\r\n })\r\n \r\n if (this.$refs[this.modelTableWrapper]) {\r\n this.resizeObserverModelTableWrapper.observe(this.$refs[this.modelTableWrapper])\r\n }\r\n \r\n // 监听表格容器大小变化(用于同步树高度)\r\n if (this.showTree && this.$refs[this.modelTableContainerRef]) {\r\n this.resizeObserverModelTableContainer = new ResizeObserver(entries => {\r\n for (const _ of entries) {\r\n requestAnimationFrame(() => {\r\n this.calculateTreeHeight()\r\n })\r\n }\r\n })\r\n this.resizeObserverModelTableContainer.observe(this.$refs[this.modelTableContainerRef])\r\n }\r\n \r\n // 监听窗口大小变化\r\n this.handleResize = () => {\r\n this.$nextTick(() => {\r\n setTimeout(() => {\r\n this.calculateTableHeight()\r\n if (this.showTree) {\r\n this.calculateTreeHeight()\r\n }\r\n }, 100)\r\n })\r\n }\r\n window.addEventListener('resize', this.handleResize)\r\n },\r\n destroyed () {\r\n if (this.resizeObserverModelTableWrapper) {\r\n this.resizeObserverModelTableWrapper.disconnect()\r\n }\r\n if (this.resizeObserverModelTableContainer) {\r\n this.resizeObserverModelTableContainer.disconnect()\r\n }\r\n if (this.handleResize) {\r\n window.removeEventListener('resize', this.handleResize)\r\n }\r\n if (this.model) {\r\n // 清理所有订阅\r\n this.cleanupModelSubscriptions()\r\n // 清理模型数据\r\n this.model.cleanup()\r\n }\r\n },\r\n async activated () {\r\n await this.keepAliveRefresh()\r\n }\r\n}\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n.ele {\r\n &.model__tree-table {\r\n background: transparent; \r\n display: flex;\r\n flex-direction: row;\r\n width: 100%;\r\n height: 100%;\r\n overflow: hidden;\r\n .model__tree-table--container {\r\n display: flex;\r\n flex-direction: column;\r\n height: 100%;\r\n .model__tree--wrapper {\r\n width: 240px;\r\n background: #fff;\r\n flex-shrink: 0;\r\n padding: 16px;\r\n box-sizing: border-box;\r\n margin-right: 16px;\r\n overflow-y: auto;\r\n overflow-x: hidden;\r\n }\r\n }\r\n .model__table--container {\r\n width: 100%;\r\n min-width: 0;\r\n background: #fff;\r\n display: flex;\r\n flex-direction: column;\r\n height: 100%;\r\n overflow: hidden;\r\n .model__table--title {\r\n .model__table-title--bar {\r\n width: 100%;\r\n height: 8px;\r\n background: var(--idooel-primary-color);\r\n border-top-left-radius: 4px;\r\n border-top-right-radius: 4px;\r\n }\r\n .model__table-title--text {\r\n text-align: left;\r\n padding: 16px;\r\n font-size: 16px;\r\n font-weight: bold;\r\n background: #fff;\r\n border-bottom: 1px solid;\r\n border-color: var(--idoole-black-016);\r\n }\r\n }\r\n .model__table--wrapper {\r\n background: #fff;\r\n display: flex;\r\n flex-direction: column;\r\n height: 100%;\r\n overflow: hidden;\r\n .button-row__area {\r\n width: 100%;\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding-top: 16px;\r\n padding-bottom: 8px;\r\n padding-right: 16px;\r\n flex-shrink: 0;\r\n }\r\n .g-table__wrapper {\r\n flex: 1;\r\n min-height: 0;\r\n overflow: hidden;\r\n .fsm {\r\n cursor: pointer;\r\n color: var(--idooel-primary-color);\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}\r\n</style>\r\n", ".ele.model__tree-table {\n background: transparent;\n display: flex;\n flex-direction: row;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n.ele.model__tree-table .model__tree-table--container {\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n.ele.model__tree-table .model__tree-table--container .model__tree--wrapper {\n width: 240px;\n background: #fff;\n flex-shrink: 0;\n padding: 16px;\n box-sizing: border-box;\n margin-right: 16px;\n overflow-y: auto;\n overflow-x: hidden;\n}\n.ele.model__tree-table .model__table--container {\n width: 100%;\n min-width: 0;\n background: #fff;\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--bar {\n width: 100%;\n height: 8px;\n background: var(--idooel-primary-color);\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--text {\n text-align: left;\n padding: 16px;\n font-size: 16px;\n font-weight: bold;\n background: #fff;\n border-bottom: 1px solid;\n border-color: var(--idoole-black-016);\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper {\n background: #fff;\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .button-row__area {\n width: 100%;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n padding-top: 16px;\n padding-bottom: 8px;\n padding-right: 16px;\n flex-shrink: 0;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .g-table__wrapper {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .g-table__wrapper .fsm {\n cursor: pointer;\n color: var(--idooel-primary-color);\n}\n\n/*# sourceMappingURL=index.vue.map */"]
4009
+ "sourcesContent": ["<template>\r\n <section class=\"ele model__tree-table\">\r\n <section class=\"model__tree-table--container\" v-if=\"showTree\">\r\n <div class=\"model__tree--title\"></div>\r\n <section :ref=\"modelTreeWrapper\" class=\"model__tree--wrapper\" :style=\"{height: `${treeWrapperHeight}px`}\">\r\n <ele-tree\r\n :tree-data=\"treeData\"\r\n :defaultExpandedKeys=\"defaultExpandedKeys\"\r\n :defaultSelectedKeys=\"defaultSelectedKeys\"\r\n @select=\"selectTreeNode\"\r\n :replace-fields=\"mapFields\">\r\n </ele-tree>\r\n </section>\r\n </section>\r\n <section class=\"model__table--container\" :ref=\"modelTableContainerRef\">\r\n <div class=\"model__table--title\" v-if=\"title\">\r\n <template v-if=\"titleMode\">\r\n <div :class=\"[`model__table-title--${titleMode}`]\"></div>\r\n </template>\r\n <template v-else>\r\n <div class=\"model__table-title--text\">{{ title }}</div>\r\n </template>\r\n </div>\r\n <section :ref=\"modelTableWrapper\" class=\"model__table--wrapper\">\r\n <ele-search-area :ref=\"searchArea\" @search=\"onSearch\" :data-source=\"searchMeta.elements\"></ele-search-area>\r\n <div class=\"button-row__area\">\r\n <ele-button-group class=\"model-table__button-group\" v-on=\"overrideButtonGroupEvent\" :ref=\"buttonGroup\" @click=\"handleClickButtonGroup\" :data-source=\"getButtonGroupElements\"></ele-button-group>\r\n <slot name=\"tags\"></slot>\r\n <slot v-if=\"$slots['sub-center']\" name=\"sub-center\"></slot>\r\n </div>\r\n <ele-table\r\n v-on=\"overrideTableEvent\"\r\n :ref=\"tableRef\"\r\n :row-selection=\"rowSelection\"\r\n :loading=\"loading\" \r\n :columns=\"columns\"\r\n :total=\"total\"\r\n :x=\"x\"\r\n :y=\"y\"\r\n :bordered=\"setBorder\"\r\n :height=\"tableHeight\"\r\n :width=\"tableWidth\"\r\n :actions=\"actions\"\r\n :pageSize=\"pageSize\"\r\n :pageSizeOptions=\"pageSizeOptions\"\r\n :data-source=\"tableData\"\r\n :mode=\"mode\"\r\n @change-page=\"onChangePage\"\r\n ></ele-table>\r\n </section>\r\n </section>\r\n <ele-modal-form v-model=\"modalFormValue\" v-on=\"overrideModalFormEvent\" :meta=\"modalFormMeta\"></ele-modal-form>\r\n <ele-modal-fsm v-model=\"showFsmModal\" :contextProp=\"fsmContextProp\" :meta=\"fsmMeta\" @cancel=\"handleCloseFsmModal\"></ele-modal-fsm>\r\n <ele-modal-table\r\n :meta=\"modalTableMeta\"\r\n v-model=\"modalTableValue\"\r\n v-on=\"overrideModalTableEvent\"\r\n ></ele-modal-table>\r\n </section>\r\n</template>\r\n\r\n<script>\r\nimport { type, net } from '@idooel/shared'\r\nimport { v4 as uuidv4 } from 'uuid'\r\nimport { BUILT_IN_EVENT_NAMES, RESERVE_EVENT_NAMES, BUILT_IN_TRIGGER, CONTEXT } from '../../../utils'\r\nimport { createTreeTableModel } from '@idooel/runtime-context'\r\nexport default {\r\n name: 'ele-tree-table-model',\r\n props: {\r\n title: {\r\n type: [Object, String]\r\n },\r\n overHeight: {\r\n type: Number,\r\n default: 0\r\n },\r\n treeMeta: {\r\n type: Object,\r\n default: () => ({})\r\n },\r\n searchMeta: {\r\n type: Object,\r\n default: () => ({})\r\n },\r\n buttonGroupMeta: {\r\n typeof: Object,\r\n default: () => ({})\r\n },\r\n tableMeta: {\r\n type: Object,\r\n default: () => ({})\r\n },\r\n createMeta: {\r\n type: Object\r\n },\r\n editMeta: {\r\n type: Object\r\n }\r\n },\r\n inject: {\r\n parentScopeId: {\r\n default: () => (() => null)\r\n },\r\n parentEvalExpression: {\r\n from: 'evalExpression',\r\n default: () => (() => undefined)\r\n }\r\n },\r\n provide () {\r\n return {\r\n evalExpression: this.parseExpression,\r\n requestTreeData: this.requestTreeData,\r\n requestTableData: this.requestTableData,\r\n keepAliveRefresh: this.keepAliveRefresh,\r\n parentScopeId: () => this.model?.scopeId,\r\n [CONTEXT]: () => {\r\n return {\r\n exposed: this.exposed\r\n }\r\n }\r\n }\r\n },\r\n data () {\r\n return {\r\n tableHeight: 0,\r\n tableWidth: 0,\r\n modalFormMeta: {},\r\n modalFormValue: false,\r\n treeData: [],\r\n tableData: [],\r\n defaultExpandedKeys: [],\r\n defaultSelectedKeys: [],\r\n replaceFields: {\r\n title: 'title',\r\n children: 'children',\r\n key: 'id'\r\n },\r\n loading: false,\r\n total: 0,\r\n tableQuerys: {},\r\n resizeObserverModelTableWrapper: null,\r\n resizeObserverModelTableContainer: null,\r\n resizeObserverSearchArea: null,\r\n modelTableWrapperHeight: 0,\r\n currentTreeNodeData: {},\r\n currentRowData: {},\r\n treeWrapperHeight: 0,\r\n currentTableSelection: this.currentTableMode == 'radio' ? {} : [],\r\n showFsmModal: false,\r\n fsmMeta: {},\r\n fsmContextProp: {},\r\n modalTableValue: false,\r\n modalTableMeta: {}\r\n }\r\n },\r\n computed: {\r\n setBorder () {\r\n return this.tableMeta.bordered === false ? false : true\r\n },\r\n rowSelection () {\r\n if (!this.currentTableMode) return void 0\r\n return {\r\n columnTitle: this.currentSelectionColumn.columnTitle,\r\n fixed: true,\r\n type: this.currentTableMode,\r\n onChange: this.onChangeTableSelection\r\n }\r\n },\r\n currentSelectionColumn () {\r\n const { multiple } = this.tableMeta\r\n const target = this.columns.find(item => Object.keys(item).includes('multiple'))\r\n const isGlobalExistMultiple = Object.keys(this.tableMeta).includes('multiple')\r\n if (target) {\r\n return target\r\n } else if (isGlobalExistMultiple) {\r\n return { multiple }\r\n }\r\n return void 0\r\n },\r\n x () {\r\n const { x } = this.tableMeta\r\n return x\r\n },\r\n y () {\r\n const { y } = this.tableMeta\r\n return y\r\n },\r\n currentTableMode () {\r\n if (!this.currentSelectionColumn) return void 0\r\n const { multiple } = this.currentSelectionColumn\r\n if (type.isBool(multiple)) {\r\n if (multiple) {\r\n return 'checkbox'\r\n } else {\r\n return 'radio'\r\n }\r\n } else {\r\n return void 0\r\n }\r\n },\r\n modelTableContainerRef () {\r\n return uuidv4()\r\n },\r\n titleMode () {\r\n if (type.isObject(this.title)) {\r\n const { mode = '' } = this.title\r\n return mode\r\n }\r\n return void 0\r\n },\r\n tableRef () {\r\n return uuidv4()\r\n },\r\n exposed () {\r\n return {\r\n showModalForm: this.showModalForm,\r\n closeModalForm: this.closeModalForm,\r\n showModalTable: this.showModalTable,\r\n closeModalTable: this.closeModalTable,\r\n currentTableSelection: this.currentTableSelection,\r\n currentTreeNode: this.currentTreeNodeData,\r\n requestTableData: this.requestTableData,\r\n keepAliveRefresh: this.keepAliveRefresh,\r\n refreshTreeData: this.refreshTreeData,\r\n querys: this.tableQuerys,\r\n currentRowData: this.getCurrentRowData(),\r\n getCurrentRowData: this.getCurrentRowData,\r\n setCurrentRowData: this.setCurrentRowData,\r\n setCurrentTableSelection: this.setCurrentTableSelection,\r\n getCurrentTableSelection: this.getCurrentTableSelection,\r\n cleanCurrentModelEffect: this.cleanCurrentModelEffect,\r\n route: this.$route,\r\n _route: this.$route.query,\r\n _routeMeta: this.$route.meta\r\n }\r\n },\r\n overrideTableEvent () {\r\n const events = this.actions.reduce((ret, action) => {\r\n ret[action.eventName || action.key] = (e) => {\r\n this.setCurrentRowData(e.exposed.currentRowData)\r\n const { target } = action\r\n const targetMeta = this.findMetaByKey(target)\r\n const { mode } = targetMeta\r\n mode && this.dispatchTrigger({ mode, record: e.exposed.currentRowData, modeMeta: targetMeta })\r\n this.$emit(action.eventName || action.key, { ...e, currentTreeNode: this.currentTreeNodeData, exposed: { ...this.exposed, ...e.exposed } })\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...this.$listeners,\r\n ...events,\r\n [BUILT_IN_EVENT_NAMES.EDIT]: this[BUILT_IN_EVENT_NAMES.EDIT],\r\n [BUILT_IN_EVENT_NAMES.SUBMIT]: this[BUILT_IN_EVENT_NAMES.SUBMIT]\r\n }\r\n },\r\n overrideModalFormEvent () {\r\n const { footerMeta } = this.modalFormMeta\r\n const { elements = [] } = footerMeta || {}\r\n const eles = type.isFunction(elements) ? elements.call(this) : elements\r\n const events = eles.reduce((ret, ele) => {\r\n ret[ele.eventName] = (e = {}) => {\r\n if (ele.eventName === 'cancel') {\r\n this.closeModalForm()\r\n } else {\r\n const { exposed = {} } = e\r\n this.$emit(`${ele.eventName || ele.key}`, { ...e, currentTreeNode: this.currentTreeNodeData, exposed: Object.assign({}, exposed )})\r\n }\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...events\r\n }\r\n },\r\n overrideModalTableEvent () {\r\n const { footerMeta } = this.modalTableMeta\r\n const { elements = [] } = footerMeta || {}\r\n const eles = type.isFunction(elements) ? elements.call(this) : elements\r\n const events = eles.reduce((ret, ele) => {\r\n ret[ele.eventName] = (e = {}) => {\r\n if (ele.eventName === 'cancel') {\r\n this.closeModalTable()\r\n } else {\r\n const { exposed = {} } = e\r\n this.$emit(`${ele.eventName || ele.key}`, { ...e, currentTreeNode: this.currentTreeNodeData, exposed: Object.assign({}, exposed )})\r\n }\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...events,\r\n exposed: this.exposed\r\n }\r\n },\r\n overrideButtonGroupEvent () {\r\n const events = this.getButtonGroupElements.reduce((ret, ele) => {\r\n ret[ele.eventName] = (e) => {\r\n this.$emit(ele.eventName || 'click', { ...e, currentTreeNode: this.currentTreeNodeData, exposed: Object.assign({}, e.exposed || {}, this.exposed)})\r\n }\r\n return ret\r\n }, {})\r\n return {\r\n ...this.$listeners,\r\n ...events,\r\n [BUILT_IN_EVENT_NAMES.CREATE]: this[BUILT_IN_EVENT_NAMES.CREATE],\r\n exposed: this.exposed\r\n }\r\n },\r\n showTree () {\r\n return !!Object.keys(this.treeMeta).length\r\n },\r\n buttonGroup () {\r\n return uuidv4()\r\n },\r\n searchArea () {\r\n return uuidv4()\r\n },\r\n modelTreeWrapper () {\r\n return uuidv4()\r\n },\r\n modelTableWrapper () {\r\n return uuidv4()\r\n },\r\n actions () {\r\n const { operations } = this.tableMeta\r\n if (operations) {\r\n return operations.elements\r\n } else {\r\n return []\r\n }\r\n },\r\n pageSize () {\r\n const { page = {} } = this.tableMeta\r\n return page.pageSize || 10\r\n },\r\n mode () {\r\n const { page = {} } = this.tableMeta\r\n return page.mode\r\n },\r\n pageSizeOptions () {\r\n const { page = {} } = this.tableMeta\r\n return page.pageSizeOptions || ['10', '20', '30', '40']\r\n },\r\n columns () {\r\n const { columns, operations } = this.tableMeta\r\n if (type.get(columns) === 'array') {\r\n const columnsOptions = columns.map(item => {\r\n const { mode = 'text' } = item\r\n if (item.render) {\r\n return {\r\n ...item,\r\n customRender: (text, record, index) => {\r\n const { $createElement } = this\r\n return item.render.call(this, \r\n { h: $createElement, ctx: this },\r\n text ? typeof text == 'object' ? text[item.dataIndex] : text : '', \r\n record, index)\r\n }\r\n }\r\n } else if (mode !== BUILT_IN_TRIGGER.TEXT) {\r\n const { [`${mode}Meta`]: modeMeta } = item\r\n return {\r\n ...item,\r\n customRender: (text, record, index) => {\r\n return <span onClick={() => this.dispatchTrigger({ mode, record, modeMeta, index })} class={ mode }>{ text }</span>\r\n }\r\n }\r\n }\r\n return {\r\n ...item\r\n }\r\n })\r\n if (operations) {\r\n return [\r\n ...columnsOptions,\r\n {\r\n title: '操作',\r\n width: operations.width,\r\n key: 'action',\r\n fixed: 'right',\r\n scopedSlots: { customRender: 'action' }\r\n }\r\n ]\r\n }\r\n return columnsOptions\r\n } else {\r\n console.error('Error: columns is invalid, please check it')\r\n return []\r\n }\r\n },\r\n getButtonGroupElements () {\r\n const { elements } = this.buttonGroupMeta\r\n if (type.get(elements) === 'function') {\r\n return elements.call(this)\r\n } else if (type.get(elements) === 'array') {\r\n return elements\r\n } else {\r\n return []\r\n }\r\n },\r\n mapFields () {\r\n const { replaceFields = {} } = this.treeMeta\r\n const mapFields = type.isEmpty(replaceFields) ? this.replaceFields : replaceFields\r\n return mapFields\r\n }\r\n },\r\n async created () {\r\n // onSearch会初始化请求表格数据,所以不需要在这里请求表格数据\r\n // 初始化数据池管理器(使用新的 Mutix-based runtime-context)\r\n try {\r\n // 使用组件名 + UUID 作为调试名称,方便在 DevTools 中区分多个实例\r\n // 这里的 Symbol description 将被 mutix devtools plugin 提取显示\r\n const debugName = `TreeTableModel_${this.tableRef || uuidv4().slice(0, 8)}`\r\n // 获取父作用域 ID\r\n const parentId = this.parentScopeId?.() || null\r\n // 创建模型时传入父作用域\r\n this.model = createTreeTableModel(Symbol(debugName), undefined, parentId)\r\n \r\n if (!this.model) {\r\n throw new Error('Failed to create tree table model')\r\n }\r\n } catch (error) {\r\n console.error('Error creating tree table model:', error)\r\n this.model = null\r\n return\r\n }\r\n \r\n // 初始化 currentRowData\r\n this.initializeCurrentRowData()\r\n \r\n // 订阅数据池变化,实现精确的状态同步\r\n this.setupModelSubscriptions()\r\n \r\n if (this.showTree) {\r\n this.treeData = await this.requestTreeData()\r\n const [defaultTreeNode = {}] = this.treeData\r\n this.defaultExpandedKeys = [defaultTreeNode[this.mapFields.key]]\r\n this.defaultSelectedKeys = [defaultTreeNode[this.mapFields.key]]\r\n \r\n // 1. 先更新模型(Single Source of Truth)\r\n if (this.model) {\r\n this.model.setSelectedNode(defaultTreeNode)\r\n }\r\n \r\n // 2. 组件层数据通过订阅自动更新,无需手动设置\r\n // this.currentTreeNodeData = defaultTreeNode // 不再需要\r\n \r\n const { params = {}, fieldMap = {}, overrideInit = false } = this.tableMeta\r\n \r\n // 使用最新的 currentRowData 和统一的表达式上下文\r\n const currentRowData = this.getCurrentRowData()\r\n const extraContext = { \r\n currentRowData: currentRowData\r\n }\r\n \r\n const initQuerys = Object.assign({}, params, this.parseFieldMap(fieldMap, extraContext))\r\n if (overrideInit) {\r\n this.$emit(RESERVE_EVENT_NAMES.INIT, { ...this.exposed })\r\n } else {\r\n this.tableData = await this.requestTableData(initQuerys)\r\n }\r\n } else {\r\n const { params = {}, fieldMap = {} } = this.tableMeta\r\n const currentRowData = this.getCurrentRowData()\r\n const extraContext = { \r\n currentRowData: currentRowData\r\n }\r\n this.tableQuerys = Object.assign({}, params, this.parseFieldMap(fieldMap, extraContext))\r\n }\r\n },\r\n methods: {\r\n /**\r\n * 构建标准化的表达式数据上下文\r\n * @returns {Object} 表达式求值上下文\r\n */\r\n buildExpressionContext () {\r\n let parentSelectedNode = {}\r\n let parentCurrentRowData = {}\r\n \r\n if (this.parentEvalExpression) {\r\n try {\r\n parentSelectedNode = this.parentEvalExpression('selectedNode') || {}\r\n parentCurrentRowData = this.parentEvalExpression('currentRowData') || {}\r\n } catch (e) {\r\n // ignore\r\n }\r\n }\r\n\r\n const currentTreeNode = this.model ? this.model.getSelectedNode() : this.currentTreeNodeData\r\n const currentRowData = this.getCurrentRowData()\r\n\r\n return {\r\n // 路由数据\r\n _route: this.$route.query,\r\n _routeMeta: this.$route.meta,\r\n \r\n // 树节点数据 - 合并父级数据以支持字段级 fallback\r\n currentTreeNode: { ...parentSelectedNode, ...currentTreeNode },\r\n selectedNode: { ...parentSelectedNode, ...currentTreeNode },\r\n \r\n // 表格数据 - 合并父级数据以支持字段级 fallback\r\n currentRowData: { ...parentCurrentRowData, ...currentRowData },\r\n currentTableSelection: this.getCurrentTableSelection(),\r\n \r\n // 分页数据\r\n pagination: {\r\n currentPage: this.tableQuerys.currentPage || 1,\r\n pageSize: this.tableQuerys.pageSize || this.pageSize,\r\n total: this.total\r\n },\r\n \r\n // 查询参数\r\n queryParams: this.tableQuerys,\r\n \r\n // 表格元数据\r\n tableMeta: this.tableMeta,\r\n treeMeta: this.treeMeta,\r\n \r\n // 暴露的方法和数据\r\n exposed: this.exposed\r\n }\r\n },\r\n \r\n /**\r\n * 统一表达式解析方法\r\n * @param {string} expression - 表达式字符串\r\n * @param {Object} extraContext - 额外上下文数据\r\n * @returns {any} 表达式求值结果\r\n */\r\n parseExpression (expression, extraContext = {}) {\r\n if (!this.model) {\r\n console.warn('[TreeTableModel] Model not initialized, cannot parse expression')\r\n return undefined\r\n }\r\n \r\n try {\r\n const context = {\r\n ...this.buildExpressionContext(),\r\n ...extraContext\r\n }\r\n const val = this.model.eval(expression, context)\r\n if ((val === undefined || val === null) && this.parentEvalExpression) {\r\n return this.parentEvalExpression(expression, extraContext)\r\n }\r\n return val\r\n } catch (error) {\r\n console.error(`[TreeTableModel] Expression parsing failed: \"${expression}\"`, error)\r\n return undefined\r\n }\r\n },\r\n \r\n /**\r\n * 统一字段映射解析方法\r\n * @param {Object} fieldMap - 字段映射配置\r\n * @param {Object} extraContext - 额外上下文数据\r\n * @returns {Object} 解析后的参数字段\r\n */\r\n parseFieldMap (fieldMap = {}, extraContext = {}) {\r\n if (!fieldMap || Object.keys(fieldMap).length === 0) {\r\n return {}\r\n }\r\n \r\n const ret = {}\r\n Object.keys(fieldMap).forEach((expression) => {\r\n const targetKey = fieldMap[expression]\r\n // Use this.parseExpression which internally uses model.eval for better context access\r\n ret[targetKey] = this.parseExpression(expression, extraContext)\r\n })\r\n \r\n return ret\r\n },\r\n \r\n /**\r\n * 设置模型订阅,实现精确的状态同步\r\n */\r\n setupModelSubscriptions () {\r\n if (!this.model) {\r\n console.warn('[TreeTableModel] Model not initialized, cannot setup subscriptions')\r\n return\r\n }\r\n \r\n // 订阅当前行数据变化\r\n this.unsubscribeRowData = this.model.subscribe('currentRowData', (newValue, oldValue) => {\r\n // 模型已经确保只有真正变化时才会调用这里\r\n this.currentRowData = newValue || {}\r\n // 触发视图更新,但避免过度使用$forceUpdate\r\n this.$nextTick(() => {\r\n this.$emit('x:current-row-changed', { \r\n currentRowData: this.currentRowData,\r\n previousRowData: oldValue \r\n })\r\n })\r\n })\r\n \r\n // 订阅表格数据变化\r\n this.unsubscribeTableData = this.model.subscribe('tableData', (newValue) => {\r\n // 模型已经确保只有真正变化时才会调用这里\r\n this.tableData = newValue || []\r\n this.$emit('x:table-data-changed', { tableData: this.tableData })\r\n })\r\n \r\n // 订阅树数据变化\r\n this.unsubscribeTreeData = this.model.subscribe('treeData', (newValue) => {\r\n // 模型已经确保只有真正变化时才会调用这里\r\n this.treeData = newValue || []\r\n this.$emit('x:tree-data-changed', { treeData: this.treeData })\r\n })\r\n \r\n // 订阅加载状态变化\r\n this.unsubscribeLoading = this.model.subscribe('loading', (newValue) => {\r\n if (this.loading !== newValue) {\r\n this.loading = newValue\r\n this.$emit('x:loading-changed', { loading: this.loading })\r\n }\r\n })\r\n \r\n // 订阅总记录数变化\r\n this.unsubscribeTotal = this.model.subscribe('total', (newValue) => {\r\n if (this.total !== newValue) {\r\n this.total = newValue\r\n this.$emit('x:total-changed', { total: this.total })\r\n }\r\n })\r\n \r\n // 订阅共享数据变化(用于弹窗表格等场景)\r\n this.unsubscribeShared = this.model.subscribe('_sharedData', (newValue) => {\r\n if (newValue && Object.keys(newValue).length > 0) {\r\n this.setCurrentRowData(newValue)\r\n this.$emit('x:shared-data-changed', { sharedData: newValue })\r\n }\r\n })\r\n \r\n // 订阅选择数据变化\r\n this.unsubscribeSelection = this.model.subscribe('currentTableSelection', (newValue, oldValue) => {\r\n // 模型已经确保只有真正变化时才会调用这里\r\n this.currentTableSelection = newValue || {}\r\n // 触发视图更新和事件通知\r\n this.$nextTick(() => {\r\n this.$emit('x:current-table-selection-changed', { \r\n currentTableSelection: this.currentTableSelection,\r\n previousCurrentTableSelection: oldValue \r\n })\r\n })\r\n })\r\n \r\n // 订阅选中的树节点变化\r\n this.unsubscribeSelectedNode = this.model.subscribe('selectedNode', (newValue, oldValue) => {\r\n // 模型已经确保只有真正变化时才会调用这里\r\n this.currentTreeNodeData = newValue || {}\r\n // 触发视图更新和事件通知\r\n this.$nextTick(() => {\r\n this.$emit('x:selected-node-changed', { \r\n selectedNode: this.currentTreeNodeData,\r\n previousSelectedNode: oldValue \r\n })\r\n })\r\n })\r\n \r\n console.log('[TreeTableModel] Model subscriptions setup completed')\r\n },\r\n \r\n /**\r\n * 性能监控工具方法\r\n * @param {string} operation - 操作名称\r\n * @param {Function} fn - 要监控的函数\r\n * @returns {Promise<any>} 函数执行结果\r\n */\r\n async withPerformanceMonitoring (operation, fn) {\r\n const startTime = performance.now()\r\n try {\r\n const result = await fn()\r\n const duration = performance.now() - startTime\r\n console.log(`[TreeTableModel] ${operation} took ${duration.toFixed(2)}ms`)\r\n return result\r\n } catch (error) {\r\n const duration = performance.now() - startTime\r\n console.error(`[TreeTableModel] ${operation} failed after ${duration.toFixed(2)}ms:`, error)\r\n throw error\r\n }\r\n },\r\n \r\n /**\r\n * 错误处理工具方法\r\n * @param {string} context - 错误上下文\r\n * @param {Function} fn - 要执行的函数\r\n * @param {any} fallbackValue - 错误时的默认值\r\n * @returns {any} 函数执行结果或默认值\r\n */\r\n withErrorHandling (context, fn, fallbackValue = undefined) {\r\n try {\r\n return fn()\r\n } catch (error) {\r\n console.error(`[TreeTableModel] Error in ${context}:`, error)\r\n return fallbackValue\r\n }\r\n },\r\n \r\n /**\r\n * 清理所有模型订阅\r\n */\r\n cleanupModelSubscriptions () {\r\n const subscriptions = [\r\n 'unsubscribeRowData',\r\n 'unsubscribeTableData', \r\n 'unsubscribeTreeData',\r\n 'unsubscribeLoading',\r\n 'unsubscribeTotal',\r\n 'unsubscribeShared',\r\n 'unsubscribeSelection',\r\n 'unsubscribeSelectedNode'\r\n ]\r\n \r\n subscriptions.forEach(subscriptionName => {\r\n if (this[subscriptionName] && typeof this[subscriptionName] === 'function') {\r\n try {\r\n this[subscriptionName]()\r\n this[subscriptionName] = null\r\n } catch (error) {\r\n console.warn(`[TreeTableModel] Failed to cleanup subscription: ${subscriptionName}`, error)\r\n }\r\n }\r\n })\r\n \r\n console.log('[TreeTableModel] Model subscriptions cleanup completed')\r\n },\r\n \r\n initializeCurrentRowData () {\r\n if (!this.model) return\r\n \r\n let initialData = {}\r\n\r\n // 1. 优先检查共享数据(来自父级)\r\n const sharedData = this.model.getSharedData()\r\n if (sharedData && Object.keys(sharedData).length > 0) {\r\n initialData = sharedData\r\n } else {\r\n // 2. 其次检查 props 或路由参数 (示例逻辑)\r\n // const { rowId } = this.$route.query\r\n // if (rowId) { ... }\r\n }\r\n\r\n // 3. 设置到 Model,这是唯一的数据源初始化入口\r\n this.model.setCurrentRowData(initialData)\r\n \r\n // 4. 同步回本地(虽然 subscribe 会处理,但为了立即能用,这里也设置一下)\r\n this.currentRowData = initialData\r\n },\r\n async refreshTreeData () {\r\n return this.withPerformanceMonitoring('refreshTreeData', async () => {\r\n this.treeData = await this.requestTreeData()\r\n const [defaultTreeNode = {}] = this.treeData\r\n this.defaultExpandedKeys = [defaultTreeNode[this.mapFields.key]]\r\n this.defaultSelectedKeys = [defaultTreeNode[this.mapFields.key]]\r\n \r\n // 1. 先更新模型(Single Source of Truth)\r\n if (this.model) {\r\n this.model.setSelectedNode(defaultTreeNode)\r\n }\r\n \r\n // 2. 组件层数据通过订阅自动更新,无需手动设置\r\n // this.currentTreeNodeData = defaultTreeNode // 不再需要\r\n \r\n this.$emit('x:tree-refreshed', { \r\n treeData: this.treeData,\r\n currentTreeNode: defaultTreeNode \r\n })\r\n \r\n return this.treeData\r\n })\r\n },\r\n dispatchTrigger ({ mode, record = {}, modeMeta = { } }) {\r\n switch (mode) {\r\n case BUILT_IN_TRIGGER.FSM:\r\n this[`${BUILT_IN_TRIGGER.FSM}Trigger`](record, modeMeta = type.isEmpty(modeMeta) ? { \r\n url: 'api-fsm/workbench/fsm/auditFlow',\r\n requestType: 'GET',\r\n fieldMap: {\r\n modelCode: 'modelCode',\r\n businessId: 'businessId'\r\n }\r\n } : modeMeta)\r\n break\r\n case BUILT_IN_TRIGGER.ELE_MODAL_FORM:\r\n this.modalFormMeta = modeMeta\r\n this.showModalForm(modeMeta)\r\n break\r\n case BUILT_IN_TRIGGER.ELE_MODAL_TABLE:\r\n this.modalTableMeta = modeMeta\r\n // 将当前行的 record 数据传递给 modal table\r\n this.showModalTable(modeMeta, record)\r\n break\r\n default:\r\n break\r\n }\r\n },\r\n handleCloseFsmModal () {\r\n this.showFsmModal = false\r\n },\r\n [`${BUILT_IN_TRIGGER.FSM}Trigger`] (record, meta) {\r\n this.fsmMeta = meta\r\n this.fsmContextProp = record\r\n this.showFsmModal = true\r\n },\r\n onChangeTableSelection (_, selectedRows = []) {\r\n // 1. 先更新模型(Single Source of Truth)\r\n if (this.model) {\r\n this.model.setCurrentTableSelection(selectedRows)\r\n }\r\n \r\n // 2. 组件层数据通过订阅自动更新,无需手动设置\r\n // this.setCurrentTableSelection(selectedRows) // 不再需要\r\n \r\n // 3. 继续后续逻辑\r\n this.$emit('on-change-table-selection', this.currentTableSelection)\r\n this.$emit('x:refresh-exposed', { exposed: this.exposed })\r\n },\r\n setCurrentTableSelection (props = {}) {\r\n return this.withErrorHandling('setCurrentTableSelection', () => {\r\n // 处理数据格式\r\n let processedData\r\n if (this.currentTableMode === 'radio') {\r\n processedData = (type.isArray(props) && props.length > 0) ? props[0] : type.isObject(props) ? props : {}\r\n } else {\r\n processedData = type.isArray(props) ? props : []\r\n }\r\n \r\n // 1. 始终先更新 Model (Single Source of Truth)\r\n if (this.model) {\r\n this.model.setCurrentTableSelection(processedData)\r\n }\r\n \r\n // 2. 组件层数据通过订阅自动更新,无需手动设置\r\n // this.$set(this, 'currentTableSelection', processedData) // 不再需要\r\n \r\n // 3. 对外广播\r\n this.$emit('x:current-table-selection-set', { currentTableSelection: processedData })\r\n \r\n return processedData\r\n }, {})\r\n },\r\n getCurrentTableSelection () {\r\n return this.withErrorHandling('getCurrentTableSelection', () => {\r\n if (this.model) {\r\n return this.model.getCurrentTableSelection()\r\n }\r\n console.warn('[TreeTableModel] Model not initialized, getCurrentTableSelection returning local data')\r\n return this.currentTableSelection || {}\r\n }, {})\r\n },\r\n setCurrentRowData (props = {}) {\r\n return this.withErrorHandling('setCurrentRowData', () => {\r\n // 关键:通过浅克隆断开与 Vue 2 Observer 的引用,保证数据纯净\r\n const newData = props ? { ...props } : {}\r\n \r\n // 1. 始终先更新 Model (Single Source of Truth)\r\n if (this.model) {\r\n this.model.setCurrentRowData(newData)\r\n } else {\r\n console.warn('[TreeTableModel] Model not initialized, cannot setCurrentRowData in model')\r\n }\r\n \r\n // 2. 更新本地视图 (双重保证,避免订阅延迟导致的闪烁)\r\n this.currentRowData = newData\r\n \r\n // 3. 对外广播\r\n // 注意:不再需要手动 emit 事件给外部来同步状态,因为外部如果想监听,应该监听 model 变化\r\n // 但为了兼容旧代码,保持一些必要的 emit 可能是好的,比如 'row-change'\r\n this.$emit('x:current-row-data-set', { currentRowData: this.currentRowData })\r\n \r\n return this.currentRowData\r\n }, {})\r\n },\r\n getCurrentRowData () {\r\n return this.withErrorHandling('getCurrentRowData', () => {\r\n if (this.model) {\r\n return this.model.getCurrentRowData()\r\n }\r\n console.warn('[TreeTableModel] Model not initialized, getCurrentRowData returning local data')\r\n return this.currentRowData || {}\r\n }, {})\r\n },\r\n cleanCurrentModelEffect (clearRowData = true) {\r\n const action = () => {\r\n this.setCurrentTableSelection()\r\n if (clearRowData) {\r\n this.setCurrentRowData({})\r\n }\r\n }\r\n \r\n if (this.model) {\r\n this.model.batch(action)\r\n } else {\r\n action()\r\n }\r\n },\r\n [BUILT_IN_EVENT_NAMES.SUBMIT] (props = {}) {\r\n this.cleanCurrentModelEffect()\r\n this.requestTableData()\r\n },\r\n [BUILT_IN_EVENT_NAMES.EDIT] (props = {}) {\r\n const { record = {} } = props\r\n this.setCurrentRowData(record)\r\n this.modalFormMeta = this.editMeta\r\n this.modalFormValue = true\r\n },\r\n [BUILT_IN_EVENT_NAMES.CREATE] () {\r\n this.modalFormMeta = this.createMeta\r\n this.modalFormValue = true\r\n },\r\n showModalForm (modeMeta = {}) {\r\n if (type.isStr(modeMeta)) {\r\n const targetMeta = this.findMetaByKey(modeMeta)\r\n this.modalFormMeta = targetMeta\r\n } else {\r\n this.modalFormMeta = modeMeta\r\n }\r\n this.modalFormValue = true\r\n },\r\n showModalTable (modeMeta = {}, record = null) {\r\n // 获取当前行数据并设置到共享命名空间\r\n const currentRowData = record || this.getCurrentRowData()\r\n \r\n // 🔍 关键调试日志 - 开始\r\n console.log('[DEBUG] showModalTable - currentRowData:', currentRowData)\r\n console.log('[DEBUG] showModalTable - currentTreeNodeData:', this.currentTreeNodeData)\r\n console.log('[DEBUG] showModalTable - 是否有model:', !!this.model)\r\n \r\n if (this.model) {\r\n this.model.setSharedData(currentRowData)\r\n } else {\r\n console.warn('Model not initialized, cannot setSharedData')\r\n }\r\n \r\n let targetMeta = modeMeta\r\n if (type.isStr(modeMeta)) {\r\n targetMeta = this.findMetaByKey(modeMeta)\r\n }\r\n \r\n // 🔍 关键调试日志 - fieldMap处理\r\n console.log('[DEBUG] showModalTable - targetMeta:', targetMeta)\r\n console.log('[DEBUG] showModalTable - 是否有fieldMap:', !!(targetMeta && targetMeta.fieldMap))\r\n \r\n // 解析 fieldMap 参数,使用完整的上下文包括 currentRowData 和父级数据\r\n if (targetMeta && targetMeta.tableMeta.fieldMap) {\r\n const { fieldMap, params = {} } = targetMeta.tableMeta\r\n \r\n console.log('[DEBUG] showModalTable - fieldMap:', fieldMap)\r\n \r\n // 关键:构建完整的上下文,保留原有数据并添加父级节点\r\n const extraContext = {}\r\n \r\n console.log('[DEBUG] showModalTable - extraContext:', extraContext)\r\n \r\n const parsedParams = this.parseFieldMap(fieldMap, extraContext)\r\n \r\n console.log('[DEBUG] showModalTable - parsedParams:', parsedParams)\r\n \r\n // 将解析后的参数传递给 modal table\r\n targetMeta.tableMeta = {\r\n ...targetMeta.tableMeta,\r\n params: {\r\n ...params,\r\n ...parsedParams\r\n }\r\n }\r\n \r\n console.log('[DEBUG] showModalTable - 最终targetMeta.params:', targetMeta.params)\r\n }\r\n \r\n this.modalTableMeta = targetMeta\r\n this.modalTableValue = true\r\n \r\n console.log('[DEBUG] showModalTable - 最终modalTableMeta:', this.modalTableMeta)\r\n },\r\n closeModalForm () {\r\n this.modalFormValue = false\r\n },\r\n closeModalTable () {\r\n this.modalTableValue = false\r\n },\r\n findMetaByKey (key) {\r\n return this.$attrs[key] || {}\r\n },\r\n handleClickButtonGroup (props) {\r\n const { eventName, target } = props\r\n const targetMeta = this.findMetaByKey(target)\r\n const { mode } = targetMeta\r\n mode && this.dispatchTrigger({ mode, modeMeta: targetMeta })\r\n this.$emit(eventName || 'click', { currentTreeNode: this.currentTreeNodeData })\r\n },\r\n async onSearch (props) {\r\n const { overrideInit = false } = this.tableMeta\r\n this.tableQuerys = Object.assign(this.tableQuerys, props)\r\n if (overrideInit) {\r\n this.$emit(RESERVE_EVENT_NAMES.TREE_CHANGE, { ...this.exposed })\r\n } else {\r\n const { initSearch = false } = props\r\n if (this.showTree && initSearch) return\r\n this.tableData = await this.requestTableData()\r\n }\r\n },\r\n async selectTreeNode (selectedKeys, e) {\r\n const { fieldMap } = this.tableMeta\r\n const selectedNodeData = e.node.$vnode.data.props.dataRef || {}\r\n \r\n // 1. 先更新模型(Single Source of Truth)\r\n if (this.model) {\r\n this.model.setSelectedNode(selectedNodeData)\r\n }\r\n\r\n // 2. 组件层数据通过订阅自动更新,无需手动设置\r\n // this.currentTreeNodeData = selectedNodeData // 不再需要\r\n \r\n // 3. 继续后续逻辑\r\n // 使用统一的字段映射解析方法,自动包含所需的上下文\r\n const execFieldMapRet = this.parseFieldMap(fieldMap)\r\n const { overrideInit = false } = this.tableMeta\r\n if (overrideInit) {\r\n this.$emit(RESERVE_EVENT_NAMES.TREE_CHANGE, { ...this.exposed })\r\n } else {\r\n this.tableData = await this.requestTableData(execFieldMapRet)\r\n }\r\n },\r\n async requestTreeData () {\r\n const { url, requestType = 'GET', params = {}, fieldMap = {} } = this.treeMeta\r\n \r\n // 使用统一的字段映射解析方法\r\n const fieldMapRet = this.parseFieldMap(fieldMap)\r\n \r\n try {\r\n const startTime = performance.now()\r\n const ret = await net[requestType.toLowerCase()](\r\n url,\r\n { ...params, ...fieldMapRet }\r\n ).then(resp => {\r\n const { data } = resp || {}\r\n return data\r\n })\r\n \r\n // 性能监控\r\n const duration = performance.now() - startTime\r\n console.log(`[TreeTableModel] Tree data request took ${duration.toFixed(2)}ms`)\r\n \r\n // 通过模型设置树数据\r\n if (this.model) {\r\n this.model.setTreeData(ret)\r\n }\r\n \r\n return ret\r\n } catch (error) {\r\n console.error('[TreeTableModel] Failed to request tree data:', error)\r\n // 错误时设置空数据\r\n if (this.model) {\r\n this.model.setTreeData([])\r\n }\r\n return []\r\n }\r\n },\r\n async onChangePage (page, pageSize) {\r\n this.tableData = await this.requestTableData({ currentPage: page, pageSize })\r\n },\r\n async requestTableData (props = {}) {\r\n const { url, requestType = 'GET', page = {} } = this.tableMeta\r\n \r\n // 更新查询参数\r\n this.tableQuerys = Object.assign(this.tableQuerys, { \r\n currentPage: this.tableQuerys.currentPage || 1, \r\n pageSize: this.tableQuerys.pageSize || page.pageSize || 10 \r\n }, props)\r\n \r\n // 通过模型设置加载状态\r\n if (this.model) {\r\n this.model.setLoading(true)\r\n }\r\n \r\n this.$emit(RESERVE_EVENT_NAMES.WATCH, { ...this.exposed })\r\n \r\n try {\r\n const startTime = performance.now()\r\n const ret = await net[requestType.toLowerCase()](\r\n url,\r\n this.tableQuerys\r\n ).then(resp => {\r\n const { data = [], count } = resp || {}\r\n \r\n // 通过模型设置数据\r\n if (this.model) {\r\n this.model.batch(() => {\r\n this.model.setTableData(data || [])\r\n this.model.setTotal(count || 0)\r\n this.model.setPagination(\r\n this.tableQuerys.currentPage || 1,\r\n this.tableQuerys.pageSize || page.pageSize || 10\r\n )\r\n })\r\n }\r\n \r\n // 性能监控\r\n const duration = performance.now() - startTime\r\n console.log(`[TreeTableModel] Table data request took ${duration.toFixed(2)}ms, returned ${(data || []).length} items`)\r\n \r\n return (data || []).map(item => {\r\n delete item.children\r\n return {\r\n key: uuidv4(),\r\n ...item\r\n }\r\n })\r\n })\r\n \r\n // 更新本地数据(保持兼容)\r\n this.tableData = ret\r\n this.total = this.model ? this.model.getTotal() : 0\r\n \r\n return ret\r\n } catch (error) {\r\n console.error('[TreeTableModel] Failed to request table data:', error)\r\n \r\n // 错误时设置空数据\r\n if (this.model) {\r\n this.model.setTableData([])\r\n this.model.setTotal(0)\r\n }\r\n \r\n this.tableData = []\r\n this.total = 0\r\n return []\r\n } finally {\r\n const finalize = () => {\r\n if (this.model) {\r\n this.model.setLoading(false)\r\n }\r\n this.loading = false\r\n // 清理当前模型效果(不清空currentRowData)\r\n this.cleanCurrentModelEffect(false)\r\n }\r\n\r\n // 使用 batch 确保 loading 状态改变和清理操作原子化\r\n if (this.model) {\r\n this.model.batch(finalize)\r\n } else {\r\n finalize()\r\n }\r\n }\r\n },\r\n calculateTableHeight () {\r\n const currentViewportHeight = window.innerHeight\r\n const tableRef = this.$refs[this.tableRef]\r\n if (!tableRef || !tableRef.$el) return\r\n \r\n const { top: tableToTop, width } = tableRef.$el.getBoundingClientRect()\r\n this.tableWidth = width\r\n\r\n // 计算表格高度:外层容器高度应覆盖到视口底部;分页高度由 ele-table 内部自行扣减\r\n const calculatedHeight = currentViewportHeight - tableToTop - this.overHeight\r\n // 确保最小高度,避免表格过小\r\n this.tableHeight = Math.max(calculatedHeight, 200)\r\n },\r\n calculateTreeHeight () {\r\n if (!this.showTree) return\r\n const modelTableContainerRef = this.$refs[this.modelTableContainerRef]\r\n if (!modelTableContainerRef) return\r\n \r\n const { height } = modelTableContainerRef.getBoundingClientRect()\r\n // 确保树的高度和表格容器高度一致\r\n this.treeWrapperHeight = height\r\n \r\n // 如果表格容器有标题,需要减去标题高度\r\n const titleEl = modelTableContainerRef.querySelector('.model__table--title')\r\n if (titleEl) {\r\n const titleHeight = titleEl.getBoundingClientRect().height\r\n this.treeWrapperHeight = height - titleHeight\r\n }\r\n },\r\n observeSearchAreaHeight () {\r\n const searchAreaRef = this.$refs[this.searchArea]\r\n const searchAreaEl = searchAreaRef && searchAreaRef.$el\r\n if (!searchAreaEl || typeof ResizeObserver === 'undefined') return\r\n\r\n this.resizeObserverSearchArea = new ResizeObserver(entries => {\r\n for (const entry of entries) {\r\n requestAnimationFrame(() => {\r\n this.calculateTableHeight()\r\n if (this.showTree) {\r\n this.calculateTreeHeight()\r\n }\r\n })\r\n }\r\n })\r\n this.resizeObserverSearchArea.observe(searchAreaEl)\r\n },\r\n async keepAliveRefresh () {\r\n return this.withPerformanceMonitoring('keepAliveRefresh', async () => {\r\n // 重新计算表格高度(应对窗口大小变化)\r\n this.$nextTick(() => {\r\n setTimeout(() => {\r\n this.calculateTableHeight()\r\n if (this.showTree) {\r\n this.calculateTreeHeight()\r\n }\r\n }, 200)\r\n })\r\n \r\n // 刷新列表数据\r\n const { overrideInit = false } = this.tableMeta\r\n if (overrideInit) {\r\n // 如果使用自定义初始化模式,触发 INIT 事件\r\n this.$emit(RESERVE_EVENT_NAMES.INIT, { ...this.exposed })\r\n } else {\r\n // 使用当前查询参数刷新表格数据\r\n await this.requestTableData(this.tableQuerys)\r\n }\r\n \r\n this.$emit('x:refresh-completed', { \r\n tableData: this.tableData,\r\n treeData: this.treeData,\r\n currentRowData: this.currentRowData\r\n })\r\n })\r\n }\r\n },\r\n mounted () {\r\n // 初始化时先设置一个默认高度,避免布局混乱\r\n this.tableHeight = 400\r\n if (this.showTree) {\r\n this.treeWrapperHeight = 400\r\n }\r\n \r\n // 延迟计算,确保所有组件都已渲染\r\n this.$nextTick(() => {\r\n setTimeout(() => {\r\n this.calculateTableHeight()\r\n this.calculateTreeHeight()\r\n this.observeSearchAreaHeight()\r\n }, 200)\r\n })\r\n \r\n // 使用 ResizeObserver 监听容器大小变化\r\n this.resizeObserverModelTableWrapper = new ResizeObserver(entries => {\r\n for (const _ of entries) {\r\n requestAnimationFrame(() => {\r\n // 延迟重新计算,确保分页组件高度已更新\r\n setTimeout(() => {\r\n this.calculateTableHeight()\r\n if (this.showTree) {\r\n this.calculateTreeHeight()\r\n }\r\n }, 100)\r\n })\r\n }\r\n })\r\n \r\n if (this.$refs[this.modelTableWrapper]) {\r\n this.resizeObserverModelTableWrapper.observe(this.$refs[this.modelTableWrapper])\r\n }\r\n \r\n // 监听表格容器大小变化(用于同步树高度)\r\n if (this.showTree && this.$refs[this.modelTableContainerRef]) {\r\n this.resizeObserverModelTableContainer = new ResizeObserver(entries => {\r\n for (const _ of entries) {\r\n requestAnimationFrame(() => {\r\n this.calculateTreeHeight()\r\n })\r\n }\r\n })\r\n this.resizeObserverModelTableContainer.observe(this.$refs[this.modelTableContainerRef])\r\n }\r\n \r\n // 监听窗口大小变化\r\n this.handleResize = () => {\r\n this.$nextTick(() => {\r\n setTimeout(() => {\r\n this.calculateTableHeight()\r\n if (this.showTree) {\r\n this.calculateTreeHeight()\r\n }\r\n }, 100)\r\n })\r\n }\r\n window.addEventListener('resize', this.handleResize)\r\n },\r\n destroyed () {\r\n if (this.resizeObserverModelTableWrapper) {\r\n this.resizeObserverModelTableWrapper.disconnect()\r\n }\r\n if (this.resizeObserverModelTableContainer) {\r\n this.resizeObserverModelTableContainer.disconnect()\r\n }\r\n if (this.resizeObserverSearchArea) {\r\n this.resizeObserverSearchArea.disconnect()\r\n }\r\n if (this.handleResize) {\r\n window.removeEventListener('resize', this.handleResize)\r\n }\r\n if (this.model) {\r\n // 清理所有订阅\r\n this.cleanupModelSubscriptions()\r\n // 清理模型数据\r\n this.model.cleanup()\r\n }\r\n },\r\n async activated () {\r\n await this.keepAliveRefresh()\r\n }\r\n}\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n.ele {\r\n &.model__tree-table {\r\n background: transparent; \r\n display: flex;\r\n flex-direction: row;\r\n width: 100%;\r\n height: 100%;\r\n overflow: hidden;\r\n .model__tree-table--container {\r\n display: flex;\r\n flex-direction: column;\r\n height: 100%;\r\n .model__tree--wrapper {\r\n width: 240px;\r\n background: #fff;\r\n flex-shrink: 0;\r\n padding: 16px;\r\n box-sizing: border-box;\r\n margin-right: 16px;\r\n overflow-y: auto;\r\n overflow-x: hidden;\r\n }\r\n }\r\n .model__table--container {\r\n width: 100%;\r\n min-width: 0;\r\n background: #fff;\r\n display: flex;\r\n flex-direction: column;\r\n height: 100%;\r\n overflow: hidden;\r\n .model__table--title {\r\n .model__table-title--bar {\r\n width: 100%;\r\n height: 8px;\r\n background: var(--idooel-primary-color);\r\n border-top-left-radius: 4px;\r\n border-top-right-radius: 4px;\r\n }\r\n .model__table-title--text {\r\n text-align: left;\r\n padding: 16px;\r\n font-size: 16px;\r\n font-weight: bold;\r\n background: #fff;\r\n border-bottom: 1px solid;\r\n border-color: var(--idoole-black-016);\r\n }\r\n }\r\n .model__table--wrapper {\r\n background: #fff;\r\n display: flex;\r\n flex-direction: column;\r\n height: 100%;\r\n overflow: hidden;\r\n .button-row__area {\r\n width: 100%;\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding-top: 16px;\r\n padding-bottom: 8px;\r\n padding-right: 16px;\r\n flex-shrink: 0;\r\n }\r\n .g-table__wrapper {\r\n flex: 1;\r\n min-height: 0;\r\n overflow: hidden;\r\n .fsm {\r\n cursor: pointer;\r\n color: var(--idooel-primary-color);\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}\r\n</style>\r\n", ".ele.model__tree-table {\n background: transparent;\n display: flex;\n flex-direction: row;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n.ele.model__tree-table .model__tree-table--container {\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n.ele.model__tree-table .model__tree-table--container .model__tree--wrapper {\n width: 240px;\n background: #fff;\n flex-shrink: 0;\n padding: 16px;\n box-sizing: border-box;\n margin-right: 16px;\n overflow-y: auto;\n overflow-x: hidden;\n}\n.ele.model__tree-table .model__table--container {\n width: 100%;\n min-width: 0;\n background: #fff;\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--bar {\n width: 100%;\n height: 8px;\n background: var(--idooel-primary-color);\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.ele.model__tree-table .model__table--container .model__table--title .model__table-title--text {\n text-align: left;\n padding: 16px;\n font-size: 16px;\n font-weight: bold;\n background: #fff;\n border-bottom: 1px solid;\n border-color: var(--idoole-black-016);\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper {\n background: #fff;\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .button-row__area {\n width: 100%;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n padding-top: 16px;\n padding-bottom: 8px;\n padding-right: 16px;\n flex-shrink: 0;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .g-table__wrapper {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n.ele.model__tree-table .model__table--container .model__table--wrapper .g-table__wrapper .fsm {\n cursor: pointer;\n color: var(--idooel-primary-color);\n}\n\n/*# sourceMappingURL=index.vue.map */"]
3928
4010
  },
3929
4011
  media: undefined
3930
4012
  });
3931
4013
  };
3932
4014
  /* scoped */
3933
- var __vue_scope_id__$B = "data-v-98a9f3fc";
4015
+ var __vue_scope_id__$B = "data-v-08876ae3";
3934
4016
  /* module identifier */
3935
4017
  var __vue_module_identifier__$B = undefined;
3936
4018
  /* functional template */