index.vue 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  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. minWidth:{
  96. type:String,
  97. default:'unset'
  98. }
  99. })
  100. // Emits 定义
  101. const emit = defineEmits(['update:modelValue', 'change', 'search', 'clear'])
  102. // 响应式数据
  103. const isOpen = ref(false)
  104. const selectedValue = ref(props.modelValue)
  105. const searchKeyword = ref('')
  106. // 计算属性
  107. const optionsList = computed(() => props.options || [])
  108. const displayText = computed(() => {
  109. if (!selectedValue.value) return props.placeholder
  110. const selectedOption = optionsList.value.find(
  111. option => option[props.mapping.value] === selectedValue.value
  112. )
  113. return selectedOption ? selectedOption[props.mapping.text] : props.placeholder
  114. })
  115. // 方法
  116. const toggleDropdown = () => {
  117. if (props.disabled) return
  118. if (!isOpen.value) {
  119. // 关闭其他下拉框
  120. uni.$emit('closeAllSelects')
  121. }
  122. isOpen.value = !isOpen.value
  123. }
  124. const selectOption = (option) => {
  125. const value = option[props.mapping.value]
  126. selectedValue.value = value
  127. isOpen.value = false
  128. // 触发事件
  129. emit('update:modelValue', value)
  130. emit('change', value, option)
  131. }
  132. const closeDropdown = () => {
  133. isOpen.value = false
  134. }
  135. // 设置值的方法(供外部调用)
  136. const setValue = (value) => {
  137. if (!value) {
  138. selectedValue.value = ''
  139. emit('update:modelValue', '')
  140. return true
  141. }
  142. const option = optionsList.value.find(
  143. opt => opt[props.mapping.value] === value
  144. )
  145. if (option) {
  146. selectedValue.value = value
  147. emit('update:modelValue', value)
  148. emit('change', value, option)
  149. return true
  150. }
  151. return false
  152. }
  153. // 监听外部值变化
  154. watch(() => props.modelValue, (newValue) => {
  155. selectedValue.value = newValue
  156. })
  157. // 监听全局关闭事件
  158. const handleGlobalClose = () => {
  159. closeDropdown()
  160. }
  161. onMounted(() => {
  162. uni.$on('closeAllSelects', handleGlobalClose)
  163. // 点击页面其他地方关闭下拉框
  164. uni.$on('pageClick', closeDropdown)
  165. })
  166. onUnmounted(() => {
  167. uni.$off('closeAllSelects', handleGlobalClose)
  168. uni.$off('pageClick', closeDropdown)
  169. })
  170. // 暴露方法给父组件
  171. defineExpose({
  172. setValue,
  173. getValue: () => selectedValue.value,
  174. getSelectedOption: () => {
  175. return optionsList.value.find(
  176. option => option[props.mapping.value] === selectedValue.value
  177. )
  178. }
  179. })
  180. </script>
  181. <style lang="scss" scoped>
  182. /* ss暂用下拉框样式 */
  183. .ss-select-container {
  184. position: relative;
  185. width: v-bind(width);
  186. min-width: v-bind(minWidth);
  187. font-size: 32rpx;
  188. }
  189. .ss-select {
  190. padding: 10rpx 0;
  191. border-radius: 10rpx;
  192. cursor: pointer;
  193. position: relative;
  194. display: flex;
  195. align-items: center;
  196. justify-content: space-between;
  197. min-height: 28rpx;
  198. box-sizing: border-box;
  199. &.disabled {
  200. opacity: 0.6;
  201. cursor: not-allowed;
  202. .select-text {
  203. color: #ccc;
  204. }
  205. }
  206. }
  207. .select-text {
  208. flex: 1;
  209. color: #333;
  210. &.placeholder {
  211. color: #999;
  212. }
  213. }
  214. .select-arrow {
  215. display: flex;
  216. align-items: center;
  217. transition: transform 0.3s ease;
  218. margin-left: 20rpx;
  219. &.rotate {
  220. transform: rotate(180deg);
  221. }
  222. }
  223. .ss-options {
  224. display: none;
  225. position: absolute;
  226. top: 100%;
  227. left: 0;
  228. width: 100%;
  229. background-color: #393D51;
  230. z-index: 1000;
  231. color: #fff;
  232. border: 2rpx solid #393D51;
  233. box-sizing: border-box;
  234. border-radius: 10rpx;
  235. overflow: hidden;
  236. max-height: 600rpx;
  237. overflow: auto;
  238. }
  239. .ss-select-container.open .ss-options {
  240. display: block;
  241. }
  242. .option-item {
  243. padding: 20rpx 20rpx 20rpx 46rpx;
  244. cursor: pointer;
  245. position: relative;
  246. &::after {
  247. content: "";
  248. position: absolute;
  249. bottom: 0%;
  250. left: 50%;
  251. transform: translateX(-50%);
  252. width: 80%;
  253. height: 2rpx;
  254. background-color: #303445;
  255. }
  256. &:last-child::after,
  257. &:hover::after,
  258. &.selected::after {
  259. display: none;
  260. }
  261. &:hover {
  262. background-color: #fff;
  263. color: #393D51;
  264. }
  265. &.selected {
  266. background-color: #fff;
  267. color: #393D51;
  268. &::before {
  269. content: "";
  270. 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;
  271. position: absolute;
  272. width: 30rpx;
  273. height: 100%;
  274. left: 10rpx;
  275. top: 0;
  276. color: #393D51;
  277. }
  278. }
  279. &.no-options {
  280. text-align: center;
  281. color: #999;
  282. cursor: default;
  283. &:hover {
  284. background-color: #393D51;
  285. color: #999;
  286. }
  287. }
  288. &.loading-item {
  289. text-align: center;
  290. color: #999;
  291. cursor: default;
  292. &:hover {
  293. background-color: #393D51;
  294. color: #999;
  295. }
  296. .loading-text {
  297. animation: loading-pulse 1.5s ease-in-out infinite;
  298. }
  299. }
  300. }
  301. @keyframes loading-pulse {
  302. 0%, 100% { opacity: 0.6; }
  303. 50% { opacity: 1; }
  304. }
  305. </style>