tn-cascade-selection.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. <template>
  2. <view class="tn-cascade-selection tn-cascade-selection-class">
  3. <scroll-view
  4. class="selection__scroll-view"
  5. :class="[{'tn-border-solid-bottom': headerLine}]"
  6. :style="[scrollViewStyle]"
  7. scroll-x
  8. scroll-with-animation
  9. :scroll-into-view="scrollViewId"
  10. >
  11. <view class="selection__header" :class="[backgroundColorClass]" :style="[headerStyle]">
  12. <view
  13. v-for="(item, index) in selectedArr"
  14. :key="index"
  15. :id="`select__${index}`"
  16. class="selection__header__item"
  17. :class="[headerItemClass(index)]"
  18. :style="[headerItemStyle(index)]"
  19. @tap.stop="clickNav(index)"
  20. >
  21. {{ item.text }}
  22. <view
  23. v-if="index===currentTab && showActiveLine"
  24. class="selection__header__line"
  25. :style="{backgroundColor: activeLineColor}"
  26. ></view>
  27. </view>
  28. </view>
  29. </scroll-view>
  30. <swiper
  31. class="selection__list"
  32. :class="[backgroundColorClass]"
  33. :style="[listStyle]"
  34. :current="currentTab"
  35. :duration="300"
  36. @change="switchTab"
  37. >
  38. <swiper-item
  39. v-for="(item, index) in selectedArr"
  40. :key="index"
  41. >
  42. <scroll-view
  43. class="selection__list__item"
  44. :style="{height: selectionContainerHeight + 'rpx'}"
  45. scroll-y
  46. :scroll-into-view="item.scrollViewId"
  47. >
  48. <view class="selection__list__item--first"></view>
  49. <view
  50. v-for="(subItem, subIndex) in item.list"
  51. :key="subIndex"
  52. :id="`select__${subIndex}`"
  53. class="selection__list__item__cell"
  54. :style="[itemStyle]"
  55. @tap="change(index, subIndex, subItem)"
  56. >
  57. <view
  58. v-if="item.index === subIndex"
  59. class="selection__list__item__icon tn-icon-success"
  60. :style="[itemIconStyle]"
  61. ></view>
  62. <image
  63. v-if="subItem.src"
  64. class="selection__list__item__image"
  65. :style="[itemImageStyle]"
  66. :src="subItem.src"
  67. ></image>
  68. <view
  69. class="selection__list__item__title"
  70. :class="[{'tn-text-bold': item.index === subIndex && itemActiveBold}]"
  71. :style="[itemTitleStyle(index, subIndex)]"
  72. >
  73. {{ subItem.text }}
  74. </view>
  75. <view
  76. v-if="subItem.subText"
  77. class="selection__list__item__title--sub"
  78. :style="[itemSubTitleStyle]"
  79. >
  80. {{ subItem.subText }}
  81. </view>
  82. </view>
  83. </scroll-view>
  84. </swiper-item>
  85. </swiper>
  86. </view>
  87. </template>
  88. <script>
  89. import componentsColorMixin from '../../libs/mixin/components_color.js'
  90. export default {
  91. name: 'tn-cascade-selection',
  92. mixins: [ componentsColorMixin ],
  93. props: {
  94. // 如果下一级是请求返回,则为第一级数据,否则为所有数据
  95. /* {
  96. text: '', // 标题
  97. subText: '', // 子标题
  98. src: '', // 图片地址
  99. value: 0, // 选中的值
  100. children: [
  101. {
  102. text: '',
  103. subText: '',
  104. value: 0,
  105. children: []
  106. }
  107. ]
  108. } */
  109. list: {
  110. type: Array,
  111. default() {
  112. return []
  113. }
  114. },
  115. // 默认选中值
  116. // ['value1','value2','value3']
  117. defaultValue: {
  118. type: Array,
  119. default() {
  120. return []
  121. }
  122. },
  123. // 子集数据通过请求来获取
  124. request: {
  125. type: Boolean,
  126. default: false
  127. },
  128. // request为true时生效, 获取到的子集数据
  129. receiveData: {
  130. type: Array,
  131. default() {
  132. return []
  133. }
  134. },
  135. // 显示header底部细线
  136. headerLine: {
  137. type: Boolean,
  138. default: true
  139. },
  140. // header背景颜色
  141. headerBgColor: {
  142. type: String,
  143. default: ''
  144. },
  145. // 顶部标签栏高度,单位rpx
  146. tabsHeight: {
  147. type: Number,
  148. default: 88
  149. },
  150. // 默认显示文字
  151. text: {
  152. type: String,
  153. default: '请选择'
  154. },
  155. // 选中的颜色
  156. activeColor: {
  157. type: String,
  158. default: '#01BEFF'
  159. },
  160. // 选中后加粗
  161. activeBold: {
  162. type: Boolean,
  163. default: true
  164. },
  165. // 选中显示底部线条
  166. showActiveLine: {
  167. type: Boolean,
  168. default: true
  169. },
  170. // 线条颜色
  171. activeLineColor: {
  172. type: String,
  173. default: '#01BEFF'
  174. },
  175. // icon大小,单位rpx
  176. activeIconSize: {
  177. type: Number,
  178. default: 0
  179. },
  180. // icon颜色
  181. activeIconColor: {
  182. type: String,
  183. default: '#01BEFF'
  184. },
  185. // item图片宽度, 单位rpx
  186. itemImgWidth: {
  187. type: Number,
  188. default: 0
  189. },
  190. // item图片高度, 单位rpx
  191. itemImgHeight: {
  192. type: Number,
  193. default: 0
  194. },
  195. // item图片圆角
  196. itemImgRadius: {
  197. type: String,
  198. default: '50%'
  199. },
  200. // item text颜色
  201. itemTextColor: {
  202. type: String,
  203. default: ''
  204. },
  205. // item text选中颜色
  206. itemActiveTextColor: {
  207. type: String,
  208. default: ''
  209. },
  210. // item text选中加粗
  211. itemActiveBold: {
  212. type: Boolean,
  213. default: true
  214. },
  215. // item text文字大小, 单位rpx
  216. itemTextSize: {
  217. type: Number,
  218. default: 0
  219. },
  220. // item subText颜色
  221. itemSubTextColor: {
  222. type: String,
  223. default: ''
  224. },
  225. // item subText字体大小, 单位rpx
  226. itemSubTextSize: {
  227. type: Number,
  228. default: 0
  229. },
  230. // item样式
  231. itemStyle: {
  232. type: Object,
  233. default() {
  234. return {}
  235. }
  236. },
  237. // selection选项容器高度, 单位rpx
  238. selectionContainerHeight: {
  239. type: Number,
  240. default: 300
  241. }
  242. },
  243. computed: {
  244. scrollViewStyle() {
  245. let style = {}
  246. if (this.headerBgColor) {
  247. style.backgroundColor = this.headerBgColor
  248. }
  249. return style
  250. },
  251. headerStyle() {
  252. let style = {}
  253. style.height = `${this.tabsHeight}rpx`
  254. if (this.backgroundColorStyle) {
  255. style.backgroundColor = this.backgroundColorStyle
  256. }
  257. return style
  258. },
  259. headerItemClass() {
  260. return (index) => {
  261. let clazz = ''
  262. if (index !== this.currentTab) {
  263. clazz += ` ${this.fontColorClass}`
  264. } else {
  265. if (this.activeBold) {
  266. clazz += ' tn-text-bold'
  267. }
  268. }
  269. return clazz
  270. }
  271. },
  272. headerItemStyle() {
  273. return (index) => {
  274. let style = {}
  275. style.color = index === this.currentTab ? this.activeColor : (this.fontColorStyle ? this.fontColorStyle : '')
  276. if (this.fontSizeStyle) {
  277. style.fontSize = this.fontSizeStyle
  278. }
  279. return style
  280. }
  281. },
  282. listStyle() {
  283. let style = {}
  284. style.height = `${this.selectionContainerHeight}rpx`
  285. if (this.backgroundColorStyle) {
  286. style.color = this.backgroundColorStyle
  287. }
  288. return style
  289. },
  290. itemIconStyle() {
  291. let style = {}
  292. if (this.activeIconColor) {
  293. style.color = this.activeIconColor
  294. }
  295. if (this.activeIconSize) {
  296. style.fontSize = this.activeIconSize + 'rpx'
  297. }
  298. return style
  299. },
  300. itemImageStyle() {
  301. let style = {}
  302. if (this.itemImgWidth) {
  303. style.width = this.itemImgWidth + 'rpx'
  304. }
  305. if (this.itemImgHeight) {
  306. style.height = this.itemImgHeight + 'rpx'
  307. }
  308. if (this.itemImgRadius) {
  309. style.borderRadius = this.itemImgRadius
  310. }
  311. return style
  312. },
  313. itemTitleStyle() {
  314. return (index, subIndex) => {
  315. let style = {}
  316. if (index === subIndex) {
  317. if (this.itemActiveTextColor) {
  318. style.color = this.itemActiveTextColor
  319. }
  320. } else {
  321. if (this.itemTextColor) {
  322. style.color = this.itemTextColor
  323. }
  324. }
  325. if (this.itemTextSize) {
  326. style.fontSize = this.itemTextSize + 'rpx'
  327. }
  328. return style
  329. }
  330. },
  331. itemSubTitleStyle() {
  332. let style = {}
  333. if (this.itemSubTextColor) {
  334. style.color = this.itemSubTextColor
  335. }
  336. if (this.itemSubTextSize) {
  337. style.fontSize = this.itemSubTextSize + 'rpx'
  338. }
  339. return {}
  340. }
  341. },
  342. watch: {
  343. list(val) {
  344. this.initData(val, -1)
  345. },
  346. defaultValue(val) {
  347. this.setDefaultValue(val)
  348. },
  349. receiveData(val) {
  350. this.addSubData(val, this.currentTab)
  351. },
  352. },
  353. data() {
  354. return {
  355. // 当前选中的子集
  356. currentTab: 0,
  357. // tabs栏scrollView滚动的位置
  358. scrollViewId: 'select__0',
  359. // 选项数组
  360. selectedArr: []
  361. }
  362. },
  363. created() {
  364. this.setDefaultValue(this.defaultValue)
  365. },
  366. methods: {
  367. // 初始化数据
  368. initData(data, index) {
  369. if (!data || data.length === 0) return
  370. if (this.request) {
  371. // 第一级数据
  372. this.addSubData(data, index)
  373. } else {
  374. this.addSubData(this.getItemList(index, -1), index)
  375. }
  376. },
  377. // 重置数据
  378. reset() {
  379. this.initData(this.list, -1)
  380. },
  381. // 滚动切换
  382. switchTab(e) {
  383. this.currentTab = e.detail.current
  384. this.checkSelectPosition()
  385. },
  386. // 点击标题切换
  387. clickNav(index) {
  388. if (this.currentTab !== index) {
  389. this.currentTab = index
  390. }
  391. },
  392. // 列表数据发生改变
  393. change(index, subIndex, subItem) {
  394. let item = this.selectedArr[index]
  395. if (item.index === subIndex) return
  396. item.index = subIndex
  397. item.text = subItem.text
  398. item.subText = subItem.subText || ''
  399. item.value = subItem.value
  400. item.src = subItem.src || ''
  401. this.$emit('change', {
  402. index: index,
  403. subIndex: subIndex,
  404. ...subItem
  405. })
  406. // 如果不是异步加载,则取出对应的数据
  407. if (!this.request) {
  408. let data = this.getItemList(index, subIndex)
  409. this.addSubData(data, index)
  410. }
  411. },
  412. // 设置默认的数据
  413. setDefaultValue(val) {
  414. let defaultValues = val || []
  415. if (defaultValues.length > 0) {
  416. this.selectedArr = this.getItemListWithValues(JSON.parse(JSON.stringify(this.list)), defaultValues)
  417. if (!this.selectedArr) return
  418. this.currentTab = this.selectedArr.length - 1
  419. this.$nextTick(() => {
  420. this.checkSelectPosition()
  421. })
  422. // defaultItemList.map((item) => {
  423. // item.scrollViewId = `select__${item.index}`
  424. // })
  425. // this.selectedArr = defaultItemList
  426. // this.currentTab = defaultItemList.length - 1
  427. // this.$nextTick(() => {
  428. // this.checkSelectPosition()
  429. // })
  430. } else {
  431. this.initData(this.list, -1)
  432. }
  433. },
  434. // 获取对应选项的item数据
  435. getItemList(index, subIndex) {
  436. let list = []
  437. let arr = JSON.parse(JSON.stringify(this.list))
  438. // 初始化数据
  439. if (index === -1) {
  440. list = this.removeChildren(arr)
  441. } else {
  442. // 判断第一项是否已经选择
  443. let value = this.selectedArr[0].index
  444. value = value === -1 ? subIndex : value
  445. list = arr[value].children || []
  446. if (index > 0) {
  447. for (let i = 1; i < index + 1; i++) {
  448. // 获取当前数据选中的序号
  449. let val = index === i ? subIndex : this.selectedArr[i].index
  450. list = list[val].children || []
  451. if (list.length === 0) break
  452. }
  453. }
  454. list = this.removeChildren(list)
  455. }
  456. return list
  457. },
  458. // 根据数组中的值获取对应的item数据
  459. getItemListWithValues(data, values) {
  460. const defaultValues = JSON.parse(JSON.stringify(values))
  461. if (!defaultValues || defaultValues.length === 0) return
  462. // 取出第一个值所对应的item
  463. const itemIndex = data.findIndex((item) => {
  464. return item.value === defaultValues[0]
  465. })
  466. if (itemIndex === -1) return
  467. const item = data[itemIndex]
  468. item.index = itemIndex
  469. item.scrollViewId = `select__${itemIndex}`
  470. item.list = this.removeChildren(JSON.parse(JSON.stringify(data)))
  471. // 判断是否只有1个值
  472. if (defaultValues.length === 1 || (!item.hasOwnProperty('children') || item.children.length === 0)) {
  473. return this.removeChildren([item])
  474. } else {
  475. let selectItemList = []
  476. const children = item.children
  477. selectItemList.push(item)
  478. // 移除已经获取的值
  479. defaultValues.splice(0, 1)
  480. const childrenValue = this.getItemListWithValues(children, defaultValues)
  481. selectItemList = selectItemList.concat(childrenValue)
  482. return this.removeChildren(selectItemList)
  483. }
  484. },
  485. // 删除子元素
  486. removeChildren(data) {
  487. let list = data.map((item) => {
  488. if (item.hasOwnProperty('children')) {
  489. delete item['children']
  490. }
  491. return item
  492. })
  493. return list
  494. },
  495. // 新增子集数据时处理
  496. addSubData(data, index) {
  497. // 判断是否已经完成选择数据或者为初始化数据
  498. if (!data || data.length === 0) {
  499. if (index == -1) return
  500. // 完成选择
  501. let arr = this.selectedArr
  502. // 如果当前选中项的序号比已选数据的长度小,则表示当前重新选择了数据
  503. if (index < arr.length - 1) {
  504. let newArr = arr.slice(0, index + 1)
  505. this.selectedArr = newArr
  506. }
  507. let result = JSON.parse(JSON.stringify(this.selectedArr))
  508. let lastItem = result[result.length - 1] || {}
  509. let text = ''
  510. result.map(item => {
  511. text += item.text
  512. delete item['list']
  513. delete item['scrollViewId']
  514. return item
  515. })
  516. this.$emit('complete', {
  517. result: result,
  518. value: lastItem.value,
  519. text: text,
  520. subText: lastItem.subText,
  521. src: lastItem.src
  522. })
  523. } else {
  524. // 重置数据
  525. let item = [{
  526. text: this.text,
  527. subText: '',
  528. value: '',
  529. src: '',
  530. index: -1,
  531. scrollViewId: 'select__0',
  532. list: data
  533. }]
  534. // 初始化数据
  535. if (index === -1) {
  536. this.selectedArr = item
  537. } else {
  538. // 拼接新旧数据并且判断是否为重新选择了数据(如果为重新选择了数据则重置之后的选项数据)
  539. let retainArr = this.selectedArr.slice(0, index + 1)
  540. this.selectedArr = retainArr.concat(item)
  541. }
  542. this.$nextTick(() => {
  543. this.currentTab = this.selectedArr.length - 1
  544. })
  545. }
  546. },
  547. // 检查当前选中项,并将选项设置位置信息
  548. checkSelectPosition() {
  549. let item = this.selectedArr[this.currentTab]
  550. item.scrollViewId = 'select__0'
  551. this.$nextTick(() => {
  552. setTimeout(() => {
  553. // 设置当前数据滚动到的位置
  554. let val = item.index < 2 ? 0 : Number(item.index - 2)
  555. item.scrollViewId = `select__${val}`
  556. }, 10)
  557. })
  558. // 设置选项滚动到所在的位置
  559. if (this.currentTab > 1) {
  560. this.scrollViewId = `select__${this.currentTab - 1}`
  561. } else {
  562. this.scrollViewId = `select__0`
  563. }
  564. }
  565. }
  566. }
  567. </script>
  568. <style lang="scss" scoped>
  569. .tn-cascade-selection {
  570. width: 100%;
  571. }
  572. .selection {
  573. &__scroll-view {
  574. background-color: #FFFFFF;
  575. }
  576. &__header {
  577. width: 100%;
  578. display: flex;
  579. align-items: center;
  580. position: relative;
  581. &__item {
  582. max-width: 240rpx;
  583. padding: 15rpx 30rpx;
  584. flex-shrink: 0;
  585. overflow: hidden;
  586. white-space: nowrap;
  587. text-overflow: ellipsis;
  588. position: relative;
  589. }
  590. &__line {
  591. width: 60rpx;
  592. height: 6rpx;
  593. border-radius: 4rpx;
  594. position: absolute;
  595. bottom: 0;
  596. right: 0;
  597. left: 50%;
  598. transform: translateX(-50%);
  599. }
  600. }
  601. &__list {
  602. background-color: #FFFFFF;
  603. &__item {
  604. &--first {
  605. width: 100%;
  606. height: 20rpx;
  607. }
  608. &__cell {
  609. width: 100%;
  610. display: flex;
  611. align-items: center;
  612. padding: 20rpx 30rpx;
  613. }
  614. &__icon {
  615. margin-right: 12rpx;
  616. font-size: 24rpx;
  617. }
  618. &__image {
  619. width: 40rpx;
  620. height: 40rpx;
  621. margin-right: 12rpx;
  622. flex-shrink: 0;
  623. }
  624. &__title {
  625. word-break: break-all;
  626. color: #333333;
  627. font-size: 28rpx;
  628. &--sub {
  629. margin-left: 20rpx;
  630. word-break: break-all;
  631. color: $tn-font-sub-color;
  632. font-size: 24rpx;
  633. }
  634. }
  635. }
  636. }
  637. }
  638. </style>