You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

410 lines
12 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view :class="`wd-picker-view ${customClass}`">
<view class="wd-picker-view__loading" v-if="loading">
<wd-loading :color="loadingColor" />
</view>
<view :style="`height: ${columnsHeight - 20}px;`">
<picker-view
mask-class="wd-picker-view__mask"
indicator-class="wd-picker-view__roller"
:indicator-style="`height: ${itemHeight}px;`"
:style="`height: ${columnsHeight - 20}px;`"
:value="selectedIndex"
@change="onChange"
@pickstart="onPickStart"
@pickend="onPickEnd"
>
<picker-view-column v-for="(col, colIndex) in formatColumns" :key="colIndex" class="wd-picker-view-column">
<view
v-for="(row, rowIndex) in col"
:key="rowIndex"
:class="`wd-picker-view-column__item ${row['disabled'] ? 'wd-picker-view-column__item--disabled' : ''} ${
selectedIndex[colIndex] == rowIndex ? 'wd-picker-view-column__item--active' : ''
}`"
:style="`line-height: ${itemHeight}px;`"
>
{{ row[labelKey] }}
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-picker-view',
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { getCurrentInstance, ref, watch, nextTick } from 'vue'
import { deepClone, getType, isEqual, range } from '../common/util'
import { type ColumnItem, formatArray } from './type'
interface Props {
customClass?: string
// 加载中
loading?: boolean
loadingColor?: string
// 选项总高度
columnsHeight?: number
// 选项对象中value对应的 key
valueKey?: string
// 选项对象中,展示的文本对应的 key
labelKey?: string
// 初始值
modelValue: string | number | boolean | Array<string | number | boolean>
// 选择器数据
columns: Array<string | number | ColumnItem | Array<string | number | ColumnItem>>
// 多级联动
// eslint-disable-next-line @typescript-eslint/ban-types
columnChange?: Function
}
const props = withDefaults(defineProps<Props>(), {
customClass: '',
loading: false,
loadingColor: '#4D80F0',
columnsHeight: 217,
valueKey: 'value',
labelKey: 'label',
columns: () => []
})
// 格式化之后用于render 列表的数据
const formatColumns = ref<Array<Array<Record<string, any>>>>([])
const itemHeight = ref<number>(35)
const selectedIndex = ref<Array<number>>([]) // 格式化之后,每列选中的下标集合
const preSelectedIndex = ref<Array<number>>([])
watch(
() => props.modelValue,
(newValue, oldValue) => {
if (!isEqual(oldValue, newValue)) {
selectWithValue(newValue)
}
},
{
deep: true,
immediate: true
}
)
watch(
() => props.columns,
(newValue) => {
// props初始化的时候格式化formatColumns交给value的observer来做
formatColumns.value = formatArray(newValue, props.valueKey, props.labelKey)
/**
* 每次改变都要重置选中项
* 1.选中每列的第一个
* 2.原来的value再选一次
*/
// this.data.formatColumns.forEach((no, col) => this.selectWithIndex(col, 0))
selectWithValue(props.modelValue)
},
{
deep: true,
immediate: true
}
)
watch(
() => selectedIndex.value,
(newValue) => {
if (isEqual(newValue, preSelectedIndex.value)) return
if (!isEqual(getValues(), props.modelValue)) {
handleChange(0)
}
},
{
deep: true,
immediate: true
}
)
watch(
() => props.columnChange,
(newValue) => {
if (newValue && getType(newValue) !== 'function') {
console.error('The type of columnChange must be Function')
}
},
{
deep: true,
immediate: true
}
)
const { proxy } = getCurrentInstance() as any
const emit = defineEmits(['change', 'pickstart', 'pickend', 'update:modelValue'])
/**
* @description 根据传入的value寻找对应的索引并传递给原生选择器。
* 会保证formatColumns先设置之后会修改selectedIndex。
* @param {String|Number|Boolean|Array<String|Number|Boolean|Array<any>>}value
*/
function selectWithValue(value) {
if (props.columns.length === 0) return
// 使其默认选中首项
if (value === '' || value === null || value === undefined || (getType(value) === 'array' && value.length === 0)) {
value = formatColumns.value.map((col) => {
return col[0][props.valueKey]
})
}
const valueType = getType(value)
const type = ['string', 'number', 'boolean', 'array']
if (type.indexOf(valueType) === -1) console.error(`value must be one of ${type.toString()}`)
// 在props初始化的时候有可能会调用此函数此时需要保证formatColumns已经被设置关于此问题更多详情参考/ISSUE.md。
if (formatColumns.value.length === 0) {
formatColumns.value = formatArray(props.columns, props.valueKey, props.labelKey)
}
/**
* 1.单key转为Array<key>
* 2.根据formatColumns的长度截取Array<String>,保证下面的遍历不溢出
* 3.根据每列的key值找到选项中value为此key的下标并记录
*/
value = value instanceof Array ? value : [value]
value = value.slice(0, formatColumns.value.length)
if (value.length === 0) {
value = formatColumns.value.map(() => 0)
}
let selected: number[] = deepClone(selectedIndex.value)
value.forEach((target, col) => {
let row = formatColumns.value[col].findIndex((row) => {
return row[props.valueKey].toString() === target.toString()
})
row = row === -1 ? 0 : row
selected = selectWithIndex(col, row)
})
/** 根据formatColumns的长度去除selectWithIndex无用的部分。
* 始终保持value、selectWithIndex、formatColumns长度一致
*/
selectedIndex.value = selected.slice(0, value.length)
}
/**
* @description 根据传入的col,row传递给原生选择器
* @param {Number} columnIndex 要操作的列索引
* @param {Number} rowIndex 要选中的行索引
* @return {Boolean} 是否设置成功
*/
function selectWithIndex(columnIndex, rowIndex) {
const col = formatColumns.value[columnIndex]
if (!col || !col[rowIndex]) {
throw Error(`The value to select with Col:${columnIndex} Row:${rowIndex} is correct`)
}
const select: number[] = deepClone(selectedIndex.value)
// 被禁用的无法选中,选中距离它最近的未被禁用的
if (col[rowIndex].disabled) {
// 寻找值为0或最最近的未被禁用的节点的索引
const prev = col
.slice(0, rowIndex)
.reverse()
.findIndex((s) => !s.disabled)
const next = col.slice(rowIndex + 1).findIndex((s) => !s.disabled)
if (prev !== -1) {
select[columnIndex] = rowIndex - 1 - prev
} else if (next !== -1) {
select[columnIndex] = rowIndex + 1 + next
} else if (select[columnIndex] === undefined) {
select[columnIndex] = 0
}
} else {
select[columnIndex] = rowIndex
}
selectedIndex.value = deepClone(select)
return selectedIndex.value
}
/**
* @description 滚动选中时更新选中的索引、触发change事件
* @return {Number|Array<Number>}选中项的下标或者集合
* @return {Object}实例本身
*/
function onChange({ detail: { value } }) {
value = value.map((v) => {
return Number(v || 0)
})
const index = getChangeDiff(value)
selectedIndex.value = deepClone(value)
nextTick(() => {
// 执行多级联动
if (props.columnChange) {
// columnsChange 可能有异步操作,需要添加 resolve 进行回调通知形参小于4个则为同步
if (props.columnChange.length < 4) {
props.columnChange(proxy.$.exposed, getSelects(), index)
handleChange(index)
} else {
props.columnChange(proxy.$.exposed, getSelects(), index, () => {
// 如果selectedIndex只有一列返回此项如果是多项返回所有选中项。
handleChange(index)
})
}
} else {
// 如果selectedIndex只有一列返回此项如果是多项返回所有选中项。
handleChange(index)
}
})
}
function getChangeIndex(now, origin) {
if (!now || !origin) return
const index = now.findIndex((row, index) => row !== origin[index])
return index
}
function getChangeDiff(value: number[]) {
// 小程序bug 1. 修改原生pickerView的columns滑动触发change事件回传的数组长度为未改变columns之前的,并不会缩减
// 小程序bug 2. 当点击速度过快时会出现负数列项的操作需要将value进行限制
value = value.slice(0, formatColumns.value.length)
// 保留选中前的
const origin: number[] = deepClone(selectedIndex.value)
// 存储赋值旧值,便于外部比较
let selected: number[] = deepClone(selectedIndex.value)
// 开始应用最新的值
value.forEach((row, col) => {
row = range(row, 0, formatColumns.value[col].length - 1)
if (row === origin[col]) return
selected = selectWithIndex(col, row)
})
selectedIndex.value = selected
preSelectedIndex.value = origin
// diff出变化的列
// const diffCol = selectedIndex.findIndex((row, index) => row !== origin[index])
const diffCol = getChangeIndex(selected, origin)
if (diffCol === -1) return
// 获取变化的的行
const diffRow = selected[diffCol]
// 如果selectedIndex只有一列返回选中项的索引如果是多项返回选中项所在的列。
return selected.length === 1 ? diffRow : diffCol
}
function handleChange(index: number) {
const value = getValues()
// 避免多次触发change
if (isEqual(value, props.modelValue)) return
emit('update:modelValue', value)
// 延迟一下,避免组件刚渲染时调用者的事件未初始化好
setTimeout(() => {
emit('change', {
picker: proxy.$.exposed,
value,
index
})
}, 0)
}
/**
* @description 获取所有列选中项,返回值为一个数组
*/
function getSelects() {
const selects = selectedIndex.value.map((row, col) => formatColumns.value[col][row])
// 单列选择器,则返回单项
if (selects.length === 1) {
return selects[0]
}
return selects
}
/**
* @description 获取所有列选中项的value返回值为一个数组
*/
function getValues() {
const { valueKey } = props
const values = selectedIndex.value.map((row, col) => formatColumns.value[col][row][valueKey])
if (values.length === 1) {
return values[0]
}
return values
}
/**
* @description 获取所有列选中项的label返回值为一个数组
* @return {Array} 每列选中的label
*/
function getLabels() {
const { labelKey } = props
return selectedIndex.value.map((row, col) => formatColumns.value[col][row][labelKey])
}
/**
* @description 获取某一列的选中项下标
* @param {Number} columnIndex 列的下标
* @returns {Number} 下标
*/
function getColumnIndex(columnIndex) {
return selectedIndex.value[columnIndex]
}
/**
* @description 获取某一列的选项
* @param {Number} columnIndex 列的下标
* @returns {Array<{valueKey,labelKey}>} 当前列的集合
*/
function getColumnData(columnIndex) {
return formatColumns.value[columnIndex]
}
/**
* @description 获取某一列的选项
* @param {Number} columnIndex 列的下标
* @param {Array<原始值|Object>} 一维数组,元素仅限对象和原始值
* @param {Number} jumpTo 更换列数据后停留的地点
*/
function setColumnData(columnIndex, data, jumpTo = 0) {
/**
* @注意 以下为pickerView的坑
* 如果某一列(以下简称列)中有10个选项而且当前选中第10项。
* 如果此时把此列的选项修改后还剩下3个那么选中项会由第10项滑落到第3项同时出发change事件
*/
// 为了防止上述情况发生修改数据前先将当前列选中0
selectedIndex.value = selectWithIndex(columnIndex, jumpTo)
// 经过formatArray处理的数据会变成二维数组一定要拍成一维的。
// ps 小程序基础库v2.9.3才可以用flat
formatColumns.value[columnIndex] = formatArray(data, props.valueKey, props.labelKey).reduce((acc, val) => acc.concat(val), [])
}
function getColumnsData() {
return formatColumns.value.slice(0)
}
function onPickStart() {
emit('pickstart')
}
function onPickEnd() {
emit('pickend')
}
defineExpose({
getSelects,
getValues,
setColumnData,
getColumnsData,
getColumnData,
getColumnIndex,
getLabels,
selectedIndex
})
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>