@kine-design/core 0.0.1-beta.5 → 0.0.1-beta.7

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 (95) hide show
  1. package/components/base/affix/useAffix.ts +2 -1
  2. package/components/base/anchor/useAnchor.ts +2 -1
  3. package/components/base/autoComplete/useAutoComplete.ts +2 -1
  4. package/components/base/carousel/useCarousel.ts +2 -1
  5. package/components/base/cascader/useCascader.ts +2 -1
  6. package/components/base/checkbox/useCheckbox.ts +2 -1
  7. package/components/base/collapse/useCollapse.ts +2 -1
  8. package/components/base/datePicker/__tests__/useDatePicker.test.ts +239 -0
  9. package/components/base/dropdown/useDropdown.ts +2 -1
  10. package/components/base/image/__tests__/useImage.test.ts +174 -0
  11. package/components/base/input/useInput.ts +3 -1
  12. package/components/base/inputNumber/__tests__/useInputNumber.test.ts +153 -0
  13. package/components/base/inputNumber/useInputNumber.ts +53 -11
  14. package/components/base/popover/usePopover.ts +4 -3
  15. package/components/base/rate/useRate.ts +2 -1
  16. package/components/base/select/useSelect.ts +2 -1
  17. package/components/base/slider/useSlider.ts +2 -1
  18. package/components/base/steps/__tests__/useSteps.test.ts +46 -0
  19. package/components/base/switch/useSwitch.tsx +2 -1
  20. package/components/base/tabs/useTabs.ts +2 -1
  21. package/components/base/timePicker/__tests__/useTimePicker.test.ts +118 -0
  22. package/components/base/transfer/useTransfer.ts +2 -1
  23. package/components/base/tree/__tests__/tree.test.ts +214 -0
  24. package/components/message/notification/__tests__/useNotification.test.ts +129 -0
  25. package/components/message/popover/usePopover.ts +4 -4
  26. package/components/other/darkMode/useDarkMode.ts +2 -2
  27. package/components/template/menu/__tests__/useMenu.test.ts +157 -0
  28. package/components/template/pagination/__tests__/usePagination.test.ts +138 -0
  29. package/components/template/table/__tests__/useTable.test.ts +139 -0
  30. package/components/template/table/useTable.ts +2 -4
  31. package/components/types/hook.d.ts +11 -0
  32. package/compositions/common/__tests__/useDebounceFn.test.ts +62 -0
  33. package/compositions/common/__tests__/useEventListener.test.ts +71 -0
  34. package/compositions/common/__tests__/usePopover.test.ts +38 -0
  35. package/compositions/common/__tests__/useTeleport.test.ts +25 -0
  36. package/compositions/common/testAnchor.ts +211 -0
  37. package/compositions/common/useEventListener.ts +3 -3
  38. package/compositions/common/useResizeObserver.ts +6 -2
  39. package/compositions/input/__tests__/useBooleanInput.test.ts +73 -0
  40. package/compositions/modal/__tests__/useModal.test.ts +92 -0
  41. package/compositions/modal/useModal.ts +2 -2
  42. package/compositions/popper/useClickAway.ts +4 -4
  43. package/compositions/utils/__tests__/filters.test.ts +136 -0
  44. package/compositions/virtualList/__tests__/useHeightCache.test.ts +97 -0
  45. package/dist/components/base/affix/useAffix.d.ts +2 -1
  46. package/dist/components/base/anchor/useAnchor.d.ts +2 -1
  47. package/dist/components/base/autoComplete/useAutoComplete.d.ts +2 -1
  48. package/dist/components/base/carousel/useCarousel.d.ts +2 -1
  49. package/dist/components/base/cascader/useCascader.d.ts +2 -1
  50. package/dist/components/base/checkbox/useCheckbox.d.ts +2 -1
  51. package/dist/components/base/collapse/useCollapse.d.ts +2 -1
  52. package/dist/components/base/datePicker/__tests__/useDatePicker.test.d.ts +1 -0
  53. package/dist/components/base/dropdown/useDropdown.d.ts +2 -1
  54. package/dist/components/base/image/__tests__/useImage.test.d.ts +1 -0
  55. package/dist/components/base/input/useInput.d.ts +2 -1
  56. package/dist/components/base/inputNumber/__tests__/useInputNumber.test.d.ts +1 -0
  57. package/dist/components/base/inputNumber/useInputNumber.d.ts +2 -1
  58. package/dist/components/base/popover/usePopover.d.ts +2 -1
  59. package/dist/components/base/rate/useRate.d.ts +2 -1
  60. package/dist/components/base/select/useSelect.d.ts +2 -1
  61. package/dist/components/base/slider/useSlider.d.ts +2 -1
  62. package/dist/components/base/steps/__tests__/useSteps.test.d.ts +1 -0
  63. package/dist/components/base/switch/useSwitch.d.ts +2 -1
  64. package/dist/components/base/tabs/useTabs.d.ts +2 -1
  65. package/dist/components/base/timePicker/__tests__/useTimePicker.test.d.ts +1 -0
  66. package/dist/components/base/transfer/useTransfer.d.ts +2 -1
  67. package/dist/components/base/tree/__tests__/tree.test.d.ts +1 -0
  68. package/dist/components/message/notification/__tests__/useNotification.test.d.ts +1 -0
  69. package/dist/components/message/popover/usePopover.d.ts +1 -1
  70. package/dist/components/other/darkMode/useDarkMode.d.ts +2 -3
  71. package/dist/components/template/menu/__tests__/useMenu.test.d.ts +1 -0
  72. package/dist/components/template/pagination/__tests__/usePagination.test.d.ts +1 -0
  73. package/dist/components/template/table/__tests__/useTable.test.d.ts +1 -0
  74. package/dist/compositions/common/__tests__/useDebounceFn.test.d.ts +1 -0
  75. package/dist/compositions/common/__tests__/useEventListener.test.d.ts +1 -0
  76. package/dist/compositions/common/__tests__/usePopover.test.d.ts +1 -0
  77. package/dist/compositions/common/__tests__/useTeleport.test.d.ts +1 -0
  78. package/dist/compositions/common/testAnchor.d.ts +40 -0
  79. package/dist/compositions/common/useEventListener.d.ts +2 -2
  80. package/dist/compositions/input/__tests__/useBooleanInput.test.d.ts +1 -0
  81. package/dist/compositions/modal/__tests__/useModal.test.d.ts +1 -0
  82. package/dist/compositions/modal/useModal.d.ts +2 -1
  83. package/dist/compositions/popper/useClickAway.d.ts +3 -3
  84. package/dist/compositions/utils/__tests__/filters.test.d.ts +1 -0
  85. package/dist/compositions/virtualList/__tests__/useHeightCache.test.d.ts +1 -0
  86. package/dist/core.js +64 -18
  87. package/dist/tools/__tests__/empty.test.d.ts +1 -0
  88. package/dist/tools/empty.d.ts +2 -2
  89. package/dist/tools/types.d.ts +1 -1
  90. package/dist/vitest.config.d.ts +10 -0
  91. package/package.json +6 -2
  92. package/tools/__tests__/empty.test.ts +72 -0
  93. package/tools/empty.ts +2 -2
  94. package/tools/types.ts +1 -1
  95. package/vitest.config.ts +17 -0
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @description useNotificationQueue 测试
3
+ * @author 阿怪
4
+ * @date 2026/3/23
5
+ * @version v1.0.0
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
10
+ import { useNotificationQueue } from '../useNotification';
11
+
12
+ beforeEach(() => { vi.useFakeTimers(); });
13
+ afterEach(() => { vi.useRealTimers(); });
14
+
15
+ describe('useNotificationQueue', () => {
16
+ it('初始通知列表为空', () => {
17
+ const { notifications } = useNotificationQueue();
18
+ expect(notifications.value).toHaveLength(0);
19
+ });
20
+
21
+ it('add 添加通知并返回 id', () => {
22
+ const { notifications, add } = useNotificationQueue();
23
+
24
+ const id = add({ title: '测试通知' });
25
+ expect(typeof id).toBe('number');
26
+ expect(notifications.value).toHaveLength(1);
27
+ expect(notifications.value[0].title).toBe('测试通知');
28
+ });
29
+
30
+ it('add 使用默认值', () => {
31
+ const { notifications, add } = useNotificationQueue();
32
+
33
+ add({ title: 'test' });
34
+ const item = notifications.value[0];
35
+
36
+ expect(item.message).toBe('');
37
+ expect(item.type).toBe('info');
38
+ expect(item.duration).toBe(4500);
39
+ expect(item.position).toBe('top-right');
40
+ expect(item.closable).toBe(true);
41
+ });
42
+
43
+ it('add 自定义所有字段', () => {
44
+ const { notifications, add } = useNotificationQueue();
45
+
46
+ add({
47
+ title: '错误',
48
+ message: '操作失败',
49
+ type: 'error',
50
+ duration: 3000,
51
+ position: 'bottom-left',
52
+ closable: false,
53
+ });
54
+
55
+ const item = notifications.value[0];
56
+ expect(item.type).toBe('error');
57
+ expect(item.message).toBe('操作失败');
58
+ expect(item.duration).toBe(3000);
59
+ expect(item.position).toBe('bottom-left');
60
+ expect(item.closable).toBe(false);
61
+ });
62
+
63
+ it('remove 按 id 移除通知', () => {
64
+ const { notifications, add, remove } = useNotificationQueue();
65
+
66
+ const id1 = add({ title: '通知1' });
67
+ const id2 = add({ title: '通知2' });
68
+
69
+ remove(id1);
70
+ expect(notifications.value).toHaveLength(1);
71
+ expect(notifications.value[0].id).toBe(id2);
72
+ });
73
+
74
+ it('remove 不存在的 id 不报错', () => {
75
+ const { notifications, add, remove } = useNotificationQueue();
76
+
77
+ add({ title: '通知' });
78
+ remove(999);
79
+ expect(notifications.value).toHaveLength(1);
80
+ });
81
+
82
+ it('duration > 0 时自动移除', () => {
83
+ const { notifications, add } = useNotificationQueue();
84
+
85
+ add({ title: '自动关闭', duration: 1000 });
86
+ expect(notifications.value).toHaveLength(1);
87
+
88
+ vi.advanceTimersByTime(1000);
89
+ expect(notifications.value).toHaveLength(0);
90
+ });
91
+
92
+ it('duration = 0 时不自动移除', () => {
93
+ const { notifications, add } = useNotificationQueue();
94
+
95
+ add({ title: '手动关闭', duration: 0 });
96
+
97
+ vi.advanceTimersByTime(10000);
98
+ expect(notifications.value).toHaveLength(1);
99
+ });
100
+
101
+ it('多条通知独立管理', () => {
102
+ const { notifications, add } = useNotificationQueue();
103
+
104
+ add({ title: '快速消失', duration: 1000 });
105
+ add({ title: '慢速消失', duration: 5000 });
106
+ add({ title: '不消失', duration: 0 });
107
+
108
+ expect(notifications.value).toHaveLength(3);
109
+
110
+ vi.advanceTimersByTime(1000);
111
+ expect(notifications.value).toHaveLength(2);
112
+
113
+ vi.advanceTimersByTime(4000);
114
+ expect(notifications.value).toHaveLength(1);
115
+ expect(notifications.value[0].title).toBe('不消失');
116
+ });
117
+
118
+ it('id 自增不重复', () => {
119
+ const { add } = useNotificationQueue();
120
+
121
+ const id1 = add({ title: '1' });
122
+ const id2 = add({ title: '2' });
123
+ const id3 = add({ title: '3' });
124
+
125
+ expect(id1).not.toBe(id2);
126
+ expect(id2).not.toBe(id3);
127
+ expect(id3 - id1).toBe(2);
128
+ });
129
+ });
@@ -162,7 +162,7 @@ export function usePopover(
162
162
  }
