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.

491 lines
14 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 ${disabled ? 'is-disabled' : ''} ${size ? 'is-' + size : ''} ${cell.border.value ? 'is-border' : ''} ${
alignRight ? 'is-align-right' : ''
} ${error ? 'is-error' : ''} ${customClass}`"
>
<!--文案-->
<view class="wd-picker__field" @click="showPopup">
<slot v-if="useDefaultSlot"></slot>
<view v-else class="wd-picker__cell">
<view
v-if="label || useLabelSlot"
:class="`wd-picker__label ${customLabelClass} ${isRequired ? 'is-required' : ''}`"
:style="labelWidth ? 'min-width:' + labelWidth + ';max-width:' + labelWidth + ';' : ''"
>
<template v-if="label">{{ label }}</template>
<slot v-else name="label"></slot>
</view>
<view class="wd-picker__body">
<view class="wd-picker__value-wraper">
<view :class="`wd-picker__value ${ellipsis && 'is-ellipsis'} ${customValueClass} ${showValue ? '' : 'wd-picker__placeholder'}`">
{{ showValue ? showValue : placeholder }}
</view>
<wd-icon v-if="!disabled && !readonly" custom-class="wd-picker__arrow" name="arrow-right" />
</view>
<view v-if="errorMessage" class="wd-picker__error-message">{{ errorMessage }}</view>
</view>
</view>
</view>
<!--弹出层picker-view 在隐藏时修改值会触发多次change事件从而导致所有列选中第一项因此picker在关闭时不隐藏 -->
<wd-popup
v-model="popupShow"
position="bottom"
:hide-when-close="false"
:close-on-click-modal="closeOnClickModal"
:z-index="zIndex"
:safe-area-inset-bottom="safeAreaInsetBottom"
@close="onCancel"
custom-class="wd-picker__popup"
>
<view class="wd-picker__wraper">
<!--toolBar-->
<view class="wd-picker__toolbar" @touchmove="noop">
<!--取消按钮-->
<view class="wd-picker__action wd-picker__action--cancel" @click="onCancel">
{{ cancelButtonText }}
</view>
<!--标题-->
<view v-if="title" class="wd-picker__title">{{ title }}</view>
<!--确定按钮-->
<view :class="`wd-picker__action ${isLoading ? 'is-loading' : ''}`" @click="onConfirm">
{{ confirmButtonText }}
</view>
</view>
<!--pickerView-->
<wd-picker-view
ref="pickerViewWd"
:custom-class="customViewClass"
v-model="pickerValue"
:columns="displayColumns"
:loading="isLoading"
:loading-color="loadingColor"
:columns-height="columnsHeight"
:value-key="valueKey"
:label-key="labelKey"
@change="pickerViewChange"
@pickstart="onPickStart"
@pickend="onPickEnd"
:column-change="columnChange"
/>
</view>
</wd-popup>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-picker',
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { getCurrentInstance, onBeforeMount, ref, watch, computed, onMounted, nextTick } from 'vue'
import { deepClone, defaultDisplayFormat, getType, isDef } from '../common/util'
import { useCell } from '../composables/useCell'
import { type ColumnItem, formatArray } from '../wd-picker-view/type'
import { FORM_KEY, type FormItemRule } from '../wd-form/types'
import { useParent } from '../composables/useParent'
interface Props {
customClass?: string
customLabelClass?: string
customValueClass?: string
customViewClass?: string
// 选择器左侧文案
label?: string
// 选择器占位符
placeholder?: string
// 禁用
disabled?: boolean
// 只读
readonly?: boolean
loading?: boolean
loadingColor?: string
/* popup */
// 弹出层标题
title?: string
// 取消按钮文案
cancelButtonText?: string
// 确认按钮文案
confirmButtonText?: string
// 是否必填
required?: boolean
size?: string
labelWidth?: string
useDefaultSlot?: boolean
useLabelSlot?: boolean
error?: boolean
alignRight?: boolean
// eslint-disable-next-line @typescript-eslint/ban-types
beforeConfirm?: Function
closeOnClickModal?: boolean
safeAreaInsetBottom?: boolean
ellipsis?: boolean
// 选项总高度
columnsHeight?: number
// 选项对象中value对应的 key
valueKey?: string
// 选项对象中,展示的文本对应的 key
labelKey?: string
// 初始值
modelValue?: string | number | Array<string | number>
// 选择器数据
columns?: Array<string | number | ColumnItem | Array<string | number | ColumnItem>>
// 多级联动
// eslint-disable-next-line @typescript-eslint/ban-types
columnChange?: Function
// 外部展示格式化函数
// eslint-disable-next-line @typescript-eslint/ban-types
displayFormat?: Function
// 自定义层级
zIndex?: number
prop?: string
rules?: FormItemRule[]
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
customClass: '',
customViewClass: '',
customLabelClass: '',
customValueClass: '',
// 选择器占位符
placeholder: '请选择',
// 禁用
disabled: false,
// 只读
readonly: false,
loading: false,
loadingColor: '#4D80F0',
// 取消按钮文案
cancelButtonText: '取消',
// 确认按钮文案
confirmButtonText: '完成',
// 是否必填
required: false,
useDefaultSlot: false,
useLabelSlot: false,
error: false,
alignRight: false,
closeOnClickModal: true,
safeAreaInsetBottom: true,
ellipsis: false,
columnsHeight: 217,
valueKey: 'value',
labelKey: 'label',
columns: () => [],
zIndex: 15,
rules: () => []
})
const pickerViewWd = ref<any>(null)
const cell = useCell()
const innerLoading = ref<boolean>(false) // 内部控制是否loading
// 弹出层是否显示
const popupShow = ref<boolean>(false)
// 选定后展示的选中项
const showValue = ref<string>('')
const pickerValue = ref<string | number | boolean | (string | number | boolean)[]>('')
const displayColumns = ref<Array<string | number | ColumnItem | Array<string | number | ColumnItem>>>([]) // 传入 pickerView 的columns
const resetColumns = ref<Array<string | number | ColumnItem | Array<string | number | ColumnItem>>>([]) // 保存之前的 columns当取消时将数据源回滚避免多级联动数据源不正确的情况
const isPicking = ref<boolean>(false) // 判断pickview是否还在滑动中
const hasConfirmed = ref<boolean>(false) // 判断用户是否点击了确认按钮
const isLoading = computed(() => {
return props.loading || innerLoading.value
})
watch(
() => props.displayFormat,
(fn) => {
if (fn && getType(fn) !== 'function') {
console.error('The type of displayFormat must be Function')
}
if (pickerViewWd.value && pickerViewWd.value.selectedIndex && pickerViewWd.value.selectedIndex.length !== 0) {
if (props.modelValue) {
setShowValue(pickerViewWd.value.getSelects())
} else {
showValue.value = ''
}
}
},
{
immediate: true,
deep: true
}
)
watch(
() => props.modelValue,
(newValue) => {
pickerValue.value = newValue
// 获取初始选中项,并展示初始选中文案
if (isDef(newValue)) {
if (pickerViewWd.value && pickerViewWd.value.getSelects) {
nextTick(() => {
setShowValue(pickerViewWd.value!.getSelects())
})
} else {
setShowValue(getSelects(props.modelValue))
}
} else {
showValue.value = ''
}
},
{
deep: true,
immediate: true
}
)
watch(
() => props.columns,
(newValue) => {
displayColumns.value = newValue
resetColumns.value = newValue
// 获取初始选中项,并展示初始选中文案
if (props.modelValue) {
if (pickerViewWd.value && pickerViewWd.value.getSelects) {
nextTick(() => {
setShowValue(pickerViewWd.value!.getSelects())
})
} else {
setShowValue(getSelects(props.modelValue))
}
} else {
showValue.value = ''
}
},
{
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 { parent: form } = useParent(FORM_KEY)
// 表单校验错误信息
const errorMessage = computed(() => {
if (form && props.prop && form.errorMessages && form.errorMessages[props.prop]) {
return form.errorMessages[props.prop]
} else {
return ''
}
})
// 是否展示必填
const isRequired = computed(() => {
let formRequired = false
if (form && form.props.rules) {
const rules = form.props.rules
for (const key in rules) {
if (Object.prototype.hasOwnProperty.call(rules, key) && key === props.prop && Array.isArray(rules[key])) {
formRequired = rules[key].some((rule: FormItemRule) => rule.required)
}
}
}
return props.required || props.rules.some((rule) => rule.required) || formRequired
})
const { proxy } = getCurrentInstance() as any
const emit = defineEmits(['confirm', 'open', 'cancel', 'update:modelValue'])
onMounted(() => {
props.modelValue && setShowValue(getSelects(props.modelValue))
if (props.modelValue && pickerViewWd.value && pickerViewWd.value.getSelects) {
setShowValue(pickerViewWd.value!.getSelects())
}
})
onBeforeMount(() => {
displayColumns.value = deepClone(props.columns)
resetColumns.value = deepClone(props.columns)
})
/**
* @description 根据传入的valuepicker组件获取当前cell展示值。
* @param {String|Number|Array<String|Number|Array<any>>}value
*/
function getSelects(value) {
const formatColumns = formatArray(props.columns, props.valueKey, props.labelKey)
if (props.columns.length === 0) return
// 使其默认选中首项
if (value === '' || value === null || value === undefined || (getType(value) === 'array' && value.length === 0)) {
value = formatColumns.map((col) => {
return col[0][props.valueKey]
})
}
const valueType = getType(value)
const type = ['string', 'number', 'boolean', 'array']
if (type.indexOf(valueType) === -1) return []
/**
* 1.单key转为Array<key>
* 2.根据formatColumns的长度截取Array<String>,保证下面的遍历不溢出
* 3.根据每列的key值找到选项中value为此key的下标并记录
*/
value = value instanceof Array ? value : [value]
value = value.slice(0, formatColumns.length)
if (value.length === 0) {
value = formatColumns.map(() => 0)
}
let selected: number[] = []
value.forEach((target, col) => {
let row = formatColumns[col].findIndex((row) => {
return row[props.valueKey].toString() === target.toString()
})
row = row === -1 ? 0 : row
selected.push(row)
})
const selects = selected.map((row, col) => formatColumns[col][row])
// 单列选择器,则返回单项
if (selects.length === 1) {
return selects[0]
}
return selects
}
// 对外暴露方法,打开弹框
function open() {
showPopup()
}
// 对外暴露方法,关闭弹框
function close() {
onCancel()
}
/**
* @description 展示popup小程序有个bug在picker-view弹出时设置value会触发change事件而且会将picker-view的value多次触发change重置为第一项
*/
function showPopup() {
if (props.disabled || props.readonly) return
emit('open')
popupShow.value = true
pickerValue.value = props.modelValue
displayColumns.value = resetColumns.value
}
/**
* @description 点击取消按钮触发。关闭popup触发cancel事件。
*/
function onCancel() {
popupShow.value = false
emit('cancel')
}
/**
* @description 点击确定按钮触发。展示选中值触发cancel事件。
*/
function onConfirm() {
if (isLoading.value) return
// 如果当前还在滑动且未停止下来则锁住先不确认等滑完再自动确认避免pickview值未更新
if (isPicking.value) {
hasConfirmed.value = true
return
}
const { beforeConfirm } = props
if (beforeConfirm && getType(beforeConfirm) === 'function') {
beforeConfirm(
pickerValue.value,
(isPass) => {
isPass && handleConfirm()
},
proxy.$.exposed
)
} else {
handleConfirm()
}
}
function handleConfirm() {
if (isLoading.value || props.disabled) {
popupShow.value = false
return
}
const selects = pickerViewWd.value!.getSelects()
const values = pickerViewWd.value!.getValues()
// 获取当前的数据源,并设置给 resetColumns用于取消时可以回退数据源
const columns = pickerViewWd.value!.getColumnsData()
popupShow.value = false
resetColumns.value = deepClone(columns)
emit('update:modelValue', values)
setShowValue(selects)
emit('confirm', {
value: values,
selectedItems: selects
})
}
/**
* @description 初始change事件
* @param event
*/
function pickerViewChange({ value }) {
pickerValue.value = value
}
/**
* @description 设置展示值
* @param {Array<String>} items
*/
function setShowValue(items) {
// 避免值为空时调用自定义展示函数
if ((items instanceof Array && !items.length) || !items) return
const { valueKey, labelKey } = props
showValue.value = (props.displayFormat || defaultDisplayFormat)(items, { valueKey, labelKey })
}
function noop() {}
function onPickStart() {
isPicking.value = true
}
function onPickEnd() {
isPicking.value = false
if (hasConfirmed.value) {
hasConfirmed.value = false
onConfirm()
}
}
/**
* 外部设置是否loading
* @param loading 是否loading
*/
function setLoading(loading: boolean) {
innerLoading.value = loading
}
defineExpose({
close,
open,
setLoading
})
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>