@gx-design-vue/pro-table 0.2.0-alpha.20 → 0.2.0-alpha.21

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.
Files changed (42) hide show
  1. package/dist/ProTable.js +61 -2
  2. package/dist/components/Drag/Handle.d.ts +90 -0
  3. package/dist/components/Drag/Handle.js +64 -0
  4. package/dist/components/Drag/Tbody.d.ts +17 -0
  5. package/dist/components/Drag/Tbody.js +27 -0
  6. package/dist/components/EllipsisText/index.d.ts +50 -0
  7. package/dist/components/EllipsisText/index.js +65 -0
  8. package/dist/components/SearchForm/CollapseToggle.d.ts +1 -1
  9. package/dist/components/SearchForm/FormItemWrapper.d.ts +1 -1
  10. package/dist/components/SearchForm/SearchForm.d.ts +2 -2
  11. package/dist/components/Toolbar/ListToolBar.d.ts +4 -4
  12. package/dist/components/Toolbar/index.d.ts +5 -5
  13. package/dist/hooks/index.d.ts +4 -1
  14. package/dist/hooks/index.js +4 -1
  15. package/dist/hooks/useAutoScroll.d.ts +32 -0
  16. package/dist/hooks/useAutoScroll.js +127 -0
  17. package/dist/hooks/useCellRender.js +14 -2
  18. package/dist/hooks/useColumnResize.js +2 -0
  19. package/dist/hooks/useDragHandleColumn.d.ts +26 -0
  20. package/dist/hooks/useDragHandleColumn.js +52 -0
  21. package/dist/hooks/useEllipsis.d.ts +20 -0
  22. package/dist/hooks/useEllipsis.js +51 -0
  23. package/dist/hooks/useRowDrag.d.ts +42 -0
  24. package/dist/hooks/useRowDrag.js +268 -0
  25. package/dist/index.d.ts +3 -2
  26. package/dist/index.js +2 -1
  27. package/dist/interface.d.ts +47 -9
  28. package/dist/pro-table.esm.js +6156 -613
  29. package/dist/pro-table.js +4 -1
  30. package/dist/style/ellipsis.d.ts +13 -0
  31. package/dist/style/ellipsis.js +16 -0
  32. package/dist/style/index.js +4 -0
  33. package/dist/style/row-drag.d.ts +8 -0
  34. package/dist/style/row-drag.js +44 -0
  35. package/dist/utils/arrayMove.d.ts +9 -0
  36. package/dist/utils/arrayMove.js +15 -0
  37. package/dist/utils/ellipsis.d.ts +19 -0
  38. package/dist/utils/ellipsis.js +30 -0
  39. package/dist/utils/flipAnimate.d.ts +25 -0
  40. package/dist/utils/flipAnimate.js +40 -0
  41. package/dist/utils/formConstants.js +3 -3
  42. package/package.json +15 -13
