wd-picker-view.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. <template>
  2. <view :class="`wd-picker-view ${customClass}`" :style="customStyle">
  3. <view class="wd-picker-view__loading" v-if="loading">
  4. <wd-loading :color="loadingColor" />
  5. </view>
  6. <view :style="`height: ${columnsHeight - 20}px;`">
  7. <picker-view
  8. mask-class="wd-picker-view__mask"
  9. indicator-class="wd-picker-view__roller"
  10. :indicator-style="`height: ${itemHeight}px;`"
  11. :style="`height: ${columnsHeight - 20}px;`"
  12. :value="selectedIndex"
  13. :immediate-change="immediateChange"
  14. @change="onChange"
  15. @pickstart="onPickStart"
  16. @pickend="onPickEnd"
  17. >
  18. <picker-view-column v-for="(col, colIndex) in formatColumns" :key="colIndex" class="wd-picker-view-column">
  19. <view
  20. v-for="(row, rowIndex) in col"
  21. :key="rowIndex"
  22. :class="`wd-picker-view-column__item ${row['disabled'] ? 'wd-picker-view-column__item--disabled' : ''} ${
  23. selectedIndex[colIndex] == rowIndex ? 'wd-picker-view-column__item--active' : ''
  24. }`"
  25. :style="`line-height: ${itemHeight}px;`"
  26. >
  27. {{ row[labelKey] }}
  28. </view>
  29. </picker-view-column>
  30. </picker-view>
  31. </view>
  32. </view>
  33. </template>
  34. <script lang="ts">
  35. export default {
  36. name: 'wd-picker-view',
  37. options: {
  38. virtualHost: true,
  39. addGlobalClass: true,
  40. styleIsolation: 'shared'
  41. }
  42. }
  43. </script>
  44. <script lang="ts" setup>
  45. import wdLoading from '../wd-loading/wd-loading.vue'
  46. import { getCurrentInstance, ref, watch, nextTick } from 'vue'
  47. import { deepClone, getType, isArray, isDef, isEqual, range } from '../common/util'
  48. import { formatArray, pickerViewProps, type ColumnItem, type PickerViewExpose } from './types'
  49. const props = defineProps(pickerViewProps)
  50. const emit = defineEmits(['change', 'pickstart', 'pickend', 'update:modelValue'])
  51. const formatColumns = ref<ColumnItem[][]>([]) // 格式化后的列数据
  52. const selectedIndex = ref<Array<number>>([]) // 格式化之后,每列选中的下标集合
  53. watch(
  54. [() => props.modelValue, () => props.columns],
  55. (newValue, oldValue) => {
  56. if (!isEqual(oldValue[1], newValue[1])) {
  57. if (isArray(newValue[1]) && newValue[1].length > 0) {
  58. formatColumns.value = formatArray(newValue[1], props.valueKey, props.labelKey)
  59. } else {
  60. // 当 columns 变为空时,清空 formatColumns 和 selectedIndex
  61. formatColumns.value = []
  62. selectedIndex.value = []
  63. }
  64. }
  65. if (isDef(newValue[0])) {
  66. selectWithValue(newValue[0])
  67. }
  68. },
  69. {
  70. deep: true,
  71. immediate: true
  72. }
  73. )
  74. const { proxy } = getCurrentInstance() as any
  75. /**
  76. * 根据传入的value,寻找对应的索引,并传递给原生选择器。
  77. * 需要保证formatColumns先设置,之后会修改selectedIndex。
  78. * @param {String|Number|Boolean|Array<String|Number|Boolean|Array<any>>}value
  79. */
  80. function selectWithValue(value: string | number | boolean | number[] | string[] | boolean[]) {
  81. if (formatColumns.value.length === 0) {
  82. selectedIndex.value = [] // 如果列为空,直接清空选中索引
  83. return
  84. }
  85. // 使其默认选中首项
  86. if (value === '' || !isDef(value) || (isArray(value) && value.length === 0)) {
  87. value = formatColumns.value.map((col) => {
  88. return col[0][props.valueKey]
  89. })
  90. }
  91. const valueType = getType(value)
  92. const type = ['string', 'number', 'boolean', 'array']
  93. if (type.indexOf(valueType) === -1) console.error(`value must be one of ${type.toString()}`)
  94. /**
  95. * 1.单key转为Array<key>
  96. * 2.根据formatColumns的长度截取Array<String>,保证下面的遍历不溢出
  97. * 3.根据每列的key值找到选项中value为此key的下标并记录
  98. */
  99. value = isArray(value) ? value : [value as string]
  100. value = value.slice(0, formatColumns.value.length)
  101. let selected: number[] = deepClone(selectedIndex.value)
  102. value.forEach((target, col) => {
  103. let row = formatColumns.value[col].findIndex((row) => {
  104. return row[props.valueKey].toString() === target.toString()
  105. })
  106. row = row === -1 ? 0 : row
  107. selected = correctSelectedIndex(col, row, selected)
  108. })
  109. /** 根据formatColumns的长度去除selectWithIndex无用的部分。
  110. * 始终保持value、selectWithIndex、formatColumns长度一致
  111. */
  112. selectedIndex.value = selected.slice(0, value.length)
  113. }
  114. /**
  115. * 修正选中项的值
  116. * @param value 当前picker选择器选中的值
  117. * @param origin 原始选中的值
  118. */
  119. function correctSelected(value: number[]) {
  120. let selected = deepClone(value)
  121. value.forEach((row, col) => {
  122. row = range(row, 0, formatColumns.value[col].length - 1)
  123. selected = correctSelectedIndex(col, row, selected)
  124. })
  125. return selected
  126. }
  127. /**
  128. * 修正选中项指定列行的值
  129. * @param columnIndex 列下标
  130. * @param rowIndex 行下标
  131. * @param selected 选中值列表
  132. */
  133. function correctSelectedIndex(columnIndex: number, rowIndex: number, selected: number[]) {
  134. const col = formatColumns.value[columnIndex]
  135. if (!col || !col[rowIndex]) {
  136. throw Error(`The value to select with Col:${columnIndex} Row:${rowIndex} is incorrect`)
  137. }
  138. const select: number[] = deepClone(selected)
  139. select[columnIndex] = rowIndex
  140. // 被禁用的无法选中,选中距离它最近的未被禁用的
  141. if (col[rowIndex].disabled) {
  142. // 寻找值为0或最最近的未被禁用的节点的索引
  143. const prev = col
  144. .slice(0, rowIndex)
  145. .reverse()
  146. .findIndex((s) => !s.disabled)
  147. const next = col.slice(rowIndex + 1).findIndex((s) => !s.disabled)
  148. if (prev !== -1) {
  149. select[columnIndex] = rowIndex - 1 - prev
  150. } else if (next !== -1) {
  151. select[columnIndex] = rowIndex + 1 + next
  152. } else if (select[columnIndex] === undefined) {
  153. select[columnIndex] = 0
  154. }
  155. }
  156. return select
  157. }
  158. /**
  159. * 选择器选中项变化时触发
  160. * @param param0
  161. */
  162. function onChange({ detail: { value } }: { detail: { value: number[] } }) {
  163. value = value.map((v: any) => {
  164. return Number(v || 0)
  165. })
  166. const index = getChangeDiff(value)
  167. // 先将picker选择器的值赋给selectedIndex,然后重新赋予修正后的值,防止两次操作修正结果一致时pikcer视图不刷新
  168. selectedIndex.value = deepClone(value)
  169. nextTick(() => {
  170. // 重新赋予修正后的值
  171. selectedIndex.value = correctSelected(value)
  172. if (props.columnChange) {
  173. // columnsChange 可能有异步操作,需要添加 resolve 进行回调通知,形参小于4个则为同步
  174. if (props.columnChange.length < 4) {
  175. props.columnChange(proxy.$.exposed, getSelects(), index || 0, () => {})
  176. handleChange(index || 0)
  177. } else {
  178. props.columnChange(proxy.$.exposed, getSelects(), index || 0, () => {
  179. // 如果selectedIndex只有一列,返回此项;如果是多项,返回所有选中项。
  180. handleChange(index || 0)
  181. })
  182. }
  183. } else {
  184. // 如果selectedIndex只有一列,返回此项;如果是多项,返回所有选中项。
  185. handleChange(index || 0)
  186. }
  187. })
  188. }
  189. /**
  190. * 获取选中项变化的列的下标
  191. * @param now 当前选中项值
  192. * @param origin 旧选中项值
  193. */
  194. function getChangeColumn(now: number[], origin: number[]) {
  195. if (!now || !origin) return -1
  196. const index = now.findIndex((row, index) => row !== origin[index])
  197. return index
  198. }
  199. function getChangeDiff(value: number[]) {
  200. value = value.slice(0, formatColumns.value.length)
  201. // 保留选中前的
  202. const origin: number[] = deepClone(selectedIndex.value)
  203. // 存储赋值旧值,便于外部比较
  204. let selected: number[] = deepClone(selectedIndex.value)
  205. value.forEach((row, col) => {
  206. row = range(row, 0, formatColumns.value[col].length - 1)
  207. if (row === origin[col]) return
  208. selected = correctSelectedIndex(col, row, selected)
  209. })
  210. // 值变化的列
  211. const diffCol = getChangeColumn(selected, origin)
  212. if (diffCol === -1) return
  213. // 获取变化的的行
  214. const diffRow = selected[diffCol]
  215. // 如果selectedIndex只有一列,返回选中项的索引;如果是多项,返回选中项所在的列。
  216. return selected.length === 1 ? diffRow : diffCol
  217. }
  218. /**
  219. * 列更新
  220. * @param index 列下标
  221. */
  222. function handleChange(index: number) {
  223. const value = getValues()
  224. // 避免多次触发change
  225. if (isEqual(value, props.modelValue)) return
  226. emit('update:modelValue', value)
  227. // 延迟一下,避免组件刚渲染时调用者的事件未初始化好
  228. setTimeout(() => {
  229. emit('change', {
  230. picker: proxy.$.exposed,
  231. value,
  232. index
  233. })
  234. }, 0)
  235. }
  236. /**
  237. * @description 获取所有列选中项,返回值为一个数组
  238. */
  239. function getSelects() {
  240. const selects = selectedIndex.value.map((row, col) => formatColumns.value[col][row])
  241. // 单列选择器,则返回单项
  242. if (selects.length === 1) {
  243. return selects[0]
  244. }
  245. return selects
  246. }
  247. /**
  248. * 获取所有列的选中值
  249. * 如果values只有一项则将第一项返回
  250. */
  251. function getValues() {
  252. const { valueKey } = props
  253. const values = selectedIndex.value.map((row, col) => {
  254. return formatColumns.value[col][row][valueKey]
  255. })
  256. if (values.length === 1) {
  257. return values[0]
  258. }
  259. return values
  260. }
  261. /**
  262. * 获取所有列选中项的label,返回值为一个数组
  263. */
  264. function getLabels() {
  265. const { labelKey } = props
  266. return selectedIndex.value.map((row, col) => formatColumns.value[col][row][labelKey])
  267. }
  268. /**
  269. * 获取某一列的选中项下标
  270. * @param {Number} columnIndex 列的下标
  271. * @returns {Number} 下标
  272. */
  273. function getColumnIndex(columnIndex: number) {
  274. return selectedIndex.value[columnIndex]
  275. }
  276. /**
  277. * 获取某一列的选项
  278. * @param {Number} columnIndex 列的下标
  279. * @returns {Array<{valueKey,labelKey}>} 当前列的集合
  280. */
  281. function getColumnData(columnIndex: number) {
  282. return formatColumns.value[columnIndex]
  283. }
  284. /**
  285. * 设置列数据
  286. * @param columnIndex 列下标
  287. * @param data // 列数据
  288. * @param rowIndex // 行下标
  289. */
  290. function setColumnData(columnIndex: number, data: Array<string | number | ColumnItem | Array<string | number | ColumnItem>>, rowIndex: number = 0) {
  291. formatColumns.value[columnIndex] = formatArray(data, props.valueKey, props.labelKey).reduce((acc, val) => acc.concat(val), [])
  292. selectedIndex.value = correctSelectedIndex(columnIndex, rowIndex, selectedIndex.value)
  293. }
  294. /**
  295. * 获取列数据
  296. */
  297. function getColumnsData() {
  298. return deepClone(formatColumns.value)
  299. }
  300. /**
  301. * 获取选中数据
  302. */
  303. function getSelectedIndex() {
  304. return selectedIndex.value
  305. }
  306. /**
  307. * 用于重置列数据为指定列数据
  308. */
  309. function resetColumns(columns: (string | number | string[] | number[] | ColumnItem | ColumnItem[])[]) {
  310. if (isArray(columns) && columns.length) {
  311. formatColumns.value = formatArray(columns, props.valueKey, props.labelKey)
  312. }
  313. }
  314. function onPickStart() {
  315. emit('pickstart')
  316. }
  317. function onPickEnd() {
  318. emit('pickend')
  319. }
  320. defineExpose<PickerViewExpose>({
  321. getSelects,
  322. getValues,
  323. setColumnData,
  324. getColumnsData,
  325. getColumnData,
  326. getColumnIndex,
  327. getLabels,
  328. getSelectedIndex,
  329. resetColumns
  330. })
  331. </script>
  332. <style lang="scss" scoped>
  333. @import './index.scss';
  334. </style>