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.

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