@@ -0,0 +1,127 @@
1
+ //#region src/hooks/useAutoScroll.ts
2
+ const ScrollDirection = {
3
+ Idle: 0,
4
+ Forward: 1,
5
+ Reverse: -1
6
+ };
7
+ const SCROLL_EPSILON = .5;
8
+ /**
9
+ * 纵向自动滚动 —— 精简复刻 `@dnd-kit/dom` 的 `detectScrollIntent` + `Scroller` 思路,
10
+ * 与 pro-layout Tabs 的横向 `useAutoScroll` 同构(仅轴向不同)。
11
+ *
12
+ * 指针进入 `scrollEl` 上/下边缘 threshold 区域时,按距边缘的比例计算速度
13
+ * (越靠边越快)。滚动方向必须先被用户同向移动解锁;如果当前方向已经到达边界,
14
+ * 则不产生滚动 intent,避免触底后因为鼠标还在边缘区被反复重新激活。
15
+ *
16
+ * 关键:`maxScrollTop` 在 `start()` 时快照一次(`??` 仅在 undefined 时 fallback 到实时值),
17
+ * 拖拽全程都用快照值判定能否滚动。这样即使拖拽中 `transform` 撑大了 `scrollHeight`,
18
+ * 也不会污染滚动判定,避免死循环。
19
+ */
20
+ function useAutoScroll(scrollEl, getPointerY, options = {}) {
21
+ const { threshold = .2, acceleration = 18, tolerance = 10, onScroll } = options;
22
+ let rafId;
23
+ let lastPointerY = null;
24
+ let maxScrollTop;
25
+ const unlockedDirections = /* @__PURE__ */ new Set();
26
+ function getMaxScrollTop() {
27
+ const el = scrollEl.value;
28
+ if (!el) return 0;
29
+ return maxScrollTop ?? Math.max(0, el.scrollHeight - el.clientHeight);
30
+ }
31
+ function canScroll(direction) {
32
+ const el = scrollEl.value;
33
+ if (!el) return false;
34
+ const max = getMaxScrollTop();
35
+ if (direction === ScrollDirection.Forward) return el.scrollTop < max - SCROLL_EPSILON;
36
+ if (direction === ScrollDirection.Reverse) return el.scrollTop > SCROLL_EPSILON;
37
+ return false;
38
+ }
39
+ function getEdgeIntent() {
40
+ const el = scrollEl.value;
41
+ const pointerY = getPointerY();
42
+ if (!el || pointerY == null) return {
43
+ direction: ScrollDirection.Idle,
44
+ speed: 0
45
+ };
46
+ const rect = el.getBoundingClientRect();
47
+ const triggerHeight = rect.height * threshold;
48
+ if (triggerHeight <= 0) return {
49
+ direction: ScrollDirection.Idle,
50
+ speed: 0
51
+ };
52
+ let direction = ScrollDirection.Idle;
53
+ let speed = 0;
54
+ const distBottom = rect.bottom - pointerY;
55
+ const distTop = pointerY - rect.top;
56
+ const canScrollDown = canScroll(ScrollDirection.Forward);
57
+ const canScrollUp = canScroll(ScrollDirection.Reverse);
58
+ if (canScrollDown && distBottom <= triggerHeight && pointerY <= rect.bottom + tolerance) {
59
+ const ratio = Math.abs((rect.bottom - triggerHeight - pointerY) / triggerHeight);
60
+ direction = ScrollDirection.Forward;
61
+ speed = acceleration * Math.min(ratio, 1);
62
+ } else if (canScrollUp && distTop <= triggerHeight && pointerY >= rect.top - tolerance) {
63
+ const ratio = Math.abs((rect.top + triggerHeight - pointerY) / triggerHeight);
64
+ direction = ScrollDirection.Reverse;
65
+ speed = acceleration * Math.min(ratio, 1);
66
+ }
67
+ return {
68
+ direction,
69
+ speed
70
+ };
71
+ }
72
+ function applyScroll() {
73
+ const el = scrollEl.value;
74
+ if (!el) return false;
75
+ const { direction, speed } = getEdgeIntent();
76
+ if (direction === ScrollDirection.Idle) return false;
77
+ if (!unlockedDirections.has(direction)) return false;
78
+ if (!canScroll(direction)) return false;
79
+ const delta = direction * speed;
80
+ if (delta !== 0) {
81
+ const before = el.scrollTop;
82
+ const next = Math.min(getMaxScrollTop(), Math.max(0, before + delta));
83
+ if (Math.abs(next - before) <= SCROLL_EPSILON) return false;
84
+ el.scrollTop = next;
85
+ onScroll?.();
86
+ return true;
87
+ }
88
+ return false;
89
+ }
90
+ function tick() {
91
+ rafId = void 0;
92
+ if (applyScroll()) rafId = requestAnimationFrame(tick);
93
+ }
94
+ function scroll() {
95
+ const pointerY = getPointerY();
96
+ if (pointerY == null) return false;
97
+ if (lastPointerY != null) {
98
+ const moveDirection = Math.sign(pointerY - lastPointerY);
99
+ if (moveDirection !== ScrollDirection.Idle) unlockedDirections.add(moveDirection);
100
+ }
101
+ lastPointerY = pointerY;
102
+ const didScroll = applyScroll();
103
+ if (didScroll) rafId ??= requestAnimationFrame(tick);
104
+ return didScroll;
105
+ }
106
+ function start() {
107
+ const el = scrollEl.value;
108
+ maxScrollTop = el ? Math.max(0, el.scrollHeight - el.clientHeight) : void 0;
109
+ scroll();
110
+ }
111
+ function stop() {
112
+ if (rafId != null) {
113
+ cancelAnimationFrame(rafId);
114
+ rafId = void 0;
115
+ }
116
+ lastPointerY = null;
117
+ maxScrollTop = void 0;
118
+ unlockedDirections.clear();
119
+ }
120
+ return {
121
+ scroll,
122
+ start,
123
+ stop
124
+ };
125
+ }
126
+ //#endregion
127
+ export { useAutoScroll };
@@ -1,6 +1,8 @@
1
+ import EllipsisText_default from "../components/EllipsisText/index.js";
2
+ import { normalizeEllipsis } from "../utils/ellipsis.js";
1
3
  import { valueFormat } from "../utils/valueFormat.js";
2
- import { isVNode } from "vue";
3
- import { handleEmptyField } from "@gx-design-vue/pro-utils";
4
+ import { h, isVNode } from "vue";
5
+ import { handleEmptyField, isNil } from "@gx-design-vue/pro-utils";
4
6
  //#region src/hooks/useCellRender.ts
