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.

493 lines
14 KiB
Vue

<template>
<view :class="`wd-select-picker ${cell.border.value ? 'is-border' : ''} ${customClass}`">
<view class="wd-select-picker__field" @click="open">
<slot v-if="useDefaultSlot"></slot>
<view
v-else
:class="`wd-select-picker__cell ${disabled && 'is-disabled'} ${readonly && 'is-readonly'} ${alignRight && 'is-align-right'} ${
error && 'is-error'
} ${size && 'is-' + size}`"
>
<view
v-if="label || useLabelSlot"
:class="`wd-select-picker__label ${isRequired && 'is-required'} ${customLabelClass}`"
:style="labelWidth ? 'min-width:' + labelWidth + ';max-width:' + labelWidth + ';' : ''"
>
<block v-if="label">{{ label }}</block>
<slot v-else name="label"></slot>
</view>
<view class="wd-select-picker__body">
<view class="wd-select-picker__value-wraper">
<view
:class="`wd-select-picker__value ${ellipsis && 'is-ellipsis'} ${customValueClass} ${
showValue ? '' : 'wd-select-picker__value--placeholder'
}`"
>
{{ showValue || placeholder || '请选择' }}
</view>
<wd-icon v-if="!disabled && !readonly" custom-class="wd-select-picker__arrow" name="arrow-right" />
</view>
<view v-if="errorMessage" class="wd-select-picker__error-message">{{ errorMessage }}</view>
</view>
</view>
</view>
<wd-action-sheet
v-model="pickerShow"
:duration="250"
:title="title || '请选择'"
:close-on-click-modal="closeOnClickModal"
:z-index="zIndex"
:safe-area-inset-bottom="safeAreaInsetBottom"
@close="close"
@opened="scrollIntoView ? setScrollIntoView() : ''"
custom-header-class="wd-select-picker__header"
>
<wd-search v-if="filterable" v-model="filterVal" :placeholder="filterPlaceholder" hide-cancel placeholder-left @change="handleFilterChange" />
<scroll-view
:class="`wd-select-picker__wrapper ${filterable ? 'is-filterable' : ''} ${loading ? 'is-loading' : ''} ${customContentClass}`"
:scroll-y="!loading"
:scroll-top="scrollTop"
:scroll-with-animation="true"
>
<!-- 多选 -->
<view v-if="type === 'checkbox'" id="wd-checkbox-group">
<wd-checkbox-group v-model="selectList" cell :size="selectSize" :checked-color="checkedColor" :min="min" :max="max" @change="handleChange">
<view v-for="item in filterColumns" :key="item[valueKey]" :id="'check' + item[valueKey]">
<wd-checkbox :modelValue="item[valueKey]" :disabled="item.disabled">
<block v-if="filterable && filterVal">
<block v-for="text in item[labelKey]" :key="text.label">
<text v-if="text.type === 'active'" class="wd-select-picker__text-active">{{ text.label }}</text>
<block v-else>{{ text.label }}</block>
</block>
</block>
<block v-else>
{{ item[labelKey] }}
</block>
</wd-checkbox>
</view>
</wd-checkbox-group>
</view>
<!-- 单选 -->
<view v-if="type === 'radio'" id="wd-radio-group">
<wd-radio-group v-model="selectList" cell :size="selectSize" :checked-color="checkedColor" @change="handleChange">
<view v-for="(item, index) in filterColumns" :key="index" :id="'radio' + item[valueKey]">
<wd-radio :value="item[valueKey]" :disabled="item.disabled">
<block v-if="filterable && filterVal">
<block v-for="text in item[labelKey]" :key="text.label">
<text :clsss="`${text.type === 'active' ? 'wd-select-picker__text-active' : ''}`">{{ text.label }}</text>
</block>
</block>
<block v-else>
{{ item[labelKey] }}
</block>
</wd-radio>
</view>
</wd-radio-group>
</view>
<view v-if="loading" class="wd-select-picker__loading" @touchmove="noop">
<wd-loading :color="loadingColor" />
</view>
</scroll-view>
<!-- 确认按钮 -->
<view class="wd-select-picker__footer">
<wd-button block size="large" @click="onConfirm" :disabled="loading">{{ confirmButtonText }}</wd-button>
</view>
</wd-action-sheet>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-select-picker',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { getCurrentInstance, onBeforeMount, ref, watch, nextTick, computed } from 'vue'
import { useCell } from '../composables/useCell'
import { getRect, getType, isArray, isDef, requestAnimationFrame } from '../common/util'
import { useParent } from '../composables/useParent'
import { FORM_KEY, type FormItemRule } from '../wd-form/types'
type SelectPickerType = 'checkbox' | 'radio'
interface Props {
customClass?: string
customContentClass?: string
customLabelClass?: string
customValueClass?: string
label?: string
labelWidth?: string
disabled?: boolean
readonly?: boolean
placeholder?: string
title?: string
alignRight?: boolean
error?: boolean
required?: boolean
useLabelSlot?: boolean
useDefaultSlot?: boolean
size?: string
checkedColor?: string
min?: number
max?: number
selectSize?: string
loading?: boolean
loadingColor?: string
closeOnClickModal?: boolean
modelValue: Array<number | boolean | string> | number | boolean | string
columns: Array<Record<string, any>>
type?: SelectPickerType
valueKey?: string
labelKey?: string
confirmButtonText?: string
// 外部展示格式化函数
// eslint-disable-next-line @typescript-eslint/ban-types
displayFormat?: Function
// eslint-disable-next-line @typescript-eslint/ban-types
beforeConfirm?: Function
zIndex?: number
safeAreaInsetBottom?: boolean
filterable?: boolean
filterPlaceholder?: string
ellipsis?: boolean
scrollIntoView?: boolean
prop?: string
rules?: FormItemRule[]
}
const props = withDefaults(defineProps<Props>(), {
customClass: '',
customContentClass: '',
customLabelClass: '',
customValueClass: '',
columns: () => [],
type: 'checkbox',
valueKey: 'value',
labelKey: 'label',
placeholder: '请选择',
disabled: false,
loading: false,
loadingColor: '#4D80F0',
readonly: false,
confirmButtonText: '确认',
labelWidth: '33%',
error: false,
required: false,
alignRight: false,
min: 0,
max: 0,
checkedColor: '#4D80F0',
useDefaultSlot: false,
useLabelSlot: false,
closeOnClickModal: true,
zIndex: 15,
safeAreaInsetBottom: true,
filterable: false,
filterPlaceholder: '搜索',
ellipsis: false,
scrollIntoView: true,
rules: () => []
})
const pickerShow = ref<boolean>(false)
const selectList = ref<Array<number | boolean | string> | number | boolean | string>([])
const showValue = ref<string>('')
const isConfirm = ref<boolean>(false)
const lastSelectList = ref<Array<number | boolean | string>>([])
const filterVal = ref<string>('')
const filterColumns = ref<Array<Record<string, any>>>([])
const scrollTop = ref<number | null>(0) // 滚动位置
const cell = useCell()
watch(
() => props.modelValue,
(newValue) => {
if (newValue === selectList.value) return
selectList.value = valueFormat(newValue)
lastSelectList.value = valueFormat(newValue)
setShowValue(valueFormat(newValue))
},
{
deep: true,
immediate: true
}
)
watch(
() => props.columns,
(newValue) => {
if (props.filterable && filterVal.value) {
formatFilterColumns(newValue, filterVal.value)
} else {
filterColumns.value = newValue
}
},
{
deep: true,
immediate: true
}
)
watch(
() => props.displayFormat,
(fn) => {
if (fn && getType(fn) !== 'function') {
console.error('The type of displayFormat must be Function')
}
},
{
deep: true,
immediate: true
}
)
watch(
() => props.beforeConfirm,
(fn) => {
if (fn && getType(fn) !== 'function') {
console.error('The type of beforeConfirm 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.rules) {
const rules = form.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
})
onBeforeMount(() => {
selectList.value = valueFormat(props.modelValue)
filterColumns.value = props.columns
})
const emit = defineEmits(['change', 'cancel', 'confirm', 'update:modelValue'])
const { proxy } = getCurrentInstance() as any
function setScrollIntoView() {
let wraperSelector: string = ''
let selectorPromise: Promise<UniApp.NodeInfo | UniApp.NodeInfo[]>[] = []
if (isDef(selectList.value) && !isArray(selectList.value)) {
wraperSelector = '#wd-radio-group'
selectorPromise = [getRect(`#radio${selectList.value}`, false, proxy)]
} else if (isArray(selectList.value) && selectList.value.length > 0) {
selectList.value.forEach((value) => {
selectorPromise.push(getRect(`#check${value}`, false, proxy))
})
wraperSelector = '#wd-checkbox-group'
}
if (wraperSelector) {
requestAnimationFrame().then(() => {
requestAnimationFrame().then(() => {
Promise.all([getRect('.wd-select-picker__wrapper', false, proxy), getRect(wraperSelector, false, proxy), ...selectorPromise])
.then((res: any[]) => {
if (isDef(res) && isArray(res)) {
const scrollView = res[0]
const wraper = res[1]
const target = res.slice(2) || []
if (isDef(wraper) && isDef(scrollView)) {
const index = target.findIndex((item) => {
return item.top >= scrollView.top && item.bottom <= scrollView.bottom
})
if (index < 0) {
scrollTop.value = -1
nextTick(() => {
scrollTop.value = Math.max(0, target[0].top - wraper.top - scrollView.height / 2)
})
}
}
}
})
.catch((error) => {
console.log(error)
})
})
})
}
}
function noop() {}
function getSelectedItem(value) {
const { valueKey, labelKey, columns } = props
const selecteds = columns.filter((item) => {
return item[valueKey] === value
})
if (selecteds.length > 0) {
return selecteds[0]
}
return {
[valueKey]: value,
[labelKey]: ''
}
}
function valueFormat(value) {
return props.type === 'checkbox' ? Array.prototype.slice.call(value) : value
}
function handleChange({ value }) {
selectList.value = value
emit('change', { value })
}
function close() {
pickerShow.value = false
// 未确定选项时,数据还原复位
if (!isConfirm.value) {
selectList.value = valueFormat(lastSelectList.value)
}
emit('cancel')
}
function open() {
if (props.disabled || props.readonly) return
selectList.value = valueFormat(props.modelValue)
pickerShow.value = true
isConfirm.value = false
}
function onConfirm() {
if (props.loading) {
pickerShow.value = false
emit('confirm')
return
}
if (props.beforeConfirm) {
props.beforeConfirm(selectList.value, (isPass) => {
isPass && handleConfirm()
})
} else {
handleConfirm()
}
}
function handleConfirm() {
isConfirm.value = true
pickerShow.value = false
lastSelectList.value = valueFormat(selectList.value)
let selectedItems: Record<string, any> = {}
if (props.type === 'checkbox') {
selectedItems = lastSelectList.value.map((item) => {
return getSelectedItem(item)
})
} else {
selectedItems = getSelectedItem(lastSelectList.value)
}
emit('update:modelValue', lastSelectList.value)
emit('confirm', {
value: lastSelectList.value,
selectedItems
})
setShowValue(lastSelectList.value)
}
function setShowValue(value) {
let showValueTemp: string = ''
if (props.displayFormat) {
showValueTemp = props.displayFormat(value, props.columns)
} else {
const { type, labelKey } = props
if (type === 'checkbox') {
const selectedItems = value.map((item) => {
return getSelectedItem(item)
})
showValueTemp = selectedItems
.map((item) => {
return item[labelKey]
})
.join(', ')
} else if (type === 'radio') {
const selectedItem = getSelectedItem(value)
showValueTemp = selectedItem[labelKey]
} else {
showValueTemp = value
}
}
showValue.value = showValueTemp
}
function getFilterText(label, filterVal) {
const reg = new RegExp(`(${filterVal})`, 'g')
return label.split(reg).map((text) => {
return {
type: text === filterVal ? 'active' : 'normal',
label: text
}
})
}
function handleFilterChange({ value }) {
if (value === '') {
filterColumns.value = []
filterVal.value = value
nextTick(() => {
filterColumns.value = props.columns
})
} else {
filterVal.value = value
formatFilterColumns(props.columns, value)
}
}
function formatFilterColumns(columns, filterVal) {
const filterColumnsTemp = columns.filter((item) => {
return item[props.labelKey].indexOf(filterVal) > -1
})
const formatFilterColumns = filterColumnsTemp.map((item) => {
return {
...item,
[props.labelKey]: getFilterText(item[props.labelKey], filterVal)
}
})
filterColumns.value = []
nextTick(() => {
filterColumns.value = formatFilterColumns
})
}
defineExpose({
close,
open
})
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>