@veltra/desktop 1.5.0 → 1.6.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"allocate-column-widths.js","names":[],"sources":["../../../src/components/table/allocate-column-widths.ts"],"sourcesContent":["import type { ColumnNode } from './node/col'\n\n/**\n * 按容器宽度分配叶子列宽。\n *\n * - `explicitWidth` 列:保持配置/拖拽锁定的宽度\n * - `fixed` 且未显式设宽的列:保持 `minWidth`,不参与剩余宽度均分\n * - 其余未显式设宽的列:均分容器剩余宽度\n */\nexport function allocateLeafColumnWidths(leafColumns: ColumnNode[], containerWidth: number): void {\n if (!containerWidth || !leafColumns.length) return\n\n const growColumns = leafColumns.filter((column) => !column.explicitWidth && !column.fixed)\n // fixed 且未显式设宽:清掉可能被旧算法撑大的残留 width,回落到 minWidth\n const fixedFlexColumns = leafColumns.filter((column) => !column.explicitWidth && column.fixed)\n\n fixedFlexColumns.forEach((column) => {\n column.data.width = undefined\n })\n\n const explicitWidthTotal = leafColumns\n .filter((column) => column.explicitWidth)\n .reduce((acc, cur) => acc + (cur.width ?? cur.minWidth!), 0)\n const growBaseTotal = growColumns.reduce((acc, cur) => acc + cur.minWidth!, 0)\n const fixedFlexBaseTotal = fixedFlexColumns.reduce((acc, cur) => acc + cur.minWidth!, 0)\n const totalBase = explicitWidthTotal + growBaseTotal + fixedFlexBaseTotal\n\n if (growColumns.length > 0 && totalBase < containerWidth) {\n const allocatedWidth = (containerWidth - totalBase) / growColumns.length\n growColumns.forEach((column) => {\n column.width = column.minWidth! + allocatedWidth\n })\n } else {\n growColumns.forEach((column) => {\n column.data.width = undefined\n })\n }\n}\n"],"mappings":";;;;;;;;AASA,SAAgB,yBAAyB,aAA2B,gBAA8B;CAChG,IAAI,CAAC,kBAAkB,CAAC,YAAY,QAAQ;CAE5C,MAAM,cAAc,YAAY,QAAQ,WAAW,CAAC,OAAO,iBAAiB,CAAC,OAAO,KAAK;CAEzF,MAAM,mBAAmB,YAAY,QAAQ,WAAW,CAAC,OAAO,iBAAiB,OAAO,KAAK;CAE7F,iBAAiB,SAAS,WAAW;EACnC,OAAO,KAAK,QAAQ,KAAA;CACtB,CAAC;CAED,MAAM,qBAAqB,YACxB,QAAQ,WAAW,OAAO,aAAa,CAAC,CACxC,QAAQ,KAAK,QAAQ,OAAO,IAAI,SAAS,IAAI,WAAY,CAAC;CAC7D,MAAM,gBAAgB,YAAY,QAAQ,KAAK,QAAQ,MAAM,IAAI,UAAW,CAAC;CAC7E,MAAM,qBAAqB,iBAAiB,QAAQ,KAAK,QAAQ,MAAM,IAAI,UAAW,CAAC;CACvF,MAAM,YAAY,qBAAqB,gBAAgB;CAEvD,IAAI,YAAY,SAAS,KAAK,YAAY,gBAAgB;EACxD,MAAM,kBAAkB,iBAAiB,aAAa,YAAY;EAClE,YAAY,SAAS,WAAW;GAC9B,OAAO,QAAQ,OAAO,WAAY;EACpC,CAAC;CACH,OACE,YAAY,SAAS,WAAW;EAC9B,OAAO,KAAK,QAAQ,KAAA;CACtB,CAAC;AAEL"}
1
+ {"version":3,"file":"allocate-column-widths.js","names":[],"sources":["../../../src/components/table/allocate-column-widths.ts"],"sourcesContent":["import type { ColumnNode } from './node/col'\n\n/**\n * 按容器宽度分配叶子列宽。\n *\n * - `explicitWidth` 列:保持配置/拖拽锁定的宽度\n * - `fixed` 且未显式设宽的列:保持 `minWidth`,不参与剩余宽度均分\n * - 其余未显式设宽的列:均分容器剩余宽度\n *\n * 分配结果写入列节点自身的 `allocatedWidth`,不回写列配置的 `data.width`,\n * 避免污染用户数据后在列森林重建时被误判为显式宽度。\n */\nexport function allocateLeafColumnWidths(leafColumns: ColumnNode[], containerWidth: number): void {\n if (!containerWidth || !leafColumns.length) return\n\n const growColumns = leafColumns.filter((column) => !column.explicitWidth && !column.fixed)\n // fixed 且未显式设宽:清掉之前的分配结果,回落到 minWidth\n const fixedFlexColumns = leafColumns.filter((column) => !column.explicitWidth && column.fixed)\n\n fixedFlexColumns.forEach((column) => {\n column.allocatedWidth = undefined\n })\n\n const explicitWidthTotal = leafColumns\n .filter((column) => column.explicitWidth)\n .reduce((acc, cur) => acc + (cur.width ?? cur.minWidth!), 0)\n const growBaseTotal = growColumns.reduce((acc, cur) => acc + cur.minWidth!, 0)\n const fixedFlexBaseTotal = fixedFlexColumns.reduce((acc, cur) => acc + cur.minWidth!, 0)\n const totalBase = explicitWidthTotal + growBaseTotal + fixedFlexBaseTotal\n\n if (growColumns.length > 0 && totalBase < containerWidth) {\n const allocatedWidth = (containerWidth - totalBase) / growColumns.length\n growColumns.forEach((column) => {\n column.allocatedWidth = column.minWidth! + allocatedWidth\n })\n } else {\n growColumns.forEach((column) => {\n column.allocatedWidth = undefined\n })\n }\n}\n"],"mappings":";;;;;;;;;;;AAYA,SAAgB,yBAAyB,aAA2B,gBAA8B;CAChG,IAAI,CAAC,kBAAkB,CAAC,YAAY,QAAQ;CAE5C,MAAM,cAAc,YAAY,QAAQ,WAAW,CAAC,OAAO,iBAAiB,CAAC,OAAO,KAAK;CAEzF,MAAM,mBAAmB,YAAY,QAAQ,WAAW,CAAC,OAAO,iBAAiB,OAAO,KAAK;CAE7F,iBAAiB,SAAS,WAAW;EACnC,OAAO,iBAAiB,KAAA;CAC1B,CAAC;CAED,MAAM,qBAAqB,YACxB,QAAQ,WAAW,OAAO,aAAa,CAAC,CACxC,QAAQ,KAAK,QAAQ,OAAO,IAAI,SAAS,IAAI,WAAY,CAAC;CAC7D,MAAM,gBAAgB,YAAY,QAAQ,KAAK,QAAQ,MAAM,IAAI,UAAW,CAAC;CAC7E,MAAM,qBAAqB,iBAAiB,QAAQ,KAAK,QAAQ,MAAM,IAAI,UAAW,CAAC;CACvF,MAAM,YAAY,qBAAqB,gBAAgB;CAEvD,IAAI,YAAY,SAAS,KAAK,YAAY,gBAAgB;EACxD,MAAM,kBAAkB,iBAAiB,aAAa,YAAY;EAClE,YAAY,SAAS,WAAW;GAC9B,OAAO,iBAAiB,OAAO,WAAY;EAC7C,CAAC;CACH,OACE,YAAY,SAAS,WAAW;EAC9B,OAAO,iBAAiB,KAAA;CAC1B,CAAC;AAEL"}
@@ -37,13 +37,26 @@ var ColumnNode = class extends TreeNode {
37
37
  }
38
38
  /** 宽度 */
