<script setup lang="ts">
import { TreeNode } from "element-plus/es/components/tree-v2/src/types"
import { TreeNodeData } from "element-plus/es/components/tree/src/tree.type"
import { getTreeLabelValue, findParents } from "@/utils/tree"
import { isAllEmpty } from "@pureadmin/utils"
import { onMounted, nextTick, PropType, reactive, ref, watch } from "vue"
interface PropsIter {
value: string
label: string
children: string
disabled?: string
}
const TreeProps: PropsIter = {
value: "id",
label: "name",
children: "children"
}
interface TreeIter {
id: string
name: string
children?: TreeIter[]
}
const props = defineProps({
// 组件绑定的options
options: {
type: Array as PropType<TreeIter[]>,
required: true
},
// 配置选项
keyProps: {
type: Object as PropType<PropsIter>
},
// 双向绑定值
modelValue: {
type: [String, Number, Array<any>]
},
// 组件样式宽
width: {
type: String,
default: ""
},
// 空占位字符
placeholder: {
type: String,
default: "请选择"
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否多选
multiple: {
type: Boolean,
default: false
},
// 是否筛选
filterable: {
type: Boolean,
default: true
},
// 是否清空
clearable: {
type: Boolean,
default: true
},
checkStrictly: {
type: Boolean,
default: true
},
className: {
type: String,
default: ""
}
})
const emits = defineEmits(["update:modelValue", "change"])
const uniqueClass = "dm-tree-select-v2-" + new Date().getTime()
const select = reactive({
value: undefined,
currentNodeKey: "",
currentNodeLabel: ""
})
const treeSelect = ref<HTMLElement | null>(null)
const treeHeight = ref(26)
function getHeightFirst() {
let currHeight = props.options.length * 26 || 0
let height = currHeight > 240 ? 240 : currHeight < 26 ? 26 : currHeight
return height
}
const nodeClick = (data: TreeNodeData, _node: TreeNode) => {
if (!props.multiple) {
select.value = data[props.keyProps.value]
(treeSelect.value as any).blur()
emits("update:modelValue", select.value)
nextTick(() => {
emits("change", select.value)
})
}
}
const hancleCheck = (data: TreeNodeData, info) => {
select.value = info.checkedNodes.map(item => item[props.keyProps.value])
emits("update:modelValue", select.value)
nextTick(() => {
emits("change", select.value)
})
}
const translate = value => {
let needProps = props.keyProps || TreeProps
return getTreeLabelValue(
props.options,
{ label: needProps.label, value: needProps.value },
value,
needProps.children
)
}
// select 筛选方法 treeV2 refs
const treeV2: any = ref<HTMLElement | null>(null)
const selectFocus = () => {
const ids = findParents(props.options, props.modelValue).map(
item => item[props.keyProps.value]
)
treeV2.value.setExpandedKeys(ids)
}
const selectFilter = (query: string) => {
treeV2.value.filter(query)
initHeight()
setTimeout(() => {
// 默认不展开全部
if (isAllEmpty(query) && isAllEmpty(props.modelValue)) {
treeV2.value.setExpandedKeys([])
} else {
// 避免影响多选的情况
if (!Array.isArray(props.modelValue)) {
selectFocus()
}
}
})
}
// ztree-v2 筛选方法
const treeFilter = (query: string, node: TreeNode) => {
if (!isAllEmpty(query)) {
return node[props.keyProps.label]?.indexOf(query) !== -1
} else {
return true
}
}
// 直接清空选择数据
const clearSelected = (flag = false) => {
select.currentNodeKey = ""
select.currentNodeLabel = ""
if (!props.multiple) {
select.value = ""
treeV2.value.setCurrentKey(select.value)
} else {
select.value = []
treeV2.value.setCheckedKeys(select.value)
}
if (flag) {
emits("update:modelValue", undefined)
emits("change", undefined)
}
}
const removeTag = tagValue => {
select.value = select.value.filter(item => item !== tagValue)
emits("update:modelValue", select.value)
nextTick(() => {
emits("change", select.value)
})
}
// setCurrent通过select.value 设置下拉选择tree 显示绑定的v-model值
const setCurrent = () => {
if (!props.multiple) {
select.currentNodeKey = select.value
treeV2.value.setCurrentKey(select.value)
select.currentNodeLabel = translate(select.value)
} else {
select.currentNodeKey = select.value.join(",")
treeV2.value.setCheckedKeys(select.value)
select.currentNodeLabel = select.value
.map(item => translate(item))
.join(",")
}
}
// 动态加载操作列的高度
function initHeight() {
setTimeout(() => {
// 初始高度
let height = 0
let Columns = document.querySelector(
"." + uniqueClass + " .el-vl__window"
)?.children
if (Columns && Columns.length > 0) {
for (let i = 0
height += (Columns[i] as HTMLElement).offsetHeight
}
treeHeight.value =
height > 240 ? 240 : height < 26 ? getHeightFirst() : height
}
}, 20)
}
// 监听外部清空数据源 清空组件数据
watch(
() => props.modelValue,
v => {
if (isAllEmpty(v) && select.currentNodeKey !== "") {
clearSelected()
}
// 动态赋值
if (!isAllEmpty(v)) {
select.value = v
setCurrent()
}
}
)
watch(
() => props.options,
() => {
if (!isAllEmpty(props.modelValue)) {
select.value = props.modelValue
setCurrent()
}
}
)
onMounted(() => {
nextTick(() => {
if (!isAllEmpty(props.modelValue)) {
select.value = props.modelValue
setCurrent()
}
})
})
</script>
<template>
<div
class="dm-tree-select-v2"
:style="
props.width && {
width: props.width.includes('px') ? props.width : props.width + 'px'
}
"
>
<el-select
v-model="select.value"
:disabled="props.disabled"
:clearable="props.clearable"
:filterable="props.filterable"
:multiple="props.multiple"
ref="treeSelect"
popper-class="dm-tree-select-v2__poper"
:placeholder="props.placeholder"
:filter-method="selectFilter"
@clear="clearSelected(true)"
@remove-tag="removeTag"
@focus="selectFocus"
>
<template
<span>{{ select.currentNodeLabel }} </span>
</template>
<template
<span>{{ translate(value) }}</span>
</template>
<el-option
:value="select.currentNodeKey"
:label="select.currentNodeLabel"
>
<el-tree-v2
id="tree_v2"
ref="treeV2"
:data="props.options"
:props="props.keyProps || TreeProps"
:height="treeHeight"
:current-node-key="select.currentNodeKey"
:filter-method="treeFilter"
:show-checkbox="multiple"
:check-strictly="props.checkStrictly"
:class="[uniqueClass, props.className]"
@node-click="nodeClick"
@check="hancleCheck"
@node-expand="initHeight"
@node-collapse="initHeight"
>
<template
<slot name="default" :node="node" />
<slot>
<span>{{ node.label }}</span>
</slot>
</template>
</el-tree-v2>
</el-option>
</el-select>
</div>
</template>
<style lang="scss">
.dm-tree-select-v2__poper {
.el-tree-node {
position: relative !important
height: 0 !important
}
}
</style>
<style lang="scss" scoped>
.dm-tree-select-v2 {
width: 214px
}
.el-scrollbar .el-scrollbar__view .el-select-dropdown__item {
height: auto
max-height: 274px
padding: 0
overflow: hidden
overflow-y: auto
}
ul li :deep(.el-tree .el-tree-node__content) {
height: auto
padding: 0 20px
font-weight: normal !important
}
.el-tree-node__label {
font-weight: normal
}
.el-tree :deep(.is-current .el-tree-node__label) {
color:
font-weight: 700
}
.el-tree :deep(.is-current .el-tree-node__children .el-tree-node__label) {
color:
font-weight: normal
}
.el-select {
width: 100% !important
}
.el-select-dropdown.is-multiple .el-select-dropdown__item.is-selected:after {
background-color: transparent
}
</style>