magiccube-vue3
Version:
vue3-js版组件库
397 lines (382 loc) • 15.3 kB
JavaScript
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 }