e-select-built-in.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. <template>
  2. <view
  3. class="e-select-box"
  4. :style="{ width: width, minWidth: minWidth }">
  5. <view
  6. class="e-select"
  7. :class="{ 'e-select-disabled': disabled }">
  8. <view
  9. class="e-select-input-box"
  10. @click="inputClick ? toggleSelector() : ''"
  11. >
  12. <!-- @click="toggleSelector" -->
  13. <!-- 微信小程序input组件在部分安卓机型上会出现文字重影,placeholder抖动问题,2019年时微信小程序就有这个问题,一直没修复,估计短时间内也别指望修复了 -->
  14. <input
  15. class="e-select-input-text"
  16. :placeholder="placeholder"
  17. placeholder-class="e-select-input-placeholder"
  18. v-model="currentData"
  19. @input="filter"
  20. v-if="search && !disabled" />
  21. <view
  22. class="e-select-input-text"
  23. :class="{
  24. 'e-select-input-placeholder': !(currentData || currentData === 0),
  25. }"
  26. v-else>
  27. {{ currentData || currentData === 0 ? currentData : placeholder }}
  28. </view>
  29. <!-- 清空图标,用一个更大的盒子包裹图标,便于点击 -->
  30. <view
  31. class="e-select-icon"
  32. @click.stop="clearVal"
  33. v-if="currentData && clearable && !disabled">
  34. <uni-icons
  35. type="clear"
  36. color="#e1e1e1"
  37. size="16"></uni-icons>
  38. </view>
  39. <!-- 箭头图标,同上 -->
  40. <view
  41. class="e-select-icon"
  42. @click.stop="toggleSelector"
  43. v-else>
  44. <uni-icons
  45. size="16"
  46. color="#6A6A6A"
  47. type="top"
  48. class="arrowAnimation"
  49. :class="showSelector ? 'e-select-top' : 'e-select-bottom'"></uni-icons>
  50. </view>
  51. </view>
  52. <!-- 全屏遮罩-->
  53. <view
  54. class="e-select--mask"
  55. v-if="showSelector"
  56. @click.stop="toggleSelector" />
  57. <!-- 选项列表 这里用v-show是因为微信小程序会报警告 [Component] slot "" is not found,v-if会导致开发工具不能正确识别到slot -->
  58. <!-- https://developers.weixin.qq.com/community/minihome/doc/000c8295730700d1cd7c81b9656c00 -->
  59. <view
  60. class="e-select-selector"
  61. :style="
  62. position === 'top'
  63. ? 'bottom: calc(0px + 42px)'
  64. : 'top: calc(100% + 12px)'
  65. "
  66. v-show="showSelector">
  67. <!-- 三角小箭头 -->
  68. <view
  69. :class="
  70. position === 'top' ? 'e-popper-arrow-bottom' : 'e-popper-arrow'
  71. "></view>
  72. <scroll-view
  73. scroll-y="true"
  74. :scroll-top="scrollTop"
  75. class="e-select-selector-scroll"
  76. :style="{ maxHeight: maxHeight }"
  77. :scroll-into-view="scrollToId"
  78. :scroll-with-animation="scrollWithAnimation"
  79. v-if="showSelector">
  80. <view
  81. class="e-select-selector-empty"
  82. v-if="currentOptions.length === 0">
  83. <text>{{ emptyTips }}</text>
  84. </view>
  85. <!-- 非空,渲染选项列表 -->
  86. <view
  87. v-else
  88. class="e-select-selector-item"
  89. :class="[
  90. { highlight: currentData == item[props.text] },
  91. {
  92. 'e-select-selector-item-disabled': item[props.disabled],
  93. },
  94. ]"
  95. v-for="(item, index) in currentOptions"
  96. :key="index"
  97. @click="change(item, index)">
  98. <view
  99. id="scrollToId"
  100. v-if="currentData == item[props.text]"></view>
  101. <text>{{ item[props.text] }}</text>
  102. </view>
  103. </scroll-view>
  104. <slot />
  105. </view>
  106. </view>
  107. </view>
  108. </template>
  109. <script>
  110. export default {
  111. name: 'e-select',
  112. data() {
  113. return {
  114. // 是否显示下拉选择列表
  115. showSelector: false,
  116. // 当前选项列表
  117. currentOptions: [],
  118. // 过滤后的选项列表
  119. filterOptions: [],
  120. // 当前值
  121. currentData: '',
  122. // 滚动高度
  123. scrollTop: 0,
  124. // 滚动至的id
  125. scrollToId: 'scrollToId',
  126. // 滚动动画
  127. scrollWithAnimation: false,
  128. };
  129. },
  130. props: {
  131. // vue2 v-model传值方式
  132. value: {
  133. type: [String, Number],
  134. default: '',
  135. },
  136. // vue3 v-model传值方式
  137. modelValue: {
  138. type: [String, Number],
  139. default: '',
  140. },
  141. // 选项列表
  142. options: {
  143. type: Array,
  144. default() {
  145. return [];
  146. },
  147. },
  148. // 选项列表自定义数据格式
  149. props: {
  150. type: Object,
  151. default: () => {
  152. return {
  153. text: 'text',
  154. value: 'value',
  155. disabled: 'disabled',
  156. };
  157. },
  158. },
  159. // 是否允许输入框响应点击事件
  160. inputClick: {
  161. type: Boolean,
  162. default: true,
  163. },
  164. // 占位文本
  165. placeholder: {
  166. type: String,
  167. default: '请选择',
  168. },
  169. // 输入框宽度
  170. width: {
  171. type: String,
  172. default: '100%',
  173. },
  174. // 输入框最小宽度
  175. minWidth: {
  176. type: String,
  177. default: '120rpx',
  178. },
  179. // 选项列表悬浮框最大高度
  180. maxHeight: {
  181. type: String,
  182. default: '160px',
  183. },
  184. // 选项列表空值占位空值占位
  185. emptyTips: {
  186. type: String,
  187. default: '暂无选项',
  188. },
  189. // 是否可清空
  190. clearable: {
  191. type: Boolean,
  192. default: false,
  193. },
  194. // 是否禁用
  195. disabled: {
  196. type: Boolean,
  197. default: false,
  198. },
  199. // 是否开启搜索
  200. search: {
  201. type: Boolean,
  202. default: true,
  203. },
  204. // 是否开启搜索的滚动动画
  205. animation: {
  206. type: Boolean,
  207. default: true,
  208. },
  209. // 悬浮框位置top/bottom
  210. position: {
  211. type: String,
  212. default: 'bottom',
  213. },
  214. // 分页每页条数
  215. pageSize: {
  216. type: Number,
  217. default: 0,
  218. },
  219. // 分页当前页数
  220. pageIndex: {
  221. type: Number,
  222. default: 1,
  223. },
  224. },
  225. watch: {
  226. options: {
  227. handler(val) {
  228. this.filterOptions = val.slice();
  229. this.initOptions();
  230. this.initData();
  231. },
  232. immediate: true,
  233. deep: true,
  234. },
  235. modelValue: {
  236. handler() {
  237. this.initData();
  238. },
  239. immediate: true,
  240. },
  241. value: {
  242. handler() {
  243. this.initData();
  244. },
  245. immediate: true,
  246. },
  247. pageSize() {
  248. this.initOptions();
  249. },
  250. pageIndex() {
  251. this.initOptions();
  252. },
  253. },
  254. methods: {
  255. /** 处理数据,此函数用于兼容vue2 vue3 */
  256. initData() {
  257. this.currentData = '';
  258. // vue2
  259. if (this.value || this.value === 0) {
  260. for (let i = 0; i < this.options.length; i++) {
  261. const item = this.options[i];
  262. if (item[this.props.value] === this.value) {
  263. this.currentData = item[this.props.text];
  264. this.$emit('getText', this.currentData);
  265. // 如果分页,初始化分页当前页数
  266. if (this.pageSize && this.pageIndex) {
  267. this.$emit('update:pageIndex', Math.floor(i / this.pageSize) + 1);
  268. }
  269. return;
  270. }
  271. }
  272. }
  273. // vue3
  274. else if (this.modelValue || this.modelValue === 0) {
  275. for (let i = 0; i < this.options.length; i++) {
  276. const item = this.options[i];
  277. if (item[this.props.value] === this.modelValue) {
  278. this.currentData = item[this.props.text];
  279. this.$emit('getText', this.currentData);
  280. if (this.pageSize && this.pageIndex) {
  281. this.$emit('update:pageIndex', Math.floor(i / this.pageSize) + 1);
  282. }
  283. return;
  284. }
  285. }
  286. }
  287. },
  288. /** 初始化选项列表 */
  289. initOptions() {
  290. // 设置分页情况下列表
  291. if (this.pageSize && this.pageIndex) {
  292. this.currentOptions = this.filterOptions.slice(
  293. (this.pageIndex - 1) * this.pageSize,
  294. this.pageIndex * this.pageSize
  295. );
  296. } else {
  297. this.currentOptions = this.filterOptions;
  298. }
  299. // scrollTop变化,才能触发滚动顶部,再低如0.01则不能触发,真神奇
  300. this.scrollTop = 0.1;
  301. this.$nextTick(() => {
  302. this.scrollTop = 0;
  303. });
  304. },
  305. /** 过滤选项列表,会自动回到顶部 */
  306. filter() {
  307. // 回到分页第一页
  308. this.$emit('update:pageIndex', 1);
  309. this.$emit('getText', this.currentData);
  310. if (this.currentData) {
  311. this.filterOptions = this.options.filter((item) => {
  312. return item[this.props.text].indexOf(this.currentData) > -1;
  313. });
  314. this.$emit('update:total', this.filterOptions.length);
  315. }
  316. // 等待update:pageIndex事件执行完成
  317. setTimeout(() => {
  318. this.initOptions();
  319. }, 0);
  320. },
  321. /** 改变值 */
  322. change(item, index) {
  323. if (item[this.props.disabled]) return;
  324. const data = {
  325. index,
  326. ...item,
  327. };
  328. this.$emit('change', data);
  329. this.emit(data);
  330. this.toggleSelector();
  331. },
  332. /** 传递父组件值 */
  333. emit(item) {
  334. this.$emit('input', item[this.props.value]);
  335. this.$emit('update:modelValue', item[this.props.value]);
  336. },
  337. /** 清空值 */
  338. clearVal() {
  339. this.$emit('change', 'clear');
  340. this.$emit('input', '');
  341. this.$emit('update:modelValue', '');
  342. },
  343. /** 切换下拉显示 */
  344. toggleSelector() {
  345. if (this.disabled) return;
  346. this.showSelector = !this.showSelector;
  347. if (this.showSelector) {
  348. // 设计理念:只在filter时触发滚动动画,因为每次打开就触发,用户体验不好
  349. if (this.animation) {
  350. setTimeout(() => {
  351. // 开启滚动动画
  352. this.scrollWithAnimation = true;
  353. }, 100);
  354. }
  355. } else {
  356. // 关闭时重新初始化
  357. this.filterOptions = this.options.slice();
  358. this.initData();
  359. this.initOptions();
  360. this.$emit('update:total', this.options.length);
  361. this.scrollWithAnimation = false;
  362. }
  363. },
  364. },
  365. };
  366. </script>
  367. <style lang="scss" scoped>
  368. .e-select-box {
  369. display: flex;
  370. align-items: center;
  371. width: 100%;
  372. box-sizing: border-box;
  373. cursor: pointer;
  374. -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
  375. }
  376. .e-select {
  377. width: 100%;
  378. border-radius: 4px;
  379. box-sizing: border-box;
  380. display: flex;
  381. align-items: center;
  382. user-select: none;
  383. position: relative;
  384. border: 1px solid #dcdfe6;
  385. }
  386. .e-select-disabled {
  387. background-color: #f5f7fa;
  388. cursor: not-allowed;
  389. }
  390. .e-select-input-box {
  391. width: 100%;
  392. padding: 0px 20rpx;
  393. min-height: 34px;
  394. position: relative;
  395. display: flex;
  396. flex: 1;
  397. flex-direction: row;
  398. align-items: center;
  399. .e-select-input-text {
  400. color: #303030;
  401. width: 100%;
  402. color: #333;
  403. white-space: nowrap;
  404. text-overflow: ellipsis;
  405. -o-text-overflow: ellipsis;
  406. overflow: hidden;
  407. font-size: 28rpx;
  408. }
  409. .e-select-input-placeholder {
  410. font-size: 28rpx;
  411. color: #999999;
  412. }
  413. .e-select-icon {
  414. width: 50px;
  415. padding-right: 3px;
  416. height: 100%;
  417. display: flex;
  418. justify-content: flex-end;
  419. align-items: center;
  420. }
  421. .arrowAnimation {
  422. transition: transform 0.3s;
  423. }
  424. // .top {
  425. .e-select-top {
  426. transform: rotateZ(0deg);
  427. }
  428. // .bottom {
  429. .e-select-bottom {
  430. transform: rotateZ(180deg);
  431. }
  432. }
  433. .e-select--mask {
  434. position: fixed;
  435. top: 0;
  436. bottom: 0;
  437. right: 0;
  438. left: 0;
  439. z-index: 999;
  440. }
  441. .e-select-selector {
  442. box-sizing: border-box;
  443. position: absolute;
  444. left: 0;
  445. width: 100%;
  446. background-color: #ffffff;
  447. border: 1px solid #ebeef5;
  448. border-radius: 6px;
  449. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  450. z-index: 999;
  451. padding: 4px 2px;
  452. transition: all 1s;
  453. .e-select-selector-scroll {
  454. box-sizing: border-box;
  455. .e-select-selector-empty,
  456. .e-select-selector-item {
  457. display: flex;
  458. cursor: pointer;
  459. line-height: 35rpx;
  460. font-size: 28rpx;
  461. text-align: left;
  462. padding: 15rpx 10px;
  463. }
  464. .e-select-selector-item:hover {
  465. background-color: #f9f9f9;
  466. }
  467. .e-select-selector-empty:last-child,
  468. .e-select-selector-item:last-child {
  469. border-bottom: none;
  470. }
  471. .e-select-selector-item-disabled {
  472. color: #b1b1b1;
  473. cursor: not-allowed;
  474. }
  475. .highlight {
  476. color: #409eff;
  477. font-weight: bold;
  478. background-color: #f5f7fa;
  479. border-radius: 3px;
  480. }
  481. }
  482. }
  483. .e-popper-arrow,
  484. .e-popper-arrow::after,
  485. .e-popper-arrow-bottom,
  486. .e-popper-arrow-bottom::after {
  487. position: absolute;
  488. display: block;
  489. width: 0;
  490. height: 0;
  491. left: 50%;
  492. border-color: transparent;
  493. border-style: solid;
  494. border-width: 6px;
  495. }
  496. .e-popper-arrow {
  497. filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
  498. top: -6px;
  499. left: 50%;
  500. transform: translateX(-50%);
  501. margin-right: 3px;
  502. border-top-width: 0;
  503. border-bottom-color: #ebeef5;
  504. }
  505. .e-popper-arrow::after {
  506. content: ' ';
  507. top: 1px;
  508. margin-left: -6px;
  509. border-top-width: 0;
  510. border-bottom-color: #fff;
  511. }
  512. .e-popper-arrow-bottom {
  513. filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
  514. bottom: -6px;
  515. left: 50%;
  516. transform: translateX(-50%);
  517. margin-right: 3px;
  518. border-bottom-width: 0;
  519. border-top-color: #ebeef5;
  520. }
  521. .e-popper-arrow-bottom::after {
  522. content: ' ';
  523. bottom: 1px;
  524. margin-left: -6px;
  525. border-bottom-width: 0;
  526. border-top-color: #fff;
  527. }
  528. /* 设置定位元素的位置 */
  529. #scrollToId {
  530. margin-top: -15rpx;
  531. }
  532. </style>