355 lines
8.7 KiB
Vue
355 lines
8.7 KiB
Vue
<template>
|
|
<el-popover
|
|
popper-class="tree-selector"
|
|
ref="popover"
|
|
placement="bottom-start"
|
|
trigger="click"
|
|
width="auto"
|
|
:disabled="disabled"
|
|
@show="showTree = true"
|
|
@hide="showTree = false">
|
|
<div class="popover-panel" v-if="showTree" :style="popoverPanelStyle">
|
|
<el-input type="text" name="query" v-model="query" v-if="!lazyTree" :placeholder="queryPlaceHolder"
|
|
class="query-input">
|
|
<template #prefix>
|
|
<FontAwesomeIcon :icon="['fas','search']"></FontAwesomeIcon>
|
|
</template>
|
|
</el-input>
|
|
<el-tree
|
|
class="tree-panel"
|
|
:filter-node-method="filterMethod"
|
|
:props="treeProps"
|
|
:lazy="lazyTree"
|
|
:load="loadNode"
|
|
:node-key="nodeKey"
|
|
:show-checkbox="multiple"
|
|
:data="data"
|
|
:expand-on-click-node="false"
|
|
:default-expanded-keys="defaultExpandedKeys"
|
|
:default-checked-keys="modelValue instanceof Array ? modelValue.map(v => v[nodeKey]) : [modelValue ? modelValue[nodeKey]: null]"
|
|
@node-click="nodeClickHandler"
|
|
@check="nodeCheckHandler"
|
|
@current-change="currentNodeChange"
|
|
@node-expand="nodeExpandHandler"
|
|
ref="treeRef"></el-tree>
|
|
</div>
|
|
<template #reference>
|
|
<div class="el-select">
|
|
<div
|
|
class="el-select__tags"
|
|
v-if="multiple"
|
|
ref="tags">
|
|
<span v-if="modelValue && modelValue.length">
|
|
<el-tag
|
|
:closable="false"
|
|
:hit="modelValue[0].hitState"
|
|
type="info"
|
|
size="small"
|
|
disable-transitions>
|
|
<span class="el-select__tags-text">{{ getNodeLabel(modelValue[0]) }}</span>
|
|
</el-tag>
|
|
<el-tag
|
|
v-if="modelValue.length > 1"
|
|
:closable="false"
|
|
size="small"
|
|
type="info"
|
|
disable-transitions>
|
|
<span class="el-select__tags-text">+ {{ modelValue.length - 1 }}</span>
|
|
</el-tag>
|
|
</span>
|
|
</div>
|
|
<el-input
|
|
ref="select"
|
|
:disabled="disabled"
|
|
:placeholder="placeholder"
|
|
:readonly="true"
|
|
:model-value="inputValue">
|
|
<template #suffix>
|
|
<font-awesome-icon v-if="showClose" :icon="['fas', 'times-circle']" class="el-select__caret" @click.stop="handleClearClick" />
|
|
<font-awesome-icon v-if="!showClose" :icon="['fas', 'chevron-down']" class="el-select__caret" :class="iconClass" />
|
|
</template>
|
|
</el-input>
|
|
</div>
|
|
</template>
|
|
</el-popover>
|
|
</template>
|
|
|
|
<script setup>
|
|
// 引入需要的组件和API
|
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|
import { ref, computed, watch, nextTick, onMounted, onUpdated, useAttrs } from 'vue'
|
|
|
|
// 定义props
|
|
const props = defineProps({
|
|
filterMethod: Function,
|
|
multiple: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
lazyTree: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
loadNode: {
|
|
type: Function,
|
|
default: () => {}
|
|
},
|
|
nodeKey: {
|
|
type: String,
|
|
default: (props) => {
|
|
return props.valueProp && typeof props.valueProp === 'string' ? props.valueProp : ''
|
|
}
|
|
},
|
|
treeProps: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
data: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
defaultExpandedKeys: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
placeholder: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
queryPlaceHolder: {
|
|
type: String,
|
|
default: '请输入筛选内容'
|
|
},
|
|
modelValue: {
|
|
type: [Array, String, Number, Object],
|
|
default: () => []
|
|
},
|
|
valueProp: {
|
|
type: [Object, String],
|
|
default: null
|
|
},
|
|
selectionFilter: {
|
|
type: Function,
|
|
default: (data, node, tree) => true
|
|
}
|
|
})
|
|
|
|
// 定义emit
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
// 获取attrs
|
|
const attrs = useAttrs()
|
|
|
|
// 定义响应式数据
|
|
const popoverPanelStyle = ref({})
|
|
const query = ref(null)
|
|
const showTree = ref(false)
|
|
const showClose = ref(false)
|
|
const popShow = ref(false)
|
|
|
|
// 定义refs
|
|
const popover = ref(null)
|
|
const treeRef = ref(null)
|
|
const select = ref(null)
|
|
const tags = ref(null)
|
|
|
|
// 计算属性
|
|
const iconClass = computed(() => {
|
|
return (showTree.value ? 'is-reverse' : '')
|
|
})
|
|
|
|
// 获取节点标签文本
|
|
const getNodeLabel = (node) => {
|
|
if (!node) return '';
|
|
|
|
// 如果 treeProps.label 是函数,则调用该函数获取标签文本
|
|
if (typeof props.treeProps.label === 'function') {
|
|
return props.treeProps.label(node);
|
|
}
|
|
|
|
// 如果 treeProps.label 是字符串,则直接获取对应属性
|
|
return node[props.treeProps.label] || '';
|
|
};
|
|
|
|
const inputValue = computed(() => {
|
|
let value
|
|
let nodeKey
|
|
if (!props.multiple) {
|
|
if (props.modelValue) {
|
|
if (props.modelValue instanceof Object) {
|
|
// 使用 getNodeLabel 获取标签文本
|
|
value = getNodeLabel(props.modelValue);
|
|
if (!value) {
|
|
nodeKey = props.modelValue[props.nodeKey]
|
|
}
|
|
} else {
|
|
nodeKey = props.modelValue
|
|
}
|
|
|
|
if (!value) {
|
|
const valueObj = findFirstTreeNodeData(props.data, data => data[props.nodeKey] === nodeKey)
|
|
value = valueObj ? getNodeLabel(valueObj) : null
|
|
}
|
|
}
|
|
}
|
|
toggleShowClose(value)
|
|
return value
|
|
})
|
|
|
|
// 监听query变化
|
|
watch(query, (val) => {
|
|
treeRef.value.filter(val)
|
|
})
|
|
|
|
// 方法
|
|
const findFirstTreeNodeData = (treeData, queryPredicate) => {
|
|
// 树形节点里筛选符合表达式的数据
|
|
const findData = treeData.find(queryPredicate)
|
|
if (!findData) {
|
|
return treeData.map(td => td[props.treeProps.children] || []).reduce((result, next) => !result ? findFirstTreeNodeData(next, queryPredicate) : result, null)
|
|
}
|
|
return findData
|
|
}
|
|
|
|
const nodeClickHandler = (data, node, tree) => {
|
|
if (!props.multiple) {
|
|
if (selection(data, node, tree)) {
|
|
closeSelectWindow()
|
|
}
|
|
} else {
|
|
// 多选模式下,点击节点时切换复选框状态
|
|
treeRef.value.setChecked(node, !node.checked, true)
|
|
// 获取当前所有选中的节点
|
|
const checkedNodes = treeRef.value.getCheckedNodes()
|
|
// 更新选中值
|
|
selection(checkedNodes)
|
|
}
|
|
}
|
|
|
|
const nodeExpandHandler = (data, node, self) => {
|
|
// 节点展开处理
|
|
}
|
|
|
|
const nodeCheckHandler = (data, { checkedKeys, checkedNodes, halfCheckedKeys, halfCheckedNodes }) => {
|
|
selection(checkedNodes)
|
|
}
|
|
|
|
const currentNodeChange = (data, node) => {
|
|
// 当前节点变化处理
|
|
console.log(data, node);
|
|
|
|
}
|
|
|
|
const selection = (data, node, tree) => {
|
|
const result = props.selectionFilter(data, node, tree)
|
|
if (result) {
|
|
updateModelValue(data)
|
|
}
|
|
return result
|
|
}
|
|
|
|
const updateModelValue = (val) => {
|
|
const value = val instanceof Array ? val.map(computedValue) : computedValue(val)
|
|
emit('update:modelValue', value)
|
|
toggleShowClose(value)
|
|
}
|
|
|
|
const computedValue = (data) => {
|
|
if (props.valueProp) {
|
|
if (props.valueProp instanceof Object) {
|
|
return Object.fromEntries(Object.entries(props.valueProp).map(([valueProperty, dataProperty]) => {
|
|
return [valueProperty, data[dataProperty]]
|
|
}))
|
|
} else {
|
|
return data[props.valueProp]
|
|
}
|
|
} else {
|
|
return data
|
|
}
|
|
}
|
|
|
|
const closeSelectWindow = () => {
|
|
popover.value.hide()
|
|
}
|
|
|
|
const handleClearClick = () => {
|
|
if (props.disabled) {
|
|
return false
|
|
}
|
|
showTree.value = false
|
|
// 在更新模型值之前清空树的选中状态
|
|
if (treeRef.value) {
|
|
treeRef.value.setCheckedKeys([])
|
|
}
|
|
if (props.multiple) {
|
|
updateModelValue([])
|
|
} else {
|
|
updateModelValue('')
|
|
}
|
|
}
|
|
|
|
const toggleShowClose = (val) => {
|
|
showClose.value = props.multiple ? val?.length > 0 : !!val
|
|
}
|
|
|
|
const expandDefaultKeys = () => {
|
|
if (!treeRef.value || !props.defaultExpandedKeys || !props.defaultExpandedKeys.length) return
|
|
props.defaultExpandedKeys.forEach((key) => {
|
|
const node = treeRef.value.getNode(key)
|
|
if (node && !node.expanded) {
|
|
if (typeof node.expand === 'function') {
|
|
node.expand()
|
|
} else {
|
|
node.expanded = true
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
watch(() => showTree.value, async (v) => {
|
|
if (v) {
|
|
await nextTick()
|
|
expandDefaultKeys()
|
|
}
|
|
})
|
|
|
|
watch(() => props.data, async () => {
|
|
if (showTree.value) {
|
|
await nextTick()
|
|
expandDefaultKeys()
|
|
}
|
|
})
|
|
|
|
// 生命周期钩子
|
|
onMounted(() => {
|
|
nextTick(() => {
|
|
popoverPanelStyle.value = {
|
|
'min-width': select.value.$el.clientWidth + 'px'
|
|
}
|
|
})
|
|
})
|
|
|
|
onUpdated(() => {
|
|
if (showTree.value) {
|
|
if (props.modelValue && props.multiple) {
|
|
treeRef.value.setCheckedKeys([...props.modelValue].map(n => n[props.nodeKey]))
|
|
}
|
|
}
|
|
})
|
|
</script>
|
|
<style lang="scss">
|
|
.tree-selector {
|
|
.tree-panel {
|
|
max-height: 300px;
|
|
overflow: auto;
|
|
}
|
|
.query-input {
|
|
margin-bottom: 4px;
|
|
}
|
|
}
|
|
</style>
|