5
7
  function resolveEmptyText(column, record, tableLevelEmptyText) {
6
8
  if (column.columnEmptyText !== void 0) return typeof column.columnEmptyText === "function" ? column.columnEmptyText(record) : column.columnEmptyText;
@@ -10,15 +12,25 @@ function resolveEmptyText(column, record, tableLevelEmptyText) {
10
12
  function useCellRender(options) {
11
13
  function wrapColumnRender(column) {
12
14
  const userRender = column.render;
15
+ const normalizedEllipsis = normalizeEllipsis(column.ellipsis);
13
16
  const wrappedRender = (value, record, index) => {
14
17
  let cellValue = userRender ? userRender(value, record, index) : value;
15
18
  if (!userRender && column.valueType && !isVNode(cellValue)) cellValue = valueFormat(cellValue, column.valueType, { className: options.prefixCls.value });
16
19
  const emptyText = resolveEmptyText(column, record, options.columnEmptyText?.value);
17
20
  const { value: finalValue } = handleEmptyField(cellValue, emptyText);
21
+ let measurableText = null;
22
+ if (typeof cellValue === "string" && cellValue !== "" || typeof cellValue === "number") measurableText = String(cellValue);
23
+ else if (column.valueType === "link" && !isNil(value) && value !== "" && (typeof value === "string" || typeof value === "number")) measurableText = String(value);
24
+ if (normalizedEllipsis.enabled && measurableText !== null) return h(EllipsisText_default, {
25
+ text: measurableText,
26
+ ellipsis: normalizedEllipsis,
27
+ prefixCls: options.prefixCls.value
28
+ }, { default: () => finalValue });
18
29
  return finalValue;
19
30
  };
20
31
  return {
21
32
  ...column,
33
+ ellipsis: false,
22
34
  render: wrappedRender
23
35
  };
24
36
  }
@@ -1,4 +1,5 @@
1
1
  import { getColumnKey } from "./useColumns.js";
2
+ import "./useDragHandleColumn.js";
2
3
  import { computed, defineComponent, h, onScopeDispose, shallowRef } from "vue";
3
4
  //#region src/hooks/useColumnResize.ts
4
5
  const MIN_COLUMN_WIDTH = 40;
@@ -82,6 +83,7 @@ function useColumnResize(options) {
82
83
  const source = options.columns.value;
83
84
  const overrides = columnWidthOverrides.value;
84
85
  return source.map((column) => {
86
+ if (column.key === "__drag_handle__" || column.dataIndex === "__drag_handle__") return column;
85
87
  const columnKey = getColumnKey(column);
86
88
  const overriddenWidth = overrides.get(columnKey);
87
89
  return {
@@ -0,0 +1,26 @@
1
+ import { ProColumnType, ProTableRowDragOptions } from "../interface.js";
2
+ import { ComputedRef } from "vue";
3
+ import { RecordType } from "@gx-design-vue/pro-utils";
4
+
5
+ //#region src/hooks/useDragHandleColumn.d.ts
6
+ interface UseDragHandleColumnOptions<T = RecordType> {
7
+ columns: ComputedRef<ProColumnType<T>[]>;
8
+ rowDraggable: ComputedRef<boolean | ProTableRowDragOptions | undefined>;
9
+ prefixCls: ComputedRef<string>;
10
+ cssVarCls: ComputedRef<string>;
11
+ }
12
+ interface UseDragHandleColumnReturn<T = RecordType> {
13
+ dragColumns: ComputedRef<ProColumnType<T>[]>;
14
+ }
15
+ /** 把手列内部标记 key */
16
+ declare const DRAG_HANDLE_COLUMN_KEY = "__drag_handle__";
17
+ /**
18
+ * 在列首注入行拖拽把手列。
19
+ *
20
+ * 纯函数实现(对标 `applyIndexColumn`),在 `useColumns` 的 `cacheColumns` computed 中、
21
+ * `applyIndexColumn` 之前调用,确保把手列比序号列更靠前。
22
+ */
23
+ declare function applyDragHandleColumn<T>(columns: ProColumnType<T>[], rowDraggable: boolean | ProTableRowDragOptions | undefined, prefixCls: string, cssVarCls: string): ProColumnType<T>[];
24
+ declare function useDragHandleColumn<T = RecordType>(options: UseDragHandleColumnOptions<T>): UseDragHandleColumnReturn<T>;
25
+ //#endregion
26
+ export { DRAG_HANDLE_COLUMN_KEY, UseDragHandleColumnOptions, UseDragHandleColumnReturn, applyDragHandleColumn, useDragHandleColumn };
@@ -0,0 +1,52 @@
1
+ import { DragHandle } from "../components/Drag/Handle.js";
2
+ import { computed, createVNode } from "vue";
3
+ import { createUUIDFactory } from "@gx-design-vue/pro-utils";
4
+ //#region src/hooks/useDragHandleColumn.tsx
5
+ /** 把手列内部标记 key */
6
+ const DRAG_HANDLE_COLUMN_KEY = "__drag_handle__";
7
+ /**
8
+ * 创建行拖拽把手列配置。
9
+ *
10
+ * 把手列固定在最前面(比 indexColumn 序号列更靠前),包含 DragHandle 组件。
11
+ * 通过 `disabled(record)` 判断每行是否禁用拖拽。
12
+ */
13
+ function createDragHandleColumn(options, prefixCls, cssVarCls) {
14
+ return {
15
+ key: DRAG_HANDLE_COLUMN_KEY,
16
+ dataIndex: DRAG_HANDLE_COLUMN_KEY,
17
+ title: "排序",
18
+ width: options.width ?? 40,
19
+ align: options.align ?? "center",
20
+ fixed: options.fixed ?? "start",
21
+ uuid: createUUIDFactory().uuid(15),
22
+ render: (_value, record, index) => {
23
+ const disabled = options.disabled?.(record) ?? false;
24
+ return createVNode(DragHandle, {
25
+ "handleRender": options.handleRender,
26
+ "record": record,
27
+ "index": index,
28
+ "disabled": disabled,
29
+ "cssVarCls": cssVarCls,
30
+ "prefixCls": prefixCls
31
+ }, null);
32
+ }
33
+ };
34
+ }
35
+ /**
36
+ * 在列首注入行拖拽把手列。
37
+ *
38
+ * 纯函数实现(对标 `applyIndexColumn`),在 `useColumns` 的 `cacheColumns` computed 中、
39
+ * `applyIndexColumn` 之前调用,确保把手列比序号列更靠前。
40
+ */
41
+ function applyDragHandleColumn(columns, rowDraggable, prefixCls, cssVarCls) {
42
+ if (!rowDraggable || columns.length === 0) return columns;
43
+ if (columns.some((column) => column.dataIndex === "__drag_handle__")) return columns;
44
+ return [createDragHandleColumn(typeof rowDraggable === "object" ? rowDraggable : {}, prefixCls, cssVarCls), ...columns];
45
+ }
46
+ function useDragHandleColumn(options) {
47
+ return { dragColumns: computed(() => {
48
+ return applyDragHandleColumn(options.columns.value, options.rowDraggable.value, options.prefixCls.value, options.cssVarCls.value);
49
+ }) };
50
+ }
51
+ //#endregion
52
+ export { DRAG_HANDLE_COLUMN_KEY, applyDragHandleColumn, useDragHandleColumn };
@@ -0,0 +1,20 @@
1
+ import { Ref } from "vue";
2
+
3
+ //#region src/hooks/useEllipsis.d.ts
4
+ interface UseEllipsisReturn {
5
+ /** 当前文本在给定宽度/行数下是否溢出 */
6
+ isOverflow: Ref<boolean>;
7
+ }
8
+ /**
9
+ * 基于 @chenglou/pretext 的溢出判断。
10
+ *
11
+ * - prepare(text, font) 一次性预处理(命中 pretext 内部缓存),仅随 text/font 变化重跑;
12
+ * - layout(prepared, width, lineHeight) 纯算力预算行数,随列宽/行数变化重算(符合 pretext 性能模型);
13
+ * - 宽度取自 ResizeObserver 的 contentRect.width(已扣除 padding);
14
+ * - font/lineHeight 于 mount 时通过 getComputedStyle 读取,保证与实际 CSS 渲染一致。
15
+ *
16
+ * lineCount > 期望行数 即判定为溢出。
17
+ */
18
+ declare function useEllipsis(textRef: Ref<string>, linesRef: Ref<number>, cellElRef: Ref<HTMLElement | undefined>): UseEllipsisReturn;
19
+ //#endregion
20
+ export { UseEllipsisReturn, useEllipsis };
@@ -0,0 +1,51 @@
1
+ import { computed, onMounted, ref } from "vue";
2
+ import { useResizeObserver } from "@vueuse/core";
3
+ import { layout, prepare } from "@chenglou/pretext";
4
+ //#region src/hooks/useEllipsis.ts
5
+ /**
6
+ * 基于 @chenglou/pretext 的溢出判断。
7
+ *
8
+ * - prepare(text, font) 一次性预处理(命中 pretext 内部缓存),仅随 text/font 变化重跑;
9
+ * - layout(prepared, width, lineHeight) 纯算力预算行数,随列宽/行数变化重算(符合 pretext 性能模型);
10
+ * - 宽度取自 ResizeObserver 的 contentRect.width(已扣除 padding);
11
+ * - font/lineHeight 于 mount 时通过 getComputedStyle 读取,保证与实际 CSS 渲染一致。
12
+ *
13
+ * lineCount > 期望行数 即判定为溢出。
14
+ */
15
+ function useEllipsis(textRef, linesRef, cellElRef) {
16
+ const contentWidth = ref(0);
17
+ const fontInfo = ref({
18
+ font: "14px sans-serif",
19
+ lineHeight: 22
20
+ });
21
+ useResizeObserver(cellElRef, (entries) => {
22
+ const entry = entries[0];
23
+ if (entry) contentWidth.value = entry.contentRect.width;
24
+ });
25
+ onMounted(() => {
26
+ const el = cellElRef.value;
27
+ if (!el) return;
28
+ const computedStyle = getComputedStyle(el);
29
+ const fontSize = Number.parseFloat(computedStyle.fontSize) || 14;
30
+ let lineHeight = Number.parseFloat(computedStyle.lineHeight);
31
+ if (!lineHeight || Number.isNaN(lineHeight)) lineHeight = fontSize * 1.5715;
32
+ fontInfo.value = {
33
+ font: `${fontSize}px ${computedStyle.fontFamily}`,
34
+ lineHeight
35
+ };
36
+ });
37
+ const prepared = computed(() => {
38
+ const text = textRef.value;
39
+ if (!text) return null;
40
+ return prepare(text, fontInfo.value.font);
41
+ });
42
+ return { isOverflow: computed(() => {
43
+ const preparedText = prepared.value;
44
+ const width = contentWidth.value;
45
+ if (!preparedText || width <= 0) return false;
46
+ const { lineCount } = layout(preparedText, width, fontInfo.value.lineHeight);
47
+ return lineCount > linesRef.value;
48
+ }) };
49
+ }
50
+ //#endregion
51
+ export { useEllipsis };
@@ -0,0 +1,42 @@
1
+ import { ProTableRowDragOptions } from "../interface.js";
2
+ import { ComputedRef, Ref } from "vue";
3
+ import { RecordType } from "@gx-design-vue/pro-utils";
4
+
5
+ //#region src/hooks/useRowDrag.d.ts
6
+ interface UseRowDragOptions<T = RecordType> {
7
+ /** tbody DOM 引用(通过 components.body.wrapper 覆写拿到) */
8
+ tbodyEl: Ref<HTMLElement | undefined>;
9
+ /** 表格滚动容器(用于垂直自动滚动) */
10
+ scrollEl: Ref<HTMLElement | undefined>;
11
+ /** 数据源(只读 computed) */
12
+ dataSource: ComputedRef<T[]>;
13
+ /** 行唯一标识字段 */
14
+ rowKey: ComputedRef<string>;
15
+ /** 行拖拽配置(含 disabled 谓词) */
16
+ rowDraggable: ComputedRef<boolean | ProTableRowDragOptions | undefined>;
17
+ /** 拖拽排序完成回调(仅通知重排结果,由调用方决定是否回写数据源) */
18
+ onSortEnd?: (from: number, to: number, newData: T[], record: T) => void;
19
+ }
20
+ /** onRow 返回的行属性(对标 antdv 的 CellAttributes 子集) */
21
+ type RowDragCellProps = Record<string, any>;
22
+ interface UseRowDragReturn {
23
+ isDragging: Ref<boolean>;
24
+ draggingRowKey: Ref<string | number>;
25
+ /** 返回 onRow 函数(antdv Table 的 onRow 签名) */
26
+ getRowProps: (record: any, index: number) => RowDragCellProps;
27
+ }
28
+ /**
29
+ * 表格行拖拽排序 —— 精简复刻 `useTabDrag` 的垂直版本。
30
+ *
31
+ * - 仅从把手列(`[data-drag-handle]`)发起拖拽,单元格交互不受影响
32
+ * - 移动阈值(3px)区分点击与拖拽
33
+ * - 拖拽行 `transform: translateY(delta)` 跟随指针
34
+ * - 同列表其他行按虚拟插入位置实时 `translate` 让位(CSS translate 属性 + WAAPI)
35
+ * - drop 锚点用最近中心法(被拖行中心 Y 与全量各行中心比距离取最近者;含自身,半身位即换位)
36
+ * - 松手 → onSortEnd(仅 emit 排序结果,由使用方回写数据源)→ nextTick 用 flipAnimate 平滑归位
37
+ * - 拖拽结束后捕获抑制下一次 click
38
+ * - 指针接近滚动容器上下边缘时自动滚动(垂直版 detectScrollIntent)
39
+ */
40
+ declare function useRowDrag<T = RecordType>(options: UseRowDragOptions<T>): UseRowDragReturn;
41
+ //#endregion
42
+ export { RowDragCellProps, UseRowDragOptions, UseRowDragReturn, useRowDrag };
@@ -0,0 +1,268 @@
1
+ import { arrayMove } from "../utils/arrayMove.js";
2
+ import { flipAnimate } from "../utils/flipAnimate.js";
3
+ import { useAutoScroll } from "./useAutoScroll.js";
4
+ import { nextTick, ref } from "vue";
5
+ import { useEventListener } from "@vueuse/core";
6
+ //#region src/hooks/useRowDrag.ts
7
+ /** 移动阈值(px),超过才视为拖拽,避免误触影响点击 */
8
+ const DRAG_THRESHOLD = 3;
9
+ const SCROLL_RESTORE_FRAMES = 2;
10
+ const DISPLACEMENT_DURATION = 160;
11
+ const DISPLACEMENT_EASING = "cubic-bezier(0.2, 0, 0, 1)";
12
+ const DRAGGING_Z_INDEX = "1000";
13
+ function isFromDragHandle(target) {
14
+ const el = target;
15
+ if (!el || typeof el.closest !== "function") return false;
16
+ const handle = el.closest("[data-drag-handle]");
17
+ if (!handle) return false;
18
+ return handle.getAttribute("data-drag-disabled") !== "true";
19
+ }
20
+ /**
21
+ * 表格行拖拽排序 —— 精简复刻 `useTabDrag` 的垂直版本。
22
+ *
23
+ * - 仅从把手列(`[data-drag-handle]`)发起拖拽,单元格交互不受影响
24
+ * - 移动阈值(3px)区分点击与拖拽
25
+ * - 拖拽行 `transform: translateY(delta)` 跟随指针
26
+ * - 同列表其他行按虚拟插入位置实时 `translate` 让位(CSS translate 属性 + WAAPI)
27
+ * - drop 锚点用最近中心法(被拖行中心 Y 与全量各行中心比距离取最近者;含自身,半身位即换位)
28
+ * - 松手 → onSortEnd(仅 emit 排序结果,由使用方回写数据源)→ nextTick 用 flipAnimate 平滑归位
29
+ * - 拖拽结束后捕获抑制下一次 click
30
+ * - 指针接近滚动容器上下边缘时自动滚动(垂直版 detectScrollIntent)
31
+ */
32
+ function useRowDrag(options) {
33
+ const { tbodyEl, scrollEl, dataSource, rowKey, rowDraggable, onSortEnd } = options;
34
+ const isDragging = ref(false);
35
+ const draggingRowKey = ref("");
36
+ let dragStartClientY = 0;
37
+ let dragStartScrollTop = 0;
38
+ let dragPointerOffsetY = 0;
39
+ let pointerClientY = 0;
40
+ let dragEl = null;
41
+ let rowRects = [];
42
+ let pendingKey = "";
43
+ let pendingStartY = 0;
44
+ let pendingRecord = null;
45
+ const { scroll: scrollOnDragMove, start: startAutoScroll, stop: stopAutoScroll } = useAutoScroll(scrollEl, () => pointerClientY, { onScroll: updateDragPosition });
46
+ function queryRowEl(key) {
47
+ return tbodyEl.value?.querySelector(`tr[data-row-key="${key}"]`) ?? null;
48
+ }
49
+ function recordRowRects() {
50
+ rowRects = [];
51
+ const data = dataSource.value;
52
+ for (const record of data) {
53
+ const key = record?.[rowKey.value];
54
+ if (key == null) continue;
55
+ const el = queryRowEl(key);
56
+ if (!el) continue;
57
+ const rect = el.getBoundingClientRect();
58
+ rowRects.push({
59
+ el,
60
+ key,
61
+ top: rect.top,
62
+ height: rect.height,
63
+ translate: 0
64
+ });
65
+ }
66
+ }
67
+ function getCurrentScrollOffset() {
68
+ return (scrollEl.value?.scrollTop ?? 0) - dragStartScrollTop;
69
+ }
70
+ function getVisualPointerY() {
71
+ return pointerClientY + getCurrentScrollOffset();
72
+ }
73
+ function getDragCenterY() {
74
+ const dragRect = rowRects.find((r) => r.key === draggingRowKey.value);
75
+ if (!dragRect) return getVisualPointerY();
76
+ return getVisualPointerY() - dragPointerOffsetY + dragRect.height / 2;
77
+ }
78
+ /**
79
+ * 计算拖拽行应去的目标索引(最近中心法,对齐 dnd-kit `closestCenter`)。
80
+ *
81
+ * 在全量行(含拖拽行自身)里找离拖拽行视觉中心最近的行,返回其索引作为
82
+ * `arrayMove(data, fromIdx, 返回值)` 的 to。返回值的几何意义即"拖拽行想占据的位置":
83
+ * - 最近行是自己(fromIdx) → from===to,不移动
84
+ * - 最近行在下方 → 向下移动占据其位置
85
+ * - 最近行在上方 → 向上移动占据其位置
86
+ *
87
+ * 关键:拖拽行自身必须参与比较,否则切换阈值会从「半个身位」退化成「一个身位」——
88
+ * 缺少起始参考点后,需要整行越过下一行中心才会换位(表现为完全覆盖才触发);
89
+ * 含自身时切换点落在相邻两行中心的中点,即拖动半个行高即换位。
90
+ */
91
+ function computeTargetIdx(dragCenterY) {
92
+ const fromIdx = rowRects.findIndex((r) => r.key === draggingRowKey.value);
93
+ if (fromIdx < 0) return -1;
94
+ let nearestIdx = fromIdx;
95
+ let nearestDist = Infinity;
96
+ for (let i = 0; i < rowRects.length; i++) {
97
+ const center = rowRects[i].top + rowRects[i].height / 2;
98
+ const dist = Math.abs(dragCenterY - center);
99
+ if (dist < nearestDist) {
100
+ nearestDist = dist;
101
+ nearestIdx = i;
102
+ }
103
+ }
104
+ return nearestIdx;
105
+ }
106
+ /** 同区间其他行实时让位 */
107
+ function applyDisplacement(targetIdx) {
108
+ const fromIdx = rowRects.findIndex((r) => r.key === draggingRowKey.value);
109
+ if (fromIdx < 0) return;
110
+ const dragHeight = rowRects.find((r) => r.key === draggingRowKey.value)?.height ?? 0;
111
+ for (let i = 0; i < rowRects.length; i++) {
112
+ const rect = rowRects[i];
113
+ if (rect.key === draggingRowKey.value) continue;
114
+ let translate = 0;
115
+ if (fromIdx < targetIdx && i > fromIdx && i <= targetIdx) translate = -dragHeight;
116
+ else if (fromIdx > targetIdx && i >= targetIdx && i < fromIdx) translate = dragHeight;
117
+ applyRowTranslate(rect, translate);
118
+ }
119
+ }
120
+ function applyRowTranslate(rect, nextTranslate) {
121
+ const prevTranslate = rect.translate;
122
+ if (prevTranslate === nextTranslate) return;
123
+ rect.animation?.cancel();
124
+ rect.translate = nextTranslate;
125
+ rect.el.style.translate = nextTranslate ? `0px ${nextTranslate}px` : "";
126
+ if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches || typeof rect.el.animate !== "function") return;
127
+ rect.animation = rect.el.animate({ translate: [`0px ${prevTranslate}px`, `0px ${nextTranslate}px`] }, {
128
+ duration: DISPLACEMENT_DURATION,
129
+ easing: DISPLACEMENT_EASING
130
+ });
131
+ const clear = () => {
132
+ rect.animation = void 0;
133
+ };
134
+ rect.animation.addEventListener("finish", clear, { once: true });
135
+ rect.animation.addEventListener("cancel", clear, { once: true });
136
+ }
137
+ function updateDragPosition() {
138
+ if (!isDragging.value) return;
139
+ if (dragEl) dragEl.style.transform = `translateY(${getVisualPointerY() - dragStartClientY}px)`;
140
+ applyDisplacement(computeTargetIdx(getDragCenterY()));
141
+ }
142
+ function clearInlineStyles(rects = rowRects, activeDragEl = dragEl) {
143
+ for (const rect of rects) {
144
+ rect.animation?.cancel();
145
+ rect.animation = void 0;
146
+ rect.translate = 0;
147
+ rect.el.style.translate = "";
148
+ rect.el.style.zIndex = "";
149
+ }
150
+ if (activeDragEl) {
151
+ activeDragEl.style.transform = "";
152
+ activeDragEl.classList.remove("row-dragging");
153
+ }
154
+ }
155
+ function restoreScrollTop(scrollTop, frames = SCROLL_RESTORE_FRAMES) {
156
+ const el = scrollEl.value;
157
+ if (!el) return;
158
+ el.scrollTop = scrollTop;
159
+ if (frames <= 0 || typeof requestAnimationFrame === "undefined") return;
160
+ requestAnimationFrame(() => restoreScrollTop(scrollTop, frames - 1));
161
+ }
162
+ function beginDrag(clientY) {
163
+ isDragging.value = true;
164
+ dragStartClientY = clientY;
165
+ dragStartScrollTop = scrollEl.value?.scrollTop ?? 0;
166
+ pointerClientY = clientY;
167
+ dragEl = queryRowEl(draggingRowKey.value);
168
+ recordRowRects();
169
+ const dragRect = rowRects.find((r) => r.key === draggingRowKey.value);
170
+ dragPointerOffsetY = dragRect ? clientY - dragRect.top : 0;
171
+ if (dragEl) {
172
+ dragEl.classList.add("row-dragging");
173
+ dragEl.style.zIndex = DRAGGING_Z_INDEX;
174
+ }
175
+ startAutoScroll();
176
+ }
177
+ /** 捕获阶段抑制拖拽结束后的下一次 click */
178
+ function suppressNextClick() {
179
+ const target = dragEl;
180
+ if (!target) return;
181
+ const suppress = (event) => {
182
+ event.stopPropagation();
183
+ event.preventDefault();
184
+ target.removeEventListener("click", suppress, true);
185
+ };
186
+ target.addEventListener("click", suppress, {
187
+ capture: true,
188
+ once: true
189
+ });
190
+ }
191
+ function handleMousedown(event, record) {
192
+ if (event.button !== 0) return;
193
+ if (!isFromDragHandle(event.target)) return;
194
+ const cfg = rowDraggable.value;
195
+ if (!cfg) return;
196
+ if ((typeof cfg === "object" ? cfg : {}).disabled?.(record)) return;
197
+ event.preventDefault();
198
+ const key = record?.[rowKey.value];
199
+ pendingKey = key;
200
+ pendingStartY = event.clientY;
201
+ pendingRecord = record;
202
+ draggingRowKey.value = key;
203
+ }
204
+ function handleMousemove(event) {
205
+ if (!isDragging.value) {
206
+ if (!pendingKey) return;
207
+ if (Math.abs(event.clientY - pendingStartY) < DRAG_THRESHOLD) return;
208
+ beginDrag(pendingStartY);
209
+ }
210
+ pointerClientY = event.clientY;
211
+ updateDragPosition();
212
+ scrollOnDragMove();
213
+ }
214
+ function endDrag() {
215
+ const targetIdx = computeTargetIdx(getDragCenterY());
216
+ const fromIdx = rowRects.findIndex((r) => r.key === draggingRowKey.value);
217
+ const data = dataSource.value;
218
+ const prevRects = /* @__PURE__ */ new Map();
219
+ for (const { key, el } of rowRects) prevRects.set(key, el.getBoundingClientRect());
220
+ stopAutoScroll();
221
+ const currentRowRects = rowRects;
222
+ const currentDragEl = dragEl;
223
+ const currentScrollTop = scrollEl.value?.scrollTop ?? 0;
224
+ const record = pendingRecord;
225
+ if (targetIdx >= 0 && targetIdx !== fromIdx && targetIdx < data.length) {
226
+ const newData = arrayMove(data, fromIdx, targetIdx);
227
+ onSortEnd?.(fromIdx, targetIdx, newData, record);
228
+ nextTick(() => {
229
+ restoreScrollTop(currentScrollTop);
230
+ clearInlineStyles(currentRowRects, currentDragEl);
231
+ flipAnimate(data.map((r) => ({
232
+ key: r[rowKey.value],
233
+ el: queryRowEl(r[rowKey.value])
234
+ })).filter((entry) => Boolean(entry.el)), prevRects);
235
+ restoreScrollTop(currentScrollTop);
236
+ });
237
+ } else clearInlineStyles();
238
+ suppressNextClick();
239
+ isDragging.value = false;
240
+ draggingRowKey.value = "";
241
+ dragEl = null;
242
+ rowRects = [];
243
+ dragPointerOffsetY = 0;
244
+ pendingKey = "";
245
+ pendingRecord = null;
246
+ }
247
+ function handleMouseup() {
248
+ if (isDragging.value) endDrag();
249
+ else if (pendingKey) {
250
+ pendingKey = "";
251
+ draggingRowKey.value = "";
252
+ pendingRecord = null;
253
+ }
254
+ }
255
+ const documentTarget = typeof document === "undefined" ? void 0 : document;
256
+ useEventListener(documentTarget, "mousemove", handleMousemove);
257
+ useEventListener(documentTarget, "mouseup", handleMouseup);
258
+ function getRowProps(record, _index) {
259
+ return { onMousedown: (event) => handleMousedown(event, record) };
260
+ }
261
+ return {
262
+ isDragging,
263
+ draggingRowKey,
264
+ getRowProps
265
+ };
266
+ }
267
+ //#endregion
268
+ export { useRowDrag };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { BeforeSearchSubmit, ColConfig, ColumnWithKeyOrDataIndex, CustomDataFn, CustomDataRender, CustomRenderResult, DefaultProColumn, DefaultProColumnWithKeyOrDataIndex, DefaultRender, FormConfig, OperateRowProps, PageItemFn, PageItemRender, PageState, ProColumn, ProColumnType, ProColumnsValueType, ProCoreActionType, ProCoreFormType, ProFieldValueFormat, ProFieldValueType, ProSchemaValueEnumType, ProSearchMap, ProTableBodyCellProps, ProTableClassNamesType, ProTableCustomProps, ProTableEmits, ProTableFetchParams, ProTableIndexColumn, ProTablePagination, ProTablePaginationConfig, ProTableProps, ProTableRef, ProTableScrollPolicy, ProTableSizeType, ProTableSlots, ProTableStylesType, RequestConfig, RequestData, RequestFunction, RowSelectionActions, TableSemanticName, TableSorterRecord, ToolbarOptionConfig, ToolbarPlacement } from "./interface.js";
1
+ import { BeforeSearchSubmit, ColConfig, ColumnWithKeyOrDataIndex, CustomDataFn, CustomDataRender, CustomRenderResult, DefaultProColumn, DefaultProColumnWithKeyOrDataIndex, DefaultRender, FormConfig, OperateRowProps, PageItemFn, PageItemRender, PageState, ProColumn, ProColumnEllipsis, ProColumnType, ProColumnsValueType, ProCoreActionType, ProCoreFormType, ProFieldValueFormat, ProFieldValueType, ProSchemaValueEnumType, ProSearchMap, ProTableBodyCellProps, ProTableClassNamesType, ProTableCustomProps, ProTableEmits, ProTableFetchParams, ProTableIndexColumn, ProTablePagination, ProTablePaginationConfig, ProTableProps, ProTableRef, ProTableRowDragOptions, ProTableRowSortEndEvent, ProTableScrollPolicy, ProTableSizeType, ProTableSlots, ProTableStylesType, RequestConfig, RequestData, RequestFunction, RowSelectionActions, TableSemanticName, TableSorterRecord, ToolbarOptionConfig, ToolbarPlacement } from "./interface.js";
2
2
  import _default from "./ProTable.js";
3
3
  import { useInjectTableContext } from "./context/TableContext.js";
4
4
  import { UseProBreakpointsReturn, useProBreakpoints } from "./hooks/useBreakpoints.js";
@@ -7,4 +7,5 @@ import { usePagination } from "./hooks/usePagination.js";
7
7
  import { UseRequestOptionsParams, UseRequestOptionsReturn, useRequestOptions } from "./hooks/useRequestOptions.js";
8
8
  import { UseTableOptions, UseTableReturn, useTable } from "./hooks/useTable.js";
9
9
  import { UseTableFormOptions, UseTableFormReturn, useTableForm } from "./hooks/useTableForm.js";
10
- export { BeforeSearchSubmit, ColConfig, ColumnWithKeyOrDataIndex, CustomDataFn, CustomDataRender, CustomRenderResult, DefaultProColumn, DefaultProColumnWithKeyOrDataIndex, DefaultRender, type FetchDataAction, type FetchDataConfig, FormConfig, _default as GProTable, OperateRowProps, PageItemFn, PageItemRender, PageState, ProColumn, ProColumnType, ProColumnsValueType, ProCoreActionType, ProCoreFormType, ProFieldValueFormat, ProFieldValueType, ProSchemaValueEnumType, ProSearchMap, ProTableBodyCellProps, ProTableClassNamesType, ProTableCustomProps, ProTableEmits, ProTableFetchParams, ProTableIndexColumn, ProTablePagination, ProTablePaginationConfig, ProTableProps, ProTableRef, ProTableScrollPolicy, ProTableSizeType, ProTableSlots, ProTableStylesType, RequestConfig, RequestData, RequestFunction, RowSelectionActions, TableSemanticName, TableSorterRecord, ToolbarOptionConfig, ToolbarPlacement, type UseProBreakpointsReturn, type UseRequestOptionsParams, type UseRequestOptionsReturn, type UseTableFormOptions, type UseTableFormReturn, type UseTableOptions, type UseTableReturn, useFetchData, useInjectTableContext, usePagination, useProBreakpoints, useRequestOptions, useTable, useTableForm };
10
+ import { arrayMove } from "./utils/arrayMove.js";
11
+ export { BeforeSearchSubmit, ColConfig, ColumnWithKeyOrDataIndex, CustomDataFn, CustomDataRender, CustomRenderResult, DefaultProColumn, DefaultProColumnWithKeyOrDataIndex, DefaultRender, type FetchDataAction, type FetchDataConfig, FormConfig, _default as GProTable, OperateRowProps, PageItemFn, PageItemRender, PageState, ProColumn, ProColumnEllipsis, ProColumnType, ProColumnsValueType, ProCoreActionType, ProCoreFormType, ProFieldValueFormat, ProFieldValueType, ProSchemaValueEnumType, ProSearchMap, ProTableBodyCellProps, ProTableClassNamesType, ProTableCustomProps, ProTableEmits, ProTableFetchParams, ProTableIndexColumn, ProTablePagination, ProTablePaginationConfig, ProTableProps, ProTableRef, ProTableRowDragOptions, ProTableRowSortEndEvent, ProTableScrollPolicy, ProTableSizeType, ProTableSlots, ProTableStylesType, RequestConfig, RequestData, RequestFunction, RowSelectionActions, TableSemanticName, TableSorterRecord, ToolbarOptionConfig, ToolbarPlacement, type UseProBreakpointsReturn, type UseRequestOptionsParams, type UseRequestOptionsReturn, type UseTableFormOptions, type UseTableFormReturn, type UseTableOptions, type UseTableReturn, arrayMove, useFetchData, useInjectTableContext, usePagination, useProBreakpoints, useRequestOptions, useTable, useTableForm };
package/dist/index.js CHANGED
@@ -3,8 +3,9 @@ import { useRequestOptions } from "./hooks/useRequestOptions.js";
3
3
  import { useInjectTableContext } from "./context/TableContext.js";
4
4
  import { useFetchData } from "./hooks/useFetchData.js";
5
5
  import { usePagination } from "./hooks/usePagination.js";
6
+ import { arrayMove } from "./utils/arrayMove.js";
6
7
  import { useTable } from "./hooks/useTable.js";
7
8
  import { useTableForm } from "./hooks/useTableForm.js";
8
9
  import "./hooks/index.js";
9
10
  import ForwardProTable from "./ProTable.js";
10
- export { ForwardProTable as GProTable, useFetchData, useInjectTableContext, usePagination, useProBreakpoints, useRequestOptions, useTable, useTableForm };
11
+ export { ForwardProTable as GProTable, arrayMove, useFetchData, useInjectTableContext, usePagination, useProBreakpoints, useRequestOptions, useTable, useTableForm };