wd-picker.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. <template>
  2. <view
  3. :class="`wd-picker ${disabled ? 'is-disabled' : ''} ${size ? 'is-' + size : ''} ${alignRight ? 'is-align-right' : ''} ${
  4. error ? 'is-error' : ''
  5. } ${customClass}`"
  6. :style="customStyle"
  7. >
  8. <wd-cell
  9. v-if="!$slots.default"
  10. :title="label"
  11. :value="showValue ? showValue : placeholder || translate('placeholder')"
  12. :required="required"
  13. :size="size"
  14. :title-width="labelWidth"
  15. :prop="prop"
  16. :rules="rules"
  17. :clickable="!disabled && !readonly"
  18. :value-align="alignRight ? 'right' : 'left'"
  19. :custom-class="cellClass"
  20. :custom-style="customStyle"
  21. :custom-title-class="customLabelClass"
  22. :custom-value-class="customValueClass"
  23. :ellipsis="ellipsis"
  24. :use-title-slot="!!$slots.label"
  25. :marker-side="markerSide"
  26. @click="showPopup"
  27. >
  28. <template v-if="$slots.label" #title>
  29. <slot name="label"></slot>
  30. </template>
  31. <template #right-icon>
  32. <wd-icon v-if="showArrow" custom-class="wd-picker__arrow" name="arrow-right" />
  33. <view v-else-if="showClear" @click.stop="handleClear">
  34. <wd-icon custom-class="wd-picker__clear" name="error-fill" />
  35. </view>
  36. </template>
  37. </wd-cell>
  38. <view v-else @click="showPopup">
  39. <slot></slot>
  40. </view>
  41. <wd-popup
  42. v-model="popupShow"
  43. position="bottom"
  44. :hide-when-close="false"
  45. :close-on-click-modal="closeOnClickModal"
  46. :z-index="zIndex"
  47. :safe-area-inset-bottom="safeAreaInsetBottom"
  48. :root-portal="rootPortal"
  49. @close="onCancel"
  50. custom-class="wd-picker__popup"
  51. >
  52. <view class="wd-picker__wraper">
  53. <view class="wd-picker__toolbar" @touchmove="noop">
  54. <view class="wd-picker__action wd-picker__action--cancel" @click="onCancel">
  55. {{ cancelButtonText || translate('cancel') }}
  56. </view>
  57. <view v-if="title" class="wd-picker__title">{{ title }}</view>
  58. <view :class="`wd-picker__action ${isLoading ? 'is-loading' : ''}`" @click="onConfirm">
  59. {{ confirmButtonText || translate('done') }}
  60. </view>
  61. </view>
  62. <wd-picker-view
  63. ref="pickerViewWd"
  64. :custom-class="customViewClass"
  65. v-model="pickerValue"
  66. :columns="displayColumns"
  67. :loading="isLoading"
  68. :loading-color="loadingColor"
  69. :columns-height="columnsHeight"
  70. :value-key="valueKey"
  71. :label-key="labelKey"
  72. :immediate-change="immediateChange"
  73. @change="pickerViewChange"
  74. @pickstart="onPickStart"
  75. @pickend="onPickEnd"
  76. :column-change="columnChange"
  77. />
  78. </view>
  79. </wd-popup>
  80. </view>
  81. </template>
  82. <script lang="ts">
  83. export default {
  84. name: 'wd-picker',
  85. options: {
  86. virtualHost: true,
  87. addGlobalClass: true,
  88. styleIsolation: 'shared'
  89. }
  90. }
  91. </script>
  92. <script lang="ts" setup>
  93. import wdIcon from '../wd-icon/wd-icon.vue'
  94. import wdPopup from '../wd-popup/wd-popup.vue'
  95. import wdPickerView from '../wd-picker-view/wd-picker-view.vue'
  96. import wdCell from '../wd-cell/wd-cell.vue'
  97. import { getCurrentInstance, onBeforeMount, ref, watch, computed, onMounted, nextTick } from 'vue'
  98. import { deepClone, defaultDisplayFormat, getType, isArray, isDef, isFunction } from '../common/util'
  99. import { type ColumnItem, formatArray, type PickerViewInstance } from '../wd-picker-view/types'
  100. import { FORM_KEY, type FormItemRule } from '../wd-form/types'
  101. import { useParent } from '../composables/useParent'
  102. import { useTranslate } from '../composables/useTranslate'
  103. import { pickerProps, type PickerExpose } from './types'
  104. const { translate } = useTranslate('picker')
  105. const props = defineProps(pickerProps)
  106. const emit = defineEmits(['confirm', 'open', 'cancel', 'clear', 'update:modelValue'])
  107. const pickerViewWd = ref<PickerViewInstance | null>(null)
  108. const innerLoading = ref<boolean>(false) // 内部控制是否loading
  109. // 弹出层是否显示
  110. const popupShow = ref<boolean>(false)
  111. // 选定后展示的选中项
  112. const showValue = ref<string>('')
  113. const pickerValue = ref<string | number | boolean | string[] | number[] | boolean[]>('')
  114. const displayColumns = ref<Array<string | number | ColumnItem | Array<string | number | ColumnItem>>>([]) // 传入 pickerView 的columns
  115. const resetColumns = ref<Array<string | number | ColumnItem | Array<string | number | ColumnItem>>>([]) // 保存之前的 columns,当取消时,将数据源回滚,避免多级联动数据源不正确的情况
  116. const isPicking = ref<boolean>(false) // 判断pickview是否还在滑动中
  117. const hasConfirmed = ref<boolean>(false) // 判断用户是否点击了确认按钮
  118. const isLoading = computed(() => {
  119. return props.loading || innerLoading.value
  120. })
  121. watch(
  122. () => props.displayFormat,
  123. (fn) => {
  124. if (fn && !isFunction(fn)) {
  125. console.error('The type of displayFormat must be Function')
  126. }
  127. if (pickerViewWd.value && pickerViewWd.value.getSelectedIndex().length !== 0) {
  128. handleShowValueUpdate(props.modelValue)
  129. }
  130. },
  131. {
  132. immediate: true,
  133. deep: true
  134. }
  135. )
  136. watch(
  137. () => props.modelValue,
  138. (newValue) => {
  139. pickerValue.value = newValue
  140. // 获取初始选中项,并展示初始选中文案
  141. handleShowValueUpdate(newValue)
  142. },
  143. {
  144. deep: true,
  145. immediate: true
  146. }
  147. )
  148. watch(
  149. () => props.columns,
  150. (newValue) => {
  151. displayColumns.value = deepClone(newValue)
  152. resetColumns.value = deepClone(newValue)
  153. if (newValue.length === 0) {
  154. // 当 columns 变为空时,清空 pickerValue 和 showValue
  155. pickerValue.value = isArray(props.modelValue) ? [] : ''
  156. showValue.value = ''
  157. } else {
  158. // 非空时正常更新显示值
  159. handleShowValueUpdate(props.modelValue)
  160. }
  161. },
  162. {
  163. deep: true,
  164. immediate: true
  165. }
  166. )
  167. watch(
  168. () => props.columnChange,
  169. (newValue) => {
  170. if (newValue && !isFunction(newValue)) {
  171. console.error('The type of columnChange must be Function')
  172. }
  173. },
  174. {
  175. deep: true,
  176. immediate: true
  177. }
  178. )
  179. // 是否展示清除按钮
  180. const showClear = computed(() => {
  181. return props.clearable && !props.disabled && !props.readonly && showValue.value.length > 0
  182. })
  183. // 是否展示箭头
  184. const showArrow = computed(() => {
  185. return !props.disabled && !props.readonly && !showClear.value
  186. })
  187. const cellClass = computed(() => {
  188. const classes = ['wd-picker__cell']
  189. if (props.disabled) classes.push('is-disabled')
  190. if (props.readonly) classes.push('is-readonly')
  191. if (props.error) classes.push('is-error')
  192. if (!showValue.value) classes.push('wd-picker__cell--placeholder')
  193. return classes.join(' ')
  194. })
  195. const { proxy } = getCurrentInstance() as any
  196. onMounted(() => {
  197. handleShowValueUpdate(props.modelValue)
  198. })
  199. onBeforeMount(() => {
  200. displayColumns.value = deepClone(props.columns)
  201. resetColumns.value = deepClone(props.columns)
  202. })
  203. /**
  204. * 值变更时更新显示内容
  205. * @param value
  206. */
  207. function handleShowValueUpdate(value: string | number | Array<string | number>) {
  208. // 获取初始选中项,并展示初始选中文案
  209. if ((isArray(value) && value.length > 0) || (isDef(value) && !isArray(value) && value !== '')) {
  210. if (pickerViewWd.value) {
  211. nextTick(() => {
  212. setShowValue(pickerViewWd.value!.getSelects())
  213. })
  214. } else {
  215. setShowValue(getSelects(value)!)
  216. }
  217. } else {
  218. showValue.value = ''
  219. }
  220. }
  221. /**
  222. * @description 根据传入的value,picker组件获取当前cell展示值。
  223. * @param {String|Number|Array<String|Number|Array<any>>}value
  224. */
  225. function getSelects(value: string | number | Array<string | number | Array<any>>) {
  226. const formatColumns = formatArray(props.columns, props.valueKey, props.labelKey)
  227. if (props.columns.length === 0) return
  228. // 使其默认选中首项
  229. if (value === '' || !isDef(value) || (isArray(value) && value.length === 0)) {
  230. return
  231. }
  232. const valueType = getType(value)
  233. const type = ['string', 'number', 'boolean', 'array']
  234. if (type.indexOf(valueType) === -1) return []
  235. /**
  236. * 1.单key转为Array<key>
  237. * 2.根据formatColumns的长度截取Array<String>,保证下面的遍历不溢出
  238. * 3.根据每列的key值找到选项中value为此key的下标并记录
  239. */
  240. value = isArray(value) ? value : [value]
  241. value = value.slice(0, formatColumns.length)
  242. if (value.length === 0) {
  243. value = formatColumns.map(() => 0)
  244. }
  245. let selected: number[] = []
  246. value.forEach((target, col) => {
  247. let row = formatColumns[col].findIndex((row) => {
  248. return row[props.valueKey].toString() === target.toString()
  249. })
  250. row = row === -1 ? 0 : row
  251. selected.push(row)
  252. })
  253. const selects = selected.map((row, col) => formatColumns[col][row])
  254. // 单列选择器,则返回单项
  255. if (selects.length === 1) {
  256. return selects[0]
  257. }
  258. return selects
  259. }
  260. // 对外暴露方法,打开弹框
  261. function open() {
  262. showPopup()
  263. }
  264. // 对外暴露方法,关闭弹框
  265. function close() {
  266. onCancel()
  267. }
  268. /**
  269. * 展示popup
  270. */
  271. function showPopup() {
  272. if (props.disabled || props.readonly) return
  273. emit('open')
  274. popupShow.value = true
  275. pickerValue.value = props.modelValue
  276. displayColumns.value = resetColumns.value
  277. }
  278. /**
  279. * 点击取消按钮触发。关闭popup,触发cancel事件。
  280. */
  281. function onCancel() {
  282. popupShow.value = false
  283. emit('cancel')
  284. let timmer = setTimeout(() => {
  285. clearTimeout(timmer)
  286. isDef(pickerViewWd.value) && pickerViewWd.value.resetColumns(resetColumns.value)
  287. }, 300)
  288. }
  289. /**
  290. * 点击确定按钮触发。展示选中值,触发cancel事件。
  291. */
  292. function onConfirm() {
  293. if (isLoading.value) return
  294. // 如果当前还在滑动且未停止下来,则锁住先不确认,等滑完再自动确认,避免pickview值未更新
  295. if (isPicking.value) {
  296. hasConfirmed.value = true
  297. return
  298. }
  299. const { beforeConfirm } = props
  300. if (beforeConfirm && isFunction(beforeConfirm)) {
  301. beforeConfirm(
  302. pickerValue.value,
  303. (isPass: boolean) => {
  304. isPass && handleConfirm()
  305. },
  306. proxy.$.exposed
  307. )
  308. } else {
  309. handleConfirm()
  310. }
  311. }
  312. function handleConfirm() {
  313. if (isLoading.value || props.disabled) {
  314. popupShow.value = false
  315. return
  316. }
  317. const selects = pickerViewWd.value!.getSelects()
  318. const values = pickerViewWd.value!.getValues()
  319. // 获取当前的数据源,并设置给 resetColumns,用于取消时可以回退数据源
  320. const columns = pickerViewWd.value!.getColumnsData()
  321. popupShow.value = false
  322. resetColumns.value = deepClone(columns)
  323. emit('update:modelValue', values)
  324. setShowValue(selects)
  325. emit('confirm', {
  326. value: values,
  327. selectedItems: selects
  328. })
  329. }
  330. /**
  331. * 初始change事件
  332. * @param event
  333. */
  334. function pickerViewChange({ value }: any) {
  335. pickerValue.value = value
  336. }
  337. /**
  338. * 设置展示值
  339. * @param items
  340. */
  341. function setShowValue(items: ColumnItem | ColumnItem[]) {
  342. // 避免值为空时调用自定义展示函数
  343. if ((isArray(items) && !items.length) || !items) return
  344. const { valueKey, labelKey } = props
  345. showValue.value = (props.displayFormat || defaultDisplayFormat)(items, { valueKey, labelKey })
  346. }
  347. function noop() {}
  348. function onPickStart() {
  349. isPicking.value = true
  350. }
  351. function onPickEnd() {
  352. isPicking.value = false
  353. if (hasConfirmed.value) {
  354. hasConfirmed.value = false
  355. onConfirm()
  356. }
  357. }
  358. /**
  359. * 外部设置是否loading
  360. * @param loading 是否loading
  361. */
  362. function setLoading(loading: boolean) {
  363. innerLoading.value = loading
  364. }
  365. function handleClear() {
  366. const clearValue = isArray(pickerValue.value) ? [] : ''
  367. emit('update:modelValue', clearValue)
  368. emit('clear')
  369. }
  370. defineExpose<PickerExpose>({
  371. close,
  372. open,
  373. setLoading
  374. })
  375. </script>
  376. <style lang="scss" scoped>
  377. @import './index.scss';
  378. </style>