UNPKG

magiccube-vue3

Version:

vue3-js版组件库

409 lines (364 loc) 15 kB
import { ref, reactive, computed, getCurrentInstance, provide, watchEffect, nextTick, watch } from 'vue' import getFormValidMethod from '../../utils/form-valid' import McCheckbox from '../checkbox' import Dropdown from '../../utils/dropdown' import CLOSE_ICON from '../../img/icon_remove.svg' const Select = { name: 'McSelect', props: { modelValue: [Array, String, Number, null], multi: Boolean, search: { type: Boolean, default: true }, options: { type: Array, default: () => [] }, downMenuHeight: { type: Number, default: 240 }, placeholder: { type: String, default: '请选择' }, keyName: String, prefixIcon: String, disabled: Boolean, unchecked: Boolean, errorStatus: Boolean, }, emits: ['change', 'update:modelValue', 'visible-state'], setup(props, { emit, slots }) { /* picker element */ const pickerEl = ref(null) /* dropdown inner content element */ const innerEl = ref(null) /* dropdown element */ const dropdownEl = ref(null) // 模糊搜索输入框元素 const keywordEl = ref(null) const state = reactive({ keyword: '', slide: false, selectAll: false }) // 滚动懒加载 const loadEndIndex = ref(20) // 关键词输入input显示控制 const displayInputSearch = ref(false) const cacheEvent = ref(null) /** form校验所用参数 */ const instance = getCurrentInstance() const globalOptions = instance.appContext?.config?.globalProperties?.$ELEMENT const itemFontSize = globalOptions.dropdownFontSize? globalOptions.dropdownFontSize + 'px' : '' const { fieldName, validator, errorMessage } = getFormValidMethod(instance) const fieldError = computed(() => { return fieldName && errorMessage?.value && !props.unchecked ? errorMessage.value[fieldName] : '' }) const setSlideDown = (event) => { const dropdownWidth = innerEl.value.offsetWidth > pickerEl.value.offsetWidth ? innerEl.value.offsetWidth : pickerEl.value.offsetWidth const options = { event, pickerHeight: pickerEl.value.offsetHeight, pickerWidth: pickerEl.value.offsetWidth, dropdownHeight: innerEl.value.offsetHeight || props.downMenuHeight, dropdownWidth } dropdownEl.value.visible(options) displayInputSearch.value = props.search cacheEvent.value = event emit('visible-state', true) // 让模糊搜索框自动聚焦 nextTick(() => { keywordEl.value && keywordEl.value.focus() }) } const setSlideUp = () => { state.keyword = '' dropdownEl.value.invisible() displayInputSearch.value = false emit('visible-state', false) } const handleShowDropdown = (e) => { if (props.disabled) return false if (state.slide) { /** * 隐藏下拉 */ setSlideUp(e) return false } /** * 打开下拉 */ setSlideDown(e) } const getChecked = (item) => { return props.multi ? model.value.includes(item.value) : model.value === item } const optionsData = ref([]) watchEffect(() => optionsData.value = props.options) /* 监听模糊查找后下拉内容变更 下拉框高度改变后 需要重新计算定位 */ watch(() => state.keyword, (n, o) => n && n !== o && nextTick(() => setSlideDown(cacheEvent.value))) /* 监听外部改变参数 清空校验器报错 */ watch(() => props.modelValue, (n, o) => n && n !== o && validator && validator('change', n)) /** * _options 是下拉框内要展示的条目,每次搜索或是向下滑动时都会触发。 * 所以在每次搜索或向下滑动时,先截取出要展示的条目,再将这些条目变化选中状态即可。 * 当二次搜索或下滑时,又会重新截取,并再次将截取的变为选中状态。 * 极端情况;2万条数据滑到底,则需遍历2万次;如果在多选的情况下没有全选,getChecked也会触发2万次 */ const _options = computed(() => { const w = state.keyword.trim() const _ary = w ? optionsData.value.filter(n => n.name.indexOf(w) > -1) : optionsData.value.slice(0, loadEndIndex.value) return _ary.map(n => ({...n, isChecked: state.selectAll || getChecked(n)})) }) const resultDisplayName = computed(() => { const _val = props.multi ? model.value[0] : model.value const item = optionsData.value.find(n => n.value === _val) return item?.name || '' }) const handleCheckbox = (item) => { const selected = model.value const idx = selected.indexOf(item.value) if (idx === -1) { selected.push(item.value) } else { selected.splice(idx, 1) } model.value = selected state.selectAll = optionsData.value.every(n => n.isChecked) } const handleSelect = (item) => { if (props.multi || item.disabled) return false model.value = item.value setSlideUp() } const handleRemove = (e) => { e.stopPropagation && e.stopPropagation() if (props.multi) { const list = model.value model.value = list.slice(1, list.length) } else { model.value = '' } } const handleSelectAll = () => { if (state.selectAll) { model.value = optionsData.value.map(n => n.value) } else { model.value = [] } } const handlePushOptions = () => { if (loadEndIndex.value === optionsData.value.length) { return false } const _count = loadEndIndex.value + 20 loadEndIndex.value = _count > optionsData.value.length ? optionsData.value.length : _count } const _debounce = (function() { let timer = null let first = true return function(fn, delay) { if (first) { fn.apply(null, arguments) first = false } if (timer) { return false } timer = setTimeout(() => { fn.apply(null, arguments) clearTimeout(timer) timer = null }, delay) } })() // 懒加载 const handleEventListenScroll = (event) => { const { scrollHeight, scrollTop, clientHeight } = event.target if (Math.ceil(scrollTop + clientHeight) >= scrollHeight) { _debounce(handlePushOptions, 300) } } const model = computed({ get() { if (props.multi) { if (!props.modelValue?.length) state.selectAll = false return props.modelValue || [] } else { return props.modelValue || '' } }, set(value) { emit('update:modelValue', value) emit('change', { key: props.keyName, data: value }) validator && validator('change', value) } }) const keywordPlaceholder = computed(() => { if (props.multi) { return '请输入关键词' } else { return resultDisplayName.value || '请输入关键词' } }) /* 选取结果展示区 */ const resultNode = () => { const selectedCount = computed(() => { return props.multi ? model.value.length : 0 }) const emptyNode = () => <span class="mc-select__result--placeholder">{props.placeholder}</span> const singleReviewNode = () => <span class="mc-select__result--single" v-ellipsis={resultDisplayName.value}>{resultDisplayName.value}</span> const multiReviewNode = () => ( <> <div class="mc-select__result--wrap" v-show={resultDisplayName.value}> <span class="mc-select__result--name">{resultDisplayName.value}</span> <span class="mc-select__result--close"> <img onClick={handleRemove} src={CLOSE_ICON} /> </span> </div> { selectedCount.value > 1 ? ( <div class="mc-select__result--amount">+{selectedCount.value - 1}</div> ) : '' } </> ) const getReview = () => { if (props.multi) { if (model.value.length) { return displayInputSearch.value ? inputKeywordNode() : multiReviewNode() } else { return displayInputSearch.value ? inputKeywordNode() : emptyNode() } } else { if (Array.isArray(model.value)) { return model.value.length ? singleReviewNode() : displayInputSearch.value ? '' : emptyNode() } else if (model.value !== '') { return displayInputSearch.value ? inputKeywordNode() : singleReviewNode() } else { return displayInputSearch.value ? inputKeywordNode() : emptyNode() } } } // 关键词搜索输入框 const inputKeywordNode = () => ( <div class="mc-select__result--keyword"> <input ref={keywordEl} class="mc-select__result--keyword__input" autocomplete="off" placeholder={keywordPlaceholder.value} v-model={state.keyword} /> </div> ) return ( <div class={{ 'mc-select__result': true, error: Boolean(fieldError.value) || props.errorStatus }} onClick={handleShowDropdown}> { getReview() } <span class={[ 'mc-select__result--arrow', { 'mc-rotate': displayInputSearch.value } ]}> <img src={require('../../img/icon_picker_arrow.svg')} /> </span> </div> ) } /** * 下拉空间中的内容 */ const dropdownContentNode = () => { const listNode = () => ( _options.value.length ? ( _options.value.map((item, idx) => ( <li key={idx} class={{ disabled: item.disabled }} onClick={() => handleSelect(item)}> { props.multi ? ( <McCheckbox v-model={item.isChecked} disabled={item.disabled} onChange={() => handleCheckbox(item)}> <i style={{ fontSize: itemFontSize }}>{item.name}</i> </McCheckbox> ) : ( <span class={{ active: item.value === model.value }} style={{ fontSize: itemFontSize }}>{item.name}</span> ) } </li> )) ) : ( <div class="mc-select-dropdown__empty"> <span>暂无相应数据</span> </div> ) ) return ( <div ref={innerEl} class="mc-select-dropdown"> { props.multi ? ( <div class="mc-select-dropdown__toolbar"> <McCheckbox v-model={state.selectAll} onChange={handleSelectAll}> <i style={{ fontSize: itemFontSize }}>全选</i> </McCheckbox> </div> ) : '' } <ul class="mc-select-dropdown__list" onScroll={handleEventListenScroll} style={{ 'max-height': props.downMenuHeight + 'px', }}> {slots.default ? slots.default() : listNode()} </ul> { slots.footer ? ( <div class="mc-select-dropdown__footer"> {slots.footer()} </div> ) : '' } </div> ) } provide('optionsData', optionsData) provide('handleSelect', handleSelect) provide('getChecked', getChecked) return () => ( <div ref={pickerEl} class={{ 'mc-select': true, disabled: props.disabled }}> { resultNode() } <Dropdown ref={dropdownEl} picker={pickerEl.value} onClose={setSlideUp}> { dropdownContentNode() } </Dropdown> </div> ) } } Select.install = (app) => { app.component(Select.name, Select) } const McSelect = Select export { McSelect, McSelect as default }