@kine-design/core 0.0.1-beta.4 → 0.0.1-beta.6

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 (123) hide show
  1. package/.vlaude/last-session-id +1 -0
  2. package/components/base/affix/useAffix.ts +2 -1
  3. package/components/base/anchor/useAnchor.ts +2 -1
  4. package/components/base/autoComplete/useAutoComplete.ts +2 -1
  5. package/components/base/button/api.ts +1 -1
  6. package/components/base/button/props.d.ts +3 -1
  7. package/components/base/carousel/useCarousel.ts +2 -1
  8. package/components/base/cascader/useCascader.ts +2 -1
  9. package/components/base/checkbox/useCheckbox.ts +2 -1
  10. package/components/base/collapse/useCollapse.ts +2 -1
  11. package/components/base/datePicker/__tests__/useDatePicker.test.ts +239 -0
  12. package/components/base/datePicker/api.ts +4 -0
  13. package/components/base/datePicker/props.d.ts +26 -0
  14. package/components/base/dropdown/useDropdown.ts +2 -1
  15. package/components/base/image/__tests__/useImage.test.ts +174 -0
  16. package/components/base/input/api.ts +5 -1
  17. package/components/base/input/props.d.ts +26 -1
  18. package/components/base/input/useInput.ts +12 -2
  19. package/components/base/inputNumber/__tests__/useInputNumber.test.ts +153 -0
  20. package/components/base/inputNumber/api.ts +2 -1
  21. package/components/base/inputNumber/props.d.ts +9 -1
  22. package/components/base/inputNumber/useInputNumber.ts +53 -11
  23. package/components/base/loading/api.ts +1 -1
  24. package/components/base/loading/props.d.ts +3 -1
  25. package/components/base/popover/usePopover.ts +4 -3
  26. package/components/base/progress/api.ts +3 -1
  27. package/components/base/progress/props.d.ts +15 -0
  28. package/components/base/rate/useRate.ts +2 -1
  29. package/components/base/select/api.ts +3 -0
  30. package/components/base/select/props.d.ts +21 -1
  31. package/components/base/select/useSelect.ts +2 -1
  32. package/components/base/slider/api.ts +3 -1
  33. package/components/base/slider/props.d.ts +14 -0
  34. package/components/base/slider/useSlider.ts +6 -2
  35. package/components/base/steps/__tests__/useSteps.test.ts +46 -0
  36. package/components/base/switch/useSwitch.tsx +2 -1
  37. package/components/base/tabs/useTabs.ts +2 -1
  38. package/components/base/tag/api.ts +1 -1
  39. package/components/base/tag/props.d.ts +3 -1
  40. package/components/base/timePicker/__tests__/useTimePicker.test.ts +118 -0
  41. package/components/base/timePicker/api.ts +4 -1
  42. package/components/base/timePicker/props.d.ts +20 -0
  43. package/components/base/tooltip/api.ts +1 -0
  44. package/components/base/tooltip/props.d.ts +6 -0
  45. package/components/base/transfer/useTransfer.ts +2 -1
  46. package/components/base/tree/__tests__/tree.test.ts +214 -0
  47. package/components/message/dialog/api.ts +2 -1
  48. package/components/message/dialog/props.d.ts +7 -0
  49. package/components/message/drawer/api.ts +1 -0
  50. package/components/message/drawer/props.d.ts +7 -0
  51. package/components/message/notification/__tests__/useNotification.test.ts +129 -0
  52. package/components/message/popover/usePopover.ts +4 -4
  53. package/components/other/darkMode/useDarkMode.ts +2 -2
  54. package/components/template/menu/__tests__/useMenu.test.ts +157 -0
  55. package/components/template/pagination/__tests__/usePagination.test.ts +138 -0
  56. package/components/template/table/__tests__/useTable.test.ts +139 -0
  57. package/components/template/table/useTable.ts +2 -4
  58. package/components/types/hook.d.ts +11 -0
  59. package/components/types/props.d.ts +5 -0
  60. package/compositions/common/__tests__/useDebounceFn.test.ts +62 -0
  61. package/compositions/common/__tests__/useEventListener.test.ts +71 -0
  62. package/compositions/common/__tests__/usePopover.test.ts +38 -0
  63. package/compositions/common/__tests__/useTeleport.test.ts +25 -0
  64. package/compositions/common/useComponentSize.ts +17 -0
  65. package/compositions/common/useEventListener.ts +3 -3
  66. package/compositions/common/useResizeObserver.ts +6 -2
  67. package/compositions/input/__tests__/useBooleanInput.test.ts +73 -0
  68. package/compositions/modal/__tests__/useModal.test.ts +92 -0
  69. package/compositions/modal/useModal.ts +2 -2
  70. package/compositions/popper/useClickAway.ts +4 -4
  71. package/compositions/utils/__tests__/filters.test.ts +136 -0
  72. package/compositions/virtualList/__tests__/useHeightCache.test.ts +97 -0
  73. package/dist/components/base/affix/useAffix.d.ts +2 -1
  74. package/dist/components/base/anchor/useAnchor.d.ts +2 -1
  75. package/dist/components/base/autoComplete/useAutoComplete.d.ts +2 -1
  76. package/dist/components/base/carousel/useCarousel.d.ts +2 -1
  77. package/dist/components/base/cascader/useCascader.d.ts +2 -1
  78. package/dist/components/base/checkbox/useCheckbox.d.ts +2 -1
  79. package/dist/components/base/collapse/useCollapse.d.ts +2 -1
  80. package/dist/components/base/datePicker/__tests__/useDatePicker.test.d.ts +1 -0
  81. package/dist/components/base/dropdown/useDropdown.d.ts +2 -1
  82. package/dist/components/base/image/__tests__/useImage.test.d.ts +1 -0
  83. package/dist/components/base/input/useInput.d.ts +4 -1
  84. package/dist/components/base/inputNumber/__tests__/useInputNumber.test.d.ts +1 -0
  85. package/dist/components/base/inputNumber/useInputNumber.d.ts +2 -1
  86. package/dist/components/base/popover/usePopover.d.ts +2 -1
  87. package/dist/components/base/rate/useRate.d.ts +2 -1
  88. package/dist/components/base/select/useSelect.d.ts +2 -1
  89. package/dist/components/base/slider/useSlider.d.ts +2 -1
  90. package/dist/components/base/steps/__tests__/useSteps.test.d.ts +1 -0
  91. package/dist/components/base/switch/useSwitch.d.ts +2 -1
  92. package/dist/components/base/tabs/useTabs.d.ts +2 -1
  93. package/dist/components/base/timePicker/__tests__/useTimePicker.test.d.ts +1 -0
  94. package/dist/components/base/transfer/useTransfer.d.ts +2 -1
  95. package/dist/components/base/tree/__tests__/tree.test.d.ts +1 -0
  96. package/dist/components/message/notification/__tests__/useNotification.test.d.ts +1 -0
  97. package/dist/components/message/popover/usePopover.d.ts +1 -1
  98. package/dist/components/other/darkMode/useDarkMode.d.ts +2 -3
  99. package/dist/components/template/menu/__tests__/useMenu.test.d.ts +1 -0
  100. package/dist/components/template/pagination/__tests__/usePagination.test.d.ts +1 -0
  101. package/dist/components/template/table/__tests__/useTable.test.d.ts +1 -0
  102. package/dist/compositions/common/__tests__/useDebounceFn.test.d.ts +1 -0
  103. package/dist/compositions/common/__tests__/useEventListener.test.d.ts +1 -0
  104. package/dist/compositions/common/__tests__/usePopover.test.d.ts +1 -0
  105. package/dist/compositions/common/__tests__/useTeleport.test.d.ts +1 -0
  106. package/dist/compositions/common/useComponentSize.d.ts +6 -0
  107. package/dist/compositions/common/useEventListener.d.ts +2 -2
  108. package/dist/compositions/input/__tests__/useBooleanInput.test.d.ts +1 -0
  109. package/dist/compositions/modal/__tests__/useModal.test.d.ts +1 -0
  110. package/dist/compositions/modal/useModal.d.ts +2 -1
  111. package/dist/compositions/popper/useClickAway.d.ts +3 -3
  112. package/dist/compositions/utils/__tests__/filters.test.d.ts +1 -0
  113. package/dist/compositions/virtualList/__tests__/useHeightCache.test.d.ts +1 -0
  114. package/dist/core.js +153 -23
  115. package/dist/tools/__tests__/empty.test.d.ts +1 -0
  116. package/dist/tools/empty.d.ts +2 -2
  117. package/dist/tools/types.d.ts +1 -1
  118. package/dist/vitest.config.d.ts +10 -0
  119. package/package.json +6 -2
  120. package/tools/__tests__/empty.test.ts +72 -0
  121. package/tools/empty.ts +2 -2
  122. package/tools/types.ts +1 -1
  123. package/vitest.config.ts +17 -0
