w-select.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. <template>
  2. <view
  3. class="w-select"
  4. id="wSelect"
  5. :style="{
  6. '--select-wrap-width': width,
  7. '--select-wrap-height': height,
  8. '--select-bg-color': bgColor
  9. }"
  10. >
  11. <view :class="isShow ? 'select-wrap-active' : ''" class="select-wrap" @click="changeShow">
  12. <view v-if="multiple" class="select-content">
  13. <view class="select-content-item-default" v-if="multiSelectList.length === 0 && !filterable">
  14. {{ defaultValue }}
  15. </view>
  16. <view class="select-content-item" v-if="multiSelectList.length > 0">
  17. {{ multiSelectList[0][valueName] }}
  18. </view>
  19. <view class="select-content-item" v-if="multiSelectList.length > 1">
  20. {{ multiLength }}
  21. </view>
  22. </view>
  23. <input
  24. v-if="!multiple || filterable"
  25. type="text"
  26. @input="inputChange"
  27. @blur="blurChange"
  28. :placeholder="multiple ? multiSelectList.length === 0 ? defaultValue : '' : defaultValue"
  29. :disabled="!filterable"
  30. :style="!filterable ? 'pointer-events: none' : ''"
  31. :value="inputData"
  32. >
  33. <!-- #ifdef VUE2 -->
  34. <view
  35. @click.stop="refreshValue"
  36. class="close-icon"
  37. v-if="showClose && (multiple ? value.length > 0 : value)"
  38. >
  39. <image :src="refreshUrl" mode="" />
  40. </view>
  41. <view
  42. v-if="value.length <= 0 || !showClose"
  43. :class="isShow ? 'w-select-arrow-up' : ''"
  44. class="w-select-arrow "
  45. />
  46. <!-- #endif -->
  47. <!-- #ifdef VUE3 -->
  48. <view
  49. @click.stop="refreshValue"
  50. class="close-icon"
  51. v-if="showClose && (multiple ? modelValue.length > 0 : modelValue)"
  52. >
  53. <image :src="refreshUrl" mode="" />
  54. </view>
  55. <view
  56. v-if="modelValue.length <= 0 || !showClose"
  57. :class="isShow ? 'w-select-arrow-up' : ''"
  58. class="w-select-arrow "
  59. />
  60. <!-- #endif -->
  61. <scroll-view
  62. scroll-y
  63. v-show="optionsShow"
  64. :class="[
  65. isShow
  66. ? showPosition === 'bottom'
  67. ? 'animation-bottom-in'
  68. : 'animation-top-in'
  69. : showPosition === 'bottom'
  70. ? 'animation-bottom-out'
  71. : 'animation-top-out',
  72. showPosition === 'bottom'
  73. ? 'position-bottom'
  74. : 'position-top'
  75. ]"
  76. class="select-options"
  77. >
  78. <!-- #ifdef VUE2 -->
  79. <view
  80. @click.stop="handleClickItem(item)"
  81. :class="
  82. multiple &&
  83. multiSelectList.find(
  84. res => res[keyName] === item[keyName]
  85. )
  86. ? 'item-active'
  87. : valuea == item[keyName]
  88. ? 'item-active'
  89. : ''
  90. "
  91. v-for="item in filterList"
  92. :key="item[keyName]"
  93. class="select-option-item"
  94. >
  95. {{ item[valueName] }}
  96. </view>
  97. <!-- #endif -->
  98. <!-- #ifdef VUE3 -->
  99. <view
  100. @click.stop="handleClickItem(item)"
  101. :class="
  102. multiple &&
  103. multiSelectList.find(
  104. res => res[keyName] === item[keyName]
  105. )
  106. ? 'item-active'
  107. : modelValue == item[keyName]
  108. ? 'item-active'
  109. : ''
  110. "
  111. v-for="item in filterList"
  112. :key="item[keyName]"
  113. class="select-option-item"
  114. >
  115. {{ item[valueName] }}
  116. </view>
  117. <!-- #endif -->
  118. <view class="options-no-data" v-if="filterList.length < 1">
  119. 无匹配数据~
  120. </view>
  121. </scroll-view>
  122. </view>
  123. <view v-if="isShow" @click="closeContentSelect" class="contentMask" />
  124. </view>
  125. </template>
  126. <script>
  127. export default {
  128. props: {
  129. width: {
  130. type: String,
  131. default: '200px'
  132. },
  133. height: {
  134. type: String,
  135. default: '30px'
  136. },
  137. bgColor: {
  138. type: String,
  139. default: '#fff'
  140. },
  141. // 是否多选
  142. multiple: {
  143. type: Boolean,
  144. default: false
  145. },
  146. // 是否可搜索
  147. filterable: {
  148. type: Boolean,
  149. default: false
  150. },
  151. // 是否显示关闭按钮
  152. showClose: {
  153. type: Boolean,
  154. default: false
  155. },
  156. // 渲染列表
  157. list: {
  158. type: Array,
  159. default: () => []
  160. },
  161. // #ifdef VUE3
  162. // 双向绑定的值
  163. modelValue: {
  164. type: [Array, String, Number],
  165. default: ''
  166. },
  167. // #endif
  168. // #ifdef VUE2
  169. // 双向绑定的值
  170. value: {
  171. type: [Array, String, Number],
  172. default: ''
  173. },
  174. // #endif
  175. // 双向绑定的值
  176. valuea: {
  177. type: [Array, String, Number],
  178. default: ''
  179. },
  180. // 默认显示的内容
  181. defaultValue: {
  182. type: String,
  183. default: '请选择'
  184. },
  185. // 显示的内容
  186. valueName: {
  187. type: String,
  188. default: 'label'
  189. },
  190. // 绑定的内容
  191. keyName: {
  192. type: String,
  193. default: 'value'
  194. },
  195. chosevalue:{
  196. type: String,
  197. default: ''
  198. }
  199. },
  200. // #ifdef VUE3
  201. emits: ['update:modelValue', 'change'],
  202. // #endif
  203. watch: {
  204. chosevalue(newval){
  205. if(newval&&!this.inputData){
  206. this.inputData=newval
  207. }
  208. },
  209. list: {
  210. immediate: true,
  211. deep: true,
  212. handler (news) {
  213. this.filterList = news
  214. const findItem = news.find(item => {
  215. let isItem = ''
  216. // #ifdef VUE3
  217. if (item[this.keyName] === this.modelValue) {
  218. isItem = true
  219. } else {
  220. isItem = false
  221. }
  222. // #endif
  223. // #ifdef VUE2
  224. if (item[this.keyName] === this.valuea) {
  225. isItem = true
  226. } else {
  227. isItem = false
  228. }
  229. // #endif
  230. return isItem
  231. })
  232. if (findItem) {
  233. this.inputData = findItem[this.valueName]
  234. }
  235. }
  236. }
  237. },
  238. computed: {
  239. multiLength () {
  240. const length = this.multiSelectList.length - 1
  241. return '+' + length
  242. },
  243. bottomDistance () {
  244. return (
  245. this.windowHeight - this.distanceTop - this.curHeight
  246. ) // 当前元素距离可视屏幕底部的距离
  247. }
  248. },
  249. data () {
  250. return {
  251. inputData: '',
  252. // #ifdef VUE3
  253. multiSelectList: this.multiple ? this.modelValue : [],
  254. // #endif
  255. // #ifdef VUE2
  256. multiSelectList: this.multiple ? this.valuea : [],
  257. // #endif
  258. isShow: false,
  259. optionsShow: false,
  260. windowHeight: null,
  261. curHeight: null,
  262. distanceTop: null,
  263. showPosition: 'bottom',
  264. filterList: [],
  265. refreshUrl: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgZmlsbD0id2hpdGUiIGZpbGwtb3BhY2l0eT0iMC4wMSIvPjxwYXRoIGQ9Ik0yNCA0NEMzNS4wNDU3IDQ0IDQ0IDM1LjA0NTcgNDQgMjRDNDQgMTIuOTU0MyAzNS4wNDU3IDQgMjQgNEMxMi45NTQzIDQgNCAxMi45NTQzIDQgMjRDNCAzNS4wNDU3IDEyLjk1NDMgNDQgMjQgNDRaIiBmaWxsPSJub25lIiBzdHJva2U9IiM3YzZlNmUiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0yOS42NTY5IDE4LjM0MzFMMTguMzQzMiAyOS42NTY4IiBzdHJva2U9IiM3YzZlNmUiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTE4LjM0MzIgMTguMzQzMUwyOS42NTY5IDI5LjY1NjgiIHN0cm9rZT0iIzdjNmU2ZSIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4='
  266. }
  267. },
  268. mounted () {
  269. this.$nextTick(() => {
  270. const res = uni.getSystemInfoSync()
  271. this.windowHeight = res.windowHeight // 当前设备屏幕高度
  272. uni
  273. .createSelectorQuery()
  274. .in(this)
  275. .select('#wSelect')
  276. .boundingClientRect(data => {
  277. this.distanceTop = data.top // 当前元素距离顶部的距离
  278. this.curHeight = data.height
  279. })
  280. .exec()
  281. })
  282. },
  283. methods: {
  284. showPositon () {
  285. this.showPosition = 'bottom'
  286. if (this.bottomDistance < this.windowHeight / 3) {
  287. this.showPosition = 'top'
  288. }
  289. },
  290. changeShow () {
  291. this.isShow = !this.isShow
  292. if (this.isShow === false) {
  293. this.filterList = this.list
  294. setTimeout(() => {
  295. this.optionsShow = false
  296. }, 200)
  297. } else {
  298. this.showPositon()
  299. this.optionsShow = this.isShow;
  300. // 重置列表
  301. this.filterList = this.list
  302. }
  303. },
  304. closeContentSelect () {
  305. this.isShow = false
  306. setTimeout(() => {
  307. this.optionsShow = false
  308. }, 200)
  309. },
  310. setValue (value = '') {
  311. // #ifdef VUE3
  312. this.$emit('update:modelValue', value)
  313. // #endif
  314. // #ifdef VUE2
  315. this.$emit('input', value)
  316. // #endif
  317. },
  318. inputChange (e) {
  319. const value = e.detail.value
  320. if(this.multiple && this.filterable) {
  321. this.inputData = value
  322. }else {
  323. // this.setValue(value)
  324. this.inputData = value
  325. }
  326. this.filterList = this.list.filter(item =>
  327. item[this.valueName].includes(value)
  328. )
  329. },
  330. blurChange(e) {
  331. const value = e.detail.value
  332. this.inputData=this.chosevalue;
  333. if(this.multiple && this.filterable && value) {
  334. let curValue ={
  335. [this.keyName]:value,
  336. [this.valueName]:value
  337. }
  338. this.multiSelect(curValue)
  339. }
  340. },
  341. refreshValue () {
  342. this.setValue('')
  343. this.inputData = ''
  344. this.$emit('change', '')
  345. this.filterList = this.list
  346. if (this.multiple) {
  347. this.multiSelectList = []
  348. }
  349. },
  350. handleClickItem (e) {
  351. if (this.multiple) {
  352. this.multiSelect(e)
  353. } else {
  354. this.setValue(e[this.keyName])
  355. this.inputData = e[this.valueName]
  356. this.$emit('change', e)
  357. this.changeShow()
  358. }
  359. },
  360. multiSelect (item) {
  361. const index = this.multiSelectList.findIndex(
  362. res => res[this.valueName] === item[this.valueName]
  363. )
  364. if (index > -1) {
  365. this.multiSelectList.splice(index, 1)
  366. } else {
  367. this.multiSelectList.push(item)
  368. }
  369. this.inputData = ''
  370. this.filterList = this.list
  371. this.setValue(this.multiSelectList)
  372. this.$emit('change', item)
  373. }
  374. }
  375. }
  376. </script>
  377. <style lang="scss" scoped>
  378. .select-wrap /deep/ .input-placeholder{font-size: 30rpx;color: #AAAAAA;}
  379. .w-select {
  380. --select-wrap-width: 200px;
  381. --select-wrap-height: 30px;
  382. --select-border-radius: 4px;
  383. --select-border: 1px solid #dcdfe6;
  384. --select-active-border: 1px solid #409eff;
  385. --select-options-max-height: 150px;
  386. --select-option-item-font-size: 14px;
  387. --select-input-font-size: 14px;
  388. --no-data-default-color: #999999;
  389. --select-options-box-shadow: 0px 0px 12px rgb(0 0 0 / 12%);
  390. --select-bg-color: #ffffff;
  391. .select-wrap {
  392. position: relative;
  393. display: flex;
  394. justify-content: space-between;
  395. align-items: center;
  396. width: var(--select-wrap-width);
  397. height: var(--select-wrap-height);
  398. border: var(--select-border);
  399. border-radius: var(--select-border-radius);
  400. background-color: var(--select-bg-color);
  401. transition: all 0.2s;
  402. input {
  403. padding: 0 2px;
  404. width: 100%;
  405. min-width: 0;
  406. height: 100%;
  407. font-size: var(--select-input-font-size);
  408. flex: 1;
  409. text-align: right;
  410. }
  411. .select-content {
  412. display: flex;
  413. align-items: center;
  414. font-size: var(--select-option-item-font-size);
  415. .select-content-item {
  416. margin-left: 5px;
  417. padding: 2px 6px;
  418. border-radius: var(--select-border-radius);
  419. color: #aa93b1;
  420. background-color: #f4f4f5;
  421. }
  422. .select-content-item-default {
  423. margin-left: 5px;
  424. color: var(--no-data-default-color);
  425. }
  426. }
  427. .close-icon {
  428. position: absolute;
  429. top: 50%;
  430. right: 7px;
  431. z-index: 1000;
  432. width: 15px;
  433. height: 15px;
  434. transform: translateY(-50%);
  435. image {
  436. width: 100%;
  437. height: 100%;
  438. }
  439. }
  440. .position-bottom {
  441. top: calc(var(--select-wrap-height) + 10px);
  442. }
  443. .position-top {
  444. bottom: calc(var(--select-wrap-height) + 10px);
  445. }
  446. .select-options {
  447. position: absolute;
  448. right: 0;
  449. left: 0;
  450. z-index: 999;
  451. overflow: scroll;
  452. padding: 10px;
  453. max-height: var(--select-options-max-height);
  454. border-radius: var(--select-border-radius);
  455. background-color: var(--select-bg-color);
  456. box-shadow: var(--select-options-box-shadow);
  457. .select-option-item {
  458. margin-bottom: 5px;
  459. padding: 5px;
  460. font-size: var(--select-option-item-font-size);
  461. transition: background-color 0.2s;
  462. }
  463. .item-active {
  464. font-weight: 700;
  465. color: #409eff;
  466. background-color: #f5f7fa;
  467. }
  468. .options-no-data {
  469. font-size: var(--select-option-item-font-size);
  470. text-align: center;
  471. color: var(--no-data-default-color);
  472. }
  473. }
  474. .w-select-arrow {
  475. display: inline-block;
  476. margin: 3px 10px 0;
  477. width: 8px;
  478. height: 8px;
  479. border-top: 1px solid transparent;
  480. border-right: 1px solid transparent;
  481. border-bottom: 1px solid #999999;
  482. border-left: 1px solid #999999;
  483. transition: all 0.3s;
  484. transform: translateY(-50%) rotate(-45deg);
  485. }
  486. .w-select-arrow-up {
  487. transform: rotate(-225deg);
  488. }
  489. }
  490. .select-wrap-active {
  491. border: var(--select-active-border);
  492. }
  493. .animation-bottom-in {
  494. animation-name: bottom-in;
  495. animation-duration: 0.4s;
  496. animation-timing-function: ease-out;
  497. animation-fill-mode: both;
  498. }
  499. .animation-bottom-out {
  500. animation-name: bottom-out;
  501. animation-duration: 0.2s;
  502. animation-timing-function: ease-out;
  503. animation-fill-mode: both;
  504. }
  505. .animation-top-in {
  506. animation-name: top-in;
  507. animation-duration: 0.4s;
  508. animation-timing-function: ease-out;
  509. animation-fill-mode: both;
  510. }
  511. .animation-top-out {
  512. animation-name: top-out;
  513. animation-duration: 0.2s;
  514. animation-timing-function: ease-out;
  515. animation-fill-mode: both;
  516. }
  517. @keyframes bottom-in {
  518. 0% {
  519. opacity: 0;
  520. transform: translateY(-15%);
  521. }
  522. 100% {
  523. opacity: 1;
  524. transform: translateY(0);
  525. }
  526. }
  527. @keyframes bottom-out {
  528. 0% {
  529. opacity: 1;
  530. transform: translateY(0);
  531. }
  532. 100% {
  533. opacity: 0;
  534. transform: translateY(-20%);
  535. }
  536. }
  537. @keyframes top-in {
  538. 0% {
  539. opacity: 0;
  540. transform: translateY(15%);
  541. }
  542. 100% {
  543. opacity: 1;
  544. transform: translateY(0);
  545. }
  546. }
  547. @keyframes top-out {
  548. 0% {
  549. opacity: 1;
  550. transform: translateY(0);
  551. }
  552. 100% {
  553. opacity: 0;
  554. transform: translateY(20%);
  555. }
  556. }
  557. .contentMask {
  558. position: fixed;
  559. top: 0;
  560. right: 0;
  561. bottom: 0;
  562. left: 0;
  563. z-index: 998;
  564. width: 100%;
  565. height: 100%;
  566. }
  567. }
  568. </style>