UNPKG

magiccube-vue3

Version:

vue3-js版组件库

397 lines (382 loc) 15.3 kB
import { ref, computed, getCurrentInstance, watch, 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 axios from 'axios' const FuzzySearch = { name: 'McFuzzySearch', props: { value: [Array, Number, String], // 请求地址 fetchUrl: String, // 远程请求函数 searchResponse: Function, // 请求时的额外参数 requestParams: Object, multi: Boolean, downMenuHeight: { type: Number, default: 240 }, placeholder: { type: String, default: '请输入' }, // fuzzySearch的key keyName: String, prefixIcon: String, disabled: Boolean, unchecked: Boolean, // 外部设置回显值 externalData: Array, // 搜索关键词传值时所用的key名 keywordKeyName: { type: String, default: 'searchText' }, // option中的value键名 optionsValueKeyName: { type: String, default: 'value' }, // option中的name键名 optiosnNameKeyName: { type: String, default: 'name' }, }, 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 keyword = ref('') const dropdownOptions = ref([]) const dropdownSlideState = ref(false) // 关键词输入input显示控制 const displayInputSearch = ref(false) // 输入框是否失焦 用于下拉回收时是否让关键词输入器消失 let isInputBlur /* 选取参数后 保存相应的数值的对象 包含[{name, value}] */ const selectedOption = ref([]) /** 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] : '' }) /* dropdown下来模块 */ const setSlideDown = (event) => { if(props.disabled) return false 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) dropdownSlideState.value = true } const setSlideUp = (type) => { dropdownOptions.value = [] dropdownSlideState.value = false dropdownEl.value.invisible() if(type === 'mouse-leave-dropdown'){ /** * 鼠标从下拉菜单中移出时清空搜索词和取消输入状态 */ handleInputKeywordBlur() } else if (isInputBlur) { /** * 下拉框收起时需要判断是否同时input也失焦 是否需要恢复item样式 * 下拉框展开或收起的判断不取决于input是否出发 而是是否有查询结果 * 所以需要用更多的参数关联下拉框与输入框的状态联动 */ handleInputKeywordBlur() } } const setValue = (value = []) => { selectedOption.value = value if(!value.length){ emit('update:value', null) } else if(props.multi){ emit('update:value', value.map(n => n.value)) } else { emit('update:value', value?.[0].value) } validator && validator('change', value) } // 移除已选项 const handleRemove = (evt) => { evt?.stopPropagation?.() if(props.disabled) return false const list = [...selectedOption.value] const res = list.slice(1, list.length) setValue(res) setSlideUp() emit('change', { key: props.keyName, multi: props.multi, data: res }) } // 远程options请求后的回调,传入options数据 const callback = (list = []) => { const _list = new Set(list) if (props.multi) { // 设置已选状态 _list.forEach(item => item.isChecked = isChecked(item.value)) } dropdownOptions.value = [..._list] if (_list.length) { setSlideDown(targetElement) } else { setSlideUp(targetElement) } } const handleSearch = () => { const str = keyword.value if (str?.trim()) { if(props.searchResponse){ // 自定义远程请求options props.searchResponse(str, props.fetchUrl, callback) } else { // 内置默认远程请求options fetchOriginOptions(str, props.fetchUrl, callback) } } else { dropdownOptions.value = [] setSlideUp(targetElement) } } const fetchOriginOptions = (str) => { if(!props.fetchUrl) return window.console.error('[magiccube-vue3]-未找到请求链接') axios(props.fetchUrl, { ...(requestParams || {}), [props.keywordKeyName]: str, }).then((res) => { if(res.code === 200){ /** * 转化数据中的相应key为option中的name、key */ const list = res.data.map((item) => ({ ...item, name: item[props.optiosnNameKeyName], value: item[props.optionsValueKeyName], })) callback(list) } }) } // 判断多选是否选中 const isChecked = (value) => { return selectedOption.value.some(item => item.value === value) } // 多选中的checkbox const handleCheckbox = (item) => { const selected = [...selectedOption.value] const idx = selected.findIndex(data => data.value === item.value) if (idx === -1) { selected.push(item) } else { selected.splice(idx, 1) } setValue(selected) emit('change', { key: props.keyName, multi: props.multi, data: selected }) } // 单选所用函数 const handleSingleClick = (item) => { setValue([item]) emit('change', { key: props.keyName, multi: props.multi, data: item }) setSlideUp(targetElement) } /* 外部清除参数后 控件同步进行清除 */ watch(() => props.externalData, (newV, oldV) => { if(oldV === newV) return if(newV){ // 外部设置数据 setValue(newV) } else { // 外部清空已选数据 setValue() } }, { immediate: true, deep: true, }) // 监听value值变化 watch(() => props.value, (newV, oldV) => { if(oldV === newV) return // 单设置外部value数据而没有设置对象数据时,要清空 if(newV && !selectedOption.value.length) { setValue() return } // 清空value数据同时清空所有相应的数据 if(!newV) { // 外部清空已选数据 selectedOption.value = [] } }, { immediate: true, }) // 输入框聚焦 const handleInputKeyword = (evt) => { if (props.disabled) return false evt?.stopPropagation?.() targetElement = evt displayInputSearch.value = true if(selectedOption.value?.length) callback(selectedOption.value) nextTick(() => { isInputBlur = false keywordEl.value && keywordEl.value.focus() }) } // 输入框失焦 const handleInputKeywordBlur = () => { isInputBlur = true if (!dropdownSlideState.value) { displayInputSearch.value = false targetElement = null keyword.value = '' } } /* 选取结果展示区 */ const resultNode = () => { // 多选时显示已选数量 const selectedCount = computed(() => selectedOption.value.length) // 回显中文 const displayName = computed(() => selectedOption.value.length ? selectedOption.value[0].name : '') // 单选时的默认文案在有值时需要回显已选值的中文文案 const keywordPlaceholder = computed(() => props.multi? '请输入关键词' : 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> // 关键词搜索输入框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={keyword.value} onBlur={handleInputKeywordBlur} onInput={handleSearch} /> </div> ) // 空数据节点 const emptyNode = () => <span class="mc-fuzzy-search__result--placeholder">{props.placeholder}</span> // 显示状态判断 const getReview = () => { if(props.multi){ // 多选 if(selectedOption.value.length){ return displayInputSearch.value ? inputKeywordNode() : multiReviewNode() } else { return displayInputSearch.value ? inputKeywordNode() : emptyNode() } } else { // 单选 if(selectedOption.value.length){ 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 (dropdownOptions.value.length) { if (props.multi) { return dropdownOptions.value.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 dropdownOptions.value.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 }