<script lang="ts" setup>
import {
ref,
PropType,
toRefs,
nextTick,
watch,
onMounted,
computed,
inject,
Ref,
onBeforeMount,
shallowRef
} from 'vue'
import { useEmitt } from '@/hooks/web/useEmitt'
import { cloneDeep, debounce, sortBy } from 'lodash-es'
import { getFieldTree } from '@/api/dataset'
import colorFunctions from 'less/lib/less/functions/color.js'
import colorTree from 'less/lib/less/tree/color.js'
import { colorStringToHex } from '@/utils/color'
import { ElMessage } from 'element-plus-secondary'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
interface SelectConfig {
selectValue: any
required: false
defaultMapValue: any
defaultValue: any
queryConditionWidth: number
resultMode: number
checkedFieldsMap: object
displayType: string
id: string
placeholder: string
checkedFields: string[]
treeFieldList: Array<any>
dataset: {
id: string
}
field: {
id: string
}
defaultValueCheck: boolean
multiple: boolean
optionFilter: []
}
const customStyle: any = inject('$custom-style-filter')
const cascadeList = inject('cascade-list', Function, true)
const props = defineProps({
config: {
type: Object as PropType<SelectConfig>,
default: () => {
return {
selectValue: '',
defaultValue: '',
required: false,
queryConditionWidth: 0,
displayType: '',
resultMode: 0,
defaultValueCheck: false,
multiple: false,
checkedFieldsMap: {},
treeFieldList: [],
optionFilter: []
}
}
},
isConfig: {
type: Boolean,
default: false
}
})
const placeholder: Ref = inject('placeholder')
const placeholderText = computed(() => {
if (placeholder?.value?.placeholderShow) {
return ['', undefined].includes(props.config.placeholder) ? ' ' : props.config.placeholder
}
return ' '
})
const { config } = toRefs(props)
const fromTreeSelectConfirm = ref(false)
const multiple = ref(false)
const treeSelectConfirm = val => {
treeValue.value = val
handleValueChange()
}
const handleValueChange = () => {
fromTreeSelectConfirm.value = true
const value = Array.isArray(treeValue.value) ? [...treeValue.value] : treeValue.value
if (!props.isConfig) {
config.value.selectValue = Array.isArray(treeValue.value)
? [...treeValue.value]
: treeValue.value
nextTick(() => {
fromTreeSelectConfirm.value = false
isConfirmSearch(config.value.id)
})
return
}
config.value.defaultValue = value
fromTreeSelectConfirm.value = false
}
const changeFromId = ref(false)
watch(
() => config.value.id,
() => {
changeFromId.value = true
init()
nextTick(() => {
changeFromId.value = false
})
}
)
let oldId
watch(
() => config.value.treeFieldList,
val => {
let idStr = val.map(ele => ele.id).join('-')
if (changeFromId.value || idStr === oldId) return
oldId = idStr
treeValue.value = config.value.multiple ? [] : undefined
config.value.defaultValue = config.value.multiple ? [] : undefined
config.value.selectValue = config.value.multiple ? [] : undefined
showOrHide.value = false
getTreeOption()
}
)
const init = (fromMount = false) => {
loading.value = true
const { defaultValueCheck, multiple: plus, defaultValue } = config.value
if (defaultValueCheck) {
config.value.selectValue = Array.isArray(defaultValue)
? cloneDeep([...defaultValue])
: defaultValue
treeValue.value = Array.isArray(defaultValue) ? cloneDeep([...defaultValue]) : defaultValue
} else {
config.value.selectValue = plus ? [] : undefined
treeValue.value = plus ? [] : undefined
}
nextTick(() => {
oldId = config.value.treeFieldList?.map(ele => ele.id).join('-')
multiple.value = config.value.multiple
})
if (getCascadeFieldId().some(ele => ele.defaultValueFirstItem) && fromMount && !props.isConfig)
return
getTreeOption()
}
const tagWidth = computed(() => {
return Math.min(getCustomWidth() / 3, 50) + 'px'
})
const tagsWidth = computed(() => {
return getCustomWidth() - 40 + 'px'
})
const tagTextWidth = computed(() => {
return Math.min(getCustomWidth() / 3, 50) - 25 + 'px'
})
const showOrHide = ref(true)
const queryConditionWidth = inject('com-width', Function, true)
const isConfirmSearch = inject('is-confirm-search', Function, true)
const isConfirmSearchNoRequiredName = inject('query-data-for-id-tree', Function, true)
watch(
() => config.value.id,
() => {
getTreeOption()
}
)
onMounted(() => {
setTimeout(() => {
fromSelect = true
init(true)
}, 0)
})
watch(
() => config.value.defaultValue,
val => {
if (props.isConfig) return
if (config.value.multiple) {
treeValue.value = Array.isArray(val) ? [...val] : val
}
nextTick(() => {
multiple.value = config.value.multiple
})
}
)
watch(
() => config.value.selectValue,
val => {
if (props.isConfig || fromTreeSelectConfirm.value) return
if (config.value.multiple) {
treeValue.value = Array.isArray(val) ? [...val] : val
}
nextTick(() => {
multiple.value = config.value.multiple
if (!config.value.multiple) {
treeValue.value = Array.isArray(config.value.selectValue)
? [...config.value.selectValue]
: config.value.selectValue
}
})
}
)
const showWholePath = ref(false)
watch(
() => config.value.multiple,
val => {
if (!props.isConfig || changeFromId.value) return
showWholePath.value = false
if (val) {
treeValue.value = []
}
nextTick(() => {
multiple.value = val
if (!val) {
nextTick(() => {
treeValue.value = undefined
})
}
nextTick(() => {
showWholePath.value = true
})
})
}
)
let cacheId = ''
let treeOptionList = shallowRef([])
const filterMethod = (value, data) =>
(data.label ?? '').toLowerCase().includes((value ?? '').toLowerCase())
const dfs = arr => {
const mapped = (arr || []).map(ele => {
const label = ele.text
const children = ele.children?.length ? dfs(ele.children) : []
return { ...ele, value: ele.id, label, children }
})
return sortBy(mapped, node => (node.label ?? '').toLowerCase())
}
const cascade = computed(() => {
return cascadeList() || []
})
const loading = ref(false)
const getCascadeFieldId = () => {
const filter = []
cascade.value.forEach(ele => {
let condition = null
ele.forEach(item => {
const [_, queryId, fieldId] = item.datasetId.split('--')
const defaultValueFirstItem = item.defaultValueFirstItem
if (queryId === config.value.id && condition) {
if (item.fieldId) {
condition.fieldId = item.fieldId
}
filter.push(condition)
} else {
if (props.isConfig) {
if (!!item.selectValue?.length) {
condition = {
fieldId,
defaultValueFirstItem,
operator: 'in',
value: [...item.selectValue]
}
}
} else {
if (!!item.currentSelectValue?.length) {
condition = {
fieldId,
defaultValueFirstItem,
operator: 'in',
value: [...item.currentSelectValue]
}
}
}
}
})
})
return filter
}
let fromSelect = false
const getOptionFromCascade = () => {
fromSelect = true
getTreeOption()
}
onBeforeMount(() => {
useEmitt({
name: `${config.value.id}-select`,
callback: getOptionFromCascade
})
})
const dfsAuth = (tree, val) => {
return tree.some(ele => {
if (ele.value === val) {
return true
}
if (ele.children?.length) {
return dfsAuth(ele.children, val)
}
return false
})
}
function containsNodeById(source, params) {
// 统一处理参数为数组
const searchIds = Array.isArray(params) ? params : [params]
// 递归搜索函数
function searchById(node) {
// 检查当前节点的id是否在搜索列表中
if (searchIds.includes(node.id)) {
return true
}
// 递归搜索子节点
if (node.children && node.children.length > 0) {
for (const child of node.children) {
if (searchById(child)) {
return true
}
}
}
return false
}
// 遍历所有根节点
for (const node of source) {
if (searchById(node)) {
return true
}
}
return false
}
const getTreeOption = debounce(() => {
loading.value = true
getFieldTree({
fieldIds: props.config.treeFieldList.map(ele => ele.id),
resultMode: config.value.resultMode || 0,
filter: getCascadeFieldId()
})
.then(res => {
treeOptionList.value = filterTree(dfs(res), config.value.optionFilter)
if (config.value?.required && config.value?.optionFilter?.length > 0) {
const isValid = containsNodeById(treeOptionList.value, config.value.selectValue)
if (!isValid) {
config.value.selectValue = null
ElMessage({
message: `【${config.value?.name}】${t('v_query.before_querying')}`,
type: 'error',
duration: 3000
})
}
}
if (fromSelect) {
fromTreeSelectConfirm.value = true
if (multiple.value && Array.isArray(treeValue.value) && treeValue.value.length) {
treeValue.value = treeValue.value.filter(ele => dfsAuth(treeOptionList.value, ele))
} else if (treeValue.value && !dfsAuth(treeOptionList.value, treeValue.value)) {
treeValue.value = undefined
} else {
fromSelect = false
fromTreeSelectConfirm.value = false
}
if (fromSelect) {
config.value.selectValue = Array.isArray(treeValue.value)
? [...treeValue.value]
: treeValue.value
config.value.defaultValue = config.value.selectValue
if (props.isConfig) return
nextTick(() => {
fromTreeSelectConfirm.value = false
isConfirmSearchNoRequiredName(config.value.id)
})
}
}
})
.finally(() => {
loading.value = false
showWholePath.value = true
fromSelect = false
})
}, 300)
watch(
() => props.config.treeFieldList,
val => {
if (!props.isConfig) return
const ids = val.map(ele => ele.id).join('')
if (cacheId !== val.map(ele => ele.id).join('')) {
cacheId = ids
getTreeOption()
}
}
)
const fakeValue = ref('')
const treeValue = ref()
const getCustomWidth = () => {
if (placeholder?.value?.placeholderShow) {
if (props.config.queryConditionWidth !== undefined) {
return props.config.queryConditionWidth
}
return queryConditionWidth()
}
return 227
}
const selectStyle = computed(() => {
return props.isConfig ? {} : { width: getCustomWidth() + 'px' }
})
const tagColor = computed(() => {
if (
!customStyle ||
['#FFFFFF', 'rgba(255, 255, 255, 1)', 'rgb(255, 255, 255)'].includes(customStyle.background)
)
return ''
if (customStyle.background === '#131C42') return 'rgb(38, 53, 82)'
const hexColor = customStyle.background.startsWith('#')
? customStyle.background
: colorStringToHex(customStyle.background)
return colorFunctions
.mix(new colorTree('ffffff'), new colorTree(hexColor.substr(1)), { value: 20 })
.toRGB()
})
function filterTree(treeData, filterIds) {
if (!filterIds || filterIds.length === 0) {
return treeData
}
const filterSet = new Set(filterIds)
// 用于存储最终保留的所有节点ID
const keepIds = new Set()
// 用于查找节点的Map
const nodeMap = new Map()
// 用于构建节点关系的Map(子节点到父节点)
const parentMap = new Map()
// 遍历所有节点,构建Map和父子关系
function traverse(nodes, parentId = null) {
for (const node of nodes) {
nodeMap.set(node.id, node)
if (parentId) {
parentMap.set(node.id, parentId)
}
// 递归处理子节点
if (node.children && node.children.length > 0) {
traverse(node.children, node.id)
}
}
}
// 收集所有匹配的节点及其祖先和后代
function collectRelatedNodes(nodeId) {
if (keepIds.has(nodeId)) return
keepIds.add(nodeId)
const node = nodeMap.get(nodeId)
// 1. 收集所有祖先节点
let currentId = nodeId
while (parentMap.has(currentId)) {
const parentId = parentMap.get(currentId)
keepIds.add(parentId)
currentId = parentId
}
// 2. 收集所有后代节点(递归)
function collectDescendants(node) {
if (node.children && node.children.length > 0) {
for (const child of node.children) {
keepIds.add(child.id)
collectDescendants(child)
}
}
}
collectDescendants(node)
}
// 第二步:递归构建过滤后的树
function buildFilteredTree(nodes) {
const result = []
for (const node of nodes) {
// 如果节点ID在保留集合中,则保留该节点
if (keepIds.has(node.id)) {
const newNode = { ...node }
// 递归处理子节点
if (newNode.children && newNode.children.length > 0) {
newNode.children = buildFilteredTree(newNode.children)
}
result.push(newNode)
}
}
return result
}
// 执行遍历和构建
traverse(treeData)
for (const filterId of filterIds) {
if (nodeMap.has(filterId)) {
collectRelatedNodes(filterId)
}
}
return buildFilteredTree(treeData)
}
</script>
<template>
<el-tree-select
v-model="treeValue"
:data="treeOptionList"
clearable
v-if="multiple && !loading"
@treeSelectConfirm="treeSelectConfirm"
:render-after-expand="false"
show-checkbox
showBtn
@change="handleValueChange"
:placeholder="placeholderText"
collapse-tags
:filter-node-method="filterMethod"
:showWholePath="showWholePath"
collapse-tags-tooltip
:tagColor="tagColor"
:key="'multipleTree' + getCustomWidth()"
filterable
:style="selectStyle"
multiple
/>
<el-tree-select
v-model="treeValue"
@change="handleValueChange"
:data="treeOptionList"
check-strictly
clearable
:filter-node-method="filterMethod"
:placeholder="placeholderText"
:render-after-expand="false"
v-else-if="!multiple && !loading"
:key="'singleTree' + getCustomWidth()"
:showWholePath="showWholePath"
:style="selectStyle"
filterable
/>
<el-tree-select
v-model="fakeValue"
v-loading="loading"
:data="[]"
:placeholder="placeholderText"
:render-after-expand="false"
v-else
key="fakeTree"
:style="selectStyle"
/>
</template>
<style lang="less" scoped>
:deep(.ed-select-tags-wrapper) {
display: inline-flex !important;
}
:deep(.ed-select__tags) {
max-width: v-bind(tagsWidth) !important;
.ed-tag {
max-width: v-bind(tagWidth);
}
.ed-select__tags-text {
max-width: v-bind(tagTextWidth) !important;
}
}
</style>