163
163
  };
164
164
 
165
- const onBeforeDestroyEvents: Function[] = [];
165
+ const onBeforeUnmountEvents: Function[] = [];
166
166
 
167
167
  onMounted(() => {
168
168
  if (!popoverRef.value || !contentRef.value) {
@@ -188,8 +188,8 @@ export function usePopover(
188
188
 
189
189
  onBeforeUnmount(() => {
190
190
  if (clickAwayInstance) {
191
- const { onBeforeDestroy } = clickAwayInstance;
192
- onBeforeDestroy();
191
+ const { onBeforeUnmount } = clickAwayInstance;
192
+ onBeforeUnmount();
193
193
  }
194
194
  instance?.destroy();
195
195
  });
@@ -228,7 +228,7 @@ export function usePopover(
228
228
  popperInstance,
229
229
  style, arrowStyle,
230
230
  lifecycle: {
231
- onBeforeDestroyEvents,
231
+ onBeforeUnmountEvents,
232
232
  },
233
233
  };
234
234
  }
@@ -12,6 +12,7 @@
12
12
  * - autoMode 时监听系统媒体查询变化,并在卸载时清理监听器
13
13
  */
14
14
  import { ref, watch, onMounted, onUnmounted } from 'vue';
15
+ import { type HookContext } from '../../types/hook';
15
16
  import { DarkModeProps } from './props';
16
17
 
17
18
  /** 将暗色状态同步到 html[dark] attribute */
@@ -42,8 +43,7 @@ function writeStorage(storageKey: string, isDark: boolean) {
42
43
 
43
44
  export function useDarkMode(
44
45
  props: DarkModeProps,
45
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
- ctx: { emit: (event: string, ...args: any[]) => void },
46
+ ctx: Pick<HookContext, 'emit'>,
47
47
  ) {
48
48
  const isDark = ref(props.modelValue ?? false);
49
49
 
@@ -0,0 +1,157 @@
1
+ /**
2
+ * @description useMenu 测试
3
+ * @author 阿怪
4
+ * @date 2026/3/23
5
+ * @version v1.0.0
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ import { describe, expect, it, vi } from 'vitest';
10
+ import { useMenu } from '../useMenu';
11
+
12
+ const SAMPLE_DATA = [
13
+ {
14
+ key: 'home', label: '首页',
15
+ },
16
+ {
17
+ key: 'system', label: '系统管理', children: [
18
+ { key: 'user', label: '用户管理' },
19
+ { key: 'role', label: '角色管理' },
20
+ ],
21
+ },
22
+ {
23
+ key: 'about', label: '关于',
24
+ },
25
+ ];
26
+
27
+ describe('useMenu', () => {
28
+ it('initNodes 构建节点树', () => {
29
+ const { nodesRef, nodeMap } = useMenu({ data: SAMPLE_DATA });
30
+
31
+ expect(nodesRef.value).toHaveLength(3);
32
+ expect(nodeMap.size).toBe(5); // 3 + 2 children
33
+ });
34
+
35
+ it('根节点 isRoot=true,子节点有 parent', () => {
36
+ const { nodeMap } = useMenu({ data: SAMPLE_DATA });
37
+
38
+ expect(nodeMap.get('home')!.isRoot).toBe(true);
39
+ expect(nodeMap.get('user')!.isRoot).toBe(false);
40
+ expect(nodeMap.get('user')!.parent).toBe(nodeMap.get('system'));
41
+ });
42
+
43
+ it('defaultExpandAll 展开所有节点', () => {
44
+ const { nodeMap } = useMenu({ data: SAMPLE_DATA, defaultExpandAll: true });
45
+ expect(nodeMap.get('system')!.expand).toBe(true);
46
+ });
47
+
48
+ it('默认不展开', () => {
49
+ const { nodeMap } = useMenu({ data: SAMPLE_DATA });
50
+ expect(nodeMap.get('system')!.expand).toBe(false);
51
+ });
52
+
53
+ it('toggleExpand 切换展开', () => {
54
+ const { nodeMap, toggleExpand } = useMenu({ data: SAMPLE_DATA });
55
+ const node = nodeMap.get('system')!;
56
+
57
+ expect(node.expand).toBe(false);
58
+ toggleExpand(node);
59
+ expect(node.expand).toBe(true);
60
+ toggleExpand(node);
61
+ expect(node.expand).toBe(false);
62
+ });
63
+
64
+ it('setActive 激活节点并取消其他激活', () => {
65
+ const emit = vi.fn();
66
+ const { nodeMap, setActive } = useMenu({ data: SAMPLE_DATA }, emit);
67
+
68
+ setActive(nodeMap.get('home')!);
69
+ expect(nodeMap.get('home')!.isActive).toBe(true);
70
+ expect(emit).toHaveBeenCalledWith('update:active', 'home');
71
+
72
+ setActive(nodeMap.get('about')!);
73
+ expect(nodeMap.get('home')!.isActive).toBe(false);
74
+ expect(nodeMap.get('about')!.isActive).toBe(true);
75
+ });
76
+
77
+ it('受控 active 初始化', () => {
78
+ const { nodeMap } = useMenu({ data: SAMPLE_DATA, active: 'about' });
79
+ expect(nodeMap.get('about')!.isActive).toBe(true);
80
+ expect(nodeMap.get('home')!.isActive).toBe(false);
81
+ });
82
+
83
+ it('toggleChecked 切换选中', () => {
84
+ const { nodeMap, toggleChecked } = useMenu({ data: SAMPLE_DATA });
85
+ const node = nodeMap.get('user')!;
86
+
87
+ toggleChecked(node, true);
88
+ expect(node.checked).toBe(true);
89
+ expect(node.indeterminate).toBe(false);
90
+
91
+ toggleChecked(node, false);
92
+ expect(node.checked).toBe(false);
93
+ });
94
+
95
+ it('getCheckedKeys 获取选中 key', () => {
96
+ const { nodeMap, toggleChecked, getCheckedKeys } = useMenu({ data: SAMPLE_DATA });
97
+
98
+ toggleChecked(nodeMap.get('user')!, true);
99
+ toggleChecked(nodeMap.get('about')!, true);
100
+
101
+ const keys = getCheckedKeys();
102
+ expect(keys).toContain('user');
103
+ expect(keys).toContain('about');
104
+ expect(keys).toHaveLength(2);
105
+ });
106
+
107
+ it('半选节点不计入 getCheckedKeys', () => {
108
+ const { nodeMap, getCheckedKeys } = useMenu({ data: SAMPLE_DATA });
109
+ const node = nodeMap.get('system')!;
110
+ node.indeterminate = true;
111
+ node.checked = false;
112
+
113
+ expect(getCheckedKeys()).not.toContain('system');
114
+ });
115
+
116
+ it('getNodesByKeys 批量获取节点', () => {
117
+ const { getNodesByKeys } = useMenu({ data: SAMPLE_DATA });
118
+ const nodes = getNodesByKeys(['home', 'user']);
119
+ expect(nodes).toHaveLength(2);
120
+ });
121
+
122
+ it('getNodesByKeys 过滤不存在的 key', () => {
123
+ const { getNodesByKeys } = useMenu({ data: SAMPLE_DATA });
124
+ const nodes = getNodesByKeys(['home', 'nonexistent']);
125
+ expect(nodes).toHaveLength(1);
126
+ });
127
+
128
+ it('checkedKeys 初始化选中', () => {
129
+ const { nodeMap } = useMenu({ data: SAMPLE_DATA, checkedKeys: ['user', 'about'] });
130
+ expect(nodeMap.get('user')!.checked).toBe(true);
131
+ expect(nodeMap.get('about')!.checked).toBe(true);
132
+ expect(nodeMap.get('home')!.checked).toBe(false);
133
+ });
134
+
135
+ it('initNodes 重新初始化', () => {
136
+ const { initNodes, nodesRef, nodeMap } = useMenu({ data: SAMPLE_DATA });
137
+
138
+ initNodes([{ key: 'new', label: '新节点' }]);
139
+ expect(nodesRef.value).toHaveLength(1);
140
+ expect(nodeMap.size).toBe(1);
141
+ expect(nodeMap.get('new')!.label).toBe('新节点');
142
+ });
143
+
144
+ it('自定义 config 字段映射', () => {
145
+ const data = [
146
+ { id: 'a', name: '菜单A', items: [{ id: 'b', name: '子菜单B' }] },
147
+ ];
148
+ const { nodeMap } = useMenu({
149
+ data: data as any,
150
+ config: { key: 'id', label: 'name', children: 'items' },
151
+ });
152
+
153
+ expect(nodeMap.size).toBe(2);
154
+ expect(nodeMap.get('a')).toBeDefined();
155
+ expect(nodeMap.get('b')).toBeDefined();
156
+ });
157
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @description usePagination 测试
3
+ * @author 阿怪
4
+ * @date 2026/3/23
5
+ * @version v1.0.0
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ import { describe, expect, it } from 'vitest';
10
+ import { ref } from 'vue';
11
+ import { usePagination, type Pager } from '../usePagination';
12
+
13
+ /** 提取页码列表中的值 */
14
+ const values = (list: Pager[]) => list.map(p => p.value);
15
+ /** 提取页码列表中当前页标记 */
16
+ const currentFlags = (list: Pager[]) => list.filter(p => p.isCurrent).map(p => p.value);
17
+
18
+ describe('usePagination', () => {
19
+ describe('getPageBtnLength', () => {
20
+ it('计算总页数', () => {
21
+ const current = ref(1);
22
+ const { getPageBtnLength } = usePagination({ total: 100, pageSize: 10 }, current);
23
+ expect(getPageBtnLength()).toBe(10);
24
+ });
25
+
26
+ it('向上取整', () => {
27
+ const current = ref(1);
28
+ const { getPageBtnLength } = usePagination({ total: 101, pageSize: 10 }, current);
29
+ expect(getPageBtnLength()).toBe(11);
30
+ });
31
+
32
+ it('默认 total=0, pageSize=10', () => {
33
+ const current = ref(1);
34
+ const { getPageBtnLength } = usePagination({}, current);
35
+ expect(getPageBtnLength()).toBe(0);
36
+ });
37
+ });
38
+
39
+ describe('getPageNumList', () => {
40
+ it('只有一页时返回 [1]', () => {
41
+ const current = ref(1);
42
+ const { getPageNumList } = usePagination({ total: 5, pageSize: 10 }, current);
43
+ const list = getPageNumList();
44
+ expect(values(list)).toEqual([1]);
45
+ });
46
+
47
+ it('总页数 <= maxPageBtn 时不折叠', () => {
48
+ const current = ref(1);
49
+ const { getPageNumList } = usePagination({
50
+ total: 80, pageSize: 10, maxPageBtn: 10,
51
+ }, current);
52
+ const list = getPageNumList();
53
+ expect(values(list)).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
54
+ });
55
+
56
+ it('当前页靠左时右侧折叠', () => {
57
+ const current = ref(1);
58
+ const { getPageNumList } = usePagination({
59
+ total: 200, pageSize: 10, foldedMaxPageBtn: 5, maxPageBtn: 10,
60
+ }, current);
61
+ const list = getPageNumList();
62
+
63
+ // 应包含折叠符 "..."
64
+ expect(list.some(p => p.type === 'folded')).toBe(true);
65
+ // 最后一个应是尾页 20
66
+ expect(list[list.length - 1].value).toBe(20);
67
+ // isCurrent 标记正确
68
+ expect(currentFlags(list)).toEqual([1]);
69
+ });
70
+
71
+ it('当前页靠右时左侧折叠', () => {
72
+ const current = ref(20);
73
+ const { getPageNumList } = usePagination({
74
+ total: 200, pageSize: 10, foldedMaxPageBtn: 5, maxPageBtn: 10,
75
+ }, current);
76
+ const list = getPageNumList();
77
+
78
+ expect(list[0].value).toBe(1); // 首页
79
+ expect(list[1].type).toBe('folded'); // 左折叠
80
+ expect(currentFlags(list)).toEqual([20]);
81
+ });
82
+
83
+ it('当前页居中时两侧折叠', () => {
84
+ const current = ref(10);
85
+ const { getPageNumList } = usePagination({
86
+ total: 200, pageSize: 10, foldedMaxPageBtn: 5, maxPageBtn: 10,
87
+ }, current);
88
+ const list = getPageNumList();
89
+
90
+ expect(list[0].value).toBe(1); // 首页
91
+ expect(list[list.length - 1].value).toBe(20); // 尾页
92
+ // 两个折叠符
93
+ const foldedCount = list.filter(p => p.type === 'folded').length;
94
+ expect(foldedCount).toBe(2);
95
+ expect(currentFlags(list)).toEqual([10]);
96
+ });
97
+
98
+ it('showEdgePageNum=false 不显示首尾页', () => {
99
+ const current = ref(10);
100
+ const { getPageNumList } = usePagination({
101
+ total: 200, pageSize: 10, foldedMaxPageBtn: 5, maxPageBtn: 10, showEdgePageNum: false,
102
+ }, current);
103
+ const list = getPageNumList();
104
+
105
+ // 不应以 1 开头
106
+ expect(list[0].value).not.toBe(1);
107
+ expect(list[list.length - 1].value).not.toBe(20);
108
+ });
109
+
110
+ it('折叠符的 jump 值正确', () => {
111
+ const current = ref(10);
112
+ const { getPageNumList } = usePagination({
113
+ total: 200, pageSize: 10, foldedMaxPageBtn: 5, maxPageBtn: 10,
114
+ }, current);
115
+ const list = getPageNumList();
116
+
117
+ const foldedPagers = list.filter(p => p.type === 'folded');
118
+ // 每个折叠符都应有 jump 值
119
+ foldedPagers.forEach(p => {
120
+ expect(p.jump).toBeGreaterThan(0);
121
+ });
122
+ });
123
+
124
+ it('isCurrent 标记正确跟随 currentValue', () => {
125
+ const current = ref(5);
126
+ const { getPageNumList } = usePagination({
127
+ total: 80, pageSize: 10,
128
+ }, current);
129
+ const list = getPageNumList();
130
+
131
+ expect(currentFlags(list)).toEqual([5]);
132
+
133
+ current.value = 3;
134
+ const list2 = getPageNumList();
135
+ expect(currentFlags(list2)).toEqual([3]);
136
+ });
137
+ });
138
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @description useTable 测试
3
+ * @author 阿怪
4
+ * @date 2026/3/23
5
+ * @version v1.0.0
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ import { describe, expect, it, vi } from 'vitest';
10
+ import { useTable } from '../useTable';
11
+
12
+ describe('useTable', () => {
13
+ const createRenders = () => ({
14
+ empty: 'EMPTY',
15
+ tbodyTr: (opt: any) => `TD:${opt.param}:${opt.data}`,
16
+ theadTh: (opt: any) => `TH:${opt.param}:${opt.label}`,
17
+ thead: (ths: string[]) => `THEAD:[${ths.join(',')}]`,
18
+ tbody: (trs: string[]) => `TBODY:[${trs.join(',')}]`,
19
+ tbodyTrs: (tds: string[], i: number) => `TR${i}:[${tds.join(',')}]`,
20
+ initSlot: () => undefined,
21
+ });
22
+
23
+ it('initTable 正确构建表头和表体', () => {
24
+ const { initTable } = useTable();
25
+ const columns = [
26
+ { props: { param: 'name', label: '姓名' } },
27
+ { props: { param: 'age', label: '年龄' } },
28
+ ];
29
+ const data = [
30
+ { name: '张三', age: 25 },
31
+ { name: '李四', age: 30 },
32
+ ];
33
+
34
+ const result = initTable(createRenders(), columns, data);
35
+
36
+ expect(result.thead).toBe('THEAD:[TH:name:姓名,TH:age:年龄]');
37
+ expect(result.tbody).toContain('TD:name:张三');
38
+ expect(result.tbody).toContain('TD:age:30');
39
+ });
40
+
41
+ it('数据为空时渲染 empty', () => {
42
+ const { initTable } = useTable();
43
+ const columns = [{ props: { param: 'name', label: '姓名' } }];
44
+
45
+ const result = initTable(createRenders(), columns, []);
46
+ expect(result.tbody).toBe('EMPTY');
47
+ });
48
+
49
+ it('列没有 props 时被过滤', () => {
50
+ const { initTable } = useTable();
51
+ const errorSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
52
+ const columns = [
53
+ { props: { param: 'name', label: '姓名' } },
54
+ {}, // 无 props
55
+ ];
56
+ const data = [{ name: '张三' }];
57
+
58
+ const result = initTable(createRenders(), columns, data);
59
+ expect(result.thead).toBe('THEAD:[TH:name:姓名]');
60
+ expect(errorSpy).toHaveBeenCalled();
61
+
62
+ errorSpy.mockRestore();
63
+ });
64
+
65
+ it('列宽 number 转换为 px', () => {
66
+ const { initTable } = useTable();
67
+ const renders = {
68
+ ...createRenders(),
69
+ theadTh: (opt: any) => `TH:${opt.style?.width}`,
70
+ };
71
+ const columns = [{ props: { param: 'name', label: '姓名', width: 200 } }];
72
+
73
+ const result = initTable(renders, columns, [{ name: 'test' }]);
74
+ expect(result.thead).toContain('200px');
75
+ });
76
+
77
+ it('列宽 string 直接使用', () => {
78
+ const { initTable } = useTable();
79
+ const renders = {
80
+ ...createRenders(),
81
+ theadTh: (opt: any) => `TH:${opt.style?.width}`,
82
+ };
83
+ const columns = [{ props: { param: 'name', label: '姓名', width: '50%' } }];
84
+
85
+ const result = initTable(renders, columns, [{ name: 'test' }]);
86
+ expect(result.thead).toContain('50%');
87
+ });
88
+
89
+ it('data 中缺少字段时返回空字符串', () => {
90
+ const { initTable } = useTable();
91
+ const columns = [{ props: { param: 'missing', label: '缺失' } }];
92
+ const data = [{ name: 'test' }];
93
+
94
+ const result = initTable(createRenders(), columns, data);
95
+ expect(result.tbody).toContain('TD:missing:');
96
+ });
97
+
98
+ it('initSlot 返回 slot 时传递给 tbodyTr', () => {
99
+ const { initTable } = useTable();
100
+ const bodySlot = () => 'custom';
101
+ const renders = {
102
+ ...createRenders(),
103
+ initSlot: () => ({ body: bodySlot, head: undefined }),
104
+ tbodyTr: (opt: any) => `TD:${opt.slot ? 'has-slot' : 'no-slot'}`,
105
+ };
106
+ const columns = [{ props: { param: 'name', label: '姓名' } }];
107
+ const data = [{ name: 'test' }];
108
+
109
+ const result = initTable(renders, columns, data);
110
+ expect(result.tbody).toContain('has-slot');
111
+ });
112
+
113
+ it('slotInfo 包含行数据和索引', () => {
114
+ const { initTable } = useTable();
115
+ let capturedSlotInfo: any;
116
+ const renders = {
117
+ ...createRenders(),
118
+ tbodyTr: (opt: any) => {
119
+ capturedSlotInfo = opt.slotInfo;
120
+ return 'TD';
121
+ },
122
+ };
123
+ const columns = [{ props: { param: 'name', label: '姓名' } }];
124
+ const data = [{ name: '张三' }];
125
+
126
+ initTable(renders, columns, data);
127
+ expect(capturedSlotInfo).toEqual({ data: { name: '张三' }, index: 0 });
128
+ });
129
+
130
+ it('error 输出带前缀的警告', () => {
131
+ const { error } = useTable();
132
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
133
+
134
+ error('test error');
135
+ expect(spy).toHaveBeenCalledWith('[水墨UI表格组件] test error');
136
+
137
+ spy.mockRestore();
138
+ });
139
+ });
@@ -49,10 +49,8 @@ export function useTable() {
49
49
 
50
50
  /** 从 data[i] 中安全取值 */
51
51
  const getData = (i: number, param: string) => {
52
- if (data[i] && data[i][param]) {
53
- return data[i][param];
54
- }
55
- return '';
52
+ if (data[i] == null) return '';
53
+ return data[i][param] ?? '';
56
54
  };
57
55
 
58
56
  /** 向每行的 td 列表中追加一列 */
@@ -10,4 +10,15 @@ import { SlotsType } from '@vue/runtime-core';
10
10
 
11
11
  export type UseHookResult<Props, S extends SlotsType, Return> = Return;
12
12
 
13
+ /**
14
+ * Core hook 所需的最小 context 类型。
15
+ * 使用 `(...args: any[]) => void` 而非 `(event: string, ...) => void`,
16
+ * 以兼容 Vue defineComponent typed emits 产生的窄化 emit 签名。
17
+ */
18
+ export type HookContext = {
19
+ emit: (...args: any[]) => void;
20
+ slots: Readonly<Record<string, ((...args: any[]) => any) | undefined>>;
21
+ expose?: (exposed?: Record<string, any>) => void;
22
+ };
23
+
13
24