@@ -12,6 +12,8 @@
12
12
  * 江湖的业务千篇一律,复杂的代码好几百行。
13
13
  */
14
14
 
15
+ import { KineSize } from '../../types/props';
16
+
15
17
  export declare type TagProps = {
16
18
  /**
17
19
  * @description tag type
@@ -28,7 +30,7 @@ export declare type TagProps = {
28
30
  * @default medium
29
31
  * @enum large|medium|small
30
32
  */
31
- size?: 'large' | 'medium' | 'small';
33
+ size?: KineSize;
32
34
  /**
33
35
  * @description 是否显示关闭按钮
34
36
  * @type boolean
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @description useTimePicker 测试
3
+ * @author 阿怪
4
+ * @date 2026/3/23
5
+ * @version v1.0.0
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ import { describe, expect, it } from 'vitest';
10
+ import { useTimePicker, generateColumn } from '../useTimePicker';
11
+
12
+ describe('generateColumn', () => {
13
+ it('生成 24 小时列', () => {
14
+ expect(generateColumn(24, 1)).toHaveLength(24);
15
+ });
16
+
17
+ it('生成 60 分钟列,步进 5', () => {
18
+ expect(generateColumn(60, 5)).toEqual([0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]);
19
+ });
20
+ });
21
+
22
+ describe('useTimePicker', () => {
23
+ const defaultProps = {
24
+ modelValue: undefined as string | undefined,
25
+ showSeconds: true,
26
+ format: '',
27
+ placeholder: '请选择时间...',
28
+ hourStep: 1,
29
+ minuteStep: 1,
30
+ secondStep: 1,
31
+ };
32
+
33
+ it('初始化空值显示 placeholder', () => {
34
+ const tp = useTimePicker({ ...defaultProps });
35
+ expect(tp.displayValue.value).toBe('请选择时间...');
36
+ expect(tp.spanClass.value).toContain('m-time-picker-placeholder');
37
+ });
38
+
39
+ it('初始化有效时间正确解析', () => {
40
+ const tp = useTimePicker({ ...defaultProps, modelValue: '14:30:00' });
41
+ expect(tp.timeRef.value.hours).toBe(14);
42
+ expect(tp.timeRef.value.minutes).toBe(30);
43
+ expect(tp.timeRef.value.seconds).toBe(0);
44
+ expect(tp.displayValue.value).toBe('14:30:00');
45
+ });
46
+
47
+ it('解析 HH:mm 格式', () => {
48
+ const tp = useTimePicker({ ...defaultProps, modelValue: '08:15' });
49
+ expect(tp.timeRef.value.hours).toBe(8);
50
+ expect(tp.timeRef.value.minutes).toBe(15);
51
+ });
52
+
53
+ it('updateTimeRef 更新状态', () => {
54
+ const tp = useTimePicker({ ...defaultProps });
55
+ tp.updateTimeRef('22:45:30');
56
+
57
+ expect(tp.timeRef.value.hours).toBe(22);
58
+ expect(tp.timeRef.value.minutes).toBe(45);
59
+ expect(tp.timeRef.value.seconds).toBe(30);
60
+ });
61
+
62
+ it('updateTimeRef 空值恢复 placeholder', () => {
63
+ const tp = useTimePicker({ ...defaultProps, modelValue: '10:00:00' });
64
+ tp.updateTimeRef('');
65
+ expect(tp.displayValue.value).toBe('请选择时间...');
66
+ });
67
+
68
+ it('selectHour 更新小时并刷新显示', () => {
69
+ const tp = useTimePicker({ ...defaultProps, modelValue: '10:00:00' });
70
+ tp.selectHour(15);
71
+
72
+ expect(tp.timeRef.value.hours).toBe(15);
73
+ expect(tp.displayValue.value).toContain('15');
74
+ expect(tp.spanClass.value).not.toContain('m-time-picker-placeholder');
75
+ });
76
+
77
+ it('selectMinute 更新分钟', () => {
78
+ const tp = useTimePicker({ ...defaultProps, modelValue: '10:00:00' });
79
+ tp.selectMinute(30);
80
+ expect(tp.timeRef.value.minutes).toBe(30);
81
+ });
82
+
83
+ it('selectSecond 更新秒', () => {
84
+ const tp = useTimePicker({ ...defaultProps, modelValue: '10:00:00' });
85
+ tp.selectSecond(45);
86
+ expect(tp.timeRef.value.seconds).toBe(45);
87
+ });
88
+
89
+ it('getValue 返回格式化时间', () => {
90
+ const tp = useTimePicker({ ...defaultProps, modelValue: '10:30:45' });
91
+ expect(tp.getValue()).toBe('10:30:45');
92
+ });
93
+
94
+ it('showSeconds=false 时格式为 HH:mm', () => {
95
+ const tp = useTimePicker({ ...defaultProps, showSeconds: false, modelValue: '10:30:45' });
96
+ expect(tp.getValue()).toBe('10:30');
97
+ });
98
+
99
+ it('自定义 format', () => {
100
+ const tp = useTimePicker({ ...defaultProps, format: 'HH-mm-ss', modelValue: '10:30:45' });
101
+ expect(tp.displayValue.value).toBe('10-30-45');
102
+ });
103
+
104
+ it('hoursColumn 按 hourStep 生成', () => {
105
+ const tp = useTimePicker({ ...defaultProps, hourStep: 2 });
106
+ expect(tp.hoursColumn.value).toEqual([0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]);
107
+ });
108
+
109
+ it('minutesColumn 按 minuteStep 生成', () => {
110
+ const tp = useTimePicker({ ...defaultProps, minuteStep: 15 });
111
+ expect(tp.minutesColumn.value).toEqual([0, 15, 30, 45]);
112
+ });
113
+
114
+ it('secondsColumn 按 secondStep 生成', () => {
115
+ const tp = useTimePicker({ ...defaultProps, secondStep: 30 });
116
+ expect(tp.secondsColumn.value).toEqual([0, 30]);
117
+ });
118
+ });
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
- import { MCOPO } from '../../types/props';
9
+ import { MCOPO, MPropType } from '../../types/props';
10
10
  import { TimePickerProps } from './props';
11
11
 
12
12
  export const props: MCOPO<TimePickerProps> = {
@@ -18,4 +18,7 @@ export const props: MCOPO<TimePickerProps> = {
18
18
  hourStep: { type: Number, default: 1 },
19
19
  minuteStep: { type: Number, default: 1 },
20
20
  secondStep: { type: Number, default: 1 },
21
+ readonly: { type: Boolean, default: false },
22
+ clearable: { type: Boolean, default: false },
23
+ size: { type: String as unknown as MPropType<NonNullable<TimePickerProps['size']>>, default: undefined },
21
24
  };
@@ -7,6 +7,8 @@
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
9
 
10
+ import { KineSize } from '../../types/props';
11
+
10
12
  export declare type TimePickerProps = {
11
13
  /**
12
14
  * @description 时间值(字符串,如 "14:30:00")
@@ -56,6 +58,24 @@ export declare type TimePickerProps = {
56
58
  * @default 1
57
59
  */
58
60
  secondStep?: number;
61
+ /**
62
+ * @description 是否只读
63
+ * @type boolean
64
+ * @default false
65
+ */
66
+ readonly?: boolean;
67
+ /**
68
+ * @description whether to show clear button. 是否可清空
69
+ * @type boolean
70
+ * @default false
71
+ */
72
+ clearable?: boolean;
73
+ /**
74
+ * @description 尺寸
75
+ * @type KineSize
76
+ * @default undefined
77
+ */
78
+ size?: KineSize;
59
79
  };
