wd-col-picker.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. <template>
  2. <view :class="`wd-col-picker ${customClass}`" :style="customStyle">
  3. <wd-cell
  4. v-if="!$slots.default"
  5. :title="label"
  6. :value="showValue || placeholder || translate('placeholder')"
  7. :required="required"
  8. :size="size"
  9. :title-width="labelWidth"
  10. :prop="prop"
  11. :rules="rules"
  12. :clickable="!disabled && !readonly"
  13. :value-align="alignRight ? 'right' : 'left'"
  14. :custom-class="cellClass"
  15. :custom-style="customStyle"
  16. :custom-title-class="customLabelClass"
  17. :custom-value-class="customValueClass"
  18. :ellipsis="ellipsis"
  19. :use-title-slot="!!$slots.label"
  20. :marker-side="markerSide"
  21. @click="showPicker"
  22. >
  23. <template v-if="$slots.label" #title>
  24. <slot name="label"></slot>
  25. </template>
  26. <template #right-icon>
  27. <wd-icon v-if="showArrow" custom-class="wd-col-picker__arrow" name="arrow-right" />
  28. </template>
  29. </wd-cell>
  30. <view v-else @click="showPicker">
  31. <slot></slot>
  32. </view>
  33. <wd-action-sheet
  34. v-model="pickerShow"
  35. :duration="250"
  36. :title="title || translate('title')"
  37. :close-on-click-modal="closeOnClickModal"
  38. :z-index="zIndex"
  39. :safe-area-inset-bottom="safeAreaInsetBottom"
  40. :root-portal="rootPortal"
  41. @open="handlePickerOpend"
  42. @close="handlePickerClose"
  43. @closed="handlePickerClosed"
  44. >
  45. <view class="wd-col-picker__selected">
  46. <scroll-view :scroll-x="true" scroll-with-animation :scroll-left="scrollLeft">
  47. <view class="wd-col-picker__selected-container">
  48. <view
  49. v-for="(_, colIndex) in selectList"
  50. :key="colIndex"
  51. :class="`wd-col-picker__selected-item ${colIndex === currentCol && 'is-selected'}`"
  52. @click="handleColClick(colIndex)"
  53. >
  54. {{ selectShowList[colIndex] || translate('select') }}
  55. </view>
  56. <view class="wd-col-picker__selected-line" :style="state.lineStyle"></view>
  57. </view>
  58. </scroll-view>
  59. </view>
  60. <view class="wd-col-picker__list-container">
  61. <view
  62. v-for="(col, colIndex) in selectList"
  63. :key="colIndex"
  64. class="wd-col-picker__list"
  65. :style="colIndex === currentCol ? 'display: block;' : 'display: none;'"
  66. >
  67. <view
  68. v-for="(item, index) in col"
  69. :key="index"
  70. :class="`wd-col-picker__list-item ${pickerColSelected[colIndex] && item[valueKey] === pickerColSelected[colIndex] && 'is-selected'} ${
  71. item.disabled && 'is-disabled'
  72. }`"
  73. @click="chooseItem(colIndex, index)"
  74. >
  75. <view>
  76. <view class="wd-col-picker__list-item-label">{{ item[labelKey] }}</view>
  77. <view v-if="item[tipKey]" class="wd-col-picker__list-item-tip">{{ item[tipKey] }}</view>
  78. </view>
  79. <wd-icon custom-class="wd-col-picker__checked" name="check"></wd-icon>
  80. </view>
  81. <view v-if="loading" class="wd-col-picker__loading">
  82. <wd-loading :color="loadingColor" />
  83. </view>
  84. </view>
  85. </view>
  86. </wd-action-sheet>
  87. </view>
  88. </template>
  89. <script lang="ts">
  90. export default {
  91. name: 'wd-col-picker',
  92. options: {
  93. addGlobalClass: true,
  94. virtualHost: true,
  95. styleIsolation: 'shared'
  96. }
  97. }
  98. </script>
  99. <script lang="ts" setup>
  100. import wdIcon from '../wd-icon/wd-icon.vue'
  101. import wdLoading from '../wd-loading/wd-loading.vue'
  102. import wdActionSheet from '../wd-action-sheet/wd-action-sheet.vue'
  103. import wdCell from '../wd-cell/wd-cell.vue'
  104. import { computed, getCurrentInstance, onMounted, ref, watch, type CSSProperties, reactive } from 'vue'
  105. import { addUnit, debounce, getRect, isArray, isBoolean, isDef, isFunction, objToStyle } from '../common/util'
  106. import { useTranslate } from '../composables/useTranslate'
  107. import { colPickerProps, type ColPickerExpose } from './types'
  108. const { translate } = useTranslate('col-picker')
  109. const $container = '.wd-col-picker__selected-container'
  110. const $item = '.wd-col-picker__selected-item'
  111. const props = defineProps(colPickerProps)
  112. const emit = defineEmits(['close', 'update:modelValue', 'confirm'])
  113. const pickerShow = ref<boolean>(false)
  114. const currentCol = ref<number>(0)
  115. const selectList = ref<Record<string, any>[][]>([])
  116. const pickerColSelected = ref<(string | number)[]>([])
  117. const selectShowList = ref<Record<string, any>[]>([])
  118. const loading = ref<boolean>(false)
  119. const isChange = ref<boolean>(false)
  120. const lastSelectList = ref<Record<string, any>[][]>([])
  121. const lastPickerColSelected = ref<(string | number)[]>([])
  122. const scrollLeft = ref<number>(0)
  123. const inited = ref<boolean>(false)
  124. const isCompleting = ref<boolean>(false)
  125. const state = reactive({
  126. lineStyle: 'display:none;' // 激活项边框线样式
  127. })
  128. const { proxy } = getCurrentInstance() as any
  129. const updateLineAndScroll = debounce(function (animation = true) {
  130. setLineStyle(animation)
  131. lineScrollIntoView()
  132. }, 50)
  133. const showValue = computed(() => {
  134. const selectedItems = (props.modelValue || []).map((item, colIndex) => {
  135. return getSelectedItem(item, colIndex, selectList.value)
  136. })
  137. if (props.displayFormat) {
  138. return props.displayFormat(selectedItems)
  139. } else {
  140. return selectedItems
  141. .map((item) => {
  142. return item[props.labelKey]
  143. })
  144. .join('')
  145. }
  146. })
  147. const cellClass = computed(() => {
  148. const classes = ['wd-col-picker__cell']
  149. if (props.disabled) classes.push('is-disabled')
  150. if (props.readonly) classes.push('is-readonly')
  151. if (props.error) classes.push('is-error')
  152. if (!showValue.value) classes.push('wd-col-picker__cell--placeholder')
  153. return classes.join(' ')
  154. })
  155. watch(
  156. () => props.modelValue,
  157. (newValue) => {
  158. if (newValue === pickerColSelected.value) return
  159. pickerColSelected.value = newValue
  160. newValue.map((item, colIndex) => {
  161. return getSelectedItem(item, colIndex, selectList.value)[props.labelKey]
  162. })
  163. handleAutoComplete()
  164. },
  165. {
  166. deep: true,
  167. immediate: true
  168. }
  169. )
  170. watch(
  171. () => props.columns,
  172. (newValue, oldValue) => {
  173. if (newValue.length && !isArray(newValue[0])) {
  174. console.error('[wot ui] error(wd-col-picker): the columns props of wd-col-picker should be a two-dimensional array')
  175. return
  176. }
  177. if (newValue.length === 0 && !oldValue) return
  178. const newSelectedList = newValue.slice(0)
  179. selectList.value = newSelectedList
  180. selectShowList.value = pickerColSelected.value.map((item, colIndex) => {
  181. return getSelectedItem(item, colIndex, newSelectedList)[props.labelKey]
  182. })
  183. lastSelectList.value = newSelectedList
  184. if (newSelectedList.length > 0) {
  185. currentCol.value = newSelectedList.length - 1
  186. }
  187. },
  188. {
  189. deep: true,
  190. immediate: true
  191. }
  192. )
  193. watch(
  194. () => props.columnChange,
  195. (fn) => {
  196. if (fn && !isFunction(fn)) {
  197. console.error('The type of columnChange must be Function')
  198. }
  199. },
  200. {
  201. deep: true,
  202. immediate: true
  203. }
  204. )
  205. watch(
  206. () => props.displayFormat,
  207. (fn) => {
  208. if (fn && !isFunction(fn)) {
  209. console.error('The type of displayFormat must be Function')
  210. }
  211. },
  212. {
  213. deep: true,
  214. immediate: true
  215. }
  216. )
  217. watch(
  218. () => props.beforeConfirm,
  219. (fn) => {
  220. if (fn && !isFunction(fn)) {
  221. console.error('The type of beforeConfirm must be Function')
  222. }
  223. },
  224. {
  225. deep: true,
  226. immediate: true
  227. }
  228. )
  229. // 是否展示箭头
  230. const showArrow = computed(() => {
  231. return !props.disabled && !props.readonly
  232. })
  233. onMounted(() => {
  234. inited.value = true
  235. })
  236. // 打开弹框
  237. function open() {
  238. showPicker()
  239. }
  240. // 关闭弹框
  241. function close() {
  242. handlePickerClose()
  243. }
  244. function handlePickerOpend() {
  245. updateLineAndScroll(false)
  246. }
  247. function handlePickerClose() {
  248. pickerShow.value = false
  249. emit('close')
  250. }
  251. function handlePickerClosed() {
  252. if (isChange.value) {
  253. setTimeout(() => {
  254. selectList.value = lastSelectList.value.slice(0)
  255. pickerColSelected.value = lastPickerColSelected.value.slice(0)
  256. selectShowList.value = lastPickerColSelected.value.map((item, colIndex) => {
  257. return getSelectedItem(item, colIndex, lastSelectList.value)[props.labelKey]
  258. })
  259. currentCol.value = lastSelectList.value.length - 1
  260. isChange.value = false
  261. }, 250)
  262. }
  263. }
  264. function showPicker() {
  265. const { disabled, readonly } = props
  266. if (disabled || readonly) return
  267. pickerShow.value = true
  268. lastPickerColSelected.value = pickerColSelected.value.slice(0)
  269. lastSelectList.value = selectList.value.slice(0)
  270. }
  271. function getSelectedItem(value: string | number, colIndex: number, selectList: Record<string, any>[][]) {
  272. const { valueKey, labelKey } = props
  273. if (selectList[colIndex]) {
  274. const selecteds = selectList[colIndex].filter((item) => {
  275. return item[valueKey] === value
  276. })
  277. if (selecteds.length > 0) {
  278. return selecteds[0]
  279. }
  280. }
  281. return {
  282. [valueKey]: value,
  283. [labelKey]: ''
  284. }
  285. }
  286. function chooseItem(colIndex: number, index: number) {
  287. const item = selectList.value[colIndex][index]
  288. if (item.disabled) return
  289. const newPickerColSelected = pickerColSelected.value.slice(0, colIndex)
  290. newPickerColSelected.push(item[props.valueKey])
  291. isChange.value = true
  292. pickerColSelected.value = newPickerColSelected
  293. selectList.value = selectList.value.slice(0, colIndex + 1)
  294. selectShowList.value = newPickerColSelected.map((item, colIndex) => {
  295. return getSelectedItem(item, colIndex, selectList.value)[props.labelKey]
  296. })
  297. if (selectShowList.value[colIndex] && colIndex === currentCol.value) {
  298. updateLineAndScroll(true)
  299. }
  300. handleColChange(colIndex, item, index)
  301. }
  302. function handleColChange(colIndex: number, item: Record<string, any>, index: number, callback?: () => void) {
  303. loading.value = true
  304. const { columnChange, beforeConfirm } = props
  305. columnChange &&
  306. columnChange({
  307. selectedItem: item,
  308. index: colIndex,
  309. rowIndex: index,
  310. resolve: (nextColumn: Record<string, any>[]) => {
  311. if (!isArray(nextColumn)) {
  312. console.error('[wot ui] error(wd-col-picker): the data of each column of wd-col-picker should be an array')
  313. return
  314. }
  315. const newSelectList = selectList.value.slice(0)
  316. newSelectList[colIndex + 1] = nextColumn
  317. selectList.value = newSelectList
  318. loading.value = false
  319. currentCol.value = colIndex + 1
  320. updateLineAndScroll(true)
  321. if (typeof callback === 'function') {
  322. isCompleting.value = false
  323. selectShowList.value = pickerColSelected.value.map((item, colIndex) => {
  324. return getSelectedItem(item, colIndex, selectList.value)[props.labelKey]
  325. })
  326. callback()
  327. }
  328. },
  329. finish: (isOk?: boolean) => {
  330. // 每设置展示数据回显
  331. if (typeof callback === 'function') {
  332. loading.value = false
  333. isCompleting.value = false
  334. return
  335. }
  336. if (isBoolean(isOk) && !isOk) {
  337. loading.value = false
  338. return
  339. }
  340. if (beforeConfirm) {
  341. beforeConfirm(
  342. pickerColSelected.value,
  343. pickerColSelected.value.map((item, colIndex) => {
  344. return getSelectedItem(item, colIndex, selectList.value)
  345. }),
  346. (isPass: boolean) => {
  347. if (isPass) {
  348. onConfirm()
  349. } else {
  350. loading.value = false
  351. }
  352. }
  353. )
  354. } else {
  355. onConfirm()
  356. }
  357. }
  358. })
  359. }
  360. function onConfirm() {
  361. isChange.value = false
  362. loading.value = false
  363. pickerShow.value = false
  364. emit('update:modelValue', pickerColSelected.value)
  365. emit('confirm', {
  366. value: pickerColSelected.value,
  367. selectedItems: pickerColSelected.value.map((item, colIndex) => {
  368. return getSelectedItem(item, colIndex, selectList.value)
  369. })
  370. })
  371. }
  372. function handleColClick(index: number) {
  373. isChange.value = true
  374. currentCol.value = index
  375. updateLineAndScroll(true)
  376. }
  377. /**
  378. * @description 更新navBar underline的偏移量
  379. * @param {Boolean} animation 是否伴随动画
  380. */
  381. function setLineStyle(animation: boolean = true) {
  382. if (!inited.value) return
  383. const { lineWidth, lineHeight } = props
  384. getRect($item, true, proxy)
  385. .then((rects) => {
  386. const lineStyle: CSSProperties = {}
  387. if (isDef(lineWidth)) {
  388. lineStyle.width = addUnit(lineWidth)
  389. }
  390. if (isDef(lineHeight)) {
  391. lineStyle.height = addUnit(lineHeight)
  392. lineStyle.borderRadius = `calc(${addUnit(lineHeight)} / 2)`
  393. }
  394. const rect = rects[currentCol.value]
  395. let left = rects.slice(0, currentCol.value).reduce((prev, curr) => prev + Number(curr.width), 0) + Number(rect.width) / 2
  396. lineStyle.transform = `translateX(${left}px) translateX(-50%)`
  397. if (animation) {
  398. lineStyle.transition = 'width 300ms ease, transform 300ms ease'
  399. }
  400. state.lineStyle = objToStyle(lineStyle)
  401. })
  402. .catch(() => {})
  403. }
  404. /**
  405. * @description scroll-view滑动到active的tab_nav
  406. */
  407. function lineScrollIntoView() {
  408. if (!inited.value) return
  409. Promise.all([getRect($item, true, proxy), getRect($container, false, proxy)])
  410. .then(([navItemsRects, navRect]) => {
  411. if (!isArray(navItemsRects) || navItemsRects.length === 0) return
  412. // 选中元素
  413. const selectItem = navItemsRects[currentCol.value]
  414. // 选中元素之前的节点的宽度总和
  415. const offsetLeft = navItemsRects.slice(0, currentCol.value).reduce((prev, curr) => prev + Number(curr.width), 0)
  416. // scroll-view滑动到selectItem的偏移量
  417. scrollLeft.value = offsetLeft - ((navRect as any).width - Number(selectItem.width)) / 2
  418. })
  419. .catch(() => {})
  420. }
  421. // 递归列数据补齐
  422. function diffColumns(colIndex: number) {
  423. // colIndex 为 -1 时,item 为空对象,>=0 时则具有 value 属性
  424. const item = colIndex === -1 ? {} : { [props.valueKey]: props.modelValue[colIndex] }
  425. handleColChange(colIndex, item, -1, () => {
  426. // 如果 columns 长度还小于 value 长度,colIndex + 1,继续递归补齐
  427. if (selectList.value.length < props.modelValue.length) {
  428. diffColumns(colIndex + 1)
  429. }
  430. })
  431. }
  432. function handleAutoComplete() {
  433. if (props.autoComplete) {
  434. // 如果 columns 数组长度为空,或者长度小于 value 的长度,自动触发 columnChange 来补齐数据
  435. if (selectList.value.length < props.modelValue.length || selectList.value.length === 0) {
  436. // isCompleting 是否在自动补全,锁操作
  437. if (!isCompleting.value) {
  438. // 如果 columns 长度为空,则传入的 colIndex 为 -1
  439. const colIndex = selectList.value.length === 0 ? -1 : selectList.value.length - 1
  440. diffColumns(colIndex)
  441. }
  442. isCompleting.value = true
  443. }
  444. }
  445. }
  446. defineExpose<ColPickerExpose>({
  447. close,
  448. open
  449. })
  450. </script>
  451. <style lang="scss" scoped>
  452. @import './index.scss';
  453. </style>