@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
@@ -7,11 +7,12 @@
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
9
  import { ref, onMounted, onUnmounted } from 'vue';
10
+ import { type HookContext } from '../../types/hook';
10
11
  import { AffixProps } from './props';
11
12
 
12
13
  export function useAffix(
13
14
  props: Required<AffixProps>,
14
- emit: (...args: any[]) => void,
15
+ emit: HookContext['emit'],
15
16
  ) {
16
17
  const affixed = ref(false);
17
18
  // 固定后占位元素的高度(撑开文档流)
@@ -7,11 +7,12 @@
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
9
  import { ref, onMounted, onUnmounted } from 'vue';
10
+ import { type HookContext } from '../../types/hook';
10
11
  import { AnchorProps } from './props';
11
12
 
12
13
  export function useAnchor(
13
14
  props: Required<AnchorProps>,
14
- emit: (...args: any[]) => void,
15
+ emit: HookContext['emit'],
15
16
  ) {
16
17
  const currentLink = ref('');
17
18
  // 由子组件注册的 href 列表(按 DOM 顺序)
@@ -14,8 +14,9 @@
14
14
  */
15
15
  import { ref, toRef, watch, onBeforeUnmount } from 'vue';
16
16
  import { AutoCompleteProps, AutoCompleteOption } from './props';
17
+ import { type HookContext } from '../../types/hook';
17
18
 
18
- export function useAutoComplete(props: AutoCompleteProps, ctx: any) {
19
+ export function useAutoComplete(props: AutoCompleteProps, ctx: HookContext) {
19
20
  const modelValueRef = toRef(() => props.modelValue ?? '');
20
21
 
21
22
  // --- 状态 ---
@@ -7,9 +7,10 @@
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
9
  import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
10
+ import { type HookContext } from '../../types/hook';
10
11
  import { CarouselProps } from './props';
11
12
 
12
- export function useCarousel(props: Required<CarouselProps>, emit: (event: string, ...args: any[]) => void, count: () => number) {
13
+ export function useCarousel(props: Required<CarouselProps>, emit: HookContext['emit'], count: () => number) {
13
14
  // 逻辑索引(0..N-1),用于指示器和 v-model
14
15
  const activeIndex = ref(props.modelValue ?? props.initialIndex);
15
16
  // track 位置索引(loop 时含克隆偏移);初始时若 loop 启用则假定有克隆
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { computed, ref, toRef, watch } from 'vue';
16
16
  import { CascaderOption, CascaderProps } from './props';
17
+ import { type HookContext } from '../../types/hook';
17
18
 
18
19
  /** 扁平化后的叶子路径,用于搜索 */
19
20
  export interface FlatCascaderOption {
@@ -64,7 +65,7 @@ const findPathByValues = (
64
65
  return subPath ? [found, ...subPath] : null;
65
66
  };
66
67
 
67
- export function useCascader(props: CascaderProps, ctx: any) {
68
+ export function useCascader(props: CascaderProps, ctx: HookContext) {
68
69
  const optionsRef = toRef(() => props.options ?? []);
69
70
  const modelValueRef = toRef(() => props.modelValue ?? []);
70
71
  const multipleRef = toRef(() => props.multiple ?? false);
@@ -9,11 +9,12 @@
9
9
  import { ref, watch } from 'vue';
10
10
  import { getNewModelValue, initChecked } from '../../../compositions/input/useBooleanInput.ts';
11
11
  import { CheckboxProps } from './props';
12
+ import { type HookContext } from '../../types/hook';
12
13
 
13
14
 
14
15
  export function useCheckbox<
15
16
  Props extends Required<CheckboxProps>,
16
- >(props: Props, ctx: any) {
17
+ >(props: Props, ctx: HookContext) {
17
18
  const checkboxClass = ['m-checkbox', { 'm-disabled': props.disabled, 'm-indeterminate': props.indeterminate }];
18
19
 
19
20
  const checked = ref(initChecked(props));
@@ -8,8 +8,9 @@
8
8
  */
9
9
  import { computed } from 'vue';
10
10
  import { CollapseProps } from './props';
11
+ import { type HookContext } from '../../types/hook';
11
12
 
12
- export function useCollapse(props: Required<CollapseProps>, ctx: any) {
13
+ export function useCollapse(props: Required<CollapseProps>, ctx: HookContext) {
13
14
  const { emit } = ctx;
14
15
 
15
16
  /**
@@ -0,0 +1,239 @@
1
+ /**
2
+ * @description useDatePicker 测试
3
+ * @author 阿怪
4
+ * @date 2026/3/23
5
+ * @version v1.0.0
6
+ *
7
+ * 江湖的业务千篇一律,复杂的代码好几百行。
8
+ */
9
+ import { describe, expect, it } from 'vitest';
10
+ import { useDatePicker, generateTimeColumn, toDayjs, BASE_WEEK_NAME, BASE_MONTH_NAME } from '../useDatePicker';
11
+
12
+ describe('toDayjs', () => {
13
+ it('字符串转 dayjs', () => {
14
+ const d = toDayjs('2026-03-15');
15
+ expect(d.year()).toBe(2026);
16
+ expect(d.month()).toBe(2); // 0-indexed
17
+ expect(d.date()).toBe(15);
18
+ });
19
+
20
+ it('Date 对象转 dayjs', () => {
21
+ const d = toDayjs(new Date(2026, 5, 10));
22
+ expect(d.year()).toBe(2026);
23
+ expect(d.month()).toBe(5);
24
+ });
25
+
26
+ it('undefined 返回当前时间', () => {
27
+ const d = toDayjs(undefined);
28
+ expect(d.isValid()).toBe(true);
29
+ });
30
+ });
31
+
32
+ describe('generateTimeColumn', () => {
33
+ it('生成 24 小时列', () => {
34
+ const hours = generateTimeColumn(24, 1);
35
+ expect(hours).toHaveLength(24);
36
+ expect(hours[0]).toBe(0);
37
+ expect(hours[23]).toBe(23);
38
+ });
39
+
40
+ it('步进为 2', () => {
41
+ const hours = generateTimeColumn(24, 2);
42
+ expect(hours).toEqual([0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]);
43
+ });
44
+
45
+ it('生成 60 分钟列,步进 15', () => {
46
+ const mins = generateTimeColumn(60, 15);
47
+ expect(mins).toEqual([0, 15, 30, 45]);
48
+ });
49
+ });
50
+
51
+ describe('常量', () => {
52
+ it('BASE_WEEK_NAME 有 7 天', () => {
53
+ expect(BASE_WEEK_NAME).toHaveLength(7);
54
+ });
55
+
56
+ it('BASE_MONTH_NAME 有 12 个月', () => {
57
+ expect(BASE_MONTH_NAME).toHaveLength(12);
58
+ });
59
+ });
60
+
61
+ describe('useDatePicker', () => {
62
+ it('初始化空值显示 placeholder', () => {
63
+ const dp = useDatePicker({ type: 'date' });
64
+ expect(dp.displayValue.value).toBe('请选择日期...');
65
+ expect(dp.spanClass.value).toContain('m-date-picker-placeholder');
66
+ });
67
+
68
+ it('初始化有效日期正确解析', () => {
69
+ const dp = useDatePicker({ type: 'date', modelValue: '2026-06-15' });
70
+ expect(dp.displayValue.value).toBe('2026-06-15');
71
+ expect(dp.dateRef.value.year).toBe(2026);
72
+ expect(dp.dateRef.value.month).toBe(6);
73
+ expect(dp.dateRef.value.day).toBe(15);
74
+ });
75
+
76
+ it('自定义 placeholder', () => {
77
+ const dp = useDatePicker({ type: 'date', placeholder: '选择日期' });
78
+ expect(dp.displayValue.value).toBe('选择日期');
79
+ });
80
+
81
+ it('updateDateRef 更新状态', () => {
82
+ const dp = useDatePicker({ type: 'date' });
83
+ dp.updateDateRef('2025-12-25');
84
+
85
+ expect(dp.dateRef.value.year).toBe(2025);
86
+ expect(dp.dateRef.value.month).toBe(12);
87
+ expect(dp.dateRef.value.day).toBe(25);
88
+ expect(dp.displayValue.value).toBe('2025-12-25');
89
+ });
90
+
91
+ it('updateDateRef 空值恢复 placeholder', () => {
92
+ const dp = useDatePicker({ type: 'date', modelValue: '2026-01-01' });
93
+ dp.updateDateRef('');
94
+
95
+ expect(dp.displayValue.value).toBe('请选择日期...');
96
+ });
97
+
98
+ it('getCalendar 返回 6 行 7 列', () => {
99
+ const dp = useDatePicker({ type: 'date', modelValue: '2026-03-15' });
100
+ const calendar = dp.getCalendar(dp.dateRef);
101
+
102
+ expect(calendar).toHaveLength(6);
103
+ calendar.forEach(row => {
104
+ expect(row).toHaveLength(7);
105
+ });
106
+ });
107
+
108
+ it('getCalendar 包含当月天数', () => {
109
+ const dp = useDatePicker({ type: 'date', modelValue: '2026-02-15' });
110
+ const calendar = dp.getCalendar(dp.dateRef);
111
+ const allDays = calendar.flat();
112
+ const currentMonthDays = allDays.filter(d => d.isCurrentMonth);
113
+
114
+ // 2026 年 2 月有 28 天
115
+ expect(currentMonthDays).toHaveLength(28);
116
+ });
117
+
118
+ it('getCalendar 标记当前选中日', () => {
119
+ const dp = useDatePicker({ type: 'date', modelValue: '2026-03-15' });
120
+ const calendar = dp.getCalendar(dp.dateRef);
121
+ const allDays = calendar.flat();
122
+ const currentDay = allDays.find(d => d.isCurrent);
123
+
124
+ expect(currentDay).toBeDefined();
125
+ expect(currentDay!.day).toBe(15);
126
+ });
127
+
128
+ it('toPrevMonth 切换上月', () => {
129
+ const dp = useDatePicker({ type: 'date', modelValue: '2026-03-15' });
130
+ dp.toPrevMonth();
131
+ expect(dp.dateRef.value.month).toBe(2);
132
+ });
133
+
134
+ it('toPrevMonth 跨年', () => {
135
+ const dp = useDatePicker({ type: 'date', modelValue: '2026-01-15' });
136
+ dp.toPrevMonth();
137
+ expect(dp.dateRef.value.month).toBe(12);
138
+ expect(dp.dateRef.value.year).toBe(2025);
139
+ });
140
+
141
+ it('toNextMonth 切换下月', () => {
142
+ const dp = useDatePicker({ type: 'date', modelValue: '2026-03-15' });
143
+ dp.toNextMonth();
144
+ expect(dp.dateRef.value.month).toBe(4);
145
+ });
146
+
147
+ it('toNextMonth 跨年', () => {
148
+ const dp = useDatePicker({ type: 'date', modelValue: '2026-12-15' });
149
+ dp.toNextMonth();
150
+ expect(dp.dateRef.value.month).toBe(1);
151
+ expect(dp.dateRef.value.year).toBe(2027);
152
+ });
153
+
154
+ it('toPrevYear / toNextYear', () => {
155
+ const dp = useDatePicker({ type: 'date', modelValue: '2026-06-15' });
156
+ dp.toPrevYear();
157
+ expect(dp.dateRef.value.year).toBe(2025);
158
+ dp.toNextYear();
159
+ dp.toNextYear();
160
+ expect(dp.dateRef.value.year).toBe(2027);
161
+ });
162
+
163
+ it('getValue 返回格式化日期字符串', () => {
164
+ const dp = useDatePicker({ type: 'date' });
165
+ const value = dp.getValue({ day: 20, month: 6, year: 2026, isCurrentMonth: true });
166
+ expect(value).toBe('2026-06-20');
167
+ });
168
+
169
+ it('month 类型使用 YYYY-MM 格式', () => {
170
+ const dp = useDatePicker({ type: 'month', modelValue: '2026-03' });
171
+ expect(dp.displayValue.value).toBe('2026-03');
172
+ });
173
+
174
+ it('自定义 format', () => {
175
+ const dp = useDatePicker({ type: 'date', format: 'YYYY/MM/DD', modelValue: '2026-03-15' });
176
+ expect(dp.displayValue.value).toBe('2026/03/15');
177
+ });
178
+
179
+ it('clickCurrentYear 切换到年份选择模式', () => {
180
+ const dp = useDatePicker({ type: 'date', modelValue: '2026-03-15' });
181
+ dp.clickCurrentYear(2026);
182
+ expect(dp.calendarTypeRef.value).toBe('year');
183
+ expect(dp.yearsRef.value).toHaveLength(12);
184
+ expect(dp.yearsRef.value).toContain(2026);
185
+ });
186
+
187
+ it('clickYearItem 更新年份并切换回日期模式', () => {
188
+ const dp = useDatePicker({ type: 'date', modelValue: '2026-03-15' });
189
+ dp.clickCurrentYear(2026);
190
+ dp.clickYearItem(2028);
191
+ expect(dp.dateRef.value.year).toBe(2028);
192
+ expect(dp.calendarTypeRef.value).toBe('date');
193
+ });
194
+
195
+ it('clickCurrentMonth 切换到月份选择模式', () => {
196
+ const dp = useDatePicker({ type: 'date', modelValue: '2026-03-15' });
197
+ dp.clickCurrentMonth(3);
198
+ expect(dp.calendarTypeRef.value).toBe('month');
199
+ });
200
+
201
+ it('clickMonthItem 更新月份并切换回日期模式', () => {
202
+ const dp = useDatePicker({ type: 'date', modelValue: '2026-03-15' });
203
+ dp.clickCurrentMonth(3);
204
+ dp.clickMonthItem(8);
205
+ expect(dp.dateRef.value.month).toBe(8);
206
+ expect(dp.calendarTypeRef.value).toBe('date');
207
+ });
208
+
209
+ // datetime 模式
210
+ it('datetime 模式初始化包含时间', () => {
211
+ const dp = useDatePicker({ type: 'datetime', modelValue: '2026-03-15 14:30:45' });
212
+ expect(dp.timeRef.value.hours).toBe(14);
213
+ expect(dp.timeRef.value.minutes).toBe(30);
214
+ expect(dp.timeRef.value.seconds).toBe(45);
215
+ });
216
+
217
+ it('selectHour / selectMinute / selectSecond', () => {
218
+ const dp = useDatePicker({ type: 'datetime' });
219
+ dp.selectHour(10);
220
+ expect(dp.timeRef.value.hours).toBe(10);
221
+
222
+ dp.selectMinute(25);
223
+ expect(dp.timeRef.value.minutes).toBe(25);
224
+
225
+ dp.selectSecond(59);
226
+ expect(dp.timeRef.value.seconds).toBe(59);
227
+ });
228
+
229
+ it('getDateTimeValue 组合日期和时间', () => {
230
+ const dp = useDatePicker({ type: 'datetime', modelValue: '2026-03-15 00:00:00' });
231
+ dp.selectHour(8);
232
+ dp.selectMinute(30);
233
+ dp.selectSecond(0);
234
+
235
+ const value = dp.getDateTimeValue();
236
+ expect(value).toContain('2026-03-15');
237
+ expect(value).toContain('08:30:00');
238
+ });
239
+ });
@@ -9,8 +9,9 @@
9
9
  import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
10
10
  import { DropdownItemProps, DropdownProps } from './props';
11
11
  import useClickAway from '../../../compositions/popper/useClickAway';
12
+ import { type HookContext } from '../../types/hook';
12
13
 
13
- export function useDropdown(props: Required<DropdownProps>, ctx: any) {
14
+ export function useDropdown(props: Required<DropdownProps>, ctx: HookContext) {
14
15
  const { emit } = ctx;
15
16
 
16
17
  // 下拉菜单是否展开
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @description useImage 测试
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 { useImage } from '../useImage';
11
+
12
+ const defaultProps = {
13
+ src: 'test.png',
14
+ alt: '',
15
+ fit: 'cover' as const,
16
+ lazy: false,
17
+ previewSrcList: [] as string[],
18
+ initialIndex: 0,
19
+ width: '' as string | number,
20
+ height: '' as string | number,
21
+ zIndex: 2000,
22
+ };
23
+
24
+ describe('useImage', () => {
25
+ const createEmit = () => vi.fn();
26
+
27
+ it('初始状态为 loading', () => {
28
+ const emit = createEmit();
29
+ const { status } = useImage(defaultProps, emit);
30
+ expect(status.value).toBe('loading');
31
+ });
32
+
33
+ it('handleLoad 设置状态为 loaded 并触发 emit', () => {
34
+ const emit = createEmit();
35
+ const { status, handleLoad } = useImage(defaultProps, emit);
36
+ const event = new Event('load');
37
+
38
+ handleLoad(event);
39
+ expect(status.value).toBe('loaded');
40
+ expect(emit).toHaveBeenCalledWith('load', event);
41
+ });
42
+
43
+ it('handleError 设置状态为 error 并触发 emit', () => {
44
+ const emit = createEmit();
45
+ const { status, handleError } = useImage(defaultProps, emit);
46
+ const event = new Event('error');
47
+
48
+ handleError(event);
49
+ expect(status.value).toBe('error');
50
+ expect(emit).toHaveBeenCalledWith('error', event);
51
+ });
52
+
53
+ // 预览功能
54
+ it('openPreview 无 previewSrcList 时不打开', () => {
55
+ const emit = createEmit();
56
+ const { previewVisible, openPreview } = useImage(defaultProps, emit);
57
+
58
+ openPreview();
59
+ expect(previewVisible.value).toBe(false);
60
+ });
61
+
62
+ it('openPreview 有 previewSrcList 时打开并定位', () => {
63
+ const emit = createEmit();
64
+ const props = { ...defaultProps, previewSrcList: ['a.png', 'test.png', 'c.png'] };
65
+ const { previewVisible, previewIndex, openPreview } = useImage(props, emit);
66
+
67
+ openPreview();
68
+ expect(previewVisible.value).toBe(true);
69
+ expect(previewIndex.value).toBe(1); // test.png 在索引 1
70
+ });
71
+
72
+ it('openPreview src 不在列表中时定位到 0', () => {
73
+ const emit = createEmit();
74
+ const props = { ...defaultProps, src: 'unknown.png', previewSrcList: ['a.png', 'b.png'] };
75
+ const { previewIndex, openPreview } = useImage(props, emit);
76
+
77
+ openPreview();
78
+ expect(previewIndex.value).toBe(0);
79
+ });
80
+
81
+ it('closePreview 关闭预览', () => {
82
+ const emit = createEmit();
83
+ const props = { ...defaultProps, previewSrcList: ['a.png'] };
84
+ const { previewVisible, openPreview, closePreview } = useImage(props, emit);
85
+
86
+ openPreview();
87
+ closePreview();
88
+ expect(previewVisible.value).toBe(false);
89
+ });
90
+
91
+ it('previewNext / previewPrev 循环切换', () => {
92
+ const emit = createEmit();
93
+ const props = { ...defaultProps, src: 'a.png', previewSrcList: ['a.png', 'b.png', 'c.png'] };
94
+ const { previewIndex, openPreview, previewNext, previewPrev } = useImage(props, emit);
95
+
96
+ openPreview(); // index = 0
97
+
98
+ previewNext(); // index = 1
99
+ expect(previewIndex.value).toBe(1);
100
+ expect(emit).toHaveBeenCalledWith('switch', 1);
101
+
102
+ previewNext(); // index = 2
103
+ expect(previewIndex.value).toBe(2);
104
+
105
+ previewNext(); // 循环回 0
106
+ expect(previewIndex.value).toBe(0);
107
+
108
+ previewPrev(); // 循环到 2
109
+ expect(previewIndex.value).toBe(2);
110
+ });
111
+
112
+ it('zoomIn 放大', () => {
113
+ const emit = createEmit();
114
+ const props = { ...defaultProps, previewSrcList: ['a.png'] };
115
+ const { previewScale, openPreview, zoomIn } = useImage(props, emit);
116
+
117
+ openPreview();
118
+ expect(previewScale.value).toBe(1);
119
+
120
+ zoomIn();
121
+ expect(previewScale.value).toBe(1.25);
122
+
123
+ // 最大 5
124
+ for (let i = 0; i < 20; i++) zoomIn();
125
+ expect(previewScale.value).toBe(5);
126
+ });
127
+
128
+ it('zoomOut 缩小', () => {
129
+ const emit = createEmit();
130
+ const props = { ...defaultProps, previewSrcList: ['a.png'] };
131
+ const { previewScale, openPreview, zoomOut } = useImage(props, emit);
132
+
133
+ openPreview();
134
+ zoomOut();
135
+ expect(previewScale.value).toBe(0.75);
136
+
137
+ // 最小 0.25
138
+ for (let i = 0; i < 20; i++) zoomOut();
139
+ expect(previewScale.value).toBe(0.25);
140
+ });
141
+
142
+ it('rotate 旋转 90°', () => {
143
+ const emit = createEmit();
144
+ const props = { ...defaultProps, previewSrcList: ['a.png'] };
145
+ const { previewRotate, openPreview, rotate } = useImage(props, emit);
146
+
147
+ openPreview();
148
+ rotate();
149
+ expect(previewRotate.value).toBe(90);
150
+
151
+ rotate();
152
+ expect(previewRotate.value).toBe(180);
153
+
154
+ rotate();
155
+ rotate();
156
+ expect(previewRotate.value).toBe(0); // 360 % 360 = 0
157
+ });
158
+
159
+ it('切换预览图片时重置缩放和旋转', () => {
160
+ const emit = createEmit();
161
+ const props = { ...defaultProps, src: 'a.png', previewSrcList: ['a.png', 'b.png'] };
162
+ const { previewScale, previewRotate, openPreview, zoomIn, rotate, previewNext } = useImage(props, emit);
163
+
164
+ openPreview();
165
+ zoomIn();
166
+ rotate();
167
+ expect(previewScale.value).toBe(1.25);
168
+ expect(previewRotate.value).toBe(90);
169
+
170
+ previewNext();
171
+ expect(previewScale.value).toBe(1);
172
+ expect(previewRotate.value).toBe(0);
173
+ });
174
+ });
@@ -6,12 +6,14 @@
6
6
  *
7
7
  * 江湖的业务千篇一律,复杂的代码好几百行。
8
8
  */
9
+
9
10
  import { HTMLElementEvent } from '../../types/template';
11
+ import { type HookContext } from '../../types/hook';
10
12
 
11
13
 
12
14
  export function useInput<
13
15
  Props extends Record<string, any>,
14
- >(props: Props, ctx: any) {
16
+ >(props: Props, ctx: HookContext) {
15
17
 
16
18
  const isInput = props.type !== 'textarea';
17
19
  const inputType = isInput ? 'input' : 'textarea';
@@ -0,0 +1,153 @@
1
+ /**
2
+ * @description useInputNumber 归一 / emit 保真单测
3
+ * @author 阿怪
4
+ * @date 2026/4/24
5
+ * @version v1.0.0
6
+ */
7
+ import { describe, expect, it, vi } from 'vitest';
8
+ import { nextTick, reactive } from 'vue';
9
+ import { useInputNumber } from '../useInputNumber';
10
+ import type { InputNumberProps } from '../props';
11
+ import type { HookContext } from '../../../types/hook';
12
+
13
+ type PropsShape = Required<InputNumberProps>;
14
+
15
+ const defaultProps: PropsShape = {
16
+ modelValue: null,
17
+ max: Infinity,
18
+ min: -Infinity,
19
+ step: 1,
20
+ precision: 0,
21
+ disabled: false,
22
+ readonly: false,
23
+ placeholder: '',
24
+ controls: true,
25
+ size: 'medium' as PropsShape['size'],
26
+ };
27
+
28
+ function setup(overrides: Partial<PropsShape> = {}) {
29
+ const props = reactive<PropsShape>({ ...defaultProps, ...overrides }) as PropsShape;
30
+ const emit = vi.fn();
31
+ const ctx: HookContext = { emit, slots: {} };
32
+ const hook = useInputNumber(props, ctx);
33
+ return { props, emit, ...hook };
34
+ }
35
+
36
+ const lastUpdate = (emit: ReturnType<typeof vi.fn>) =>
37
+ emit.mock.calls.filter((c: unknown[]) => c[0] === 'update:modelValue').at(-1)!;
38
+
39
+ describe('useInputNumber emit 归一', () => {
40
+ it('number 入 → number 出', () => {
41
+ const { validate, emit } = setup({ modelValue: 1 });
42
+ validate(5);
43
+ const call = lastUpdate(emit);
44
+ expect(call[1]).toBe(5);
45
+ expect(typeof call[1]).toBe('number');
46
+ });
47
+
48
+ it('string 入 → string 出', () => {
49
+ const { validate, emit } = setup({ modelValue: '1' });
50
+ validate('5');
51
+ const call = lastUpdate(emit);
52
+ expect(call[1]).toBe('5');
53
+ expect(typeof call[1]).toBe('string');
54
+ });
55
+
56
+ it('null 入 → 用户输入整数后默认 number 出', () => {
57
+ const { validate, emit } = setup({ modelValue: null });
58
+ validate(3);
59
+ const call = lastUpdate(emit);
60
+ expect(call[1]).toBe(3);
61
+ expect(typeof call[1]).toBe('number');
62
+ });
63
+
64
+ it('空字符串 currentValue → emit null(不是空串)', () => {
65
+ const { validate, emit } = setup({ modelValue: 5 });
66
+ validate(null);
67
+ const call = lastUpdate(emit);
68
+ expect(call[1]).toBeNull();
69
+ });
70
+
71
+ it('中间态 "-" → emit null(未输完)', () => {
72
+ const { validate, emit } = setup({ modelValue: null });
73
+ validate('-');
74
+ const call = lastUpdate(emit);
75
+ expect(call[1]).toBeNull();
76
+ });
77
+
78
+ it('输入 "0." → emit 0(末尾小数点归一)', () => {
79
+ const { validate, emit } = setup({ modelValue: null, precision: 2 });
80
+ validate('0.');
81
+ const call = lastUpdate(emit);
82
+ expect(call[1]).toBe(0);
83
+ });
84
+
85
+ it('非法字符串 → emit 原样不变(validate 不触发 emit)', () => {
86
+ const { validate, emit } = setup({ modelValue: 5 });
87
+ emit.mockClear();
88
+ validate('abc' as unknown as number);
89
+ // 非法输入不触发 emit
90
+ expect(emit).not.toHaveBeenCalled();
91
+ });
92
+
93
+ it('跨类型 watch:外部传 1(number)→ 不会重复 emit 成 string', async () => {
94
+ const { props, emit } = setup({ modelValue: 1 });
95
+ emit.mockClear();
96
+ // 模拟外部 bind 把内部 string '1' 回传 number 1:这在真实 v-model 里是 watch 触发的
97
+ props.modelValue = 1;
98
+ await nextTick();
99
+ // 内部 '1' 和外部 1 的 String 归一相等,不该触发 emit
100
+ expect(emit).not.toHaveBeenCalled();
101
+ });
102
+
103
+ it('blur 末尾小数点归一:内部 "1." → emit 1', () => {
104
+ const { handleInputChange, handleInputBlur, emit } = setup({ modelValue: null, precision: 2 });
105
+ handleInputChange({ target: { value: '1.' } } as never);
106
+ emit.mockClear();
107
+ handleInputBlur();
108
+ const call = lastUpdate(emit);
109
+ expect(call[1]).toBe(1);
110
+ });
111
+
112
+ it('max 截断仍然保真类型', () => {
113
+ const { validate, emit } = setup({ modelValue: '10', max: 100 });
114
+ validate(150);
115
+ const call = lastUpdate(emit);
116
+ expect(call[1]).toBe('100');
117
+ expect(typeof call[1]).toBe('string');
118
+ });
119
+
120
+ it('min 截断仍然保真类型', () => {
121
+ const { validate, emit } = setup({ modelValue: 10, min: 0 });
122
+ validate(-5);
123
+ const call = lastUpdate(emit);
124
+ expect(call[1]).toBe(0);
125
+ expect(typeof call[1]).toBe('number');
126
+ });
127
+ });
128
+
129
+ describe('useInputNumber 输入流程', () => {
130
+ it('合法数字输入触发 emit', () => {
131
+ const { handleInputChange, emit } = setup({ modelValue: 0 });
132
+ emit.mockClear();
133
+ handleInputChange({ target: { value: '88' } } as never);
134
+ const call = lastUpdate(emit);
135
+ expect(call[1]).toBe(88);
136
+ });
137
+
138
+ it('increment 按 step 递增', () => {
139
+ const { increment, emit } = setup({ modelValue: 5, step: 3 });
140
+ emit.mockClear();
141
+ increment();
142
+ const call = lastUpdate(emit);
143
+ expect(call[1]).toBe(8);
144
+ });
145
+
146
+ it('decrement 按 step 递减', () => {
147
+ const { decrement, emit } = setup({ modelValue: 5, step: 2 });
148
+ emit.mockClear();
149
+ decrement();
150
+ const call = lastUpdate(emit);
151
+ expect(call[1]).toBe(3);
152
+ });
153
+ });