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.
@@ -42,6 +42,10 @@
42
42
  @include _setVar(textarea-padding, 0);
43
43
  }
44
44
 
45
+ &__placeholder {
46
+ color: _var(cell-placeholder-color, _var(color-disabled));
47
+ }
48
+
45
49
  &__suffix {
46
50
  margin-left: _var(cell-suffix-margin-left, 5rpx);
47
51
  color: _var(cell-suffix-color, inherit);
@@ -31,8 +31,13 @@
31
31
  })
32
32
  "
33
33
  >
34
- <text v-if="!$slots.default" :selectable="selectable" :user-select="selectable">
35
- {{ value }}
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)
@@ -14,6 +14,10 @@ export const cellProps = {
14
14
  type: [String, Number, Boolean],
15
15
  default: '',
16
16
  },
17
+ /**
18
+ * 占位提示,在 value 为 null、undefined、空字符串时显示
19
+ */
20
+ placeholder: String,
17
21
  /**
18
22
  * 标题下方的描述信息
19
23
  */
@@ -82,7 +82,8 @@
82
82
 
83
83
  <script lang="ts" setup>
84
84
  import { computed, ref, toRefs, watch } from 'vue'
85
- import { useList, LoadStatusEnum } from '../../core/useList'
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 = ref<string>('')
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
  /**
@@ -14,7 +14,7 @@
14
14
 
15
15
  &__body {
16
16
  display: flex;
17
- align-items: start;
17
+ align-items: flex-start;
18
18
  width: 100%;
19
19
  }
20
20
 
package/core/index.ts CHANGED
@@ -5,6 +5,7 @@ export * from './useDatePicker'
5
5
  export * from './useForm'
6
6
  export * from './useInputNumber'
7
7
  export * from './useList'
8
+ export * from './useModel'
8
9
  export * from './useNamespace'
9
10
  export * from './useSelect'
10
11
  export * from './useSwitch'
@@ -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
  })
@@ -1,45 +1,131 @@
1
- import type { Ref } from 'vue'
2
- import { computed, ref, toRef, watch } from 'vue'
3
- import type { IncludeRefs } from '../../types'
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
- export interface UseSelectProps<T, V> {
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
- export function useSelect<T, V = T>(props: IncludeRefs<UseSelectProps<T, V>>) {
24
- type OwnProps = UseSelectProps<T, V>
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
- const options = toRef(props, 'options', []) as Ref<Required<OwnProps>['options']>
27
- const defaultValue = toRef(props, 'defaultValue', []) as Ref<Required<OwnProps>['defaultValue']>
28
- const valueKey = toRef(props, 'valueKey', 'value' as OwnProps['valueKey']) as Ref<
29
- Required<OwnProps>['valueKey']
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
- const selectedSet = ref<Set<V>>(new Set())
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
- const selectedItems = computed<T | T[]>(() => {
38
- const result = options.value.filter((item) => selectedSet.value.has(item[valueKey.value] as V))
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<V | V[]>(() => {
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
- selectedSet.value.clear()
52
- defaultValues.forEach((value) => {
53
- if (typeof value !== 'undefined') {
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
- ;(multiple.value ? true : inverse.value) && selectedSet.value.delete(value)
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.clear()
177
+ const onClear = (): void => {
178
+ selectedSet.value = new Set()
87
179
  }
88
180
 
89
181
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "papayaui",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "适用于uniapp的ui扩展库",
5
5
  "main": "index.ts",
6
6
  "module": "index.ts",
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
+ }
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ // 使用 node 环境
6
+ environment: 'node',
7
+
8
+ // 启用全局变量
9
+ globals: true,
10
+
11
+ // 设置 setup 文件
12
+ setupFiles: ['./vitest.setup.ts'],
13
+ },
14
+ })
@@ -0,0 +1,8 @@
1
+ import { vi } from 'vitest'
2
+
3
+ vi.stubGlobal('window', {})
4
+
5
+ vi.stubGlobal('wx', {
6
+ getFileSystemManager: vi.fn(),
7
+ canIUse: vi.fn(),
8
+ })