u-cascader.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. <template>
  2. <u-popup
  3. :show="show"
  4. mode="bottom"
  5. :title="title"
  6. :titleStyle="titleStyle"
  7. :closeOnClickOverlay="closeOnClickOverlay"
  8. :zIndex="zIndex"
  9. :closeable="closeable"
  10. :safeAreaInsetBottom="safeAreaInsetBottom"
  11. :bgColor="bgColor"
  12. :round="round"
  13. @close="onClose"
  14. >
  15. <view class="u-cascader">
  16. <!-- 标签页 -->
  17. <view class="u-cascader__tabs">
  18. <u-tabs
  19. :list="tabsList"
  20. :current="activeTabIndex"
  21. :scrollable="true"
  22. :activeStyle="{ color: activeColor, fontWeight: '600' }"
  23. :inactiveStyle="{ color: color }"
  24. :lineColor="activeColor"
  25. :keyName="'name'"
  26. @change="switchTab"
  27. ></u-tabs>
  28. </view>
  29. <!-- 选项列表 -->
  30. <scroll-view class="u-cascader__content" scroll-y>
  31. <view class="u-cascader__list">
  32. <view
  33. v-for="(item, index) in currentOptions"
  34. :key="index"
  35. class="u-cascader__item"
  36. :class="{ 'u-cascader__item--selected': isSelected(item) }"
  37. :style="[{
  38. backgroundColor: isSelected(item) ? activeBgColor : '',
  39. height: $u.addUnit(itemHeight),
  40. }]"
  41. @tap="selectItem(item)"
  42. >
  43. <text class="u-cascader__item-text" :style="[{
  44. fontWeight: isSelected(item) && activeBold ? '800' : '400',
  45. fontSize: $u.addUnit(fontSize)
  46. }]">
  47. {{ getLabel(item) }}
  48. </text>
  49. <view v-if="isSelected(item)" class="u-cascader__item-right">
  50. <u-icon name="checkmark" size="18" :color="iconColor || activeColor"></u-icon>
  51. </view>
  52. </view>
  53. </view>
  54. </scroll-view>
  55. </view>
  56. </u-popup>
  57. </template>
  58. <script>
  59. import props from './props.js';
  60. import mixin from '../../libs/mixin/mixin';
  61. import mpMixin from '../../libs/mixin/mpMixin';
  62. /**
  63. * Cascader 级联选择器
  64. * @description 级联选择器,用于多级数据的选择,支持单选和多选模式
  65. * @tutorial https://uview.d3u.cn/components/cascader.html
  66. * @property {Boolean} show 是否显示级联选择器(默认 false )
  67. * @property {String} title 选择器标题(默认 '请选择' )
  68. * @property {String} titleAlign 标题对齐方式(默认 'center' )
  69. * @property {String} titleColor 标题颜色(默认 '' )
  70. * @property {String | Number} titleFontSize 标题字体大小(默认 '' )
  71. * @property {Boolean} titleBold 标题是否加粗(默认 false )
  72. * @property {Array} options 选项数据(默认 [] )
  73. * @property {String|Number|Array} modelValue 当前选中值
  74. * @property {String} placeholder 占位符文本(默认 '请选择' )
  75. * @property {Object} field 自定义字段名(默认 {text: 'text', value: 'value', children: 'children'} )
  76. * @property {Boolean} closeable 是否显示关闭按钮(默认 true )
  77. * @property {Boolean} closeOnClickOverlay 是否点击遮罩关闭(默认 true )
  78. * @property {String} bgColor 背景色(默认 '#ffffff' )
  79. * @property {String} activeColor 主题色(默认 '#3c9cff' )
  80. * @property {String} color 文本色(默认 '#303133' )
  81. * @property {String} fontSize 字体大小(默认 '16px' )
  82. * @property {String} titleFontSize 标题字体大小(默认 '18px' )
  83. * @property {String} borderRadius 圆角(默认 '12px' )
  84. * @property {String|Number} zIndex 层级(默认 10075 )
  85. * @property {Boolean} safeAreaInsetBottom 是否安全区域(默认 true )
  86. * @property {String} itemHeight 选项高度(默认 '50px' )
  87. *
  88. * @event {Function} change 选择改变时触发
  89. * @event {Function} close 关闭时触发
  90. * @event {Function} confirm 确认选择时触发
  91. * @event {Function} selected 选择时触发
  92. * @example <u-cascader :show="show" :options="options" @change="onChange"></u-cascader>
  93. */
  94. export default {
  95. name: "u-cascader",
  96. mixins: [mpMixin, mixin, props],
  97. data() {
  98. return {
  99. selectedPath: [], // 选择路径
  100. tabs: [], // 标签页数据
  101. activeTabIndex: 0, // 当前激活的标签页索引
  102. currentOptions: [], // 当前层级的选项
  103. optionsStack: [] // 选项栈,存储每一级的选项
  104. }
  105. },
  106. computed: {
  107. // 获取字段名配置
  108. fieldConfig() {
  109. return {
  110. label: this.field.text || this.field.label || 'label',
  111. value: this.field.value || 'value',
  112. children: this.field.children || 'children'
  113. }
  114. },
  115. // 当前选中的值
  116. currentValue() {
  117. // #ifdef VUE2
  118. return this.value
  119. // #endif
  120. // #ifdef VUE3
  121. return this.modelValue || this.value
  122. // #endif
  123. },
  124. // 是否可以添加新标签页
  125. canAddTab() {
  126. if (this.selectedPath.length === 0) return false
  127. const lastSelected = this.selectedPath[this.selectedPath.length - 1]
  128. return this.hasChildren(lastSelected)
  129. },
  130. // 标签页列表数据
  131. tabsList() {
  132. const list = this.tabs.map(tab => ({
  133. name: tab.label,
  134. label: tab.label
  135. }))
  136. // 首次打开或者有子项时显示占位符
  137. // 首次打开:tabs为空时
  138. // 有子项:选择了某项且该项有子项时
  139. if (this.tabs.length === 0 || this.canAddTab) {
  140. list.push({
  141. name: this.placeholder,
  142. label: this.placeholder
  143. })
  144. }
  145. return list
  146. }
  147. },
  148. watch: {
  149. show: {
  150. handler(newVal) {
  151. if (newVal) {
  152. this.init()
  153. }
  154. },
  155. immediate: true
  156. },
  157. options: {
  158. handler() {
  159. if (this.show) {
  160. this.init()
  161. }
  162. },
  163. deep: true
  164. },
  165. currentValue: {
  166. handler() {
  167. if (this.show) {
  168. this.init()
  169. }
  170. }
  171. }
  172. },
  173. // #ifdef VUE3
  174. emits: ['update:modelValue', 'change', 'close', 'confirm', 'selected'],
  175. // #endif
  176. methods: {
  177. // 初始化
  178. init() {
  179. this.selectedPath = []
  180. this.tabs = []
  181. this.activeTabIndex = 0
  182. this.optionsStack = []
  183. this.currentOptions = this.options || []
  184. this.optionsStack.push(this.currentOptions)
  185. // 如果有初始值,则定位到对应的层级
  186. if (this.currentValue) {
  187. this.initWithValue()
  188. }
  189. },
  190. // 根据初始值初始化选择状态
  191. initWithValue() {
  192. const value = Array.isArray(this.currentValue) ? this.currentValue : [this.currentValue]
  193. let currentOptions = this.options || []
  194. for (let i = 0; i < value.length; i++) {
  195. const targetValue = value[i]
  196. const item = currentOptions.find(option => this.getValue(option) === targetValue)
  197. if (item) {
  198. this.selectedPath.push(item)
  199. this.tabs.push({
  200. label: this.getLabel(item),
  201. level: i
  202. })
  203. if (this.hasChildren(item)) {
  204. currentOptions = this.getChildren(item)
  205. this.optionsStack.push(currentOptions)
  206. } else {
  207. break
  208. }
  209. } else {
  210. break
  211. }
  212. }
  213. this.activeTabIndex = this.tabs.length > 0 ? this.tabs.length - 1 : 0
  214. this.currentOptions = this.optionsStack[this.activeTabIndex] || []
  215. },
  216. // 选择项目
  217. selectItem(item) {
  218. const currentLevel = this.activeTabIndex
  219. // 更新选择路径
  220. this.selectedPath = this.selectedPath.slice(0, currentLevel)
  221. this.selectedPath.push(item)
  222. // 更新标签页
  223. this.tabs = this.tabs.slice(0, currentLevel)
  224. this.tabs.push({
  225. label: this.getLabel(item),
  226. level: currentLevel
  227. })
  228. // 清理选项栈后续数据
  229. this.optionsStack = this.optionsStack.slice(0, currentLevel + 1)
  230. // 触发选择事件
  231. this.emitSelected(item)
  232. if (this.hasChildren(item)) {
  233. // 有子项,添加新的选项到栈中
  234. const childOptions = this.getChildren(item)
  235. this.optionsStack.push(childOptions)
  236. // 自动切换到下一个标签页
  237. this.activeTabIndex = currentLevel + 1
  238. this.currentOptions = childOptions
  239. } else {
  240. // 没有子项,完成选择,直接触发confirm并关闭
  241. this.emitChange()
  242. this.emitConfirm()
  243. this.onClose()
  244. }
  245. },
  246. // 切换标签页
  247. switchTab(event) {
  248. // u-tabs组件的change事件传递的是对象 {item, index}
  249. const index = event.index
  250. this.activeTabIndex = index
  251. this.currentOptions = this.optionsStack[index] || []
  252. },
  253. // 获取选项的显示文本
  254. getLabel(item) {
  255. return item[this.fieldConfig.label] || ''
  256. },
  257. // 获取选项的值
  258. getValue(item) {
  259. return item[this.fieldConfig.value]
  260. },
  261. // 获取选项的子项
  262. getChildren(item) {
  263. return item[this.fieldConfig.children] || []
  264. },
  265. // 判断是否有子项
  266. hasChildren(item) {
  267. const children = this.getChildren(item)
  268. return Array.isArray(children) && children.length > 0
  269. },
  270. // 判断选项是否被选中
  271. isSelected(item) {
  272. if (this.selectedPath.length <= this.activeTabIndex) {
  273. return false
  274. }
  275. const selectedItem = this.selectedPath[this.activeTabIndex]
  276. return selectedItem && this.getValue(selectedItem) === this.getValue(item)
  277. },
  278. // 获取选中的值数组
  279. getSelectedValues() {
  280. return this.selectedPath.map(item => this.getValue(item))
  281. },
  282. // 获取选中的标签数组
  283. getSelectedLabels() {
  284. return this.selectedPath.map(item => this.getLabel(item))
  285. },
  286. // 触发change事件
  287. emitChange() {
  288. const values = this.getSelectedValues()
  289. const labels = this.getSelectedLabels()
  290. const selectedItems = [...this.selectedPath]
  291. const result = {
  292. value: values,
  293. label: labels,
  294. selectedItems
  295. }
  296. // #ifdef VUE2
  297. this.$emit('input', values)
  298. // #endif
  299. // #ifdef VUE3
  300. this.$emit('update:modelValue', values)
  301. // #endif
  302. this.$emit('change', result)
  303. },
  304. // 触发selected事件
  305. emitSelected(item) {
  306. this.$emit('selected', {
  307. item,
  308. level: this.activeTabIndex,
  309. selectedPath: [...this.selectedPath]
  310. })
  311. },
  312. // 触发confirm事件
  313. emitConfirm() {
  314. const values = this.getSelectedValues()
  315. const labels = this.getSelectedLabels()
  316. const selectedItems = [...this.selectedPath]
  317. this.$emit('confirm', {
  318. value: values,
  319. label: labels,
  320. selectedItems
  321. })
  322. },
  323. // 关闭选择器
  324. onClose() {
  325. this.$emit('close')
  326. }
  327. }
  328. }
  329. </script>
  330. <style lang="scss" scoped>
  331. @import "../../libs/css/components.scss";
  332. .u-cascader {
  333. @include flex(column);
  334. &__content {
  335. flex: 1;
  336. // #ifndef APP-NVUE
  337. min-height: 0;
  338. // #endif
  339. }
  340. &__list {
  341. padding: 0;
  342. height: 320px;
  343. }
  344. &__item {
  345. @include flex(row);
  346. align-items: center;
  347. justify-content: space-between;
  348. padding: 0 15px;
  349. }
  350. &__item-text {
  351. flex: 1;
  352. overflow: hidden;
  353. text-overflow: ellipsis;
  354. font-size: 15px;
  355. // #ifndef APP-NVUE
  356. white-space: nowrap;
  357. // #endif
  358. }
  359. &__item-right {
  360. @include flex(row);
  361. align-items: center;
  362. margin-left: 10px;
  363. }
  364. }
  365. </style>