MultipleSelect.vue 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. <template>
  2. <view class="multi-select">
  3. <wd-cell
  4. title-width="200rpx"
  5. :placeholder="'请选择' + taskFieldConfig.taskFiledValue"
  6. :title="taskFieldConfig.taskFiledValue"
  7. :required="taskFieldConfig.isMustfill === '1'"
  8. :value="cellValue"
  9. is-link
  10. @click="pickerClick"
  11. />
  12. <wd-select-picker
  13. v-model="pickerValue"
  14. v-model:visible="pickerShow"
  15. :columns="pickerOptions"
  16. @confirm="handleSelect"
  17. />
  18. </view>
  19. </template>
  20. <script setup lang="ts">
  21. import { computed, ref, watch } from 'vue'
  22. import type { TaskFieldConfigViewItem, TaskFieldOption } from '../composables/useTaskForm'
  23. type RawOptionValue = string | number
  24. type FieldValue = string
  25. interface SelectPickerConfirmEvent {
  26. value: RawOptionValue[]
  27. selectedItems: TaskFieldOption[]
  28. }
  29. interface Props {
  30. taskFieldConfig: TaskFieldConfigViewItem
  31. disabled?: boolean
  32. /**
  33. * 打开选择器前执行。
  34. *
  35. * 特殊字段可在页面回调中请求接口,
  36. * 然后重新赋值当前字段的 options。
  37. */
  38. beforeOpen?: () => boolean | Promise<boolean>
  39. }
  40. const props = withDefaults(defineProps<Props>(), {
  41. disabled: false,
  42. })
  43. const label = defineModel<string>('label', { default: '' })
  44. const value = defineModel<FieldValue>('value', { default: '' })
  45. const pickerShow = ref(false)
  46. const opening = ref(false)
  47. const pickerValue = ref<string[]>([])
  48. /**
  49. * 选项统一从字段配置中读取。
  50. *
  51. * 普通字段由 useTaskForm 初始化字典;
  52. * 特殊字段由页面 beforeOpen 重新赋值。
  53. */
  54. const pickerOptions = computed<TaskFieldOption[]>(() => {
  55. return props.taskFieldConfig.options ?? []
  56. })
  57. const cellValue = computed(() => label.value ?? '')
  58. const normalizeValue = (rawValue: unknown): string[] => {
  59. if (Array.isArray(rawValue)) {
  60. return rawValue.filter((item) => item !== undefined && item !== null && item !== '').map(String)
  61. }
  62. if (rawValue === undefined || rawValue === null || rawValue === '') {
  63. return []
  64. }
  65. if (typeof rawValue === 'string' && rawValue.includes(',')) {
  66. return rawValue
  67. .split(',')
  68. .map((item) => item.trim())
  69. .filter(Boolean)
  70. }
  71. return [String(rawValue)]
  72. }
  73. const getSelectedItemsByValues = (selectedValues: string[]): TaskFieldOption[] => {
  74. return selectedValues
  75. .map((selectedValue) => pickerOptions.value.find((item) => item.value === selectedValue))
  76. .filter((item): item is TaskFieldOption => Boolean(item))
  77. }
  78. const syncLabelByValue = (selectedValues: string[]) => {
  79. const selectedItems = getSelectedItemsByValues(selectedValues)
  80. label.value = selectedItems.map((item) => item.label).join(',')
  81. }
  82. const syncPickerValueByModelValue = () => {
  83. const normalizedValues = normalizeValue(value.value)
  84. const matchedValues = normalizedValues.filter((selectedValue) =>
  85. pickerOptions.value.some((item) => item.value === selectedValue)
  86. )
  87. pickerValue.value = matchedValues
  88. syncLabelByValue(matchedValues)
  89. }
  90. /**
  91. * 表单值或选项变化时,同步选择器状态。
  92. *
  93. * beforeOpen 中重新赋值 field.options 后,
  94. * 此处也会自动重新匹配已有值。
  95. */
  96. watch(
  97. [() => value.value, () => props.taskFieldConfig.options],
  98. () => {
  99. syncPickerValueByModelValue()
  100. },
  101. {
  102. immediate: true,
  103. deep: true,
  104. }
  105. )
  106. const pickerClick = async () => {
  107. if (props.disabled || opening.value) {
  108. return
  109. }
  110. opening.value = true
  111. try {
  112. /**
  113. * 页面可在 beforeOpen 中请求特殊接口,
  114. * 并重新赋值 item.options。
  115. */
  116. const canOpen = await props.beforeOpen?.()
  117. if (canOpen === false) {
  118. return
  119. }
  120. /**
  121. * beforeOpen 完成后读取最新 options。
  122. */
  123. if (!pickerOptions.value.length) {
  124. uni.showToast({
  125. title: '暂无可选内容',
  126. icon: 'none',
  127. })
  128. return
  129. }
  130. syncPickerValueByModelValue()
  131. pickerShow.value = true
  132. } catch (error) {
  133. console.error(`打开多选字段失败:${props.taskFieldConfig.taskFiledValue}`, error)
  134. uni.showToast({
  135. title: '选项加载失败,请稍后重试',
  136. icon: 'none',
  137. })
  138. } finally {
  139. opening.value = false
  140. }
  141. }
  142. const handleSelect = (event: SelectPickerConfirmEvent) => {
  143. const selectedValues = event.value.map(String)
  144. pickerValue.value = selectedValues
  145. /**
  146. * 保持后端原有提交格式:
  147. * 多选 value 存储为逗号分隔字符串。
  148. */
  149. value.value = selectedValues.join(',')
  150. label.value = event.selectedItems.map((item) => item.label).join(',')
  151. }
  152. </script>