zhzf/client/src/components/TreeSelector.vue

355 lines
8.7 KiB
Vue
Raw Normal View History

2025-02-21 11:25:09 +08:00
<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>