next-data-select.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. <template>
  2. <view class="next-stat__select">
  3. <span v-if="label" class="next-label-text">{{label + ':'}}</span>
  4. <view class="next-stat-box" :class="{'next-stat__actived': current}">
  5. <view class="next-select" :style="{height:multiple?'100%':' 35px'}"
  6. :class="{'next-select--disabled':disabled}">
  7. <view class="next-select__input-box" :style="{height:multiple?'100%':'35px'}" @tap.stop @click.stop="toggleSelector">
  8. <view class="" style="display: flex;flex-wrap: wrap;width: 100%;align-items: center;" v-if="multiple&&current.length>0">
  9. <view class="tag-calss"
  10. v-for="(item,index) in collapseTags?current.slice(0,collapseTagsNum):current"
  11. :key="item[optionsValueKey]">
  12. <span class="text">{{item[optionsLabelKey]}}</span>
  13. <view class="" @click.stop="delItem(item)">
  14. <text style="margin-left: 4px;vertical-align: middle;" color="#c0c4cc" class="icon">&#xe61c;</text>
  15. </view>
  16. </view>
  17. <view v-if="current.length>collapseTagsNum&&collapseTags" class="tag-calss">
  18. <span class="text">+{{current.length-collapseTagsNum}}</span>
  19. </view>
  20. <input v-if="filterable&&!disabled" @input="inputChange" class="next-select__input-text"
  21. type="text" style="font-size: 12px;height: 52rpx;box-sizing: border-box;margin-left: 6px;width: auto;"
  22. placeholder="请输入" v-model="filterInput">
  23. </view>
  24. <view v-else-if="current&&current.length>0&&(!showSelector || (!multiple && !filterable))" class="next-select__input-text">
  25. {{current}}
  26. </view>
  27. <input v-else-if="filterable&&showSelector" :focus="isFocus" @input="inputChange"
  28. :disabled="disabled" @click.stop="" class="next-select__input-text" type="text"
  29. style="font-size: 12px;position: absolute;z-index: 1;" :placeholder="placeholderOld"
  30. v-model="filterInput">
  31. <view v-else class="next-select__input-text next-select__input-placeholder">{{placeholder}}</view>
  32. <text @click="clearVal" v-if="(current.length>0 && clear&&!disabled)||(currentArr.length>0&&clear&&!disabled)" style="position: absolute;right: 0;font-size: 24px;z-index:2" class="icon">&#xe61c;</text>
  33. <text v-else-if="showSelector" class="icon" style="right: 5px;position: absolute;font-size: 14px">&#xe619;</text>
  34. <text v-else class="icon" style="right: 5px;position: absolute;font-size: 14px">&#xe617;</text>
  35. </view>
  36. <view class="next-select--mask" v-if="showSelector" @click="toggleSelector" />
  37. <view @tap.stop class="next-select__selector" v-if="showSelector">
  38. <view class="next-popper__arrow"></view>
  39. <scroll-view scroll-y="true" class="next-select__selector-scroll">
  40. <view class="next-select__selector-empty" v-if="filterLocalData.length === 0">
  41. <span>{{emptyTips}}</span>
  42. </view>
  43. <view v-else :style="currentArr.includes(item[optionsValueKey]) ? 'color:' + themeColor : ''" :class="['next-select__selector-item', {'next-select_selector-item_active' :currentArr.includes(item[optionsValueKey])}]"
  44. style="display: flex;justify-content: space-between;align-items: center;"
  45. v-for="(item,index) in filterLocalData" :key="index" @click.stop="change(item)">
  46. <span
  47. :class="{'next-select__selector__disabled': item[optionsDisabledKey]}">{{formatItemName(item)}}</span>
  48. <text :style="'color:' + themeColor" v-if="currentArr.includes(item[optionsValueKey])" class="icon">&#xe6cf;</text>
  49. </view>
  50. </scroll-view>
  51. </view>
  52. </view>
  53. </view>
  54. </view>
  55. </template>
  56. <script>
  57. export default {
  58. name: "next-data-select",
  59. props: {
  60. collapseTagsNum: {
  61. type: Number,
  62. default: 1
  63. },
  64. collapseTags: {
  65. type: Boolean,
  66. default: false
  67. },
  68. options: {
  69. type: Array,
  70. default () {
  71. return []
  72. }
  73. },
  74. optionsLabelKey: {
  75. type: String,
  76. default: 'text'
  77. },
  78. optionsValueKey: {
  79. type: String,
  80. default: 'value'
  81. },
  82. optionsDisabledKey: {
  83. type: String,
  84. default: 'disabled'
  85. },
  86. multiple: {
  87. type: Boolean,
  88. default: false
  89. },
  90. filterable: {
  91. type: Boolean,
  92. default: false
  93. },
  94. // #ifndef VUE3
  95. value: {
  96. type: [String, Number, Array],
  97. default: ''
  98. },
  99. // #endif
  100. // #ifdef VUE3
  101. modelValue: {
  102. type: [String, Number, Array],
  103. default: ''
  104. },
  105. // #endif
  106. label: {
  107. type: String,
  108. default: ''
  109. },
  110. placeholder: {
  111. type: String,
  112. default: '请选择'
  113. },
  114. emptyTips: {
  115. type: String,
  116. default: '无选项'
  117. },
  118. clear: {
  119. type: Boolean,
  120. default: true
  121. },
  122. disabled: {
  123. type: Boolean,
  124. default: false
  125. },
  126. // 格式化输出 用法 field="_id as value, version as text, uni_platform as label" format="{label} - {text}"
  127. format: {
  128. type: String,
  129. default: ''
  130. },
  131. themeColor: { // 主题颜色
  132. type: String,
  133. default: '#f9ae3d' // #f9ae3d
  134. },
  135. },
  136. data() {
  137. return {
  138. showSelector: false,
  139. current: [],
  140. localData: [],
  141. apps: [],
  142. channels: [],
  143. cacheKey: "next-data-select-lastSelectedValue",
  144. placeholderOld: "",
  145. currentArr: [],
  146. filterInput: "",
  147. isFocus: false
  148. };
  149. },
  150. created() {
  151. this.init();
  152. },
  153. computed: {
  154. filterLocalData() {
  155. if (this.filterable && this.filterInput) {
  156. return this.localData.filter(e => e[this.optionsLabelKey].includes(this.filterInput))
  157. } else {
  158. return this.localData
  159. }
  160. }
  161. },
  162. watch: {
  163. options: {
  164. immediate: true,
  165. handler(val, old) {
  166. if (Array.isArray(val) && old !== val) {
  167. this.localData = val || [];
  168. this.init();
  169. }
  170. }
  171. },
  172. // #ifdef VUE3
  173. modelValue() {
  174. this.init();
  175. },
  176. // #endif
  177. // #ifndef VUE3
  178. value() {
  179. this.init();
  180. },
  181. // #endif
  182. },
  183. methods: {
  184. init() {
  185. if (this.multiple) {
  186. // #ifndef VUE3
  187. this.currentArr = this.value || []
  188. // #endif
  189. // #ifdef VUE3
  190. this.currentArr = this.modelValue || []
  191. // #endif
  192. if (this.current.length > 0) {
  193. this.current = []
  194. }
  195. // #ifndef VUE3
  196. if (this.value && this.value.length > 0 && this.filterLocalData.length > 0) {
  197. this.current = this.value.map(item => {
  198. let current = this.localData.find(e =>
  199. e[this.optionsValueKey] == item
  200. )
  201. return {
  202. ...current
  203. }
  204. })
  205. }
  206. // #endif
  207. // #ifdef VUE3
  208. if (this.modelValue && this.modelValue.length > 0 && this.filterLocalData.length > 0) {
  209. this.current = this.modelValue.map(item => {
  210. let current = this.localData.find(e =>
  211. e[this.optionsValueKey] == item
  212. )
  213. return {
  214. ...current
  215. }
  216. })
  217. }
  218. // #endif
  219. } else {
  220. // #ifndef VUE3
  221. if (this.value || this.value == 0) {
  222. this.current = this.formatItemName(this.filterLocalData.find(e =>
  223. e[this.optionsValueKey] == this.value
  224. ))
  225. this.currentArr = [this.value]
  226. }
  227. // #endif
  228. // #ifdef VUE3
  229. if (this.modelValue || this.value == 0) {
  230. this.current = this.formatItemName(this.filterLocalData.find(e =>
  231. e[this.optionsValueKey] == this.modelValue
  232. ))
  233. this.currentArr = [this.modelValue]
  234. if(!this.current) {
  235. this.current = this.modelValue
  236. }
  237. }
  238. // #endif
  239. }
  240. this.placeholderOld = this.placeholder
  241. },
  242. debounce(fn, time = 100) {
  243. let timer = null
  244. return function(...args) {
  245. if (timer) clearTimeout(timer)
  246. timer = setTimeout(() => {
  247. fn.apply(this, args)
  248. }, time)
  249. }
  250. },
  251. /**
  252. * @param {[String, Number]} value
  253. * 判断用户给的 value 是否同时为禁用状态
  254. */
  255. isDisabled(value) {
  256. let isDisabled = false;
  257. this.localData.forEach(item => {
  258. if (item[this.optionsValueKey] === value) {
  259. isDisabled = item[this.optionsDisabledKey]
  260. }
  261. })
  262. return isDisabled;
  263. },
  264. inputChange(e) {
  265. this.$emit('inputChange', e.detail.value)
  266. },
  267. clearVal() {
  268. if (this.disabled) {
  269. return
  270. }
  271. if (this.multiple) {
  272. this.current = []
  273. this.currentArr = []
  274. this.emit([])
  275. } else {
  276. this.current = ""
  277. this.currentArr = []
  278. this.emit('')
  279. }
  280. this.placeholderOld = this.placeholder
  281. this.filterInput = ""
  282. },
  283. change(item) {
  284. if (!item[this.optionsDisabledKey]) {
  285. this.showSelector = false
  286. if (this.multiple) {
  287. if (!this.current) {
  288. this.current = []
  289. }
  290. if (!this.currentArr) {
  291. this.currentArr = []
  292. }
  293. if (this.currentArr.includes(item[this.optionsValueKey])) {
  294. let index = this.current.findIndex(e => {
  295. return e[this.optionsValueKey] == item[this.optionsValueKey]
  296. })
  297. this.current.splice(index, 1)
  298. this.currentArr.splice(index, 1)
  299. this.emit(this.current)
  300. } else {
  301. this.current.push(item)
  302. this.currentArr.push(item[this.optionsValueKey])
  303. this.emit(this.current)
  304. }
  305. this.filterInput = ""
  306. } else {
  307. this.current = this.formatItemName(item)
  308. this.currentArr = [item[this.optionsValueKey]]
  309. if (this.filterable) {
  310. this.filterInput = item[this.optionsLabelKey]
  311. }
  312. this.emit(item[this.optionsValueKey])
  313. }
  314. }
  315. },
  316. delItem(item) {
  317. if (this.disabled) {
  318. return
  319. }
  320. if (this.currentArr.includes(item[this.optionsValueKey])) {
  321. let index = this.current.findIndex(e => {
  322. return e[this.optionsValueKey] == item[this.optionsValueKey]
  323. })
  324. this.current.splice(index, 1)
  325. this.currentArr.splice(index, 1)
  326. this.emit(this.current)
  327. }
  328. },
  329. emit(val) {
  330. if (this.multiple) {
  331. this.$emit('input', this.currentArr)
  332. this.$emit('update:modelValue', this.currentArr)
  333. const currentArr = this.localData.filter(item => this.currentArr.includes(item[this
  334. .optionsValueKey]))
  335. this.$emit('change', currentArr)
  336. } else {
  337. this.$emit('input', val)
  338. this.$emit('update:modelValue', val)
  339. const current = this.localData.find(item => val == item[this.optionsValueKey])
  340. this.$emit('change', current)
  341. }
  342. },
  343. toggleSelector() {
  344. if (this.disabled) {
  345. return
  346. }
  347. this.showSelector = !this.showSelector
  348. this.isFocus = this.showSelector
  349. if (this.filterable && this.current && this.showSelector) {
  350. if (!this.multiple) {
  351. this.placeholderOld = this.current
  352. // this.filterInput = ""
  353. }
  354. } else if (this.filterable && !this.current && !this.showSelector) {
  355. if (this.placeholderOld != this.placeholder) {
  356. if (!this.multiple) {
  357. this.current = this.placeholderOld
  358. }
  359. }
  360. }
  361. this.filterInput = ""
  362. },
  363. formatItemName(item) {
  364. if (!item) {
  365. return ""
  366. }
  367. let text = item[this.optionsLabelKey]
  368. if (this.format) {
  369. // 格式化输出
  370. let str = "";
  371. str = this.format;
  372. for (let key in item) {
  373. str = str.replace(new RegExp(`{${key}}`, "g"), item[key]);
  374. }
  375. return str;
  376. } else {
  377. return text || '';
  378. }
  379. }
  380. }
  381. }
  382. </script>
  383. <style lang="scss">
  384. @font-face {
  385. font-family: 'iconfont';
  386. src: url('//at.alicdn.com/t/c/font_4110624_3hfahswu4mf.ttf?t=1695353456719') format('truetype');
  387. }
  388. .icon {
  389. font-family: iconfont;
  390. font-size: 32upx;
  391. font-style: normal;
  392. color: #999;
  393. }
  394. $next-base-color: #6a6a6a !default;
  395. $next-main-color: #333 !default;
  396. $next-secondary-color: #909399 !default;
  397. $next-border-3: #e5e5e5;
  398. /* #ifndef APP-NVUE */
  399. @media screen and (max-width: 500px) {
  400. .hide-on-phone {
  401. display: none;
  402. }
  403. }
  404. /* #endif */
  405. .next-stat__select {
  406. display: flex;
  407. align-items: center;
  408. // padding: 15px;
  409. cursor: pointer;
  410. width: 100%;
  411. flex: 1;
  412. box-sizing: border-box;
  413. user-select: none;
  414. -webkit-tap-highlight-color: rgba(0, 0, 0, 0); /* 禁用点击高亮效果 */
  415. }
  416. .next-stat-box {
  417. width: 100%;
  418. flex: 1;
  419. }
  420. .next-stat__actived {
  421. width: 100%;
  422. flex: 1;
  423. }
  424. .next-label-text {
  425. font-size: 14px;
  426. font-weight: bold;
  427. color: $next-base-color;
  428. margin: auto 0;
  429. margin-right: 5px;
  430. }
  431. .next-select {
  432. font-size: 14px;
  433. border: 1px solid $next-border-3;
  434. box-sizing: border-box;
  435. border-radius: 4px;
  436. padding: 0 5px;
  437. padding-left: 10px;
  438. position: relative;
  439. /* #ifndef APP-NVUE */
  440. display: flex;
  441. user-select: none;
  442. /* #endif */
  443. flex-direction: row;
  444. align-items: center;
  445. border-bottom: solid 1px $next-border-3;
  446. width: 100%;
  447. flex: 1;
  448. height: 35px;
  449. min-height: 35px;
  450. &--disabled {
  451. background-color: #f5f7fa;
  452. cursor: not-allowed;
  453. }
  454. }
  455. .next-select__label {
  456. font-size: 16px;
  457. // line-height: 22px;
  458. min-height: 35px;
  459. height: 35px;
  460. padding-right: 10px;
  461. color: $next-secondary-color;
  462. }
  463. .next-select__input-box {
  464. width: 100%;
  465. height: 35px;
  466. position: relative;
  467. /* #ifndef APP-NVUE */
  468. display: flex;
  469. /* #endif */
  470. flex: 1;
  471. flex-direction: row;
  472. align-items: center;
  473. .tag-calss {
  474. font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
  475. font-weight: 400;
  476. -webkit-font-smoothing: antialiased;
  477. -webkit-tap-highlight-color: transparent;
  478. font-size: 12px;
  479. border: 1px solid #d9ecff;
  480. border-radius: 4px;
  481. white-space: nowrap;
  482. height: 24px;
  483. padding: 0 4px 0px 8px;
  484. line-height: 22px;
  485. box-sizing: border-box;
  486. margin: 2px 0 2px 6px;
  487. display: flex;
  488. max-width: 100%;
  489. align-items: center;
  490. background-color: #f4f4f5;
  491. border-color: #e9e9eb;
  492. color: #909399;
  493. .text {
  494. font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
  495. font-weight: 400;
  496. -webkit-font-smoothing: antialiased;
  497. -webkit-tap-highlight-color: transparent;
  498. font-size: 12px;
  499. white-space: nowrap;
  500. line-height: 22px;
  501. color: #909399;
  502. overflow: hidden;
  503. text-overflow: ellipsis;
  504. }
  505. }
  506. }
  507. .next-select__input {
  508. flex: 1;
  509. font-size: 14px;
  510. height: 22px;
  511. line-height: 22px;
  512. }
  513. .next-select__input-plac {
  514. font-size: 14px;
  515. color: $next-secondary-color;
  516. }
  517. .next-select__selector {
  518. /* #ifndef APP-NVUE */
  519. box-sizing: border-box;
  520. /* #endif */
  521. position: absolute;
  522. top: calc(100% + 12px);
  523. left: 0;
  524. width: 100%;
  525. background-color: #FFFFFF;
  526. border: 1px solid #EBEEF5;
  527. border-radius: 6px;
  528. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  529. z-index: 3;
  530. padding: 4px 0;
  531. }
  532. .next-select__selector-scroll {
  533. /* #ifndef APP-NVUE */
  534. max-height: 200px;
  535. box-sizing: border-box;
  536. /* #endif */
  537. }
  538. .next-select__selector-empty,
  539. .next-select__selector-item {
  540. /* #ifndef APP-NVUE */
  541. display: flex;
  542. cursor: pointer;
  543. /* #endif */
  544. line-height: 35px;
  545. font-size: 14px;
  546. text-align: center;
  547. /* border-bottom: solid 1px $next-border-3; */
  548. padding: 0px 10px;
  549. }
  550. .next-select__selector-item:hover {
  551. background-color: #f9f9f9;
  552. }
  553. .next-select__selector-empty:last-child,
  554. .next-select__selector-item:last-child {
  555. /* #ifndef APP-NVUE */
  556. border-bottom: none;
  557. /* #endif */
  558. }
  559. .next-select_selector-item_active {
  560. font-weight: bold;
  561. background-color: #f5f7fa;
  562. border-radius: 3px;
  563. }
  564. .next-select__selector__disabled {
  565. opacity: 0.4;
  566. cursor: default;
  567. }
  568. /* picker 弹出层通用的指示小三角 */
  569. .next-popper__arrow,
  570. .next-popper__arrow::after {
  571. position: absolute;
  572. display: block;
  573. width: 0;
  574. height: 0;
  575. border-color: transparent;
  576. border-style: solid;
  577. border-width: 6px;
  578. }
  579. .next-popper__arrow {
  580. filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
  581. top: -6px;
  582. left: 10%;
  583. margin-right: 3px;
  584. border-top-width: 0;
  585. border-bottom-color: #EBEEF5;
  586. }
  587. .next-popper__arrow::after {
  588. content: " ";
  589. top: 1px;
  590. margin-left: -6px;
  591. border-top-width: 0;
  592. border-bottom-color: #fff;
  593. }
  594. .next-select__input-text {
  595. // width: 280px;
  596. width: 90%;
  597. color: $next-main-color;
  598. white-space: nowrap;
  599. text-overflow: ellipsis;
  600. -o-text-overflow: ellipsis;
  601. overflow: hidden;
  602. }
  603. .next-select__input-placeholder {
  604. color: $next-base-color;
  605. font-size: 12px;
  606. }
  607. .next-select--mask {
  608. position: fixed;
  609. top: 0;
  610. bottom: 0;
  611. right: 0;
  612. left: 0;
  613. }
  614. </style>