tn-select.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. <template>
  2. <view v-if="value" class="tn-select-class tn-select">
  3. <tn-popup
  4. v-model="value"
  5. mode="bottom"
  6. :popup="false"
  7. length="auto"
  8. :safeAreaInsetBottom="safeAreaInsetBottom"
  9. :maskCloseable="maskCloseable"
  10. :zIndex="elZIndex"
  11. @close="close"
  12. >
  13. <view class="tn-select__content">
  14. <!-- 头部 -->
  15. <view class="tn-select__content__header" @touchmove.stop.prevent>
  16. <view
  17. class="tn-select__content__header__btn tn-select__content__header--cancel"
  18. :style="{ color: cancelColor }"
  19. hover-class="tn-hover-class"
  20. hover-stay-time="150"
  21. @tap="getResult('cancel')"
  22. >{{ cancelText }}</view>
  23. <view class="tn-select__content__header__title">{{ title }}</view>
  24. <view
  25. class="tn-select__content__header__btn tn-select__content__header--confirm"
  26. :style="{ color: confirmColor }"
  27. hover-class="tn-hover-class"
  28. hover-stay-time="150"
  29. @tap="getResult('confirm')"
  30. >{{ confirmText }}</view>
  31. </view>
  32. <!-- 列表内容 -->
  33. <view class="tn-select__content__body">
  34. <picker-view
  35. class="tn-select__content__body__view"
  36. :value="defaultSelector"
  37. @pickstart="pickStart"
  38. @pickend="pickEnd"
  39. @change="columnChange"
  40. >
  41. <picker-view-column v-for="(item, index) in columnData" :key="index">
  42. <view class="tn-select__content__body__item" v-for="(sub_item, sub_index) in item" :key="sub_index">
  43. <view class="tn-text-ellipsis">
  44. {{ sub_item[labelName] }}
  45. </view>
  46. </view>
  47. </picker-view-column>
  48. </picker-view>
  49. </view>
  50. </view>
  51. </tn-popup>
  52. </view>
  53. </template>
  54. <script>
  55. export default {
  56. name: 'tn-select',
  57. props: {
  58. value: {
  59. type: Boolean,
  60. default: false
  61. },
  62. // 列表模式
  63. // single 单列 multi 多列 multi-auto 多列联动
  64. mode: {
  65. type: String,
  66. default: 'single'
  67. },
  68. // 列数据
  69. list: {
  70. type: Array,
  71. default() {
  72. return []
  73. }
  74. },
  75. // value属性名
  76. valueName: {
  77. type: String,
  78. default: 'value'
  79. },
  80. // label属性名
  81. labelName: {
  82. type: String,
  83. default: 'label'
  84. },
  85. // 当mode=multi-auto时,children的属性名
  86. childName: {
  87. type: String,
  88. default: 'children'
  89. },
  90. // 默认值
  91. defaultValue: {
  92. type: Array,
  93. default() {
  94. return [0]
  95. }
  96. },
  97. // 顶部标题
  98. title: {
  99. type: String,
  100. default: ''
  101. },
  102. // 取消按钮文字
  103. cancelText: {
  104. type: String,
  105. default: '取消'
  106. },
  107. // 取消按钮文字颜色
  108. cancelColor: {
  109. type: String,
  110. default: ''
  111. },
  112. // 确认按钮文字
  113. confirmText: {
  114. type: String,
  115. default: '确认'
  116. },
  117. // 确认按钮文字颜色
  118. confirmColor: {
  119. type: String,
  120. default: ''
  121. },
  122. // 点击遮罩关闭
  123. maskCloseable: {
  124. type: Boolean,
  125. default: true
  126. },
  127. // 预留安全区域
  128. safeAreaInsetBottom: {
  129. type: Boolean,
  130. default: false
  131. },
  132. // zIndex
  133. zIndex: {
  134. type: Number,
  135. default: 0
  136. }
  137. },
  138. computed: {
  139. elZIndex() {
  140. return this.zIndex ? this.zIndex : this.$t.zIndex.popup
  141. }
  142. },
  143. data() {
  144. return {
  145. // 列是否还在滑动中,微信小程序如果在滑动中就点确定,结果可能不准确
  146. moving: false,
  147. // 用户保存当前列的索引,用于判断下一次变化时改变的列
  148. defaultSelector: [0],
  149. // picker-view数据
  150. columnData: [],
  151. // 保存用户选择的结果
  152. selectValue: [],
  153. // 上一次改变时的index
  154. lastSelectIndex: [],
  155. // 列数
  156. columnNum: 0
  157. }
  158. },
  159. watch: {
  160. // 在select弹起的时候,重新初始化所有数据
  161. value: {
  162. handler(val) {
  163. if (val) setTimeout(() => this.init(), 10)
  164. },
  165. immediate: true
  166. }
  167. },
  168. methods: {
  169. // 标识滑动开始,只有微信小程序才有这样的事件
  170. pickStart() {
  171. // #ifdef MP-WEIXIN
  172. this.moving = true;
  173. // #endif
  174. },
  175. // 标识滑动结束
  176. pickEnd() {
  177. // #ifdef MP-WEIXIN
  178. this.moving = false;
  179. // #endif
  180. },
  181. init() {
  182. this.setColumnNum()
  183. this.setDefaultSelector()
  184. this.setColumnData()
  185. this.setSelectValue()
  186. },
  187. // 获取默认选中列下标
  188. setDefaultSelector() {
  189. // 如果没有传入默认值,生成用0填充长度为columnNum的数组
  190. this.defaultSelector = this.defaultValue.length === this.columnNum ? this.defaultValue : Array(this.columnNum).fill(0)
  191. this.lastSelectIndex = this.$t.deepClone(this.defaultSelector)
  192. },
  193. // 计算列数
  194. setColumnNum() {
  195. // 单列的数量为1
  196. if (this.mode === 'single') this.columnNum = 1
  197. // 多列时取list的长度
  198. else if (this.mode === 'multi') this.columnNum = this.list.length
  199. // 多列联动时,通过遍历list的第一个元素,得出有多少列
  200. else if (this.mode === 'multi-auto') {
  201. let num = 1
  202. let column = this.list
  203. // 如果存在children属性,再次遍历
  204. while (column[0][this.childName]) {
  205. column = column[0] ? column[0][this.childName] : {},
  206. num++
  207. }
  208. this.columnNum = num
  209. }
  210. },
  211. // 获取需要展示在picker中的列数据
  212. setColumnData() {
  213. let data = []
  214. this.selectValue = []
  215. if (this.mode === 'multi-auto') {
  216. // 获取所有数据中的第一个元素
  217. let column = this.list[this.defaultSelector.length ? this.defaultSelector[0] : 0]
  218. // 通过循环所有列数,再根据设定列的数组,得出当前需要渲染的整个列数组
  219. for (let i = 0; i < this.columnNum; i++) {
  220. // 第一列默认为整个list数组
  221. if (i === 0) {
  222. data[i] = this.list
  223. column = column[this.childName]
  224. } else {
  225. // 大于第一列时,判断是否有默认选中的,如果没有就用该列的第一项
  226. data[i] = column
  227. column = column[this.defaultSelector[i]][this.childName]
  228. }
  229. }
  230. } else if (this.mode === 'single') {
  231. data[0] = this.list
  232. } else {
  233. data = this.list
  234. }
  235. this.columnData = data
  236. },
  237. // 获取默认选中的值,如果没有设置,则默认选中第一项
  238. setSelectValue() {
  239. let tmp = null
  240. for (let i = 0; i < this.columnNum; i++) {
  241. tmp = this.columnData[i][this.defaultSelector[i]]
  242. let data = {
  243. value: tmp ? tmp[this.valueName] : null,
  244. label: tmp ? tmp[this.labelName] : null
  245. }
  246. // 判断是否存在额外的参数
  247. if (tmp && tmp.extra) data.extra = tmp.extra
  248. this.selectValue.push(data)
  249. }
  250. },
  251. // 列选项
  252. columnChange(event) {
  253. let index = null
  254. let columnIndex = event.detail.value
  255. this.selectValue = []
  256. if (this.mode === 'multi-auto') {
  257. // 对比前后两个数组,判断变更的是那一列
  258. this.lastSelectIndex.map((v, idx) => {
  259. if (v != columnIndex[idx]) index = idx
  260. })
  261. this.defaultSelector = columnIndex
  262. // 当前变化列的下一列的数据,需要获取上一列的数据,同时需要指定是上一列的第几个的children,再往后的
  263. // 默认是队列的第一个为默认选项
  264. for (let i = index + 1; i < this.columnNum; i++) {
  265. this.columnData[i] = this.columnData[i - 1][i - 1 == index ? columnIndex[index] : 0][this.childName]
  266. this.defaultSelector[i] = 0
  267. }
  268. // 在历遍的过程中,可能由于上一步修改this.columnData,导致产生连锁反应,程序触发columnChange,会有多次调用
  269. // 只有在最后一次数据稳定后的结果是正确的,此前的历遍中,可能会产生undefined,故需要判断
  270. columnIndex.map((item, index) => {
  271. let data = this.columnData[index][columnIndex[index]]
  272. let tmp = {
  273. value: data ? data[this.valueName] : null,
  274. label: data ? data[this.labelName] : null
  275. }
  276. if (data && data.extra !== undefined) tmp.extra = data.extra
  277. this.selectValue.push(tmp)
  278. })
  279. this.lastSelectIndex = columnIndex
  280. } else if (this.mode === 'single') {
  281. let data = this.columnData[0][columnIndex[0]]
  282. let tmp = {
  283. value: data ? data[this.valueName] : null,
  284. label: data ? data[this.labelName] : null
  285. }
  286. if (data && data.extra !== undefined) tmp.extra = data.extra
  287. this.selectValue.push(tmp)
  288. } else if (this.mode === 'multi') {
  289. columnIndex.map((item, index) => {
  290. let data = this.columnData[index][columnIndex[index]]
  291. let tmp = {
  292. value: data ? data[this.valueName] : null,
  293. label: data ? data[this.labelName] : null
  294. }
  295. if (data && data.extra !== undefined) tmp.extra = data.extra
  296. this.selectValue.push(tmp)
  297. })
  298. }
  299. },
  300. close() {
  301. this.$emit('input', false)
  302. },
  303. getResult(event = null) {
  304. // #ifdef MP-WEIXIN
  305. if (this.moving) return;
  306. // #endif
  307. if (event) this.$emit(event, this.selectValue)
  308. this.close()
  309. }
  310. }
  311. }
  312. </script>
  313. <style lang="scss" scoped>
  314. .tn-select {
  315. &__content {
  316. position: relative;
  317. &__header {
  318. position: relative;
  319. display: flex;
  320. flex-direction: row;
  321. width: 100%;
  322. height: 90rpx;
  323. padding: 0 40rpx;
  324. align-items: center;
  325. justify-content: space-between;
  326. box-sizing: border-box;
  327. font-size: 30rpx;
  328. background-color: #FFFFFF;
  329. &__btn {
  330. padding: 16rpx;
  331. box-sizing: border-box;
  332. text-align: center;
  333. text-decoration: none;
  334. }
  335. &__title {
  336. color: $tn-font-color;
  337. }
  338. &--cancel {
  339. color: $tn-font-sub-color;
  340. }
  341. &--confirm {
  342. color: $tn-main-color;
  343. }
  344. }
  345. &__body {
  346. width: 100%;
  347. height: 500rpx;
  348. overflow: hidden;
  349. background-color: #FFFFFF;
  350. &__view {
  351. height: 100%;
  352. box-sizing: border-box;
  353. }
  354. &__item {
  355. display: flex;
  356. flex-direction: row;
  357. align-items: center;
  358. justify-content: center;
  359. font-size: 32rpx;
  360. color: $tn-font-color;
  361. padding: 0 8rpx;
  362. }
  363. }
  364. }
  365. }
  366. </style>