39
39
  get width() {
40
- const { width } = this.data;
40
+ const width = this.data.width ?? this.derived.allocatedWidth;
41
41
  if (!width || this.minWidth && width && width < this.minWidth) return this.minWidth;
42
42
  return width;
43
43
  }
44
44
  set width(val) {
45
45
  if (this.minWidth && val && val < this.minWidth) val = this.minWidth;
46
46
  this.data.width = val;
47
+ this.derived.allocatedWidth = void 0;
48
+ }
49
+ /**
50
+ * 列宽分配算法写入的宽度。
51
+ * 与用户列配置 `data.width` 隔离:分配结果不回写列配置对象,
52
+ * 避免列森林重建(重挂载 / v-if / columns 引用变化)时把上一次的
53
+ * 分配残留误判为显式 width,导致列宽被永久锁定、容器变窄后出现滚动条。
54
+ */
55
+ get allocatedWidth() {
56
+ return this.derived.allocatedWidth;
57
+ }
58
+ set allocatedWidth(val) {
59
+ this.derived.allocatedWidth = val;
47
60
  }
48
61
  /** 最小宽度 */
49
62
  get minWidth() {
@@ -80,6 +93,8 @@ var ColumnNode = class extends TreeNode {
80
93
  */
81
94
  explicitWidth;
82
95
  style = reactive({});
96
+ /** 派生状态(响应式),与用户列配置 data 隔离 */
97
+ derived = shallowReactive({ allocatedWidth: void 0 });
83
98
  constructor(val, index, depth, parent) {
84
99
  super(isReactive(val) ? val : shallowReactive(val), index, depth, parent);
85
100
  this.explicitWidth = val.width !== void 0;
@@ -1 +1 @@
1
- {"version":3,"file":"col.js","names":[],"sources":["../../../../src/components/table/node/col.ts"],"sourcesContent":["import { TreeNode } from '@cat-kit/core'\nimport { isReactive, reactive, shallowReactive } from 'vue'\n\nimport type { TableColumn, TableColumnAlign } from '../../../types'\n\nexport class ColumnNode extends TreeNode<TableColumn, ColumnNode> {\n declare children?: ColumnNode[] | undefined\n declare parent?: ColumnNode\n /** 叶子节点数量 */\n leafs?: number\n\n nextNode: ColumnNode | null = null\n\n /** 列key */\n get key(): string {\n return this.data.key\n }\n set key(val) {\n this.data.key = val\n }\n /** 列名 */\n get name(): string {\n return this.data.name\n }\n set name(val) {\n this.data.name = val\n }\n\n /**\n * 列对齐方式\n * @default 'left'\n */\n get align(): TableColumnAlign {\n return this.data.align ?? 'left'\n }\n set align(val) {\n this.data.align = val\n }\n\n get headerAlign(): TableColumnAlign {\n return this.data.headerAlign ?? this.align\n }\n set headerAlign(val) {\n this.data.headerAlign = val\n }\n\n /** 宽度 */\n get width(): number | undefined {\n const { width } = this.data\n if (!width || (this.minWidth && width && width < this.minWidth)) {\n return this.minWidth\n }\n return width\n }\n set width(val) {\n if (this.minWidth && val && val < this.minWidth) {\n val = this.minWidth\n }\n this.data.width = val\n }\n /** 最小宽度 */\n get minWidth(): number | undefined {\n const minWidth = this.data.minWidth\n if (!minWidth && this.isLeaf) {\n return 100\n }\n return minWidth\n }\n set minWidth(val) {\n this.data.minWidth = val\n }\n\n /** 列固定方向 */\n get fixed(): 'left' | 'right' | undefined {\n if (this.depth > 0) return\n return this.data.fixed\n }\n set fixed(val) {\n this.data.fixed = val\n }\n\n /** 是否是左侧的最后一个固定列 */\n get isLastFixed(): boolean {\n return this.data.isLastFixed ?? false\n }\n\n /** 是否是右侧的第一个固定列 */\n get isFirstFixed(): boolean {\n return this.data.isFirstFixed ?? false\n }\n\n /** 是否是可调整宽度的列 */\n get resizable(): boolean {\n return this.data.resizable ?? true\n }\n\n /**\n * 是否在列配置上显式指定了 width,或经表头拖拽锁定。\n * 此类列不参与剩余宽度的均分。\n */\n explicitWidth: boolean\n\n style: Record<string, number> = reactive({})\n\n constructor(val: TableColumn, index: number, depth: number, parent?: ColumnNode) {\n super(isReactive(val) ? val : shallowReactive(val), index, depth, parent)\n this.explicitWidth = val.width !== undefined\n }\n}\n"],"mappings":";;;AAKA,IAAa,aAAb,cAAgC,SAAkC;;CAIhE;CAEA,WAA8B;;CAG9B,IAAI,MAAc;EAChB,OAAO,KAAK,KAAK;CACnB;CACA,IAAI,IAAI,KAAK;EACX,KAAK,KAAK,MAAM;CAClB;;CAEA,IAAI,OAAe;EACjB,OAAO,KAAK,KAAK;CACnB;CACA,IAAI,KAAK,KAAK;EACZ,KAAK,KAAK,OAAO;CACnB;;;;;CAMA,IAAI,QAA0B;EAC5B,OAAO,KAAK,KAAK,SAAS;CAC5B;CACA,IAAI,MAAM,KAAK;EACb,KAAK,KAAK,QAAQ;CACpB;CAEA,IAAI,cAAgC;EAClC,OAAO,KAAK,KAAK,eAAe,KAAK;CACvC;CACA,IAAI,YAAY,KAAK;EACnB,KAAK,KAAK,cAAc;CAC1B;;CAGA,IAAI,QAA4B;EAC9B,MAAM,EAAE,UAAU,KAAK;EACvB,IAAI,CAAC,SAAU,KAAK,YAAY,SAAS,QAAQ,KAAK,UACpD,OAAO,KAAK;EAEd,OAAO;CACT;CACA,IAAI,MAAM,KAAK;EACb,IAAI,KAAK,YAAY,OAAO,MAAM,KAAK,UACrC,MAAM,KAAK;EAEb,KAAK,KAAK,QAAQ;CACpB;;CAEA,IAAI,WAA+B;EACjC,MAAM,WAAW,KAAK,KAAK;EAC3B,IAAI,CAAC,YAAY,KAAK,QACpB,OAAO;EAET,OAAO;CACT;CACA,IAAI,SAAS,KAAK;EAChB,KAAK,KAAK,WAAW;CACvB;;CAGA,IAAI,QAAsC;EACxC,IAAI,KAAK,QAAQ,GAAG;EACpB,OAAO,KAAK,KAAK;CACnB;CACA,IAAI,MAAM,KAAK;EACb,KAAK,KAAK,QAAQ;CACpB;;CAGA,IAAI,cAAuB;EACzB,OAAO,KAAK,KAAK,eAAe;CAClC;;CAGA,IAAI,eAAwB;EAC1B,OAAO,KAAK,KAAK,gBAAgB;CACnC;;CAGA,IAAI,YAAqB;EACvB,OAAO,KAAK,KAAK,aAAa;CAChC;;;;;CAMA;CAEA,QAAgC,SAAS,CAAC,CAAC;CAE3C,YAAY,KAAkB,OAAe,OAAe,QAAqB;EAC/E,MAAM,WAAW,GAAG,IAAI,MAAM,gBAAgB,GAAG,GAAG,OAAO,OAAO,MAAM;EACxE,KAAK,gBAAgB,IAAI,UAAU,KAAA;CACrC;AACF"}
1
+ {"version":3,"file":"col.js","names":[],"sources":["../../../../src/components/table/node/col.ts"],"sourcesContent":["import { TreeNode } from '@cat-kit/core'\nimport { isReactive, reactive, shallowReactive } from 'vue'\n\nimport type { TableColumn, TableColumnAlign } from '../../../types'\n\nexport class ColumnNode extends TreeNode<TableColumn, ColumnNode> {\n declare children?: ColumnNode[] | undefined\n declare parent?: ColumnNode\n /** 叶子节点数量 */\n leafs?: number\n\n nextNode: ColumnNode | null = null\n\n /** 列key */\n get key(): string {\n return this.data.key\n }\n set key(val) {\n this.data.key = val\n }\n /** 列名 */\n get name(): string {\n return this.data.name\n }\n set name(val) {\n this.data.name = val\n }\n\n /**\n * 列对齐方式\n * @default 'left'\n */\n get align(): TableColumnAlign {\n return this.data.align ?? 'left'\n }\n set align(val) {\n this.data.align = val\n }\n\n get headerAlign(): TableColumnAlign {\n return this.data.headerAlign ?? this.align\n }\n set headerAlign(val) {\n this.data.headerAlign = val\n }\n\n /** 宽度 */\n get width(): number | undefined {\n const width = this.data.width ?? this.derived.allocatedWidth\n if (!width || (this.minWidth && width && width < this.minWidth)) {\n return this.minWidth\n }\n return width\n }\n set width(val) {\n if (this.minWidth && val && val < this.minWidth) {\n val = this.minWidth\n }\n this.data.width = val\n // 显式宽度(配置/拖拽)优先于分配宽度,旧的分配结果直接作废\n this.derived.allocatedWidth = undefined\n }\n\n /**\n * 列宽分配算法写入的宽度。\n * 与用户列配置 `data.width` 隔离:分配结果不回写列配置对象,\n * 避免列森林重建(重挂载 / v-if / columns 引用变化)时把上一次的\n * 分配残留误判为显式 width,导致列宽被永久锁定、容器变窄后出现滚动条。\n */\n get allocatedWidth(): number | undefined {\n return this.derived.allocatedWidth\n }\n set allocatedWidth(val: number | undefined) {\n this.derived.allocatedWidth = val\n }\n /** 最小宽度 */\n get minWidth(): number | undefined {\n const minWidth = this.data.minWidth\n if (!minWidth && this.isLeaf) {\n return 100\n }\n return minWidth\n }\n set minWidth(val) {\n this.data.minWidth = val\n }\n\n /** 列固定方向 */\n get fixed(): 'left' | 'right' | undefined {\n if (this.depth > 0) return\n return this.data.fixed\n }\n set fixed(val) {\n this.data.fixed = val\n }\n\n /** 是否是左侧的最后一个固定列 */\n get isLastFixed(): boolean {\n return this.data.isLastFixed ?? false\n }\n\n /** 是否是右侧的第一个固定列 */\n get isFirstFixed(): boolean {\n return this.data.isFirstFixed ?? false\n }\n\n /** 是否是可调整宽度的列 */\n get resizable(): boolean {\n return this.data.resizable ?? true\n }\n\n /**\n * 是否在列配置上显式指定了 width,或经表头拖拽锁定。\n * 此类列不参与剩余宽度的均分。\n */\n explicitWidth: boolean\n\n style: Record<string, number> = reactive({})\n\n /** 派生状态(响应式),与用户列配置 data 隔离 */\n private readonly derived = shallowReactive<{ allocatedWidth?: number }>({\n allocatedWidth: undefined\n })\n\n constructor(val: TableColumn, index: number, depth: number, parent?: ColumnNode) {\n super(isReactive(val) ? val : shallowReactive(val), index, depth, parent)\n this.explicitWidth = val.width !== undefined\n }\n}\n"],"mappings":";;;AAKA,IAAa,aAAb,cAAgC,SAAkC;;CAIhE;CAEA,WAA8B;;CAG9B,IAAI,MAAc;EAChB,OAAO,KAAK,KAAK;CACnB;CACA,IAAI,IAAI,KAAK;EACX,KAAK,KAAK,MAAM;CAClB;;CAEA,IAAI,OAAe;EACjB,OAAO,KAAK,KAAK;CACnB;CACA,IAAI,KAAK,KAAK;EACZ,KAAK,KAAK,OAAO;CACnB;;;;;CAMA,IAAI,QAA0B;EAC5B,OAAO,KAAK,KAAK,SAAS;CAC5B;CACA,IAAI,MAAM,KAAK;EACb,KAAK,KAAK,QAAQ;CACpB;CAEA,IAAI,cAAgC;EAClC,OAAO,KAAK,KAAK,eAAe,KAAK;CACvC;CACA,IAAI,YAAY,KAAK;EACnB,KAAK,KAAK,cAAc;CAC1B;;CAGA,IAAI,QAA4B;EAC9B,MAAM,QAAQ,KAAK,KAAK,SAAS,KAAK,QAAQ;EAC9C,IAAI,CAAC,SAAU,KAAK,YAAY,SAAS,QAAQ,KAAK,UACpD,OAAO,KAAK;EAEd,OAAO;CACT;CACA,IAAI,MAAM,KAAK;EACb,IAAI,KAAK,YAAY,OAAO,MAAM,KAAK,UACrC,MAAM,KAAK;EAEb,KAAK,KAAK,QAAQ;EAElB,KAAK,QAAQ,iBAAiB,KAAA;CAChC;;;;;;;CAQA,IAAI,iBAAqC;EACvC,OAAO,KAAK,QAAQ;CACtB;CACA,IAAI,eAAe,KAAyB;EAC1C,KAAK,QAAQ,iBAAiB;CAChC;;CAEA,IAAI,WAA+B;EACjC,MAAM,WAAW,KAAK,KAAK;EAC3B,IAAI,CAAC,YAAY,KAAK,QACpB,OAAO;EAET,OAAO;CACT;CACA,IAAI,SAAS,KAAK;EAChB,KAAK,KAAK,WAAW;CACvB;;CAGA,IAAI,QAAsC;EACxC,IAAI,KAAK,QAAQ,GAAG;EACpB,OAAO,KAAK,KAAK;CACnB;CACA,IAAI,MAAM,KAAK;EACb,KAAK,KAAK,QAAQ;CACpB;;CAGA,IAAI,cAAuB;EACzB,OAAO,KAAK,KAAK,eAAe;CAClC;;CAGA,IAAI,eAAwB;EAC1B,OAAO,KAAK,KAAK,gBAAgB;CACnC;;CAGA,IAAI,YAAqB;EACvB,OAAO,KAAK,KAAK,aAAa;CAChC;;;;;CAMA;CAEA,QAAgC,SAAS,CAAC,CAAC;;CAG3C,UAA2B,gBAA6C,EACtE,gBAAgB,KAAA,EAClB,CAAC;CAED,YAAY,KAAkB,OAAe,OAAe,QAAqB;EAC/E,MAAM,WAAW,GAAG,IAAI,MAAM,gBAAgB,GAAG,GAAG,OAAO,OAAO,MAAM;EACxE,KAAK,gBAAgB,IAAI,UAAU,KAAA;CACrC;AACF"}
@@ -1,6 +1,8 @@
1
1
  import { TableResizeKey } from "./di.js";
2
2
  import { allocateLeafColumnWidths } from "./allocate-column-widths.js";
3
- import { nextTick, provide, shallowRef, watchEffect } from "vue";
3
+ import { computed, nextTick, provide, shallowRef, watchEffect } from "vue";
4
+ import { useResizeObserver } from "@veltra/compositions";
5
+ import { debounce } from "@cat-kit/core";
4
6
  //#region src/components/table/use-col-resize.ts
5
7
  function useColResize(options) {
6
8
  const { scrollRef, leafColumns } = options;
@@ -27,6 +29,12 @@ function useColResize(options) {
27
29
  }, 0);
28
30
  }
29
31
  watchEffect(correctColumnStyle);
32
+ const debouncedCorrectColumnStyle = debounce(correctColumnStyle);
33
+ const containerRef = computed(() => scrollRef.value?.containerRef ?? null);
34
+ useResizeObserver({
35
+ targets: containerRef,
36
+ onResize: debouncedCorrectColumnStyle
37
+ });
30
38
  function updateResizeLine(transformX) {
31
39
  if (!resizeLineRef.value) return;
32
40
  resizeLineRef.value.style.transform = `translateX(${transformX}px)`;
@@ -1 +1 @@
1
- {"version":3,"file":"use-col-resize.js","names":[],"sources":["../../../src/components/table/use-col-resize.ts"],"sourcesContent":["import { nextTick, provide, shallowRef, watchEffect, type ShallowRef } from 'vue'\n\nimport type { ScrollExposed } from '../../types'\nimport { allocateLeafColumnWidths } from './allocate-column-widths'\nimport { TableResizeKey } from './di'\nimport type { ColumnNode } from './node/col'\n\ninterface Options {\n scrollRef: ShallowRef<ScrollExposed | undefined>\n leafColumns: ShallowRef<ColumnNode[]>\n}\n\ninterface UseColResizeReturned {\n resizeLineRef: ShallowRef<HTMLElement | undefined>\n colgroupRef: ShallowRef<HTMLElement | undefined>\n showResizeLine: ShallowRef<boolean>\n handleResizeMousedown: (e: MouseEvent, resizeColumn: ColumnNode) => void\n}\n\nexport function useColResize(options: Options): UseColResizeReturned {\n const { scrollRef, leafColumns } = options\n\n const showResizeLine = shallowRef(false)\n\n const resizeLineRef = shallowRef<HTMLElement>()\n const colgroupRef = shallowRef<HTMLElement>()\n const headerRef = shallowRef<HTMLElement>()\n\n /**\n * 修正列样式\n */\n function correctColumnStyle() {\n // 获取表格容器宽度\n const containerWidth = scrollRef.value?.containerRef?.clientWidth\n\n if (!containerWidth || !leafColumns.value.length) return\n\n allocateLeafColumnWidths(leafColumns.value, containerWidth)\n\n const fixedLeft = leafColumns.value.filter((column) => column.fixed === 'left')\n const fixedRight = leafColumns.value.filter((column) => column.fixed === 'right')\n\n fixedLeft.reduce((acc, cur) => {\n cur.style.left = acc\n return acc + (cur.width ?? cur.minWidth!)\n }, 0)\n\n fixedRight.reduceRight((acc, cur) => {\n cur.style.right = acc\n return acc + (cur.width ?? cur.minWidth!)\n }, 0)\n }\n\n watchEffect(correctColumnStyle)\n\n function updateResizeLine(transformX: number): void {\n if (!resizeLineRef.value) return\n resizeLineRef.value.style.transform = `translateX(${transformX}px)`\n }\n\n // 表格相对于页面左边缘的偏移量,\n // 此处作为缓存以减少频繁地调用 getBoundingClientRect 方法\n let containerLeft = 0\n let originX = 0\n let originWidth = 0\n let currentResizeColumn: ColumnNode | null = null\n\n function handleResizeMousedown(e: MouseEvent, resizeColumn: ColumnNode): void {\n const tableEl = scrollRef.value?.el\n if (!tableEl) return\n\n showResizeLine.value = true\n currentResizeColumn = resizeColumn\n originX = e.pageX\n originWidth = resizeColumn.width ?? 0\n\n const { left } = tableEl.getBoundingClientRect()\n containerLeft = left\n\n nextTick(() => {\n updateResizeLine(e.pageX - left)\n })\n\n document.addEventListener('mousemove', handleResizeMousemove)\n document.addEventListener('mouseup', handleResizeMouseup)\n }\n\n function handleResizeMousemove(e: MouseEvent): void {\n updateResizeLine(e.pageX - containerLeft)\n }\n\n function handleResizeMouseup(e: MouseEvent): void {\n currentResizeColumn!.width = Math.max(\n originWidth + e.pageX - originX,\n currentResizeColumn!.minWidth!\n )\n currentResizeColumn!.explicitWidth = true\n\n showResizeLine.value = false\n currentResizeColumn = null\n correctColumnStyle()\n\n document.removeEventListener('mouseup', handleResizeMouseup)\n document.removeEventListener('mousemove', handleResizeMousemove)\n }\n\n provide(TableResizeKey, { handleResizeMousedown, headerRef })\n\n return { resizeLineRef, colgroupRef, showResizeLine, handleResizeMousedown }\n}\n"],"mappings":";;;;AAmBA,SAAgB,aAAa,SAAwC;CACnE,MAAM,EAAE,WAAW,gBAAgB;CAEnC,MAAM,iBAAiB,WAAW,KAAK;CAEvC,MAAM,gBAAgB,WAAwB;CAC9C,MAAM,cAAc,WAAwB;CAC5C,MAAM,YAAY,WAAwB;;;;CAK1C,SAAS,qBAAqB;EAE5B,MAAM,iBAAiB,UAAU,OAAO,cAAc;EAEtD,IAAI,CAAC,kBAAkB,CAAC,YAAY,MAAM,QAAQ;EAElD,yBAAyB,YAAY,OAAO,cAAc;EAE1D,MAAM,YAAY,YAAY,MAAM,QAAQ,WAAW,OAAO,UAAU,MAAM;EAC9E,MAAM,aAAa,YAAY,MAAM,QAAQ,WAAW,OAAO,UAAU,OAAO;EAEhF,UAAU,QAAQ,KAAK,QAAQ;GAC7B,IAAI,MAAM,OAAO;GACjB,OAAO,OAAO,IAAI,SAAS,IAAI;EACjC,GAAG,CAAC;EAEJ,WAAW,aAAa,KAAK,QAAQ;GACnC,IAAI,MAAM,QAAQ;GAClB,OAAO,OAAO,IAAI,SAAS,IAAI;EACjC,GAAG,CAAC;CACN;CAEA,YAAY,kBAAkB;CAE9B,SAAS,iBAAiB,YAA0B;EAClD,IAAI,CAAC,cAAc,OAAO;EAC1B,cAAc,MAAM,MAAM,YAAY,cAAc,WAAW;CACjE;CAIA,IAAI,gBAAgB;CACpB,IAAI,UAAU;CACd,IAAI,cAAc;CAClB,IAAI,sBAAyC;CAE7C,SAAS,sBAAsB,GAAe,cAAgC;EAC5E,MAAM,UAAU,UAAU,OAAO;EACjC,IAAI,CAAC,SAAS;EAEd,eAAe,QAAQ;EACvB,sBAAsB;EACtB,UAAU,EAAE;EACZ,cAAc,aAAa,SAAS;EAEpC,MAAM,EAAE,SAAS,QAAQ,sBAAsB;EAC/C,gBAAgB;EAEhB,eAAe;GACb,iBAAiB,EAAE,QAAQ,IAAI;EACjC,CAAC;EAED,SAAS,iBAAiB,aAAa,qBAAqB;EAC5D,SAAS,iBAAiB,WAAW,mBAAmB;CAC1D;CAEA,SAAS,sBAAsB,GAAqB;EAClD,iBAAiB,EAAE,QAAQ,aAAa;CAC1C;CAEA,SAAS,oBAAoB,GAAqB;EAChD,oBAAqB,QAAQ,KAAK,IAChC,cAAc,EAAE,QAAQ,SACxB,oBAAqB,QACvB;EACA,oBAAqB,gBAAgB;EAErC,eAAe,QAAQ;EACvB,sBAAsB;EACtB,mBAAmB;EAEnB,SAAS,oBAAoB,WAAW,mBAAmB;EAC3D,SAAS,oBAAoB,aAAa,qBAAqB;CACjE;CAEA,QAAQ,gBAAgB;EAAE;EAAuB;CAAU,CAAC;CAE5D,OAAO;EAAE;EAAe;EAAa;EAAgB;CAAsB;AAC7E"}
1
+ {"version":3,"file":"use-col-resize.js","names":[],"sources":["../../../src/components/table/use-col-resize.ts"],"sourcesContent":["import { debounce } from '@cat-kit/core'\nimport { useResizeObserver } from '@veltra/compositions'\nimport { computed, nextTick, provide, shallowRef, watchEffect, type ShallowRef } from 'vue'\n\nimport type { ScrollExposed } from '../../types'\nimport { allocateLeafColumnWidths } from './allocate-column-widths'\nimport { TableResizeKey } from './di'\nimport type { ColumnNode } from './node/col'\n\ninterface Options {\n scrollRef: ShallowRef<ScrollExposed | undefined>\n leafColumns: ShallowRef<ColumnNode[]>\n}\n\ninterface UseColResizeReturned {\n resizeLineRef: ShallowRef<HTMLElement | undefined>\n colgroupRef: ShallowRef<HTMLElement | undefined>\n showResizeLine: ShallowRef<boolean>\n handleResizeMousedown: (e: MouseEvent, resizeColumn: ColumnNode) => void\n}\n\nexport function useColResize(options: Options): UseColResizeReturned {\n const { scrollRef, leafColumns } = options\n\n const showResizeLine = shallowRef(false)\n\n const resizeLineRef = shallowRef<HTMLElement>()\n const colgroupRef = shallowRef<HTMLElement>()\n const headerRef = shallowRef<HTMLElement>()\n\n /**\n * 修正列样式\n */\n function correctColumnStyle() {\n // 获取表格容器宽度\n const containerWidth = scrollRef.value?.containerRef?.clientWidth\n\n if (!containerWidth || !leafColumns.value.length) return\n\n allocateLeafColumnWidths(leafColumns.value, containerWidth)\n\n const fixedLeft = leafColumns.value.filter((column) => column.fixed === 'left')\n const fixedRight = leafColumns.value.filter((column) => column.fixed === 'right')\n\n fixedLeft.reduce((acc, cur) => {\n cur.style.left = acc\n return acc + (cur.width ?? cur.minWidth!)\n }, 0)\n\n fixedRight.reduceRight((acc, cur) => {\n cur.style.right = acc\n return acc + (cur.width ?? cur.minWidth!)\n }, 0)\n }\n\n watchEffect(correctColumnStyle)\n\n // 容器宽度变化(窗口缩放、父布局变化、纵向滚动条出现/消失等)时重新分配列宽,\n // 直到所有未显式设宽的列回落到 minWidth。计算开销较大,用防抖合并连续的 resize 触发。\n const debouncedCorrectColumnStyle = debounce(correctColumnStyle)\n const containerRef = computed(() => scrollRef.value?.containerRef ?? null)\n useResizeObserver({ targets: containerRef, onResize: debouncedCorrectColumnStyle })\n\n function updateResizeLine(transformX: number): void {\n if (!resizeLineRef.value) return\n resizeLineRef.value.style.transform = `translateX(${transformX}px)`\n }\n\n // 表格相对于页面左边缘的偏移量,\n // 此处作为缓存以减少频繁地调用 getBoundingClientRect 方法\n let containerLeft = 0\n let originX = 0\n let originWidth = 0\n let currentResizeColumn: ColumnNode | null = null\n\n function handleResizeMousedown(e: MouseEvent, resizeColumn: ColumnNode): void {\n const tableEl = scrollRef.value?.el\n if (!tableEl) return\n\n showResizeLine.value = true\n currentResizeColumn = resizeColumn\n originX = e.pageX\n originWidth = resizeColumn.width ?? 0\n\n const { left } = tableEl.getBoundingClientRect()\n containerLeft = left\n\n nextTick(() => {\n updateResizeLine(e.pageX - left)\n })\n\n document.addEventListener('mousemove', handleResizeMousemove)\n document.addEventListener('mouseup', handleResizeMouseup)\n }\n\n function handleResizeMousemove(e: MouseEvent): void {\n updateResizeLine(e.pageX - containerLeft)\n }\n\n function handleResizeMouseup(e: MouseEvent): void {\n currentResizeColumn!.width = Math.max(\n originWidth + e.pageX - originX,\n currentResizeColumn!.minWidth!\n )\n currentResizeColumn!.explicitWidth = true\n\n showResizeLine.value = false\n currentResizeColumn = null\n correctColumnStyle()\n\n document.removeEventListener('mouseup', handleResizeMouseup)\n document.removeEventListener('mousemove', handleResizeMousemove)\n }\n\n provide(TableResizeKey, { handleResizeMousedown, headerRef })\n\n return { resizeLineRef, colgroupRef, showResizeLine, handleResizeMousedown }\n}\n"],"mappings":";;;;;;AAqBA,SAAgB,aAAa,SAAwC;CACnE,MAAM,EAAE,WAAW,gBAAgB;CAEnC,MAAM,iBAAiB,WAAW,KAAK;CAEvC,MAAM,gBAAgB,WAAwB;CAC9C,MAAM,cAAc,WAAwB;CAC5C,MAAM,YAAY,WAAwB;;;;CAK1C,SAAS,qBAAqB;EAE5B,MAAM,iBAAiB,UAAU,OAAO,cAAc;EAEtD,IAAI,CAAC,kBAAkB,CAAC,YAAY,MAAM,QAAQ;EAElD,yBAAyB,YAAY,OAAO,cAAc;EAE1D,MAAM,YAAY,YAAY,MAAM,QAAQ,WAAW,OAAO,UAAU,MAAM;EAC9E,MAAM,aAAa,YAAY,MAAM,QAAQ,WAAW,OAAO,UAAU,OAAO;EAEhF,UAAU,QAAQ,KAAK,QAAQ;GAC7B,IAAI,MAAM,OAAO;GACjB,OAAO,OAAO,IAAI,SAAS,IAAI;EACjC,GAAG,CAAC;EAEJ,WAAW,aAAa,KAAK,QAAQ;GACnC,IAAI,MAAM,QAAQ;GAClB,OAAO,OAAO,IAAI,SAAS,IAAI;EACjC,GAAG,CAAC;CACN;CAEA,YAAY,kBAAkB;CAI9B,MAAM,8BAA8B,SAAS,kBAAkB;CAC/D,MAAM,eAAe,eAAe,UAAU,OAAO,gBAAgB,IAAI;CACzE,kBAAkB;EAAE,SAAS;EAAc,UAAU;CAA4B,CAAC;CAElF,SAAS,iBAAiB,YAA0B;EAClD,IAAI,CAAC,cAAc,OAAO;EAC1B,cAAc,MAAM,MAAM,YAAY,cAAc,WAAW;CACjE;CAIA,IAAI,gBAAgB;CACpB,IAAI,UAAU;CACd,IAAI,cAAc;CAClB,IAAI,sBAAyC;CAE7C,SAAS,sBAAsB,GAAe,cAAgC;EAC5E,MAAM,UAAU,UAAU,OAAO;EACjC,IAAI,CAAC,SAAS;EAEd,eAAe,QAAQ;EACvB,sBAAsB;EACtB,UAAU,EAAE;EACZ,cAAc,aAAa,SAAS;EAEpC,MAAM,EAAE,SAAS,QAAQ,sBAAsB;EAC/C,gBAAgB;EAEhB,eAAe;GACb,iBAAiB,EAAE,QAAQ,IAAI;EACjC,CAAC;EAED,SAAS,iBAAiB,aAAa,qBAAqB;EAC5D,SAAS,iBAAiB,WAAW,mBAAmB;CAC1D;CAEA,SAAS,sBAAsB,GAAqB;EAClD,iBAAiB,EAAE,QAAQ,aAAa;CAC1C;CAEA,SAAS,oBAAoB,GAAqB;EAChD,oBAAqB,QAAQ,KAAK,IAChC,cAAc,EAAE,QAAQ,SACxB,oBAAqB,QACvB;EACA,oBAAqB,gBAAgB;EAErC,eAAe,QAAQ;EACvB,sBAAsB;EACtB,mBAAmB;EAEnB,SAAS,oBAAoB,WAAW,mBAAmB;EAC3D,SAAS,oBAAoB,aAAa,qBAAqB;CACjE;CAEA,QAAQ,gBAAgB;EAAE;EAAuB;CAAU,CAAC;CAE5D,OAAO;EAAE;EAAe;EAAa;EAAgB;CAAsB;AAC7E"}
@@ -43,7 +43,7 @@ interface CollapseItemProps {
43
43
  * @default false
44
44
  */
45
45
  modelValue?: boolean;
46
- /** 标题文本(也可使用 #title 插槽) */
46
+ /** 标题文本(也可通过 #header 插槽自定义整个头部) */
47
47
  title?: string;
48
48
  /** 是否禁用 */
49
49
  disabled?: boolean;
@@ -52,6 +52,12 @@ interface CollapseItemProps {
52
52
  * 在 `UCollapse` 内由父组件 `expandIcon` 统一管理。
53
53
  */
54
54
  expandIcon?: Component;
55
+ /**
56
+ * 折叠动画结束后卸载内容 DOM(展开时重新挂载),减少长列表的内存与渲染成本。
57
+ * 注意:卸载会丢失内容区的本地组件状态。
58
+ * @default false
59
+ */
60
+ destroyOnCollapse?: boolean;
55
61
  }
56
62
  interface CollapseItemEmits {
57
63
  (e: 'update:modelValue', value: boolean): void;
@@ -37,9 +37,24 @@ interface LayoutProps {
37
37
  * @description 注意:当为true时,gap固定且需要有一项宽度为固定像素才能够拖拽
38
38
  */
39
39
  resizable?: boolean;
40
+ /**
41
+ * 每列的最小宽度(px),按列索引与 cols 对应,仅 resizable 拖拽时生效。
42
+ * 未指定的列不限制(可被压至 0)。
43
+ * @example
44
+ * ```ts
45
+ * // 三栏拖拽时,左栏不小于 120px,右栏不小于 200px
46
+ * :cols="['1fr', '2fr', '300px']" :col-min-sizes="[120, undefined, 200]"
47
+ * ```
48
+ */
49
+ colMinSizes?: (number | undefined)[];
40
50
  }
41
51
  /** 布局组件定义的事件 */
42
- interface LayoutEmits {}
52
+ interface LayoutEmits {
53
+ /** 开始拖拽调节某条间隔(index 为间隔左侧列的索引) */
54
+ (e: 'resize-start', index: number): void;
55
+ /** 拖拽调节结束 */
56
+ (e: 'resize-end', index: number): void;
57
+ }
43
58
  /** 布局组件暴露的属性和方法(组件内部使用) */
44
59
  interface _LayoutExposed {}
45
60
  /** 布局组件暴露的属性和方法(组件外部使用, 引用的值会被自动解构) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/desktop",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "files": [
5
5
  "dist",
6
6
  "src"
@@ -71,12 +71,12 @@
71
71
  "peerDependencies": {
72
72
  "@cat-kit/core": ">=1.1.8",
73
73
  "@cat-kit/fe": ">=1.1.8",
74
- "@veltra/compositions": "^1.5.0",
75
- "@veltra/directives": "^1.5.0",
74
+ "@veltra/compositions": "^1.6.1",
75
+ "@veltra/directives": "^1.6.1",
76
76
  "@veltra/icons": "^1.5.0",
77
- "@veltra/sheet-core": "^2.1.1",
78
- "@veltra/styles": "^1.5.0",
79
- "@veltra/utils": "^1.5.0",
77
+ "@veltra/sheet-core": "^2.3.0",
78
+ "@veltra/styles": "^1.6.1",
79
+ "@veltra/utils": "^1.6.1",
80
80
  "vue": ">=3.5.41"
81
81
  },
82
82
  "peerDependenciesMeta": {
@@ -65,8 +65,8 @@ const classList = computed(() => {
65
65
  })
66
66
 
67
67
  const ripple = computed(() => {
68
- if (props.disabled || props.loading) return false
69
- if ((props.plain || props.text) && props.type) {
68
+ if (props.disabled || props.loading || props.text) return false
69
+ if (props.plain && props.type) {
70
70
  return bem.is(`ripple-${props.type}`)
71
71
  }
72
72
  return true
@@ -47,17 +47,8 @@ $root-name: button;
47
47
  }
48
48
 
49
49
  @include m.is(plain) {
50
- background-color: fn.use-var(bg-color, top);
51
- border-color: fn.use-var(color, default, 'dark-1');
50
+ border-color: transparent;
52
51
  box-shadow: none;
53
-
54
- &:hover {
55
- border-color: fn.use-var(color, default, 'dark-1');
56
- }
57
-
58
- &.is-disabled {
59
- color: fn.use-var(text-color, second);
60
- }
61
52
  }
62
53
 
63
54
  // 尺寸
@@ -122,12 +113,9 @@ $root-name: button;
122
113
 
123
114
  @include m.is(text) {
124
115
  color: fn.use-var(color, $type);
116
+ border-color: transparent;
125
117
  box-shadow: none;
126
118
 
127
- &:hover {
128
- box-shadow: 0px 0px 0px 1px fn.use-var(color, $type);
129
- }
130
-
131
119
  @include m.is-not(disabled, loading) {
132
120
  &:hover,
133
121
  &:focus {
@@ -140,39 +128,30 @@ $root-name: button;
140
128
  }
141
129
 
142
130
  @include m.is(disabled, loading) {
131
+ border-color: transparent;
143
132
  color: fn.use-var(color, $type, 'light-5');
144
133
  }
145
134
  }
146
135
 
147
136
  @include m.is(plain) {
137
+ border-color: transparent;
148
138
  color: fn.component-var(button, #{$type}-plain-color, fn.use-var(color, $type));
149
139
  background-color: fn.component-var(
150
140
  button,
151
141
  #{$type}-plain-bg,
152
142
  fn.use-var(color, $type, light-9)
153
143
  );
154
- border-color: fn.component-var(
155
- button,
156
- #{$type}-plain-border,
157
- fn.use-var(color, $type, light-5)
158
- );
159
- box-shadow: none;
160
144
 
161
145
  @include m.is-not(disabled, loading) {
162
146
  &:hover {
163
- color: fn.use-var(color, $type);
164
- border-color: fn.use-var(color, $type);
165
- background-color: fn.use-var(color, $type, light-9);
147
+ background-color: fn.use-var(color, $type, light-7);
166
148
  }
167
149
  }
168
150
 
169
- &:active {
170
- box-shadow: none;
171
- }
172
-
173
- &.is-disabled {
151
+ @include m.is(disabled, loading) {
152
+ border-color: transparent;
174
153
  color: fn.use-var(color, $type, light-5);
175
- border-color: fn.use-var(color, $type, light-8);
154
+ background-color: fn.use-var(color, $type, light-9);
176
155
  }
177
156
  }
178
157
  }
@@ -353,3 +353,62 @@ describe('CollapseItem standalone', () => {
353
353
  }
354
354
  })
355
355
  })
356
+
357
+ describe('CollapseItem destroyOnCollapse', () => {
358
+ it('keeps content mounted by default even when collapsed', async () => {
359
+ const { host, unmount } = mountStandaloneItem({ title: 'Standalone' })
360
+
361
+ try {
362
+ await nextTick()
363
+ expect(host.querySelector('.u-collapse__item.is-active')).toBeFalsy()
364
+ expect(host.querySelector('.u-collapse__content')?.textContent).toBe('Standalone content')
365
+ } finally {
366
+ unmount()
367
+ }
368
+ })
369
+
370
+ it('does not mount content when initially collapsed', async () => {
371
+ const { host, unmount } = mountStandaloneItem({ title: 'Standalone', destroyOnCollapse: true })
372
+
373
+ try {
374
+ await nextTick()
375
+ expect(host.querySelector('.u-collapse__item.is-active')).toBeFalsy()
376
+ // 内容未挂载,但包装容器仍在(承载高度动画与 aria-hidden)
377
+ expect(host.querySelector('.u-collapse__content')).toBeFalsy()
378
+ expect(host.querySelector('.u-collapse__content-wrapper')).toBeTruthy()
379
+ } finally {
380
+ unmount()
381
+ }
382
+ })
383
+
384
+ it('unmounts content after collapse and remounts on expand', async () => {
385
+ const { host, model, unmount } = mountStandaloneItem({
386
+ title: 'Standalone',
387
+ modelValue: true,
388
+ destroyOnCollapse: true
389
+ })
390
+
391
+ try {
392
+ await nextTick()
393
+ expect(host.querySelector('.u-collapse__content')?.textContent).toBe('Standalone content')
394
+
395
+ const header = host.querySelector('.u-collapse__header') as HTMLElement
396
+ header.click()
397
+ await nextTick()
398
+
399
+ expect(model.value).toBe(false)
400
+ // happy-dom 无真实布局,收起动画直接落定,内容随即卸载
401
+ await nextTick()
402
+ expect(host.querySelector('.u-collapse__content')).toBeFalsy()
403
+
404
+ header.click()
405
+ await nextTick()
406
+ await nextTick()
407
+
408
+ expect(model.value).toBe(true)
409
+ expect(host.querySelector('.u-collapse__content')?.textContent).toBe('Standalone content')
410
+ } finally {
411
+ unmount()
412
+ }
413
+ })
414
+ })
@@ -10,18 +10,16 @@
10
10
  @keydown.enter.prevent="handleClick"
11
11
  @keydown.space.prevent="handleClick"
12
12
  >
13
- <span :class="cls.e('title')">
14
- <slot name="title">{{ title }}</slot>
15
- </span>
13
+ <slot name="header" :is-active="isActive">
14
+ <span :class="cls.e('title')">{{ title }}</span>
16
15
 
17
- <span :class="cls.e('icon')">
18
- <slot name="icon" :is-active="isActive">
16
+ <span :class="cls.e('icon')">
19
17
  <UIcon><component :is="iconComponent" /></UIcon>
20
- </slot>
21
- </span>
18
+ </span>
19
+ </slot>
22
20
  </div>
23
21
  <div ref="wrapperEl" :class="cls.e('content-wrapper')" :aria-hidden="!isActive">
24
- <div :class="cls.e('content')">
22
+ <div v-if="contentMounted" :class="cls.e('content')">
25
23
  <slot />
26
24
  </div>
27
25
  </div>
@@ -32,7 +30,7 @@
32
30
  import { useModel } from '@veltra/compositions'
33
31
  import { ArrowDown } from '@veltra/icons/normal'
34
32
  import { bem, ExpandTransition } from '@veltra/utils'
35
- import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from 'vue'
33
+ import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
36
34
 
37
35
  import type { CollapseItemEmits, CollapseItemProps } from '../../types'
38
36
  import { UIcon } from '../icon'
@@ -43,6 +41,13 @@ defineOptions({ name: 'UCollapseItem' })
43
41
  const props = withDefaults(defineProps<CollapseItemProps>(), { modelValue: false })
44
42
  const emit = defineEmits<CollapseItemEmits>()
45
43
 
44
+ defineSlots<{
45
+ /** 自定义整个头部,作用域参数 `isActive` 指示展开状态;缺省渲染 title + 展开图标 */
46
+ header?: (scope: { isActive: boolean }) => any
47
+ /** 折叠面板内容 */
48
+ default?: () => any
49
+ }>()
50
+
46
51
  const context = inject(CollapseDIKey, undefined)
47
52
 
48
53
  const cls = context?.cls ?? bem('collapse')
@@ -94,6 +99,9 @@ const handleClick = () => {
94
99
 
95
100
  const wrapperEl = ref<HTMLElement>()
96
101
 
102
+ /** 内容是否挂载:destroyOnCollapse 时折叠态卸载内容 DOM,展开前重新挂载 */
103
+ const contentMounted = ref(isActive.value || !props.destroyOnCollapse)
104
+
97
105
  onMounted(() => {
98
106
  if (context && props.value !== undefined) {
99
107
  context.register(props.value)
@@ -102,9 +110,22 @@ onMounted(() => {
102
110
  expandTransition.setExpanded(wrapperEl.value, isActive.value)
103
111
  })
104
112
 
105
- watch(isActive, (active) => {
113
+ watch(isActive, async (active) => {
114
+ if (active) {
115
+ // 展开前先挂载内容,待 DOM 更新后再测量高度驱动展开动画
116
+ if (!contentMounted.value) {
117
+ contentMounted.value = true
118
+ await nextTick()
119
+ }
120
+ if (!wrapperEl.value || !isActive.value) return
121
+ expandTransition.expand(wrapperEl.value)
122
+ return
123
+ }
106
124
  if (!wrapperEl.value) return
107
- active ? expandTransition.expand(wrapperEl.value) : expandTransition.collapse(wrapperEl.value)
125
+ // 收起动画落定后再卸载内容 DOM;期间被重新展开则不卸载
126
+ expandTransition.collapse(wrapperEl.value, () => {
127
+ if (!isActive.value && props.destroyOnCollapse) contentMounted.value = false
128
+ })
108
129
  })
109
130
 
110
131
  onBeforeUnmount(() => {
@@ -76,10 +76,14 @@ $root-name: dialog;
76
76
  }
77
77
 
78
78
  @include m.e(body) {
79
- padding: 8px;
80
79
  background-color: fn.use-var(bg-color, top);
81
80
  backdrop-filter: fn.use-var(bg-filter);
82
81
  flex-grow: 1;
82
+
83
+ // padding 加在滚动内容而非 body 本身:u-scroll 的 overflow 会裁切子元素 box-shadow
84
+ @include m.bem(scroll, content) {
85
+ padding: 8px;
86
+ }
83
87
  }
84
88
 
85
89
  @include m.e(footer) {
@@ -11,6 +11,7 @@
11
11
  direction="vertical"
12
12
  @resize="handleResize($event, index)"
13
13
  @resize-start="handleStartResize(index)"
14
+ @resize-end="emit('resize-end', index)"
14
15
  >
15
16
  </ULayoutResizer>
16
17
 
@@ -37,7 +38,7 @@ import {
37
38
  nextTick
38
39
  } from 'vue'
39
40
 
40
- import type { LayoutProps } from '../../types'
41
+ import type { LayoutEmits, LayoutProps } from '../../types'
41
42
  import { LayoutDIKey } from './di'
42
43
  import ULayoutResizer from './layout-resizer.vue'
43
44
 
@@ -45,6 +46,8 @@ defineOptions({ name: 'ULayout' })
45
46
 
46
47
  const props = withDefaults(defineProps<LayoutProps>(), { tag: 'div' })
47
48
 
49
+ const emit = defineEmits<LayoutEmits>()
50
+
48
51
  const cls = bem('layout')
49
52
 
50
53
  const templateCols = ref<string[]>([])
@@ -87,20 +90,25 @@ function getResizeOffsets() {
87
90
 
88
91
  watch([() => props.resizable, containerRef, () => props.cols], () => {
89
92
  nextTick(() => {
90
- getResizeOffsets()
93
+ syncResizers()
91
94
  })
92
95
  })
93
96
 
94
97
  useResizeObserver({
95
98
  targets: containerRef,
96
99
  onResize() {
97
- getResizeOffsets()
98
- resizerRefs.value.forEach((r, i) => {
99
- r.update(resizerOffsets.value![i]!)
100
- })
100
+ syncResizers()
101
101
  }
102
102
  })
103
103
 
104
+ /** 重新测量各列边界并同步所有拖拽手柄位置(容器尺寸或 cols 程序化变化后调用) */
105
+ function syncResizers() {
106
+ getResizeOffsets()
107
+ resizerRefs.value.forEach((r, i) => {
108
+ r.update(resizerOffsets.value![i]!)
109
+ })
110
+ }
111
+
104
112
  // const horizontalResizerList = []
105
113
 
106
114
  // const resizing = shallowRef(false)
@@ -117,11 +125,14 @@ const handleStartResize = (index: number) => {
117
125
  const nextDom = container.children[index + 1] as HTMLElement | undefined
118
126
  prevSize = prevDom ? prevDom.getBoundingClientRect().width : 0
119
127
  nextSize = nextDom ? nextDom.getBoundingClientRect().width : 0
128
+ emit('resize-start', index)
120
129
  }
121
130
  const handleResize = (offset: number, index: number) => {
122
- // 钳位到 [-prevSize, nextSize],避免轨道尺寸出现负值——
131
+ // 钳位到 [colMin-prevSize, nextSize-colMin],避免轨道尺寸出现负值或小于列最小宽度——
123
132
  // 一旦 grid-template-columns 中带负值,整条声明会被浏览器丢弃。
124
- const clamped = Math.max(-prevSize, Math.min(nextSize, offset))
133
+ const minPrev = props.colMinSizes?.[index] ?? 0
134
+ const minNext = props.colMinSizes?.[index + 1] ?? 0
135
+ const clamped = Math.max(minPrev - prevSize, Math.min(nextSize - minNext, offset))
125
136
  templateCols.value[index] = `${prevSize + clamped}px`
126
137
  templateCols.value[index + 1] = `${nextSize - clamped}px`
127
138
  }
@@ -13,14 +13,14 @@ describe('allocateLeafColumnWidths', () => {
13
13
  const name = createLeaf({ key: 'name', name: '姓名', width: 200 })
14
14
  const age = createLeaf({ key: 'age', name: '年龄', width: 200 })
15
15
  const action = createLeaf({ key: 'action', name: '操作', fixed: 'right' })
16
- // 模拟旧算法曾把操作列撑大
17
- action.data.width = 800
16
+ // 模拟上一次分配曾把操作列撑大
17
+ action.allocatedWidth = 800
18
18
 
19
19
  allocateLeafColumnWidths([name, age, action], 1000)
20
20
 
21
21
  expect(name.width).toBe(200)
22
22
  expect(age.width).toBe(200)
23
- expect(action.data.width).toBeUndefined()
23
+ expect(action.allocatedWidth).toBeUndefined()
24
24
  expect(action.width).toBe(100)
25
25
  })
26
26
 
@@ -45,7 +45,7 @@ describe('allocateLeafColumnWidths', () => {
45
45
  // totalBase = 100 + 100 + 100 = 300,剩余 700 只分给 age/score
46
46
  allocateLeafColumnWidths([name, age, score], 1000)
47
47
 
48
- expect(name.data.width).toBeUndefined()
48
+ expect(name.allocatedWidth).toBeUndefined()
49
49
  expect(name.width).toBe(100)
50
50
  expect(age.width).toBe(450)
51
51
  expect(score.width).toBe(450)
@@ -66,12 +66,53 @@ describe('allocateLeafColumnWidths', () => {
66
66
  it('无剩余空间时 grow 列回落到 minWidth', () => {
67
67
  const a = createLeaf({ key: 'a', name: 'A', width: 400 })
68
68
  const b = createLeaf({ key: 'b', name: 'B' })
69
- b.data.width = 300
69
+ b.allocatedWidth = 300
70
70
 
71
71
  allocateLeafColumnWidths([a, b], 450)
72
72
 
73
73
  expect(a.width).toBe(400)
74
- expect(b.data.width).toBeUndefined()
74
+ expect(b.allocatedWidth).toBeUndefined()
75
75
  expect(b.width).toBe(100)
76
76
  })
77
+
78
+ it('分配结果不写入列配置,森林重建后容器变窄仍会收缩', () => {
79
+ const config: TableColumn[] = [
80
+ { key: 'a', name: 'A' },
81
+ { key: 'b', name: 'B' }
82
+ ]
83
+
84
+ // 第一次挂载:宽容器
85
+ const first = config.map((c, i) => new ColumnNode(c, i, 0))
86
+ allocateLeafColumnWidths(first, 2000)
87
+ expect(first[0]!.width).toBe(1000)
88
+ expect(first[1]!.width).toBe(1000)
89
+
90
+ // 分配结果不得污染用户的列配置对象
91
+ expect(config[0]!.width).toBeUndefined()
92
+ expect(config[1]!.width).toBeUndefined()
93
+
94
+ // 模拟森林重建(路由返回 / v-if / columns 引用变化):
95
+ // 同一批列配置对象不应被误判为显式设宽
96
+ const rebuilt = config.map((c, i) => new ColumnNode(c, i, 0))
97
+ expect(rebuilt.every((node) => !node.explicitWidth)).toBe(true)
98
+
99
+ // 容器变窄后重新分配,列宽相应收缩
100
+ allocateLeafColumnWidths(rebuilt, 800)
101
+ expect(rebuilt[0]!.width).toBe(400)
102
+ expect(rebuilt[1]!.width).toBe(400)
103
+ })
104
+
105
+ it('显式宽度(配置/拖拽锁定)优先于分配宽度并使其作废', () => {
106
+ const col = createLeaf({ key: 'a', name: 'A' })
107
+ col.allocatedWidth = 500
108
+ expect(col.width).toBe(500)
109
+
110
+ // 模拟表头拖拽锁定
111
+ col.width = 300
112
+ col.explicitWidth = true
113
+
114
+ expect(col.data.width).toBe(300)
115
+ expect(col.allocatedWidth).toBeUndefined()
116
+ expect(col.width).toBe(300)
117
+ })
77
118
  })
@@ -6,16 +6,19 @@ import type { ColumnNode } from './node/col'
6
6
  * - `explicitWidth` 列:保持配置/拖拽锁定的宽度
7
7
  * - `fixed` 且未显式设宽的列:保持 `minWidth`,不参与剩余宽度均分
8
8
  * - 其余未显式设宽的列:均分容器剩余宽度
9
+ *
10
+ * 分配结果写入列节点自身的 `allocatedWidth`,不回写列配置的 `data.width`,
11
+ * 避免污染用户数据后在列森林重建时被误判为显式宽度。
9
12
  */
10
13
  export function allocateLeafColumnWidths(leafColumns: ColumnNode[], containerWidth: number): void {
11
14
  if (!containerWidth || !leafColumns.length) return
12
15
 
13
16
  const growColumns = leafColumns.filter((column) => !column.explicitWidth && !column.fixed)
14
- // fixed 且未显式设宽:清掉可能被旧算法撑大的残留 width,回落到 minWidth
17
+ // fixed 且未显式设宽:清掉之前的分配结果,回落到 minWidth
15
18
  const fixedFlexColumns = leafColumns.filter((column) => !column.explicitWidth && column.fixed)
16
19
 
17
20
  fixedFlexColumns.forEach((column) => {
18
- column.data.width = undefined
21
+ column.allocatedWidth = undefined
19
22
  })
20
23
 
21
24
  const explicitWidthTotal = leafColumns
@@ -28,11 +31,11 @@ export function allocateLeafColumnWidths(leafColumns: ColumnNode[], containerWid
28
31
  if (growColumns.length > 0 && totalBase < containerWidth) {
29
32
  const allocatedWidth = (containerWidth - totalBase) / growColumns.length
30
33
  growColumns.forEach((column) => {
31
- column.width = column.minWidth! + allocatedWidth
34
+ column.allocatedWidth = column.minWidth! + allocatedWidth
32
35
  })
33
36
  } else {
34
37
  growColumns.forEach((column) => {
35
- column.data.width = undefined
38
+ column.allocatedWidth = undefined
36
39
  })
37
40
  }
38
41
  }