index.vue 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <template>
  2. <view class="ss-select-container" :class="{ open: isOpen }" @click.stop="toggleDropdown">
  3. <!-- 显示区域 -->
  4. <view class="ss-select" :class="{ disabled: disabled }">
  5. <text class="select-text" :class="{ placeholder: !selectedValue }">{{ displayText }}</text>
  6. <view class="select-arrow" :class="{ rotate: isOpen }">
  7. <Icon name="icon-xiangxiajiantou" size="32" :color="disabled ? '#ccc' : '#999'"/>
  8. </view>
  9. </view>
  10. <!-- 选项列表 -->
  11. <view class="ss-options" v-show="isOpen">
  12. <!-- 加载状态 -->
  13. <view
  14. v-if="loading"
  15. class="option-item loading-item"
  16. >
  17. <text class="loading-text">加载中...</text>
  18. </view>
  19. <!-- 无选项 -->
  20. <view
  21. v-else-if="optionsList.length === 0"
  22. class="option-item no-options"
  23. >
  24. 无选项
  25. </view>
  26. <!-- 选项列表 -->
  27. <view
  28. v-else
  29. v-for="(option, index) in optionsList"
  30. :key="index"
  31. class="option-item"
  32. :class="{ selected: option[props.mapping.value] === selectedValue }"
  33. @click.stop="selectOption(option)"
  34. >
  35. {{ option[props.mapping.text] }}
  36. </view>
  37. </view>
  38. </view>
  39. </template>
  40. <script setup>
  41. import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
  42. import Icon from '@/components/icon/index.vue';
  43. // Props 定义
  44. const props = defineProps({
  45. // 选项数组
  46. options: {
  47. type: Array,
  48. default: () => []
  49. },
  50. // 字段映射
  51. mapping: {
  52. type: Object,
  53. default: () => ({ text: 'n', value: 'v' })
  54. },
  55. // 默认值
  56. modelValue: {
  57. type: [String, Number],
  58. default: ''
  59. },
  60. // 占位符
  61. placeholder: {
  62. type: String,
  63. default: '请选择'
  64. },
  65. // 校验配置
  66. validation: {
  67. type: Object,
  68. default: () => ({ enable: false, message: '' })
  69. },
  70. // 是否禁用
  71. disabled: {
  72. type: Boolean,
  73. default: false
  74. },
  75. // 是否支持搜索
  76. searchable: {
  77. type: Boolean,
  78. default: false
  79. },
  80. // 是否支持清空
  81. clearable: {
  82. type: Boolean,
  83. default: false
  84. },
  85. // 加载状态
  86. loading: {
  87. type: Boolean,
  88. default: false
  89. },
  90. // 宽度设置
  91. width: {
  92. type: String,
  93. default: '100%'
  94. }
  95. })
  96. // Emits 定义
  97. const emit = defineEmits(['update:modelValue', 'change', 'search', 'clear'])
  98. // 响应式数据
  99. const isOpen = ref(false)
  100. const selectedValue = ref(props.modelValue)
  101. const searchKeyword = ref('')
  102. // 计算属性
  103. const optionsList = computed(() => props.options || [])
  104. const displayText = computed(() => {
  105. if (!selectedValue.value) return props.placeholder
  106. const selectedOption = optionsList.value.find(
  107. option => option[props.mapping.value] === selectedValue.value
  108. )
  109. return selectedOption ? selectedOption[props.mapping.text] : props.placeholder
  110. })
  111. // 方法
  112. const toggleDropdown = () => {
  113. if (props.disabled) return
  114. if (!isOpen.value) {
  115. // 关闭其他下拉框
  116. uni.$emit('closeAllSelects')
  117. }
  118. isOpen.value = !isOpen.value
  119. }
  120. const selectOption = (option) => {
  121. const value = option[props.mapping.value]
  122. selectedValue.value = value
  123. isOpen.value = false
  124. // 触发事件
  125. emit('update:modelValue', value)
  126. emit('change', value, option)
  127. }
  128. const closeDropdown = () => {
  129. isOpen.value = false
  130. }
  131. // 设置值的方法(供外部调用)
  132. const setValue = (value) => {
  133. if (!value) {
  134. selectedValue.value = ''
  135. emit('update:modelValue', '')
  136. return true
  137. }
  138. const option = optionsList.value.find(
  139. opt => opt[props.mapping.value] === value
  140. )
  141. if (option) {
  142. selectedValue.value = value
  143. emit('update:modelValue', value)
  144. emit('change', value, option)
  145. return true
  146. }
  147. return false
  148. }
  149. // 监听外部值变化
  150. watch(() => props.modelValue, (newValue) => {
  151. selectedValue.value = newValue
  152. })
  153. // 监听全局关闭事件
  154. const handleGlobalClose = () => {
  155. closeDropdown()
  156. }
  157. onMounted(() => {
  158. uni.$on('closeAllSelects', handleGlobalClose)
  159. // 点击页面其他地方关闭下拉框
  160. uni.$on('pageClick', closeDropdown)
  161. })
  162. onUnmounted(() => {
  163. uni.$off('closeAllSelects', handleGlobalClose)
  164. uni.$off('pageClick', closeDropdown)
  165. })
  166. // 暴露方法给父组件
  167. defineExpose({
  168. setValue,
  169. getValue: () => selectedValue.value,
  170. getSelectedOption: () => {
  171. return optionsList.value.find(
  172. option => option[props.mapping.value] === selectedValue.value
  173. )
  174. }
  175. })
  176. </script>
  177. <style lang="scss" scoped>
  178. /* ss暂用下拉框样式 */
  179. .ss-select-container {
  180. position: relative;
  181. width: v-bind(width);
  182. font-size: 32rpx;
  183. }
  184. .ss-select {
  185. padding: 10rpx 0;
  186. border-radius: 10rpx;
  187. cursor: pointer;
  188. position: relative;
  189. display: flex;
  190. align-items: center;
  191. justify-content: space-between;
  192. min-height: 80rpx;
  193. box-sizing: border-box;
  194. &.disabled {
  195. opacity: 0.6;
  196. cursor: not-allowed;
  197. .select-text {
  198. color: #ccc;
  199. }
  200. }
  201. }
  202. .select-text {
  203. flex: 1;
  204. color: #333;
  205. &.placeholder {
  206. color: #999;
  207. }
  208. }
  209. .select-arrow {
  210. display: flex;
  211. align-items: center;
  212. transition: transform 0.3s ease;
  213. margin-left: 20rpx;
  214. &.rotate {
  215. transform: rotate(180deg);
  216. }
  217. }
  218. .ss-options {
  219. display: none;
  220. position: absolute;
  221. top: 100%;
  222. left: 0;
  223. width: 100%;
  224. background-color: #393D51;
  225. z-index: 1000;
  226. color: #fff;
  227. border: 2rpx solid #393D51;
  228. box-sizing: border-box;
  229. border-radius: 10rpx;
  230. overflow: hidden;
  231. max-height: 600rpx;
  232. overflow: auto;
  233. }
  234. .ss-select-container.open .ss-options {
  235. display: block;
  236. }
  237. .option-item {
  238. padding: 20rpx 20rpx 20rpx 46rpx;
  239. cursor: pointer;
  240. position: relative;
  241. &::after {
  242. content: "";
  243. position: absolute;
  244. bottom: 0%;
  245. left: 50%;
  246. transform: translateX(-50%);
  247. width: 80%;
  248. height: 2rpx;
  249. background-color: #303445;
  250. }
  251. &:last-child::after,
  252. &:hover::after,
  253. &.selected::after {
  254. display: none;
  255. }
  256. &:hover {
  257. background-color: #fff;
  258. color: #393D51;
  259. }
  260. &.selected {
  261. background-color: #fff;
  262. color: #393D51;
  263. &::before {
  264. content: "";
  265. background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23393D51"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>') no-repeat center;
  266. position: absolute;
  267. width: 30rpx;
  268. height: 100%;
  269. left: 10rpx;
  270. top: 0;
  271. color: #393D51;
  272. }
  273. }
  274. &.no-options {
  275. text-align: center;
  276. color: #999;
  277. cursor: default;
  278. &:hover {
  279. background-color: #393D51;
  280. color: #999;
  281. }
  282. }
  283. &.loading-item {
  284. text-align: center;
  285. color: #999;
  286. cursor: default;
  287. &:hover {
  288. background-color: #393D51;
  289. color: #999;
  290. }
  291. .loading-text {
  292. animation: loading-pulse 1.5s ease-in-out infinite;
  293. }
  294. }
  295. }
  296. @keyframes loading-pulse {
  297. 0%, 100% { opacity: 0.6; }
  298. 50% { opacity: 1; }
  299. }
  300. </style>