@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.
- package/components/base/affix/useAffix.ts +2 -1
- package/components/base/anchor/useAnchor.ts +2 -1
- package/components/base/autoComplete/useAutoComplete.ts +2 -1
- package/components/base/carousel/useCarousel.ts +2 -1
- package/components/base/cascader/useCascader.ts +2 -1
- package/components/base/checkbox/useCheckbox.ts +2 -1
- package/components/base/collapse/useCollapse.ts +2 -1
- package/components/base/datePicker/__tests__/useDatePicker.test.ts +239 -0
- package/components/base/dropdown/useDropdown.ts +2 -1
- package/components/base/image/__tests__/useImage.test.ts +174 -0
- package/components/base/input/useInput.ts +3 -1
- package/components/base/inputNumber/__tests__/useInputNumber.test.ts +153 -0
- package/components/base/inputNumber/useInputNumber.ts +53 -11
- package/components/base/popover/usePopover.ts +4 -3
- package/components/base/rate/useRate.ts +2 -1
- package/components/base/select/useSelect.ts +2 -1
- package/components/base/slider/useSlider.ts +2 -1
- package/components/base/steps/__tests__/useSteps.test.ts +46 -0
- package/components/base/switch/useSwitch.tsx +2 -1
- package/components/base/tabs/useTabs.ts +2 -1
- package/components/base/timePicker/__tests__/useTimePicker.test.ts +118 -0
- package/components/base/transfer/useTransfer.ts +2 -1
- package/components/base/tree/__tests__/tree.test.ts +214 -0
- package/components/message/notification/__tests__/useNotification.test.ts +129 -0
- package/components/message/popover/usePopover.ts +4 -4
- package/components/other/darkMode/useDarkMode.ts +2 -2
- package/components/template/menu/__tests__/useMenu.test.ts +157 -0
- package/components/template/pagination/__tests__/usePagination.test.ts +138 -0
- package/components/template/table/__tests__/useTable.test.ts +139 -0
- package/components/template/table/useTable.ts +2 -4
- package/components/types/hook.d.ts +11 -0
- package/compositions/common/__tests__/useDebounceFn.test.ts +62 -0
- package/compositions/common/__tests__/useEventListener.test.ts +71 -0
- package/compositions/common/__tests__/usePopover.test.ts +38 -0
- package/compositions/common/__tests__/useTeleport.test.ts +25 -0
- package/compositions/common/testAnchor.ts +211 -0
- package/compositions/common/useEventListener.ts +3 -3
- package/compositions/common/useResizeObserver.ts +6 -2
- package/compositions/input/__tests__/useBooleanInput.test.ts +73 -0
- package/compositions/modal/__tests__/useModal.test.ts +92 -0
- package/compositions/modal/useModal.ts +2 -2
- package/compositions/popper/useClickAway.ts +4 -4
- package/compositions/utils/__tests__/filters.test.ts +136 -0
- package/compositions/virtualList/__tests__/useHeightCache.test.ts +97 -0
- package/dist/components/base/affix/useAffix.d.ts +2 -1
- package/dist/components/base/anchor/useAnchor.d.ts +2 -1
- package/dist/components/base/autoComplete/useAutoComplete.d.ts +2 -1
- package/dist/components/base/carousel/useCarousel.d.ts +2 -1
- package/dist/components/base/cascader/useCascader.d.ts +2 -1
- package/dist/components/base/checkbox/useCheckbox.d.ts +2 -1
- package/dist/components/base/collapse/useCollapse.d.ts +2 -1
- package/dist/components/base/datePicker/__tests__/useDatePicker.test.d.ts +1 -0
- package/dist/components/base/dropdown/useDropdown.d.ts +2 -1
- package/dist/components/base/image/__tests__/useImage.test.d.ts +1 -0
- package/dist/components/base/input/useInput.d.ts +2 -1
- package/dist/components/base/inputNumber/__tests__/useInputNumber.test.d.ts +1 -0
- package/dist/components/base/inputNumber/useInputNumber.d.ts +2 -1
- package/dist/components/base/popover/usePopover.d.ts +2 -1
- package/dist/components/base/rate/useRate.d.ts +2 -1
- package/dist/components/base/select/useSelect.d.ts +2 -1
- package/dist/components/base/slider/useSlider.d.ts +2 -1
- package/dist/components/base/steps/__tests__/useSteps.test.d.ts +1 -0
- package/dist/components/base/switch/useSwitch.d.ts +2 -1
- package/dist/components/base/tabs/useTabs.d.ts +2 -1
- package/dist/components/base/timePicker/__tests__/useTimePicker.test.d.ts +1 -0
- package/dist/components/base/transfer/useTransfer.d.ts +2 -1
- package/dist/components/base/tree/__tests__/tree.test.d.ts +1 -0
- package/dist/components/message/notification/__tests__/useNotification.test.d.ts +1 -0
- package/dist/components/message/popover/usePopover.d.ts +1 -1
- package/dist/components/other/darkMode/useDarkMode.d.ts +2 -3
- package/dist/components/template/menu/__tests__/useMenu.test.d.ts +1 -0
- package/dist/components/template/pagination/__tests__/usePagination.test.d.ts +1 -0
- package/dist/components/template/table/__tests__/useTable.test.d.ts +1 -0
- package/dist/compositions/common/__tests__/useDebounceFn.test.d.ts +1 -0
- package/dist/compositions/common/__tests__/useEventListener.test.d.ts +1 -0
- package/dist/compositions/common/__tests__/usePopover.test.d.ts +1 -0
- package/dist/compositions/common/__tests__/useTeleport.test.d.ts +1 -0
- package/dist/compositions/common/testAnchor.d.ts +40 -0
- package/dist/compositions/common/useEventListener.d.ts +2 -2
- package/dist/compositions/input/__tests__/useBooleanInput.test.d.ts +1 -0
- package/dist/compositions/modal/__tests__/useModal.test.d.ts +1 -0
- package/dist/compositions/modal/useModal.d.ts +2 -1
- package/dist/compositions/popper/useClickAway.d.ts +3 -3
- package/dist/compositions/utils/__tests__/filters.test.d.ts +1 -0
- package/dist/compositions/virtualList/__tests__/useHeightCache.test.d.ts +1 -0
- package/dist/core.js +64 -18
- package/dist/tools/__tests__/empty.test.d.ts +1 -0
- package/dist/tools/empty.d.ts +2 -2
- package/dist/tools/types.d.ts +1 -1
- package/dist/vitest.config.d.ts +10 -0
- package/package.json +6 -2
- package/tools/__tests__/empty.test.ts +72 -0
- package/tools/empty.ts +2 -2
- package/tools/types.ts +1 -1
- package/vitest.config.ts +17 -0
|
@@ -2,28 +2,66 @@
|
|
|
2
2
|
* @description inputNumber composable
|
|
3
3
|
* @author 阿怪
|
|
4
4
|
* @date 2026/2/25 00:00
|
|
5
|
-
* @version v1.
|
|
5
|
+
* @version v1.2.0
|
|
6
6
|
*
|
|
7
7
|
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*
|
|
9
|
+
* v1.2.0 changelog:
|
|
10
|
+
* - emit 类型保真:string 入 string 出,number 入 number 出;空值一律 emit null
|
|
11
|
+
* - 内部 currentValue 保留中间态('-' / '0.' / ''),仅在 emit 时归一
|
|
8
12
|
*/
|
|
9
13
|
import { ref, watch } from 'vue';
|
|
10
14
|
import { InputNumberProps } from './props';
|
|
11
15
|
import { HTMLElementEvent } from '../../types/template';
|
|
16
|
+
import { type HookContext } from '../../types/hook';
|
|
12
17
|
|
|
13
18
|
export type InputNumber = string | number | null;
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
/** 用户输入中间态:不构成可 emit 的数字,emit null */
|
|
21
|
+
const MID_STATES = new Set(['', '-', '.', '-.']);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 把任意内部值归一为 number | null。
|
|
25
|
+
* 规则:
|
|
26
|
+
* - null/undefined/中间态 → null
|
|
27
|
+
* - 末尾小数点去掉('1.' → '1')
|
|
28
|
+
* - 非法 NaN → null
|
|
29
|
+
* - 其他 → Number(v)
|
|
30
|
+
*/
|
|
31
|
+
const normalize = (v: string | number | null | undefined): number | null => {
|
|
32
|
+
if (v === null || v === undefined) return null;
|
|
33
|
+
const s = String(v);
|
|
34
|
+
if (MID_STATES.has(s)) return null;
|
|
35
|
+
const cleaned = s.endsWith('.') ? s.slice(0, -1) : s;
|
|
36
|
+
const n = Number(cleaned);
|
|
37
|
+
return Number.isNaN(n) ? null : n;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function useInputNumber(props: Required<InputNumberProps>, ctx: HookContext) {
|
|
16
41
|
const currentValue = ref<string | number>(props.modelValue ?? '');
|
|
17
42
|
|
|
43
|
+
/**
|
|
44
|
+
* 按外部 modelValue 的当前 typeof 决定出参类型,保真 v-model 声明。
|
|
45
|
+
* - null/undefined → null
|
|
46
|
+
* - 外部当前是 string → emit String(n)
|
|
47
|
+
* - 否则 → emit n(默认 number)
|
|
48
|
+
*/
|
|
49
|
+
const toEmitValue = (n: number | null): number | string | null => {
|
|
50
|
+
if (n === null) return null;
|
|
51
|
+
return typeof props.modelValue === 'string' ? String(n) : n;
|
|
52
|
+
};
|
|
53
|
+
|
|
18
54
|
const updateInput = (oldVal: InputNumber) => {
|
|
19
|
-
|
|
20
|
-
ctx.emit('
|
|
55
|
+
const out = toEmitValue(normalize(currentValue.value));
|
|
56
|
+
ctx.emit('update:modelValue', out);
|
|
57
|
+
ctx.emit('change', out, oldVal);
|
|
21
58
|
};
|
|
22
59
|
|
|
23
60
|
const setCurrentValue = (newVal: string | number, e?: HTMLElementEvent<HTMLInputElement>) => {
|
|
24
61
|
const oldVal = currentValue.value;
|
|
25
62
|
const { min, max, precision } = props;
|
|
26
|
-
|
|
63
|
+
// 字符串归一比较:'1' 和 1 视为同值,避免跨类型重复 emit;'-' vs '-' 也命中
|
|
64
|
+
if (String(oldVal) === String(newVal)) {
|
|
27
65
|
return;
|
|
28
66
|
} else if (+newVal >= +max) {
|
|
29
67
|
// 超过最大值,截断到 max
|
|
@@ -75,18 +113,22 @@ export function useInputNumber(props: Required<InputNumberProps>, ctx: any) {
|
|
|
75
113
|
};
|
|
76
114
|
|
|
77
115
|
const handleInputBlur = () => {
|
|
116
|
+
const oldVal = currentValue.value;
|
|
78
117
|
// blur 时清理末尾的负号
|
|
79
118
|
if (currentValue.value === '-') {
|
|
80
119
|
currentValue.value = '';
|
|
81
120
|
} else if (currentValue.value === '-0') {
|
|
82
121
|
currentValue.value = 0;
|
|
122
|
+
} else {
|
|
123
|
+
const str = String(currentValue.value);
|
|
124
|
+
// 清理末尾小数点
|
|
125
|
+
if (str.indexOf('.') === str.length - 1 && str.length > 0) {
|
|
126
|
+
currentValue.value = str.replace(/\.$/g, '');
|
|
127
|
+
}
|
|
83
128
|
}
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
currentValue.value = str.indexOf('.') === str.length - 1
|
|
88
|
-
? str.replace(/\.$/g, '')
|
|
89
|
-
: currentValue.value;
|
|
129
|
+
// blur 后内部归一到数字(避免 '1' 和 1 并存)
|
|
130
|
+
const n = normalize(currentValue.value);
|
|
131
|
+
currentValue.value = n === null ? '' : n;
|
|
90
132
|
updateInput(oldVal);
|
|
91
133
|
};
|
|
92
134
|
|
|
@@ -10,6 +10,7 @@ import { Placement, PopperConfig, PositionStyle, usePopper } from '../../../comp
|
|
|
10
10
|
import useClickAway from '../../../compositions/popper/useClickAway';
|
|
11
11
|
import { PopoverProps } from './props';
|
|
12
12
|
import { onBeforeUnmount, onMounted, ref, type Ref, shallowRef } from 'vue';
|
|
13
|
+
import { type HookContext } from '../../types/hook';
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
export type IPopper = ReturnType<typeof usePopper>;
|
|
@@ -103,7 +104,7 @@ export class PopoverImpl {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
|
|
106
|
-
export function usePopover(props: Required<PopoverProps>, ctx:
|
|
107
|
+
export function usePopover(props: Required<PopoverProps>, ctx: HookContext) {
|
|
107
108
|
const style = ref();
|
|
108
109
|
const arrowStyle = ref();
|
|
109
110
|
const placement = ref<Placement>(props.placement);
|
|
@@ -175,8 +176,8 @@ export function usePopover(props: Required<PopoverProps>, ctx: any) {
|
|
|
175
176
|
|
|
176
177
|
onBeforeUnmount(() => {
|
|
177
178
|
if (clickAwayInstance) {
|
|
178
|
-
const {
|
|
179
|
-
|
|
179
|
+
const { onBeforeUnmount } = clickAwayInstance;
|
|
180
|
+
onBeforeUnmount();
|
|
180
181
|
}
|
|
181
182
|
instance?.destroy();
|
|
182
183
|
});
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
8
|
*/
|
|
9
9
|
import { ref } from 'vue';
|
|
10
|
+
import { type HookContext } from '../../types/hook';
|
|
10
11
|
import { RateProps } from './props';
|
|
11
12
|
|
|
12
|
-
export function useRate(props: Required<RateProps>, emit:
|
|
13
|
+
export function useRate(props: Required<RateProps>, emit: HookContext['emit']) {
|
|
13
14
|
// 悬停预览值,null 表示未悬停
|
|
14
15
|
const hoverValue = ref<number | null>(null);
|
|
15
16
|
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { computed, ref, watch, toRef, onBeforeUnmount } from 'vue';
|
|
15
15
|
import { SelectProps } from './props';
|
|
16
16
|
import { useSelectTools } from './useSelectTools';
|
|
17
|
+
import { type HookContext } from '../../types/hook';
|
|
17
18
|
|
|
18
19
|
export interface SelectOptionItem {
|
|
19
20
|
/** 原始值 */
|
|
@@ -24,7 +25,7 @@ export interface SelectOptionItem {
|
|
|
24
25
|
isSelected: boolean;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
export function useSelect(props: SelectProps, ctx:
|
|
28
|
+
export function useSelect(props: SelectProps, ctx: HookContext) {
|
|
28
29
|
const tools = useSelectTools(props);
|
|
29
30
|
const optionsRef = toRef(() => props.options ?? []);
|
|
30
31
|
const modelValueRef = toRef(() => props.modelValue);
|
|
@@ -10,8 +10,9 @@ import { onMounted, ref } from 'vue';
|
|
|
10
10
|
import { SliderProps } from './props';
|
|
11
11
|
import { useElementSize } from '../../../compositions/common/useElementSize.ts';
|
|
12
12
|
import useDrag, { DragOption, DragPosition, InteractEvent } from '../../../compositions/common/useDrag.ts';
|
|
13
|
+
import { type HookContext } from '../../types/hook';
|
|
13
14
|
|
|
14
|
-
export function useSlider(props: Required<SliderProps>, ctx:
|
|
15
|
+
export function useSlider(props: Required<SliderProps>, ctx: HookContext) {
|
|
15
16
|
const sliderRef = ref<HTMLElement | null>(null);
|
|
16
17
|
const sliderSize = useElementSize(sliderRef);
|
|
17
18
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description resolveStepStatus 测试
|
|
3
|
+
* @author 阿怪
|
|
4
|
+
* @date 2026/3/23
|
|
5
|
+
* @version v1.0.0
|
|
6
|
+
*
|
|
7
|
+
* 江湖的业务千篇一律,复杂的代码好几百行。
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { resolveStepStatus, type StepsContext } from '../useSteps';
|
|
11
|
+
|
|
12
|
+
const createCtx = (active: number): StepsContext => ({
|
|
13
|
+
active,
|
|
14
|
+
finishStatus: 'finish',
|
|
15
|
+
processStatus: 'process',
|
|
16
|
+
total: 4,
|
|
17
|
+
direction: 'horizontal',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('resolveStepStatus', () => {
|
|
21
|
+
it('显式状态优先', () => {
|
|
22
|
+
expect(resolveStepStatus(0, 'error', createCtx(2))).toBe('error');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('index < active 返回 finishStatus', () => {
|
|
26
|
+
expect(resolveStepStatus(0, undefined, createCtx(2))).toBe('finish');
|
|
27
|
+
expect(resolveStepStatus(1, undefined, createCtx(2))).toBe('finish');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('index === active 返回 processStatus', () => {
|
|
31
|
+
expect(resolveStepStatus(2, undefined, createCtx(2))).toBe('process');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('index > active 返回 wait', () => {
|
|
35
|
+
expect(resolveStepStatus(3, undefined, createCtx(2))).toBe('wait');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('active=0 时第一步为 process', () => {
|
|
39
|
+
expect(resolveStepStatus(0, undefined, createCtx(0))).toBe('process');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('自定义 finishStatus=success', () => {
|
|
43
|
+
const ctx = { ...createCtx(2), finishStatus: 'success' as const };
|
|
44
|
+
expect(resolveStepStatus(0, undefined, ctx)).toBe('success');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { SwitchProps } from './props';
|
|
10
10
|
import { computed, ref } from 'vue';
|
|
11
11
|
import { isEmpty } from '../../../tools';
|
|
12
|
+
import { type HookContext } from '../../types/hook';
|
|
12
13
|
|
|
13
14
|
export const switchIsBoolean = (value: SwitchProps['modelValue']) => {
|
|
14
15
|
return typeof value === 'boolean';
|
|
@@ -21,7 +22,7 @@ export const getIsActive = (value: SwitchProps['modelValue'], activeValue: Switc
|
|
|
21
22
|
|
|
22
23
|
export function useSwitch<
|
|
23
24
|
Props extends Record<string, any>,
|
|
24
|
-
>(props: Props, ctx:
|
|
25
|
+
>(props: Props, ctx: HookContext) {
|
|
25
26
|
const activeValue = ref(props.activeValue);
|
|
26
27
|
const inactiveValue = ref(props.inactiveValue);
|
|
27
28
|
const { slots, emit } = ctx;
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { computed } from 'vue';
|
|
10
10
|
import { TabsProps } from './props';
|
|
11
|
+
import { type HookContext } from '../../types/hook';
|
|
11
12
|
|
|
12
13
|
export function useTabs<
|
|
13
14
|
Props extends Record<string, unknown>,
|
|
14
|
-
>(props: Props, ctx:
|
|
15
|
+
>(props: Props, ctx: HookContext) {
|
|
15
16
|
const { emit } = ctx;
|
|
16
17
|
|
|
17
18
|
/** 当前激活的标签名 */
|
|
@@ -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
|
+
});
|
|
@@ -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:
|
|
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
|
+
});
|