60
80
 
61
81
  /** 时间内部引用 */
@@ -16,4 +16,5 @@ export const props: MCOPO<TooltipProps> = {
16
16
  default: 'top',
17
17
  },
18
18
  disabled: { type: Boolean, default: false },
19
+ maxWidth: { type: String, default: undefined },
19
20
  };
@@ -31,4 +31,10 @@ export declare type TooltipProps = {
31
31
  * @default false
32
32
  */
33
33
  disabled?: boolean;
34
+ /**
35
+ * @description max width of tooltip content. 浮层最大宽度
36
+ * @type string
37
+ * @default undefined
38
+ */
39
+ maxWidth?: string;
34
40
  };
@@ -14,8 +14,9 @@
14
14
  */
15
15
  import { computed, ref, toRef } from 'vue';
16
16
  import { TransferItem, TransferProps } from './props';
17
+ import { type HookContext } from '../../types/hook';
17
18
 
18
- export function useTransfer(props: TransferProps, ctx: any) {
19
+ export function useTransfer(props: TransferProps, ctx: HookContext) {
19
20
  const dataRef = toRef(() => props.data ?? []);
20
21
  const modelValueRef = toRef(() => props.modelValue ?? []);
21
22
 
@@ -0,0 +1,214 @@
1
+ /**
2
+ * @description Tree 类核心测试
3
+ * @author 阿怪
4
+ * @date 2026/3/23
5
+ * @version v1.0.0
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ import { describe, expect, it } from 'vitest';
10
+ import Tree, { mergeConfig, DEFAULT_CONFIG } from '../tree';
11
+ import { fixKey } from '../useTree';
12
+
13
+ const SAMPLE_DATA = [
14
+ {
15
+ key: '1', label: '节点 1', children: [
16
+ { key: '1-1', label: '子节点 1-1' },
17
+ { key: '1-2', label: '子节点 1-2', children: [
18
+ { key: '1-2-1', label: '孙节点 1-2-1' },
19
+ ] },
20
+ ],
21
+ },
22
+ { key: '2', label: '节点 2' },
23
+ { key: '3', label: '节点 3', disabled: true },
24
+ ];
25
+
26
+ describe('mergeConfig', () => {
27
+ it('返回默认配置', () => {
28
+ expect(mergeConfig(DEFAULT_CONFIG)).toBe(DEFAULT_CONFIG);
29
+ });
30
+
31
+ it('自定义字段合并', () => {
32
+ const result = mergeConfig({ key: 'id', label: 'name' });
33
+ expect(result.key).toBe('id');
34
+ expect(result.label).toBe('name');
35
+ expect(result.children).toBe('children');
36
+ });
37
+ });
38
+
39
+ describe('fixKey', () => {
40
+ it('为无 key 的节点生成 key', () => {
41
+ const data = [{ label: 'A' }, { label: 'B' }] as any[];
42
+ const result = fixKey(data, 'key') as any[];
43
+ expect(result[0].key).toBe('0');
44
+ expect(result[1].key).toBe('1');
45
+ });
46
+
47
+ it('已有 key 的节点保持不变', () => {
48
+ const data = [{ key: 'existing', label: 'A' }];
49
+ const result = fixKey(data, 'key') as any[];
50
+ expect(result[0].key).toBe('existing');
51
+ });
52
+
53
+ it('递归处理子节点', () => {
54
+ const data = [{ key: 'root', children: [{ label: 'child' }] }] as any[];
55
+ const result = fixKey(data, 'key') as any[];
56
+ expect(result[0].children[0].key).toBe('root0');
57
+ });
58
+
59
+ it('单节点也能处理', () => {
60
+ const data = { label: 'single' } as any;
61
+ const result = fixKey(data, 'key') as any;
62
+ expect(result.key).toBe('0');
63
+ });
64
+ });
65
+
66
+ describe('Tree', () => {
67
+ it('构建树并获取根节点', () => {
68
+ const tree = new Tree({ data: SAMPLE_DATA });
69
+ const roots = tree.getTreeData();
70
+ expect(roots).toHaveLength(3);
71
+ expect(roots[0].isRoot).toBe(true);
72
+ });
73
+
74
+ it('getTreeData 通过 keys 获取节点', () => {
75
+ const tree = new Tree({ data: SAMPLE_DATA });
76
+ const nodes = tree.getTreeData(['1-1', '2']);
77
+ expect(nodes).toHaveLength(2);
78
+ });
79
+
80
+ it('getChildrenKeys 返回直接子节点 key', () => {
81
+ const tree = new Tree({ data: SAMPLE_DATA });
82
+ const root = tree.getTreeData(['1'])[0];
83
+ const keys = tree.getChildrenKeys(root);
84
+ expect(keys).toEqual(['1-1', '1-2']);
85
+ });
86
+
87
+ it('toggleExpand 切换展开状态', () => {
88
+ const tree = new Tree({ data: SAMPLE_DATA });
89
+ const node = tree.getTreeData(['1'])[0];
90
+
91
+ expect(node.expand).toBe(false);
92
+ tree.toggleExpand(node);
93
+ expect(node.expand).toBe(true);
94
+ tree.toggleExpand(node);
95
+ expect(node.expand).toBe(false);
96
+ });
97
+
98
+ it('toggleExpand 指定值', () => {
99
+ const tree = new Tree({ data: SAMPLE_DATA });
100
+ const node = tree.getTreeData(['1'])[0];
101
+
102
+ tree.toggleExpand(node, true);
103
+ expect(node.expand).toBe(true);
104
+ tree.toggleExpand(node, true);
105
+ expect(node.expand).toBe(true);
106
+ });
107
+
108
+ it('defaultExpandAll 展开所有节点', () => {
109
+ const tree = new Tree({ data: SAMPLE_DATA, defaultExpandAll: true });
110
+ const root = tree.getTreeData(['1'])[0];
111
+ expect(root.expand).toBe(true);
112
+ });
113
+
114
+ it('setNodeCheckbox 选中节点', () => {
115
+ const tree = new Tree({ data: SAMPLE_DATA, checkStrictly: true });
116
+ const root = tree.getTreeData(['1'])[0];
117
+
118
+ tree.setNodeCheckbox(root, true);
119
+ expect(root.checked).toBe(true);
120
+ expect(root.indeterminate).toBe(false);
121
+
122
+ // 子节点也被选中(checkStrictly=true 级联)
123
+ const child = tree.getTreeData(['1-1'])[0];
124
+ expect(child.checked).toBe(true);
125
+ });
126
+
127
+ it('setNodeCheckbox checkStrictly=true 向上级联', () => {
128
+ const tree = new Tree({ data: SAMPLE_DATA, checkStrictly: true });
129
+ const child = tree.getTreeData(['1-1'])[0];
130
+
131
+ tree.setNodeCheckbox(child, true);
132
+
133
+ // 父节点应该是半选(只有一个子节点选中)
134
+ const root = tree.getTreeData(['1'])[0];
135
+ expect(root.indeterminate).toBe(true);
136
+ expect(root.checked).toBe(false);
137
+ });
138
+
139
+ it('setNodeCheckbox 全部子节点选中时父节点全选', () => {
140
+ const tree = new Tree({ data: SAMPLE_DATA, checkStrictly: true });
141
+
142
+ tree.setNodeCheckbox(tree.getTreeData(['1-1'])[0], true);
143
+ tree.setNodeCheckbox(tree.getTreeData(['1-2'])[0], true);
144
+
145
+ const root = tree.getTreeData(['1'])[0];
146
+ expect(root.checked).toBe(true);
147
+ expect(root.indeterminate).toBe(false);
148
+ });
149
+
150
+ it('setNodeCheckbox checkStrictly=false 不级联', () => {
151
+ const tree = new Tree({ data: SAMPLE_DATA, checkStrictly: false });
152
+ const root = tree.getTreeData(['1'])[0];
153
+
154
+ tree.setNodeCheckbox(root, true);
155
+ // 子节点不受影响
156
+ const child = tree.getTreeData(['1-1'])[0];
157
+ expect(child.checked).toBe(false);
158
+ });
159
+
160
+ it('disabled 节点不受级联影响', () => {
161
+ const data = [
162
+ {
163
+ key: 'p', children: [
164
+ { key: 'c1' },
165
+ { key: 'c2', disabled: true },
166
+ ],
167
+ },
168
+ ];
169
+ const tree = new Tree({ data, checkStrictly: true });
170
+ const parent = tree.getTreeData(['p'])[0];
171
+
172
+ tree.setNodeCheckbox(parent, true);
173
+ // c1 被选中,c2 因 disabled 不受影响
174
+ expect(tree.getTreeData(['c1'])[0].checked).toBe(true);
175
+ expect(tree.getTreeData(['c2'])[0].checked).toBe(false);
176
+ });
177
+
178
+ it('setCheckedByKeys 批量选中', () => {
179
+ const tree = new Tree({ data: SAMPLE_DATA, checkStrictly: true });
180
+ tree.setCheckedByKeys(['1-1', '2']);
181
+
182
+ expect(tree.getTreeData(['1-1'])[0].checked).toBe(true);
183
+ expect(tree.getTreeData(['2'])[0].checked).toBe(true);
184
+ });
185
+
186
+ it('getKeys 获取选中和展开的 key', () => {
187
+ const tree = new Tree({ data: SAMPLE_DATA, checkStrictly: true, defaultExpandAll: true });
188
+ tree.setNodeCheckbox(tree.getTreeData(['2'])[0], true);
189
+
190
+ const { checkedKeys, expandKeys } = tree.getKeys();
191
+ expect(checkedKeys).toContain('2');
192
+ expect(expandKeys.length).toBeGreaterThan(0);
193
+ });
194
+
195
+ it('单节点数据也能正常构建', () => {
196
+ const tree = new Tree({ data: { key: 'single', label: '单节点' } });
197
+ const roots = tree.getTreeData();
198
+ expect(roots).toHaveLength(1);
199
+ expect(roots[0].isRoot).toBe(true);
200
+ });
201
+
202
+ it('自定义 config 字段映射', () => {
203
+ const data = [{ id: 'a', name: '节点 A', items: [{ id: 'b', name: '子节点 B' }] }] as any[];
204
+ const tree = new Tree({
205
+ data,
206
+ config: { key: 'id', label: 'name', value: 'value', children: 'items' },
207
+ });
208
+ const roots = tree.getTreeData();
209
+ expect(roots).toHaveLength(1);
210
+
211
+ const childKeys = tree.getChildrenKeys(roots[0]);
212
+ expect(childKeys).toEqual(['b']);
213
+ });
214
+ });
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
- import { MCOPO } from '../../types/props';
9
+ import { MCOPO, MPropType } from '../../types/props';
10
10
  import { DialogProps } from './props';
11
11
 
12
12
  export const props: MCOPO<DialogProps> = {
@@ -16,4 +16,5 @@ export const props: MCOPO<DialogProps> = {
16
16
  teleport: { type: Object, default: () => ({ to: 'body' }) },
17
17
  title: { type: String, default: undefined },
18
18
  width: { type: String, default: undefined },
19
+ beforeClose: { type: Function as unknown as MPropType<NonNullable<DialogProps['beforeClose']>>, default: undefined },
19
20
  };
@@ -52,4 +52,11 @@ export declare type DialogProps = {
52
52
  * @default undefined
53
53
  */
54
54
  width?: string;
55
+ /**
56
+ * @description callback before close, return false or Promise.reject to prevent close
57
+ * 关闭前的回调,返回 false 或 reject 的 Promise 可阻止关闭
58
+ * @type () => boolean | Promise<boolean>
59
+ * @default undefined
60
+ */
61
+ beforeClose?: () => boolean | Promise<boolean>;
55
62
  };
@@ -29,4 +29,5 @@ export const props: MCOPO<DrawerProps> = {
29
29
  title: { type: String, default: undefined },
30
30
  width: { type: String, default: undefined },
31
31
  height: { type: String, default: undefined },
32
+ beforeClose: { type: Function as unknown as MPropType<NonNullable<DrawerProps['beforeClose']>>, default: undefined },
32
33
  };
@@ -70,4 +70,11 @@ export declare type DrawerProps = {
70
70
  * @default undefined
71
71
  */
72
72
  height?: string,
73
+ /**
74
+ * @description callback before close, return false or Promise.reject to prevent close
75
+ * 关闭前的回调,返回 false 或 reject 的 Promise 可阻止关闭
76
+ * @type () => boolean | Promise<boolean>
77
+ * @default undefined
78
+ */
79
+ beforeClose?: () => boolean | Promise<boolean>,
73
80
  };
@@ -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