Select.vue 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. <template>
  2. <div class="select-wrap" v-if="disabled">
  3. <div class="select disabled" ref="selectRef">
  4. <div class="selector">{{ value }}</div>
  5. <div class="icon">
  6. <slot name="icon">
  7. <IconDown :size="14" />
  8. </slot>
  9. </div>
  10. </div>
  11. </div>
  12. <Popover
  13. class="select-wrap"
  14. trigger="click"
  15. v-model:value="popoverVisible"
  16. placement="bottom"
  17. :contentStyle="{
  18. padding: 0,
  19. boxShadow: '0 6px 16px 0 rgba(0, 0, 0, 0.08)',
  20. }"
  21. v-else
  22. >
  23. <template #content>
  24. <template v-if="search">
  25. <Input ref="searchInputRef" simple :placeholder="searchLabel" v-model:value="searchKey" :style="{ width: width + 2 + 'px' }" />
  26. <Divider :margin="0" />
  27. </template>
  28. <div class="options" :style="{ width: width + 2 + 'px' }">
  29. <div class="option"
  30. :class="{
  31. 'disabled': option.disabled,
  32. 'selected': option.value === value,
  33. }"
  34. v-for="option in showOptions"
  35. :key="option.value"
  36. @click="handleSelect(option)"
  37. >{{ option.label }}</div>
  38. </div>
  39. </template>
  40. <div class="select" ref="selectRef">
  41. <div class="selector">{{ showLabel }}</div>
  42. <div class="icon">
  43. <slot name="icon">
  44. <IconDown :size="14" />
  45. </slot>
  46. </div>
  47. </div>
  48. </Popover>
  49. </template>
  50. <script lang="ts" setup>
  51. import { computed, onMounted, onUnmounted, ref, watch, nextTick, onBeforeUnmount, useTemplateRef } from 'vue'
  52. import Popover from './Popover.vue'
  53. import Input from './Input.vue'
  54. import Divider from './Divider.vue'
  55. interface SelectOption {
  56. label: string
  57. value: string | number
  58. disabled?: boolean
  59. }
  60. const props = withDefaults(defineProps<{
  61. value: string | number
  62. options: SelectOption[]
  63. disabled?: boolean
  64. search?: boolean
  65. searchLabel?: string
  66. }>(), {
  67. disabled: false,
  68. search: false,
  69. searchLabel: '搜索',
  70. })
  71. const emit = defineEmits<{
  72. (event: 'update:value', payload: string | number): void
  73. }>()
  74. const popoverVisible = ref(false)
  75. const width = ref(0)
  76. const searchKey = ref('')
  77. const selectRef = useTemplateRef<HTMLElement>('selectRef')
  78. const searchInputRef = useTemplateRef<InstanceType<typeof Input>>('searchInputRef')
  79. const showLabel = computed(() => {
  80. return props.options.find(item => item.value === props.value)?.label || props.value
  81. })
  82. const showOptions = computed(() => {
  83. if (!props.search) return props.options
  84. if (!searchKey.value.trim()) return props.options
  85. const opts = props.options.filter(item => {
  86. return item.label.toLowerCase().indexOf(searchKey.value.toLowerCase()) !== -1
  87. })
  88. return opts.length ? opts : props.options
  89. })
  90. watch(popoverVisible, () => {
  91. if (popoverVisible.value) {
  92. nextTick(() => {
  93. if (searchInputRef.value) searchInputRef.value.focus()
  94. })
  95. }
  96. else searchKey.value = ''
  97. })
  98. onBeforeUnmount(() => {
  99. searchKey.value = ''
  100. })
  101. const updateWidth = () => {
  102. if (!selectRef.value) return
  103. width.value = selectRef.value.clientWidth
  104. }
  105. const resizeObserver = new ResizeObserver(updateWidth)
  106. onMounted(() => {
  107. if (!selectRef.value) return
  108. resizeObserver.observe(selectRef.value)
  109. })
  110. onUnmounted(() => {
  111. if (!selectRef.value) return
  112. resizeObserver.unobserve(selectRef.value)
  113. })
  114. const handleSelect = (option: SelectOption) => {
  115. if (option.disabled) return
  116. emit('update:value', option.value)
  117. popoverVisible.value = false
  118. }
  119. </script>
  120. <style lang="scss" scoped>
  121. .select {
  122. width: 100%;
  123. height: 32px;
  124. padding-right: 32px;
  125. border-radius: $borderRadius;
  126. transition: border-color .25s;
  127. font-size: 13px;
  128. user-select: none;
  129. background-color: #fff;
  130. border: 1px solid #d9d9d9;
  131. position: relative;
  132. cursor: pointer;
  133. &:not(.disabled):hover {
  134. border-color: $themeColor;
  135. }
  136. &.disabled {
  137. background-color: #f5f5f5;
  138. border-color: #dcdcdc;
  139. color: #b7b7b7;
  140. cursor: default;
  141. }
  142. .selector {
  143. min-width: 50px;
  144. height: 30px;
  145. line-height: 30px;
  146. padding-left: 10px;
  147. @include ellipsis-oneline();
  148. }
  149. }
  150. .options {
  151. max-height: 260px;
  152. padding: 5px;
  153. overflow: auto;
  154. text-align: left;
  155. font-size: 13px;
  156. user-select: none;
  157. }
  158. .option {
  159. height: 32px;
  160. line-height: 32px;
  161. padding: 0 5px;
  162. border-radius: $borderRadius;
  163. @include ellipsis-oneline();
  164. &.disabled {
  165. color: #b7b7b7;
  166. }
  167. &:not(.disabled, .selected):hover {
  168. background-color: rgba($color: $themeColor, $alpha: .05);
  169. cursor: pointer;
  170. }
  171. &.selected {
  172. color: $themeColor;
  173. font-weight: 700;
  174. }
  175. }
  176. .icon {
  177. width: 32px;
  178. height: 30px;
  179. color: #bfbfbf;
  180. position: absolute;
  181. top: 0;
  182. right: 0;
  183. display: flex;
  184. justify-content: center;
  185. align-items: center;
  186. }
  187. </style>