papayaui 0.3.3 → 0.3.5
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/cell/cell.scss +4 -0
- package/components/cell/cell.vue +14 -2
- package/components/cell/props.ts +4 -0
- package/components/picker-popup/picker-popup.vue +3 -2
- package/components/picker-popup/props.ts +5 -0
- package/components/textarea/textarea.scss +1 -1
- package/core/index.ts +1 -0
- package/core/useModel/index.test.ts +130 -0
- package/core/useModel/index.ts +40 -0
- package/core/useSelect/index.test.ts +280 -0
- package/core/useSelect/index.ts +123 -31
- package/package.json +1 -1
- package/utils/common.ts +16 -1
- package/vitest.config.ts +14 -0
- package/vitest.setup.ts +8 -0
package/components/cell/cell.vue
CHANGED
|
@@ -31,8 +31,13 @@
|
|
|
31
31
|
})
|
|
32
32
|
"
|
|
33
33
|
>
|
|
34
|
-
<text
|
|
35
|
-
|
|
34
|
+
<text
|
|
35
|
+
v-if="!$slots.default"
|
|
36
|
+
:class="{ [ns.e('placeholder')]: showPlaceholder }"
|
|
37
|
+
:selectable="selectable"
|
|
38
|
+
:user-select="selectable"
|
|
39
|
+
>
|
|
40
|
+
{{ showPlaceholder ? placeholder : value }}
|
|
36
41
|
</text>
|
|
37
42
|
<slot v-else />
|
|
38
43
|
|
|
@@ -49,6 +54,7 @@
|
|
|
49
54
|
</template>
|
|
50
55
|
|
|
51
56
|
<script lang="ts" setup>
|
|
57
|
+
import { computed } from 'vue'
|
|
52
58
|
import useNamespace from '../../core/useNamespace'
|
|
53
59
|
import { getUnitValue } from '../../utils'
|
|
54
60
|
import IconComponent from '../icon/icon.vue'
|
|
@@ -59,6 +65,12 @@ const ns = useNamespace('cell')
|
|
|
59
65
|
const props = defineProps(cellProps)
|
|
60
66
|
const emit = defineEmits(cellEmits)
|
|
61
67
|
|
|
68
|
+
const showPlaceholder = computed(
|
|
69
|
+
() =>
|
|
70
|
+
props.placeholder &&
|
|
71
|
+
(props.value === null || props.value === undefined || props.value === '')
|
|
72
|
+
)
|
|
73
|
+
|
|
62
74
|
const onClick = (event: MouseEvent) => {
|
|
63
75
|
if (!props.clickable && !props.isLink) return
|
|
64
76
|
emit('click', event)
|
package/components/cell/props.ts
CHANGED
|
@@ -82,7 +82,8 @@
|
|
|
82
82
|
|
|
83
83
|
<script lang="ts" setup>
|
|
84
84
|
import { computed, ref, toRefs, watch } from 'vue'
|
|
85
|
-
import {
|
|
85
|
+
import { LoadStatusEnum, useList } from '../../core/useList'
|
|
86
|
+
import { useModel } from '../../core/useModel'
|
|
86
87
|
import useNamespace from '../../core/useNamespace'
|
|
87
88
|
import { useSelect } from '../../core/useSelect'
|
|
88
89
|
import { debounce } from '../../utils'
|
|
@@ -102,7 +103,7 @@ const emit = defineEmits(pickerPopupEmits)
|
|
|
102
103
|
|
|
103
104
|
const { show, data, modelValue, labelKey, valueKey, multiple, pagination } = toRefs(props)
|
|
104
105
|
|
|
105
|
-
const searchText =
|
|
106
|
+
const searchText = useModel<string>(props, 'searchValue', emit, '')
|
|
106
107
|
const createOptions = ref<Option[]>([])
|
|
107
108
|
|
|
108
109
|
const {
|
|
@@ -38,6 +38,10 @@ export const pickerPopupProps = {
|
|
|
38
38
|
* 是否显示搜索
|
|
39
39
|
*/
|
|
40
40
|
showSearch: Boolean,
|
|
41
|
+
/**
|
|
42
|
+
* 搜索文本
|
|
43
|
+
*/
|
|
44
|
+
searchValue: String,
|
|
41
45
|
/**
|
|
42
46
|
* 搜索框的props
|
|
43
47
|
*/
|
|
@@ -125,6 +129,7 @@ export const pickerPopupEmits = {
|
|
|
125
129
|
...bottomPopupEmits,
|
|
126
130
|
'update:modelValue': (value: OptionValue | OptionValue[]) =>
|
|
127
131
|
isString(value) || isNumber(value) || isArray(value),
|
|
132
|
+
'update:searchValue': (value: string) => isString(value),
|
|
128
133
|
select: (value: OptionValue, _item: Option) => !isUndefined(value),
|
|
129
134
|
change: (item: Option | Option[]) => !isUndefined(item),
|
|
130
135
|
/**
|
package/core/index.ts
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
2
|
+
import { reactive } from 'vue'
|
|
3
|
+
import { useModel } from '.'
|
|
4
|
+
|
|
5
|
+
describe('useModel test', () => {
|
|
6
|
+
test('should use default value when no value prop provided', () => {
|
|
7
|
+
const props = reactive<{ modelValue?: string }>({})
|
|
8
|
+
const emit = vi.fn()
|
|
9
|
+
const model = useModel(props, 'modelValue', emit, 'default')
|
|
10
|
+
|
|
11
|
+
expect(model.value).toBe('default')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('should use external value when value prop provided', () => {
|
|
15
|
+
const props = reactive({ modelValue: 'external' })
|
|
16
|
+
const emit = vi.fn()
|
|
17
|
+
const model = useModel(props, 'modelValue', emit)
|
|
18
|
+
|
|
19
|
+
expect(model.value).toBe('external')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('should update internal value when no external value', () => {
|
|
23
|
+
const props = reactive<{ modelValue?: string }>({})
|
|
24
|
+
const emit = vi.fn()
|
|
25
|
+
const model = useModel(props, 'modelValue', emit, 'initial')
|
|
26
|
+
|
|
27
|
+
model.value = 'updated'
|
|
28
|
+
expect(model.value).toBe('updated')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('should emit update event when value changes', () => {
|
|
32
|
+
const props = reactive<{ modelValue?: string }>({})
|
|
33
|
+
const emit = vi.fn()
|
|
34
|
+
const model = useModel(props, 'modelValue', emit, 'initial')
|
|
35
|
+
|
|
36
|
+
model.value = 'new value'
|
|
37
|
+
expect(emit).toHaveBeenCalledWith('update:modelValue', 'new value')
|
|
38
|
+
expect(emit).toHaveBeenCalledTimes(1)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('should work with external reactive value', () => {
|
|
42
|
+
const props = reactive({ modelValue: 'external' })
|
|
43
|
+
const emit = vi.fn()
|
|
44
|
+
const model = useModel(props, 'modelValue', emit)
|
|
45
|
+
|
|
46
|
+
expect(model.value).toBe('external')
|
|
47
|
+
|
|
48
|
+
model.value = 'new value'
|
|
49
|
+
expect(emit).toHaveBeenCalledWith('update:modelValue', 'new value')
|
|
50
|
+
expect(model.value).toBe('external') // 仍然是外部值,因为外部值没变
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('should switch between internal and external values', () => {
|
|
54
|
+
const props = reactive({ modelValue: 'external' as string | undefined })
|
|
55
|
+
const emit = vi.fn()
|
|
56
|
+
const model = useModel(props, 'modelValue', emit, 'default')
|
|
57
|
+
|
|
58
|
+
expect(model.value).toBe('external')
|
|
59
|
+
|
|
60
|
+
// 模拟外部值变为 undefined
|
|
61
|
+
props.modelValue = undefined
|
|
62
|
+
expect(model.value).toBe('default')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('should work with number type', () => {
|
|
66
|
+
const props = reactive<{ count?: number }>({})
|
|
67
|
+
const emit = vi.fn()
|
|
68
|
+
const model = useModel(props, 'count', emit, 0)
|
|
69
|
+
|
|
70
|
+
expect(model.value).toBe(0)
|
|
71
|
+
|
|
72
|
+
model.value = 42
|
|
73
|
+
expect(model.value).toBe(42)
|
|
74
|
+
expect(emit).toHaveBeenCalledWith('update:count', 42)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('should work with object type', () => {
|
|
78
|
+
const props = reactive<{ data?: { name: string } }>({})
|
|
79
|
+
const emit = vi.fn()
|
|
80
|
+
const defaultObj = { name: 'test' }
|
|
81
|
+
const model = useModel(props, 'data', emit, defaultObj)
|
|
82
|
+
|
|
83
|
+
expect(model.value).toEqual({ name: 'test' })
|
|
84
|
+
|
|
85
|
+
const newObj = { name: 'updated' }
|
|
86
|
+
model.value = newObj
|
|
87
|
+
expect(model.value).toEqual({ name: 'updated' })
|
|
88
|
+
expect(emit).toHaveBeenCalledWith('update:data', newObj)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('should work with array type', () => {
|
|
92
|
+
const props = reactive<{ items?: string[] }>({})
|
|
93
|
+
const emit = vi.fn()
|
|
94
|
+
const model = useModel<string[]>(props, 'items', emit, [])
|
|
95
|
+
|
|
96
|
+
expect(model.value).toEqual([])
|
|
97
|
+
|
|
98
|
+
model.value = ['a', 'b', 'c']
|
|
99
|
+
expect(model.value).toEqual(['a', 'b', 'c'])
|
|
100
|
+
expect(emit).toHaveBeenCalledWith('update:items', ['a', 'b', 'c'])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('should handle undefined default value', () => {
|
|
104
|
+
const props = reactive<{ value?: string }>({})
|
|
105
|
+
const emit = vi.fn()
|
|
106
|
+
const model = useModel(props, 'value', emit)
|
|
107
|
+
|
|
108
|
+
expect(model.value).toBeUndefined()
|
|
109
|
+
|
|
110
|
+
model.value = 'value'
|
|
111
|
+
expect(model.value).toBe('value')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('should work with multiple properties', () => {
|
|
115
|
+
const props = reactive<{ name?: string; age?: number }>({})
|
|
116
|
+
const emit = vi.fn()
|
|
117
|
+
|
|
118
|
+
const name = useModel(props, 'name', emit, 'John')
|
|
119
|
+
const age = useModel(props, 'age', emit, 18)
|
|
120
|
+
|
|
121
|
+
expect(name.value).toBe('John')
|
|
122
|
+
expect(age.value).toBe(18)
|
|
123
|
+
|
|
124
|
+
name.value = 'Jane'
|
|
125
|
+
age.value = 20
|
|
126
|
+
|
|
127
|
+
expect(emit).toHaveBeenCalledWith('update:name', 'Jane')
|
|
128
|
+
expect(emit).toHaveBeenCalledWith('update:age', 20)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 双向绑定 hook,类似 Vue 3.4+ 的 defineModel
|
|
6
|
+
* 如果外部传入了 value,则使用外部的值;否则使用内部维护的值
|
|
7
|
+
*
|
|
8
|
+
* @param props - 组件的 props 对象
|
|
9
|
+
* @param key - 要绑定的 prop 名称
|
|
10
|
+
* @param emit - emit 函数
|
|
11
|
+
* @param defaultValue - 默认值(当 props[key] 为 undefined 时使用)
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const props = defineProps<{ modelValue?: string }>()
|
|
16
|
+
* const emit = defineEmits(['update:modelValue'])
|
|
17
|
+
* const data = useModel<string>(props, 'modelValue', emit, '')
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function useModel<T, P extends Record<string, any> = any, K extends keyof P = any>(
|
|
21
|
+
props: P,
|
|
22
|
+
key: K,
|
|
23
|
+
emit: (event: any, value: any) => void,
|
|
24
|
+
defaultValue?: T,
|
|
25
|
+
): Ref<T> {
|
|
26
|
+
const internalValue = ref<T>(defaultValue as T) as Ref<T>
|
|
27
|
+
|
|
28
|
+
const model = computed<T>({
|
|
29
|
+
get: () => {
|
|
30
|
+
console.log('props[key]:', key, props[key])
|
|
31
|
+
return props[key] !== undefined ? props[key] : internalValue.value
|
|
32
|
+
},
|
|
33
|
+
set: (value: T) => {
|
|
34
|
+
internalValue.value = value
|
|
35
|
+
emit(`update:${key as string}`, value)
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return model
|
|
40
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { nextTick, ref } from 'vue'
|
|
2
3
|
import { useSelect } from '.'
|
|
3
4
|
|
|
4
5
|
interface Option {
|
|
@@ -94,4 +95,283 @@ describe('useSelect test', () => {
|
|
|
94
95
|
|
|
95
96
|
expect(selectedValues.value).toEqual([1, 2])
|
|
96
97
|
})
|
|
98
|
+
|
|
99
|
+
test('should onClear work correctly', async () => {
|
|
100
|
+
const { selectedValues, onClear, isSelected } = useSelect<Option, Option['value']>({
|
|
101
|
+
options,
|
|
102
|
+
multiple: true,
|
|
103
|
+
defaultValue: [1, 2],
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
expect(selectedValues.value).toEqual([1, 2])
|
|
107
|
+
expect(isSelected(1)).toBe(true)
|
|
108
|
+
expect(isSelected(2)).toBe(true)
|
|
109
|
+
|
|
110
|
+
onClear()
|
|
111
|
+
|
|
112
|
+
expect(selectedValues.value).toEqual([])
|
|
113
|
+
expect(isSelected(1)).toBe(false)
|
|
114
|
+
expect(isSelected(2)).toBe(false)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('should onClear work in single mode', async () => {
|
|
118
|
+
const { selectedValues, onClear, isSelected } = useSelect<Option, Option['value']>({
|
|
119
|
+
options,
|
|
120
|
+
defaultValue: 1,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(selectedValues.value).toBe(1)
|
|
124
|
+
expect(isSelected(1)).toBe(true)
|
|
125
|
+
|
|
126
|
+
onClear()
|
|
127
|
+
|
|
128
|
+
expect(selectedValues.value).toBe(undefined)
|
|
129
|
+
expect(isSelected(1)).toBe(false)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('should custom valueKey work correctly', async () => {
|
|
133
|
+
interface CustomOption {
|
|
134
|
+
label: string
|
|
135
|
+
id: number
|
|
136
|
+
}
|
|
137
|
+
const customOptions: CustomOption[] = [
|
|
138
|
+
{ label: 'a', id: 10 },
|
|
139
|
+
{ label: 'b', id: 20 },
|
|
140
|
+
{ label: 'c', id: 30 },
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
const { selectedItems, selectedValues, onSelect, isSelected } = useSelect<
|
|
144
|
+
CustomOption,
|
|
145
|
+
CustomOption['id']
|
|
146
|
+
>({
|
|
147
|
+
options: customOptions,
|
|
148
|
+
valueKey: 'id',
|
|
149
|
+
defaultValue: 10,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
expect(selectedItems.value).toEqual(customOptions[0])
|
|
153
|
+
expect(selectedValues.value).toBe(10)
|
|
154
|
+
expect(isSelected(10)).toBe(true)
|
|
155
|
+
|
|
156
|
+
expect(onSelect(20)).toBe(true)
|
|
157
|
+
expect(selectedItems.value).toEqual(customOptions[1])
|
|
158
|
+
expect(selectedValues.value).toBe(20)
|
|
159
|
+
expect(isSelected(10)).toBe(false)
|
|
160
|
+
expect(isSelected(20)).toBe(true)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('should defaultValue be reactive', async () => {
|
|
164
|
+
const defaultValue = ref<number | undefined>(1)
|
|
165
|
+
const { selectedValues, isSelected } = useSelect<Option, Option['value']>({
|
|
166
|
+
options,
|
|
167
|
+
defaultValue,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
expect(selectedValues.value).toBe(1)
|
|
171
|
+
expect(isSelected(1)).toBe(true)
|
|
172
|
+
|
|
173
|
+
// 修改 defaultValue,应该触发响应式更新
|
|
174
|
+
defaultValue.value = 2
|
|
175
|
+
await nextTick() // 等待响应式更新
|
|
176
|
+
|
|
177
|
+
expect(selectedValues.value).toBe(2)
|
|
178
|
+
expect(isSelected(1)).toBe(false)
|
|
179
|
+
expect(isSelected(2)).toBe(true)
|
|
180
|
+
|
|
181
|
+
// 修改为 undefined
|
|
182
|
+
defaultValue.value = undefined
|
|
183
|
+
await nextTick()
|
|
184
|
+
|
|
185
|
+
expect(selectedValues.value).toBe(undefined)
|
|
186
|
+
expect(isSelected(2)).toBe(false)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('should single mode without inverse not allow deselect', async () => {
|
|
190
|
+
const { selectedValues, onSelect, isSelected } = useSelect<Option, Option['value']>({
|
|
191
|
+
options,
|
|
192
|
+
inverse: false, // 明确不支持反选
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
expect(selectedValues.value).toBe(undefined)
|
|
196
|
+
|
|
197
|
+
expect(onSelect(1)).toBe(true)
|
|
198
|
+
expect(isSelected(1)).toBe(true)
|
|
199
|
+
expect(selectedValues.value).toBe(1)
|
|
200
|
+
|
|
201
|
+
// 再次选择已选中的项,不应该取消选择
|
|
202
|
+
expect(onSelect(1)).toBe(false)
|
|
203
|
+
expect(isSelected(1)).toBe(true)
|
|
204
|
+
expect(selectedValues.value).toBe(1)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('should multiple mode always allow deselect', async () => {
|
|
208
|
+
const { selectedValues, onSelect, isSelected } = useSelect<Option, Option['value']>({
|
|
209
|
+
options,
|
|
210
|
+
multiple: true,
|
|
211
|
+
inverse: false, // 即使设置为 false,多选也应该支持反选
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
expect(selectedValues.value).toEqual([])
|
|
215
|
+
|
|
216
|
+
expect(onSelect(1)).toBe(true)
|
|
217
|
+
expect(onSelect(2)).toBe(true)
|
|
218
|
+
expect(isSelected(1)).toBe(true)
|
|
219
|
+
expect(isSelected(2)).toBe(true)
|
|
220
|
+
expect(selectedValues.value).toEqual([1, 2])
|
|
221
|
+
|
|
222
|
+
// 多选模式下应该可以取消选择
|
|
223
|
+
expect(onSelect(1)).toBe(false)
|
|
224
|
+
expect(isSelected(1)).toBe(false)
|
|
225
|
+
expect(isSelected(2)).toBe(true)
|
|
226
|
+
expect(selectedValues.value).toEqual([2])
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('should work with empty options', async () => {
|
|
230
|
+
const { selectedItems, selectedValues, onSelect } = useSelect<Option, Option['value']>({
|
|
231
|
+
options: [],
|
|
232
|
+
defaultValue: 1,
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// 虽然 defaultValue 是 1,但 options 为空,所以不会匹配到任何项
|
|
236
|
+
expect(selectedItems.value).toBe(undefined)
|
|
237
|
+
expect(selectedValues.value).toBe(1)
|
|
238
|
+
|
|
239
|
+
expect(onSelect(2)).toBe(true)
|
|
240
|
+
expect(selectedValues.value).toBe(2)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('should max = 0 prevent any selection', async () => {
|
|
244
|
+
const { selectedValues, onSelect, isSelected } = useSelect<Option, Option['value']>({
|
|
245
|
+
options,
|
|
246
|
+
multiple: true,
|
|
247
|
+
max: 0,
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
expect(selectedValues.value).toEqual([])
|
|
251
|
+
|
|
252
|
+
expect(onSelect(1)).toBe(false)
|
|
253
|
+
expect(onSelect(2)).toBe(false)
|
|
254
|
+
expect(onSelect(3)).toBe(false)
|
|
255
|
+
|
|
256
|
+
expect(isSelected(1)).toBe(false)
|
|
257
|
+
expect(isSelected(2)).toBe(false)
|
|
258
|
+
expect(isSelected(3)).toBe(false)
|
|
259
|
+
|
|
260
|
+
expect(selectedValues.value).toEqual([])
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('should max = 1 in multiple mode work correctly', async () => {
|
|
264
|
+
const { selectedValues, onSelect, isSelected } = useSelect<Option, Option['value']>({
|
|
265
|
+
options,
|
|
266
|
+
multiple: true,
|
|
267
|
+
max: 1,
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
expect(selectedValues.value).toEqual([])
|
|
271
|
+
|
|
272
|
+
expect(onSelect(1)).toBe(true)
|
|
273
|
+
expect(onSelect(2)).toBe(false) // 应该失败,因为已经达到上限
|
|
274
|
+
|
|
275
|
+
expect(isSelected(1)).toBe(true)
|
|
276
|
+
expect(isSelected(2)).toBe(false)
|
|
277
|
+
|
|
278
|
+
expect(selectedValues.value).toEqual([1])
|
|
279
|
+
|
|
280
|
+
// 取消选择后,应该可以选择其他的
|
|
281
|
+
expect(onSelect(1)).toBe(false)
|
|
282
|
+
expect(selectedValues.value).toEqual([])
|
|
283
|
+
|
|
284
|
+
expect(onSelect(2)).toBe(true)
|
|
285
|
+
expect(selectedValues.value).toEqual([2])
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('should handle undefined defaultValue correctly', async () => {
|
|
289
|
+
const { selectedValues, isSelected } = useSelect<Option, Option['value']>({
|
|
290
|
+
options,
|
|
291
|
+
defaultValue: undefined,
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
expect(selectedValues.value).toBe(undefined)
|
|
295
|
+
expect(isSelected(1)).toBe(false)
|
|
296
|
+
expect(isSelected(2)).toBe(false)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
test('should handle array with undefined values in defaultValue', async () => {
|
|
300
|
+
const { selectedValues, isSelected } = useSelect<Option, Option['value']>({
|
|
301
|
+
options,
|
|
302
|
+
multiple: true,
|
|
303
|
+
defaultValue: [1, undefined, 2] as any,
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// undefined 应该被过滤掉
|
|
307
|
+
expect(selectedValues.value).toEqual([1, 2])
|
|
308
|
+
expect(isSelected(1)).toBe(true)
|
|
309
|
+
expect(isSelected(2)).toBe(true)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test('should selecting already selected item return false', async () => {
|
|
313
|
+
const { onSelect } = useSelect<Option, Option['value']>({
|
|
314
|
+
options,
|
|
315
|
+
multiple: true,
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
expect(onSelect(1)).toBe(true)
|
|
319
|
+
expect(onSelect(1)).toBe(false) // 再次选择应该返回 false(取消选择)
|
|
320
|
+
expect(onSelect(1)).toBe(true) // 再次选择应该返回 true(重新选择)
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test('should return correct value when reaching max limit', async () => {
|
|
324
|
+
const { onSelect } = useSelect<Option, Option['value']>({
|
|
325
|
+
options,
|
|
326
|
+
multiple: true,
|
|
327
|
+
max: 2,
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
expect(onSelect(1)).toBe(true)
|
|
331
|
+
expect(onSelect(2)).toBe(true)
|
|
332
|
+
expect(onSelect(3)).toBe(false) // 达到上限,返回 false
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('should handle single select mode switching selection', async () => {
|
|
336
|
+
const { selectedValues, onSelect, isSelected } = useSelect<Option, Option['value']>({
|
|
337
|
+
options,
|
|
338
|
+
multiple: false,
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
expect(onSelect(1)).toBe(true)
|
|
342
|
+
expect(selectedValues.value).toBe(1)
|
|
343
|
+
expect(isSelected(1)).toBe(true)
|
|
344
|
+
|
|
345
|
+
// 切换到另一个选项
|
|
346
|
+
expect(onSelect(2)).toBe(true)
|
|
347
|
+
expect(selectedValues.value).toBe(2)
|
|
348
|
+
expect(isSelected(1)).toBe(false)
|
|
349
|
+
expect(isSelected(2)).toBe(true)
|
|
350
|
+
|
|
351
|
+
// 再切换到第三个选项
|
|
352
|
+
expect(onSelect(3)).toBe(true)
|
|
353
|
+
expect(selectedValues.value).toBe(3)
|
|
354
|
+
expect(isSelected(1)).toBe(false)
|
|
355
|
+
expect(isSelected(2)).toBe(false)
|
|
356
|
+
expect(isSelected(3)).toBe(true)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test('should work without options prop', async () => {
|
|
360
|
+
const { selectedItems, selectedValues, onSelect, isSelected } = useSelect<
|
|
361
|
+
Option,
|
|
362
|
+
Option['value']
|
|
363
|
+
>({})
|
|
364
|
+
|
|
365
|
+
// options 为 undefined 时,selectedItems 应该返回 undefined
|
|
366
|
+
expect(selectedItems.value).toBe(undefined)
|
|
367
|
+
expect(selectedValues.value).toBe(undefined)
|
|
368
|
+
|
|
369
|
+
// 仍然可以选择值
|
|
370
|
+
expect(onSelect(1)).toBe(true)
|
|
371
|
+
expect(selectedValues.value).toBe(1)
|
|
372
|
+
expect(isSelected(1)).toBe(true)
|
|
373
|
+
|
|
374
|
+
// 但 selectedItems 仍为 undefined(因为没有 options 可以匹配)
|
|
375
|
+
expect(selectedItems.value).toBe(undefined)
|
|
376
|
+
})
|
|
97
377
|
})
|
package/core/useSelect/index.ts
CHANGED
|
@@ -1,45 +1,131 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { computed,
|
|
3
|
-
import type {
|
|
1
|
+
import type { ComputedRef } from 'vue'
|
|
2
|
+
import { computed, shallowRef, watch } from 'vue'
|
|
3
|
+
import type { MaybeRef } from '../../types'
|
|
4
|
+
import { toRefValue } from '../../utils'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
/** 基础 Props 类型 */
|
|
7
|
+
interface UseSelectBaseProps<T> {
|
|
6
8
|
/** 选项数据 */
|
|
7
9
|
options?: T[]
|
|
8
|
-
/** 默认数据 */
|
|
9
|
-
defaultValue?: V | V[]
|
|
10
10
|
/**
|
|
11
11
|
* 数据值的字段名
|
|
12
12
|
* @default value
|
|
13
13
|
*/
|
|
14
14
|
valueKey?: keyof T
|
|
15
|
-
/** 是否多选 */
|
|
16
|
-
multiple?: boolean
|
|
17
15
|
/** 是否支持反选(只在单选有效,多选强制支持) */
|
|
18
16
|
inverse?: boolean
|
|
19
17
|
/** 最大可选数量 */
|
|
20
18
|
max?: number
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
/** 单选模式 Props */
|
|
22
|
+
export interface UseSelectSingleProps<T, V> extends UseSelectBaseProps<T> {
|
|
23
|
+
/** 是否多选 */
|
|
24
|
+
multiple?: false
|
|
25
|
+
/** 默认数据 */
|
|
26
|
+
defaultValue?: V
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 多选模式 Props */
|
|
30
|
+
export interface UseSelectMultipleProps<T, V> extends UseSelectBaseProps<T> {
|
|
31
|
+
/** 是否多选 */
|
|
32
|
+
multiple: true
|
|
33
|
+
/** 默认数据 */
|
|
34
|
+
defaultValue?: V[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 动态模式 Props(multiple 为运行时 boolean) */
|
|
38
|
+
export interface UseSelectDynamicProps<T, V> extends UseSelectBaseProps<T> {
|
|
39
|
+
/** 是否多选 */
|
|
40
|
+
multiple?: boolean
|
|
41
|
+
/** 默认数据 */
|
|
42
|
+
defaultValue?: V | V[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 合并的 Props 类型 */
|
|
46
|
+
export type UseSelectProps<T, V> =
|
|
47
|
+
| UseSelectSingleProps<T, V>
|
|
48
|
+
| UseSelectMultipleProps<T, V>
|
|
49
|
+
| UseSelectDynamicProps<T, V>
|
|
50
|
+
|
|
51
|
+
/** 将 Props 的每个属性转为 MaybeRef */
|
|
52
|
+
type UseSelectPropsWithRefs<T, V> = {
|
|
53
|
+
[K in keyof UseSelectProps<T, V>]: MaybeRef<UseSelectProps<T, V>[K]>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 单选模式返回类型 */
|
|
57
|
+
interface UseSelectSingleReturn<T, V> {
|
|
58
|
+
selectedItems: ComputedRef<T>
|
|
59
|
+
selectedValues: ComputedRef<V>
|
|
60
|
+
onSelect: (value: V) => boolean
|
|
61
|
+
isSelected: (value: V) => boolean
|
|
62
|
+
onClear: () => void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 多选模式返回类型 */
|
|
66
|
+
interface UseSelectMultipleReturn<T, V> {
|
|
67
|
+
selectedItems: ComputedRef<T[]>
|
|
68
|
+
selectedValues: ComputedRef<V[]>
|
|
69
|
+
onSelect: (value: V) => boolean
|
|
70
|
+
isSelected: (value: V) => boolean
|
|
71
|
+
onClear: () => void
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 动态模式返回类型(multiple 为运行时值时) */
|
|
75
|
+
interface UseSelectDynamicReturn<T, V> {
|
|
76
|
+
selectedItems: ComputedRef<T | T[]>
|
|
77
|
+
selectedValues: ComputedRef<V | V[]>
|
|
78
|
+
onSelect: (value: V) => boolean
|
|
79
|
+
isSelected: (value: V) => boolean
|
|
80
|
+
onClear: () => void
|
|
81
|
+
}
|
|
25
82
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
>
|
|
31
|
-
const multiple = toRef(props, 'multiple') as Ref<OwnProps['multiple']>
|
|
32
|
-
const inverse = toRef(props, 'inverse') as Ref<OwnProps['inverse']>
|
|
33
|
-
const max = toRef(props, 'max') as Ref<OwnProps['max']>
|
|
83
|
+
/** 合并的返回类型 */
|
|
84
|
+
export type UseSelectReturn<T, V> =
|
|
85
|
+
| UseSelectSingleReturn<T, V>
|
|
86
|
+
| UseSelectMultipleReturn<T, V>
|
|
87
|
+
| UseSelectDynamicReturn<T, V>
|
|
34
88
|
|
|
35
|
-
|
|
89
|
+
/** 单选模式函数重载 */
|
|
90
|
+
export function useSelect<T, V = T>(props: {
|
|
91
|
+
[K in keyof UseSelectSingleProps<T, V>]: MaybeRef<UseSelectSingleProps<T, V>[K]>
|
|
92
|
+
}): UseSelectSingleReturn<T, V>
|
|
36
93
|
|
|
37
|
-
|
|
38
|
-
|
|
94
|
+
/** 多选模式函数重载 */
|
|
95
|
+
export function useSelect<T, V = T>(props: {
|
|
96
|
+
[K in keyof UseSelectMultipleProps<T, V>]: MaybeRef<UseSelectMultipleProps<T, V>[K]>
|
|
97
|
+
}): UseSelectMultipleReturn<T, V>
|
|
98
|
+
|
|
99
|
+
/** 动态模式函数重载(multiple 为运行时 boolean) */
|
|
100
|
+
export function useSelect<T, V = T>(props: {
|
|
101
|
+
[K in keyof UseSelectDynamicProps<T, V>]: MaybeRef<UseSelectDynamicProps<T, V>[K]>
|
|
102
|
+
}): UseSelectDynamicReturn<T, V>
|
|
103
|
+
|
|
104
|
+
/** 实现 */
|
|
105
|
+
export function useSelect<T, V = T>(props: UseSelectPropsWithRefs<T, V>): UseSelectReturn<T, V> {
|
|
106
|
+
const options = toRefValue(props.options)
|
|
107
|
+
const defaultValue = toRefValue(props.defaultValue)
|
|
108
|
+
const valueKey = toRefValue(props.valueKey, 'value' as keyof T)
|
|
109
|
+
const multiple = toRefValue(props.multiple)
|
|
110
|
+
const inverse = toRefValue(props.inverse)
|
|
111
|
+
const max = toRefValue(props.max)
|
|
112
|
+
|
|
113
|
+
// 使用 shallowRef 避免 Vue 对 Set 内部进行深度响应式转换
|
|
114
|
+
const selectedSet = shallowRef<Set<V>>(new Set())
|
|
115
|
+
|
|
116
|
+
const getOptions = (): T[] => options.value ?? []
|
|
117
|
+
|
|
118
|
+
// 触发响应式更新的辅助函数
|
|
119
|
+
const triggerUpdate = (): void => {
|
|
120
|
+
selectedSet.value = new Set(selectedSet.value)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const selectedItems = computed(() => {
|
|
124
|
+
const result = getOptions().filter((item) => selectedSet.value.has(item[valueKey.value] as V))
|
|
39
125
|
return multiple.value ? result : result[0]
|
|
40
126
|
})
|
|
41
127
|
|
|
42
|
-
const selectedValues = computed
|
|
128
|
+
const selectedValues = computed(() => {
|
|
43
129
|
const result = [...selectedSet.value.values()]
|
|
44
130
|
return multiple.value ? result : result[0]
|
|
45
131
|
})
|
|
@@ -47,27 +133,32 @@ export function useSelect<T, V = T>(props: IncludeRefs<UseSelectProps<T, V>>) {
|
|
|
47
133
|
watch(
|
|
48
134
|
defaultValue,
|
|
49
135
|
(value) => {
|
|
136
|
+
const newSet = new Set<V>()
|
|
50
137
|
const defaultValues = Array.isArray(value) ? value : [value]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
selectedSet.value.add(value)
|
|
138
|
+
defaultValues.forEach((v) => {
|
|
139
|
+
if (typeof v !== 'undefined') {
|
|
140
|
+
newSet.add(v)
|
|
55
141
|
}
|
|
56
142
|
})
|
|
143
|
+
selectedSet.value = newSet
|
|
57
144
|
},
|
|
58
145
|
{
|
|
59
146
|
immediate: true,
|
|
60
147
|
},
|
|
61
148
|
)
|
|
62
149
|
|
|
63
|
-
const isSelected = (value: V) => {
|
|
150
|
+
const isSelected = (value: V): boolean => {
|
|
64
151
|
return selectedSet.value.has(value)
|
|
65
152
|
}
|
|
66
153
|
|
|
67
|
-
const onSelect = (value: V) => {
|
|
154
|
+
const onSelect = (value: V): boolean => {
|
|
68
155
|
// 已选中情况
|
|
69
156
|
if (selectedSet.value.has(value)) {
|
|
70
|
-
|
|
157
|
+
const canDeselect = multiple.value ? true : inverse.value
|
|
158
|
+
if (canDeselect) {
|
|
159
|
+
selectedSet.value.delete(value)
|
|
160
|
+
triggerUpdate()
|
|
161
|
+
}
|
|
71
162
|
return false
|
|
72
163
|
}
|
|
73
164
|
// 超过最大选中数量限制情况
|
|
@@ -79,11 +170,12 @@ export function useSelect<T, V = T>(props: IncludeRefs<UseSelectProps<T, V>>) {
|
|
|
79
170
|
selectedSet.value.clear()
|
|
80
171
|
}
|
|
81
172
|
selectedSet.value.add(value)
|
|
173
|
+
triggerUpdate()
|
|
82
174
|
return true
|
|
83
175
|
}
|
|
84
176
|
|
|
85
|
-
const onClear = () => {
|
|
86
|
-
selectedSet.value
|
|
177
|
+
const onClear = (): void => {
|
|
178
|
+
selectedSet.value = new Set()
|
|
87
179
|
}
|
|
88
180
|
|
|
89
181
|
return {
|
package/package.json
CHANGED
package/utils/common.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { isRef } from 'vue'
|
|
1
|
+
import { computed, isRef, ref } from 'vue'
|
|
2
|
+
import type { Ref } from 'vue'
|
|
2
3
|
import type { MaybeRef } from '../types'
|
|
3
4
|
|
|
4
5
|
/** 判断传入的值,是否带有单位,如果没有,就默认用rpx单位 */
|
|
@@ -83,3 +84,17 @@ export function getRefValue<T>(
|
|
|
83
84
|
const value = isRef(prop) ? prop.value : prop
|
|
84
85
|
return value ?? defaultValue
|
|
85
86
|
}
|
|
87
|
+
|
|
88
|
+
/** 将 MaybeRef<T> 转换为 Ref<T> */
|
|
89
|
+
export function toRefValue<T>(value: MaybeRef<T>): Ref<T>
|
|
90
|
+
/** 将 MaybeRef<T | undefined> 转换为 Ref<T>,支持默认值 */
|
|
91
|
+
export function toRefValue<T>(value: MaybeRef<T | undefined>, defaultValue: T): Ref<T>
|
|
92
|
+
export function toRefValue<T>(value: MaybeRef<T | undefined>, defaultValue?: T): Ref<T> {
|
|
93
|
+
if (typeof defaultValue !== 'undefined') {
|
|
94
|
+
if (isRef(value)) {
|
|
95
|
+
return computed(() => value.value ?? defaultValue) as unknown as Ref<T>
|
|
96
|
+
}
|
|
97
|
+
return ref(value ?? defaultValue) as Ref<T>
|
|
98
|
+
}
|
|
99
|
+
return isRef(value) ? (value as Ref<T>) : (ref(value) as Ref<T>)
|
|
100
|
+
}
|
package/vitest.config.ts
ADDED