UNPKG

magiccube-vue3

Version:

vue3-js版组件库

416 lines (374 loc) 14.8 kB
import { ref, reactive, computed, getCurrentInstance, watchEffect, nextTick } 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' import CLEAR_ICON from '../../img/icon_clear.svg' import * as utils from '../../utils/common' const FuzzySearch = { name: 'McFuzzySearch', props: { value: [Array, Number, String], searchResponse: { type: Function, default: () => { } }, multi: Boolean, downMenuHeight: { type: Number, default: 240 }, fetchUrl: String, serverName: { type: String, default: 'bpo' }, placeholder: { type: String, default: '请输入' }, keyName: String, prefixIcon: String, disabled: Boolean, unchecked: Boolean, clear: Boolean, }, emits: ['change', 'update:value', 'fill-back-update-value'], setup(props, { emit }) { /* picker element */ const pickerEl = ref(null) /* dropdown inner content element */ const innerEl = ref(null) /* dropdown element */ const dropdownEl = ref(null) // 关键词搜索输入框 const keywordEl = ref(null) // 点击后记录事件触发元素 let targetElement = null const state = reactive({ keyword: '', list: [], slide: false, }) // 关键词输入input显示控制 const displayInputSearch = ref(false) // 输入框是否失焦 用于下拉回收时是否让关键词输入器消失 let isInputBlur /** 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] : '' }) /* 选取参数后 保存相应的数值的对象 包含{name, value} */ let _modelData = [] const setSlideDown = (event) => { const options = { event, pickerHeight: pickerEl.value.offsetHeight, // 带有checkbox的时候不设置固定宽度 pickerWidth: props.multi? '' : pickerEl.value.offsetWidth, dropdownHeight: innerEl.value.offsetHeight || props.downMenuHeight, dropdownWidth: pickerEl.value.offsetWidth } dropdownEl.value.visible(options) state.slide = true } const setSlideUp = (type) => { state.list = [] state.slide = false dropdownEl.value.invisible() if(type === 'mouse-leave-dropdown'){ /** * 鼠标从下拉菜单中移出时清空搜索词和取消输入状态 */ handleInputKeywordBlur() } else { /** * 下拉框收起时需要判断是否同时input也失焦 是否需要恢复item样式 * 下拉框展开或收起的判断不取决于input是否出发 而是是否有查询结果 * 所以需要用更多的参数关联下拉框与输入框的状态联动 */ if (isInputBlur) handleInputKeywordBlur() } } const handleRemove = (e) => { e.stopPropagation && e.stopPropagation() if(props.disabled) return false const list = model.value const res = list.slice(1, list.length) model.value = res setSlideUp() emit('change', { key: props.keyName, multi: props.multi, data: res }) } const callback = (list = []) => { const _list = utils.deepCopy(list) if (props.multi) _list.map(item => item.isChecked = isChecked(item.value)) state.list = _list if (_list.length) { setSlideDown(targetElement) } else { setSlideUp(targetElement) } } const handleSearch = () => { const str = state.keyword if (str && str.trim()) { props.searchResponse(str, props.fetchUrl, callback) } else { state.list = [] setSlideUp(targetElement) } } const isChecked = (value) => { return model.value.some(item => item.value === value) } const handleCheckbox = (item) => { const selected = model.value const idx = selected.findIndex(data => data.value === item.value) if (idx === -1) { selected.push(item) } else { selected.splice(idx, 1) } model.value = selected emit('change', { key: props.keyName, multi: props.multi, data: selected }) } const handleSingleClick = (item) => { model.value = [item] emit('change', { key: props.keyName, multi: props.multi, data: item }) setSlideUp(targetElement) nextTick(() => { state.list = [] }) } // ? props.value数据类型比较多变,需要判断数据类型进行相应的比对 const getFilterResult = () => { const res = [] _modelData.forEach(item => { props.value.forEach(v => { if(typeof v === 'object'){ if(v.value === item.value){ res.push(item) } } else { if(v === item.value){ res.push(item) } } }) }) return res } const model = computed({ get() { if (Array.isArray(props.value) && props.value?.length) { if(_modelData.length) { return getFilterResult() } else { /* 支持回填 */ _modelData = props.value emit('fill-back-update-value', { key: props.keyName, data: props.value.map(n => n.value) }) validator && validator('change', props.value) return props.value } } else if(_modelData.length){ return _modelData } _modelData = [] return [] }, set(value) { _modelData = value emit('update:value', value.map(n => n.value)) validator && validator('change', value) } }) /* 外部清除参数后 控件同步进行清除 */ watchEffect(() => { if (!props.value || (Array.isArray(props.value) && !props.value?.length)) model.value = [] }) // 输入框聚焦 const handleInputKeyword = e => { if (props.disabled) return false e && e.stopPropagation() targetElement = e displayInputSearch.value = true if(model.value?.length) callback(model.value) nextTick(() => { isInputBlur = false keywordEl.value && keywordEl.value.focus() }) } // 输入框失焦 const handleInputKeywordBlur = () => { isInputBlur = true if (!state.slide) { displayInputSearch.value = false targetElement = null state.keyword = '' } } const handleClear = (evt) => { evt.stopPropagation() model.value = [] emit('change', { key: props.keyName, multi: props.multi, data: {} }) } /* 选取结果展示区 */ const resultNode = () => { const selectedCount = computed(() => { return _modelData.length }) const emptyNode = () => <span class="mc-fuzzy-search__result--placeholder">{props.placeholder}</span> const displayName = computed(() => _modelData?.length ? _modelData[0].name : '') const keywordPlaceholder = computed(() => { if(props.multi){ return '请输入关键词' } else { return displayName.value || '请输入关键词' } }) const multiReviewNode = () => { return ( <> <div class="mc-fuzzy-search__result--wrap"> <span class="mc-fuzzy-search__result--name">{displayName.value}</span> <span class="mc-fuzzy-search__result--close"> <img onClick={handleRemove} src={CLOSE_ICON} /> </span> </div> { selectedCount.value > 1 ? ( <div class="mc-fuzzy-search__result--amount">+{selectedCount.value - 1}</div> ) : '' } </> ) } const singleReviewNode = () => ( <> <span class="mc-fuzzy-search__result--single" v-ellipsis={displayName.value}> {displayName.value} </span> { props.clear && displayName.value ? ( <i class="mc-input__clear" onClick={handleClear}> <img src={CLEAR_ICON} /> </i> ) : '' } </> ) // 关键词搜索输入框dom 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} onBlur={handleInputKeywordBlur} onInput={handleSearch} style="width: 100%" /> </div> ) const getReview = () => { const _model = model.value if(props.multi){ if(_model.length){ return displayInputSearch.value ? inputKeywordNode() : multiReviewNode() } else { return displayInputSearch.value ? inputKeywordNode() : emptyNode() } } else { if(Array.isArray(_model)){ return displayInputSearch.value ? inputKeywordNode() : (_model.length? singleReviewNode(): emptyNode()) } else if(_model !== '') { return displayInputSearch.value ? inputKeywordNode() : singleReviewNode() } else { return displayInputSearch.value ? inputKeywordNode() : emptyNode() } } } /** * outerInput */ return ( <div class={{ 'mc-fuzzy-search__result': true, error: Boolean(fieldError.value) }} onClick={handleInputKeyword}> { getReview() } </div> ) } /** * 下拉控件中的内容 */ const dropdownContentNode = () => { const listNode = () => { if (state.list.length) { if (props.multi) { return state.list.map((item, idx) => ( <li key={idx} class={{ disabled: item.disabled }}> <McCheckbox v-model={item.isChecked} disabled={item.disabled} onChange={() => handleCheckbox(item)}> <i style={{ fontSize: itemFontSize }}>{item.name}</i> </McCheckbox> </li> )) } else { return state.list.map((item, idx) => ( <li key={idx} class={{ disabled: item.disabled }} onClick={() => handleSingleClick(item)}> <span style={{ fontSize: itemFontSize }}>{item.name}</span> </li> )) } } else { return '' } } return ( <div ref={innerEl} class="mc-fuzzy-search-dropdown"> <ul class="mc-fuzzy-search-dropdown__list" style={{ 'max-height': props.downMenuHeight + 'px', }}> {listNode()} </ul> </div> ) } return () => ( <div ref={pickerEl} class={{ 'mc-fuzzy-search': true, disabled: props.disabled }}> { resultNode() } <Dropdown ref={dropdownEl} picker={pickerEl.value} onClose={setSlideUp}> { dropdownContentNode() } </Dropdown> </div> ) } } FuzzySearch.install = Vue => { Vue.component(FuzzySearch.name, FuzzySearch) } const McFuzzySearch = FuzzySearch export { McFuzzySearch, McFuzzySearch as default }