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
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, 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>
|