w-select.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  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. : value == 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. defaultValue: {
  177. type: String,
  178. default: '请选择'
  179. },
  180. // 显示的内容
  181. valueName: {
  182. type: String,
  183. default: 'label'
  184. },
  185. // 绑定的内容
  186. keyName: {
  187. type: String,
  188. default: 'value'
  189. },
  190. chosevalue:{
  191. type: String,
  192. default: ''
  193. }
  194. },
  195. // #ifdef VUE3
  196. emits: ['update:modelValue', 'change'],
  197. // #endif
  198. watch: {
  199. chosevalue(newval){
  200. if(newval&&!this.inputData){
  201. this.inputData=newval
  202. }
  203. },
  204. list: {
  205. immediate: true,
  206. deep: true,
  207. handler (news) {
  208. this.filterList = news
  209. const findItem = news.find(item => {
  210. let isItem = ''
  211. // #ifdef VUE3
  212. if (item[this.keyName] === this.modelValue) {
  213. isItem = true
  214. } else {
  215. isItem = false
  216. }
  217. // #endif
  218. // #ifdef VUE2
  219. if (item[this.keyName] === this.value) {
  220. isItem = true
  221. } else {
  222. isItem = false
  223. }
  224. // #endif
  225. return isItem
  226. })
  227. if (findItem) {
  228. this.inputData = findItem[this.valueName]
  229. }
  230. }
  231. }
  232. },
  233. computed: {
  234. multiLength () {
  235. const length = this.multiSelectList.length - 1
  236. return '+' + length
  237. },
  238. bottomDistance () {
  239. return (
  240. this.windowHeight - this.distanceTop - this.curHeight
  241. ) // 当前元素距离可视屏幕底部的距离
  242. }
  243. },
  244. data () {
  245. return {
  246. inputData: '',
  247. // #ifdef VUE3
  248. multiSelectList: this.multiple ? this.modelValue : [],
  249. // #endif
  250. // #ifdef VUE2
  251. multiSelectList: this.multiple ? this.value : [],
  252. // #endif
  253. isShow: false,
  254. optionsShow: false,
  255. windowHeight: null,
  256. curHeight: null,
  257. distanceTop: null,
  258. showPosition: 'bottom',
  259. filterList: [],
  260. refreshUrl: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgZmlsbD0id2hpdGUiIGZpbGwtb3BhY2l0eT0iMC4wMSIvPjxwYXRoIGQ9Ik0yNCA0NEMzNS4wNDU3IDQ0IDQ0IDM1LjA0NTcgNDQgMjRDNDQgMTIuOTU0MyAzNS4wNDU3IDQgMjQgNEMxMi45NTQzIDQgNCAxMi45NTQzIDQgMjRDNCAzNS4wNDU3IDEyLjk1NDMgNDQgMjQgNDRaIiBmaWxsPSJub25lIiBzdHJva2U9IiM3YzZlNmUiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0yOS42NTY5IDE4LjM0MzFMMTguMzQzMiAyOS42NTY4IiBzdHJva2U9IiM3YzZlNmUiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTE4LjM0MzIgMTguMzQzMUwyOS42NTY5IDI5LjY1NjgiIHN0cm9rZT0iIzdjNmU2ZSIgc3Ryb2tlLXdpZHRoPSI0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4='
  261. }
  262. },
  263. mounted () {
  264. this.$nextTick(() => {
  265. const res = uni.getSystemInfoSync()
  266. this.windowHeight = res.windowHeight // 当前设备屏幕高度
  267. uni
  268. .createSelectorQuery()
  269. .in(this)
  270. .select('#wSelect')
  271. .boundingClientRect(data => {
  272. this.distanceTop = data.top // 当前元素距离顶部的距离
  273. this.curHeight = data.height
  274. })
  275. .exec()
  276. })
  277. },
  278. methods: {
  279. showPositon () {
  280. this.showPosition = 'bottom'
  281. if (this.bottomDistance < this.windowHeight / 3) {
  282. this.showPosition = 'top'
  283. }
  284. },
  285. changeShow () {
  286. this.isShow = !this.isShow
  287. if (this.isShow === false) {
  288. this.filterList = this.list
  289. setTimeout(() => {
  290. this.optionsShow = false
  291. }, 200)
  292. } else {
  293. this.showPositon()
  294. this.optionsShow = this.isShow;
  295. // 重置列表
  296. this.filterList = this.list
  297. }
  298. },
  299. closeContentSelect () {
  300. this.isShow = false
  301. setTimeout(() => {
  302. this.optionsShow = false
  303. }, 200)
  304. },
  305. setValue (value = '') {
  306. // #ifdef VUE3
  307. this.$emit('update:modelValue', value)
  308. // #endif
  309. // #ifdef VUE2
  310. this.$emit('input', value)
  311. // #endif
  312. },
  313. inputChange (e) {
  314. const value = e.detail.value
  315. if(this.multiple && this.filterable) {
  316. this.inputData = value
  317. }else {
  318. // this.setValue(value)
  319. this.inputData = value
  320. }
  321. this.filterList = this.list.filter(item =>
  322. item[this.valueName].includes(value)
  323. )
  324. },
  325. blurChange(e) {
  326. const value = e.detail.value
  327. this.inputData=this.chosevalue;
  328. if(this.multiple && this.filterable && value) {
  329. let curValue ={
  330. [this.keyName]:value,
  331. [this.valueName]:value
  332. }
  333. this.multiSelect(curValue)
  334. }
  335. },
  336. refreshValue () {
  337. this.setValue('')
  338. this.inputData = ''
  339. this.$emit('change', '')
  340. this.filterList = this.list
  341. if (this.multiple) {
  342. this.multiSelectList = []
  343. }
  344. },
  345. handleClickItem (e) {
  346. if (this.multiple) {
  347. this.multiSelect(e)
  348. } else {
  349. this.setValue(e[this.keyName])
  350. this.inputData = e[this.valueName]
  351. this.$emit('change', e)
  352. this.changeShow()
  353. }
  354. },
  355. multiSelect (item) {
  356. const index = this.multiSelectList.findIndex(
  357. res => res[this.valueName] === item[this.valueName]
  358. )
  359. if (index > -1) {
  360. this.multiSelectList.splice(index, 1)
  361. } else {
  362. this.multiSelectList.push(item)
  363. }
  364. this.inputData = ''
  365. this.filterList = this.list
  366. this.setValue(this.multiSelectList)
  367. this.$emit('change', item)
  368. }
  369. }
  370. }
  371. </script>
  372. <style lang="scss" scoped>
  373. .select-wrap /deep/ .input-placeholder{font-size: 30rpx;color: #AAAAAA;}
  374. .w-select {
  375. --select-wrap-width: 200px;
  376. --select-wrap-height: 30px;
  377. --select-border-radius: 4px;
  378. --select-border: 1px solid #dcdfe6;
  379. --select-active-border: 1px solid #409eff;
  380. --select-options-max-height: 150px;
  381. --select-option-item-font-size: 14px;
  382. --select-input-font-size: 14px;
  383. --no-data-default-color: #999999;
  384. --select-options-box-shadow: 0px 0px 12px rgb(0 0 0 / 12%);
  385. --select-bg-color: #ffffff;
  386. .select-wrap {
  387. position: relative;
  388. display: flex;
  389. justify-content: space-between;
  390. align-items: center;
  391. width: var(--select-wrap-width);
  392. height: var(--select-wrap-height);
  393. padding: 0 10rpx 0 20rpx;
  394. box-sizing:border-box;
  395. // border: var(--select-border);
  396. border-radius: var(--select-border-radius);
  397. background-color: var(--select-bg-color);
  398. transition: all 0.2s;
  399. background-color: #EEEEEE;
  400. input {
  401. padding: 0 2px;
  402. width: 100%;
  403. min-width: 0;
  404. height: 100%;
  405. font-size: var(--select-input-font-size);
  406. flex: 1;
  407. text-align: left;
  408. }
  409. .select-content {
  410. display: flex;
  411. align-items: center;
  412. font-size: var(--select-option-item-font-size);
  413. .select-content-item {
  414. margin-left: 5px;
  415. padding: 2px 6px;
  416. border-radius: var(--select-border-radius);
  417. color: #aa93b1;
  418. background-color: #f4f4f5;
  419. }
  420. .select-content-item-default {
  421. margin-left: 5px;
  422. color: var(--no-data-default-color);
  423. }
  424. }
  425. .close-icon {
  426. position: absolute;
  427. top: 50%;
  428. right: 7px;
  429. z-index: 1000;
  430. width: 15px;
  431. height: 15px;
  432. transform: translateY(-50%);
  433. image {
  434. width: 100%;
  435. height: 100%;
  436. }
  437. }
  438. .position-bottom {
  439. top: calc(var(--select-wrap-height) + 10px);
  440. }
  441. .position-top {
  442. bottom: calc(var(--select-wrap-height) + 10px);
  443. }
  444. .select-options {
  445. position: absolute;
  446. right: 0;
  447. left: 0;
  448. z-index: 999;
  449. overflow: scroll;
  450. padding: 10px;
  451. max-height: var(--select-options-max-height);
  452. border-radius: var(--select-border-radius);
  453. background-color: var(--select-bg-color);
  454. box-shadow: var(--select-options-box-shadow);
  455. .select-option-item {
  456. margin-bottom: 5px;
  457. padding: 5px;
  458. font-size: var(--select-option-item-font-size);
  459. transition: background-color 0.2s;
  460. }
  461. .item-active {
  462. font-weight: 700;
  463. color: #409eff;
  464. // background-color: #f5f7fa;
  465. }
  466. .options-no-data {
  467. font-size: var(--select-option-item-font-size);
  468. text-align: center;
  469. color: var(--no-data-default-color);
  470. }
  471. }
  472. .w-select-arrow {
  473. display: inline-block;
  474. margin: 3px 10px 0;
  475. width: 8px;
  476. height: 8px;
  477. border-top: 1px solid transparent;
  478. border-right: 1px solid transparent;
  479. border-bottom: 1px solid #999999;
  480. border-left: 1px solid #999999;
  481. transition: all 0.3s;
  482. transform: translateY(-50%) rotate(-45deg);
  483. }
  484. .w-select-arrow-up {
  485. transform: rotate(-225deg);
  486. }
  487. }
  488. .select-wrap-active {
  489. // border: var(--select-active-border);
  490. }
  491. .animation-bottom-in {
  492. animation-name: bottom-in;
  493. animation-duration: 0.4s;
  494. animation-timing-function: ease-out;
  495. animation-fill-mode: both;
  496. }
  497. .animation-bottom-out {
  498. animation-name: bottom-out;
  499. animation-duration: 0.2s;
  500. animation-timing-function: ease-out;
  501. animation-fill-mode: both;
  502. }
  503. .animation-top-in {
  504. animation-name: top-in;
  505. animation-duration: 0.4s;
  506. animation-timing-function: ease-out;
  507. animation-fill-mode: both;
  508. }
  509. .animation-top-out {
  510. animation-name: top-out;
  511. animation-duration: 0.2s;
  512. animation-timing-function: ease-out;
  513. animation-fill-mode: both;
  514. }
  515. @keyframes bottom-in {
  516. 0% {
  517. opacity: 0;
  518. transform: translateY(-15%);
  519. }
  520. 100% {
  521. opacity: 1;
  522. transform: translateY(0);
  523. }
  524. }
  525. @keyframes bottom-out {
  526. 0% {
  527. opacity: 1;
  528. transform: translateY(0);
  529. }
  530. 100% {
  531. opacity: 0;
  532. transform: translateY(-20%);
  533. }
  534. }
  535. @keyframes top-in {
  536. 0% {
  537. opacity: 0;
  538. transform: translateY(15%);
  539. }
  540. 100% {
  541. opacity: 1;
  542. transform: translateY(0);
  543. }
  544. }
  545. @keyframes top-out {
  546. 0% {
  547. opacity: 1;
  548. transform: translateY(0);
  549. }
  550. 100% {
  551. opacity: 0;
  552. transform: translateY(20%);
  553. }
  554. }
  555. .contentMask {
  556. position: fixed;
  557. top: 0;
  558. right: 0;
  559. bottom: 0;
  560. left: 0;
  561. z-index: 998;
  562. width: 100%;
  563. height: 100%;
  564. }
  565. }
  566. </style>