You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

320 lines
6.7 KiB

1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
  1. <template>
  2. <div class="dropdown" ref="dropdownRef">
  3. <!-- 下拉框触发器 -->
  4. <div class="dropdown-toggle" @click="toggleMenu" :class="{ 'active': isOpen,'disabled': props.disabled }">
  5. <div class="search">
  6. <input type="text" class="search-input" :placeholder="placeholder" :value="isOpen ? searchData : selectedItem"
  7. @focus="handleSearchFocus" @blur="handleSearchBlur" @input="handleInput">
  8. <el-icon class="clear-icon" v-if="searchData" @click="clearSearch">
  9. <CircleClose />
  10. </el-icon>
  11. </div>
  12. <span class="arrow">
  13. <el-icon>
  14. <ArrowDown />
  15. </el-icon>
  16. </span>
  17. </div>
  18. <!-- 下拉菜单 -->
  19. <div class="dropdown-menu" v-if="isOpen">
  20. <!-- 选项区域按钮样式 -->
  21. <div class="menuContent">
  22. <button class="dropdown-item" v-for="(item, index) in filteredItems" :key="index" @click="handleSelect(item)"
  23. :class="{ 'selected': selectedItem === item }">
  24. {{ item }}
  25. </button>
  26. </div>
  27. </div>
  28. </div>
  29. </template>
  30. <script setup>
  31. import { ref, computed, watchEffect, onMounted, watch } from 'vue';
  32. const searchData = ref('')
  33. const isOpen = ref(false)
  34. const selectedItem = ref('')
  35. const dropdownRef = ref(null)
  36. const props = defineProps({
  37. items: {
  38. type: Array,
  39. required: true,
  40. default: () => []
  41. },
  42. placeholder: {
  43. type: String,
  44. default: '请选择支付方式'
  45. },
  46. modelValue: {
  47. type: String,
  48. default: ''
  49. },
  50. disabled: {
  51. type: Boolean,
  52. default: false
  53. }
  54. })
  55. const emit = defineEmits(['update:modelValue', 'change'])
  56. // 切换下拉菜单
  57. const toggleMenu = () => {
  58. if (props.disabled) return
  59. isOpen.value = !isOpen.value
  60. searchData.value = ''
  61. }
  62. // 清除搜索
  63. const clearSearch = () => {
  64. searchData.value = ''
  65. }
  66. // 选择选项
  67. const handleSelect = (item) => {
  68. selectedItem.value = item
  69. isOpen.value = false
  70. emit('update:modelValue', item)
  71. emit('change', item)
  72. }
  73. // 点击外部关闭菜单
  74. const handleClickOutside = (event) => {
  75. if (dropdownRef.value && !dropdownRef.value.contains(event.target)) {
  76. isOpen.value = false
  77. }
  78. }
  79. // 监听搜索框的焦点和失焦事件
  80. const handleSearchFocus = () => {
  81. // 可以在这里添加额外的逻辑
  82. }
  83. const handleSearchBlur = () => {
  84. // 可以在这里添加额外的逻辑
  85. }
  86. // 搜索过滤
  87. const filteredItems = computed(() => {
  88. if (!searchData.value) return props.items
  89. return props.items.filter(item =>
  90. item.toLowerCase().includes(searchData.value.toLowerCase())
  91. )
  92. })
  93. const handleInput = (e) => {
  94. searchData.value = e.target.value
  95. }
  96. // 挂载/卸载事件
  97. onMounted(() => {
  98. selectedItem.value = props.modelValue;
  99. document.addEventListener('click', handleClickOutside)
  100. return () => {
  101. document.removeEventListener('click', handleClickOutside)
  102. }
  103. })
  104. // 监听外部值变化
  105. watch(() => props.modelValue, (newVal) => {
  106. selectedItem.value = newVal;
  107. }, { immediate: true });
  108. </script>
  109. <style scoped lang="scss">
  110. // 下拉容器
  111. .dropdown {
  112. position: relative;
  113. width: 268px;
  114. font-family: 'Arial', sans-serif;
  115. }
  116. // 触发器:控制展开/收起
  117. .dropdown-toggle {
  118. border: 1px solid #e5e7eb;
  119. /* 调整内边距以匹配按钮高度 */
  120. height: 31px;
  121. /* 调整高度以匹配按钮 */
  122. cursor: pointer;
  123. display: flex;
  124. justify-content: space-between;
  125. align-items: center;
  126. background-color: #fff;
  127. border-radius: 6px;
  128. transition: all 0.3s ease;
  129. .placeholder {
  130. flex: 1;
  131. font-size: 14px;
  132. line-height: 18px;
  133. color: #A8ABB2;
  134. }
  135. // 搜索框区域
  136. .search {
  137. width: 100%;
  138. height: 100%;
  139. position: sticky;
  140. }
  141. // 搜索输入框
  142. .search-input {
  143. width: 100%;
  144. height: 90%;
  145. /* 左侧留出图标空间 */
  146. box-sizing: border-box;
  147. border: none;
  148. border-radius: 6px;
  149. padding-left: 11px;
  150. outline: none;
  151. font-size: 14px;
  152. transition: border-color 0.3s ease;
  153. &::placeholder {
  154. color: #999999;
  155. opacity: 1;
  156. font-style: normal;
  157. font-weight: 350;
  158. line-height: 22px;
  159. }
  160. }
  161. .clear-icon {
  162. position: absolute;
  163. top: 62%;
  164. right: 20px;
  165. transform: translateY(-50%);
  166. color: #909399;
  167. cursor: pointer;
  168. z-index: 1003;
  169. &:hover {
  170. color: #606266;
  171. }
  172. }
  173. }
  174. // 展开状态:边框+阴影高亮
  175. .dropdown-toggle.active {
  176. border-color: #678BFF;
  177. box-shadow: 0 0 0 2px rgba(103, 139, 255, 0.1);
  178. }
  179. // 箭头图标:展开时旋转
  180. .arrow {
  181. margin-right: 8px;
  182. color: #999;
  183. transition: transform 0.3s ease;
  184. }
  185. .dropdown-toggle.active .arrow {
  186. transform: rotate(180deg);
  187. }
  188. // 下拉菜单主体
  189. .dropdown-menu {
  190. position: absolute;
  191. top: 100%; // 紧贴触发器下方
  192. left: 0;
  193. width: 100%;
  194. border: 1px solid #678BFF;
  195. min-height: 50px;
  196. max-height: 300px;
  197. background-color: #fff;
  198. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  199. border-radius: 0 0 8px 8px; // 仅底部两侧圆角
  200. z-index: 1000;
  201. margin-top: 15px; // 关键:不要有上外边距,尖角要紧贴触发器
  202. overflow: visible;
  203. &::before {
  204. content: "";
  205. position: absolute;
  206. top: -11px;
  207. left: 50%;
  208. transform: translateX(-50%) scaleY(0.5);
  209. width: 30px;
  210. height: 16px;
  211. background: #fff;
  212. clip-path: polygon(0 100%, 100% 100%, 50% 0);
  213. z-index: 1001;
  214. border: none;
  215. /* 移除原来的边框 */
  216. }
  217. &::after {
  218. content: "";
  219. position: absolute;
  220. top: -12px;
  221. /* 比 ::before 往下一点,制造边框效果 */
  222. left: 50%;
  223. transform: translateX(-50%) scaleY(0.5);
  224. width: 30px;
  225. height: 16px;
  226. background: #678BFF;
  227. clip-path: polygon(0 100%, 100% 100%, 50% 0);
  228. z-index: 1000;
  229. }
  230. }
  231. // 选项容器:调整滚动区域高度
  232. .menuContent {
  233. max-height: 200px;
  234. /* 减去搜索框高度和尖角高度 */
  235. overflow-y: auto;
  236. padding: 8px;
  237. padding: 10px 14px 12px 10px;
  238. }
  239. // 选项按钮:无边框 + hover/选中效果
  240. .dropdown-item {
  241. width: 100%;
  242. height: 27px;
  243. padding: 5px 12px 5px 5px;
  244. text-align: center;
  245. cursor: pointer;
  246. border: none;
  247. border-radius: 10px;
  248. background-color: #fff;
  249. font-size: 12px;
  250. font-style: normal;
  251. font-weight: 700;
  252. line-height: 20px;
  253. margin: 5px 0;
  254. color: #040A2D;
  255. transition: all 0.2s ease;
  256. &:hover {
  257. background-color: #F3FAFE;
  258. /* hover浅灰 */
  259. }
  260. &.selected {
  261. background-color: #E5EBFE;
  262. /* 选中浅蓝 */
  263. color: #2741DE;
  264. /* 选中文字蓝色 */
  265. }
  266. }
  267. // 可选:滚动条美化(与原风格一致)
  268. .menuContent::-webkit-scrollbar {
  269. width: 6px;
  270. }
  271. .menuContent::-webkit-scrollbar-track {
  272. background: #f1f1f1;
  273. border-radius: 3px;
  274. }
  275. .menuContent::-webkit-scrollbar-thumb {
  276. background: #c0c4cc;
  277. border-radius: 3px;
  278. &:hover {
  279. background: #909399;
  280. }
  281. }
  282. </style>