Files
as-partner-frontend/src/components/org-tree-select/index.vue
2026-07-26 12:54:46 +08:00

255 lines
6.5 KiB
Vue
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="org-tree-select-wrap" v-loading="loading">
<el-tree-select
:model-value="modelValue"
:data="treeSelectData"
:props="{ label: 'label', value: 'id', children: 'children' }"
node-key="id"
check-strictly
:placeholder="inputPlaceholder"
:clearable="clearable"
:filterable="filterable"
:disabled="disabled"
:render-after-expand="false"
:default-expand-all="defaultExpandAll"
:style="{ width: width }"
popper-class="org-tree-select-popper"
@update:model-value="onUpdateModelValue"
@focus="onFocus"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { getOrgAllTree as getOrgTree } from '@/api/org'
import { treeDataTranslate } from '@/utils'
import type { OrgInfo } from '@/api/org'
import { typeLabelMap } from './index.data'
import type { OrgTreeSelectProps } from './index.data'
const props = withDefaults(defineProps<OrgTreeSelectProps>(), {
modelValue: null,
treeData: null,
autoLoad: true,
height: '320px',
width: '100%',
defaultExpandAll: true,
disabled: false,
placeholder: '请选择组织',
clearable: true,
filterable: true,
emptyText: '暂无数据',
hideNodeTypes: () => []
})
const emit = defineEmits<{
'update:modelValue': [value: string | number | null]
change: [payload: { node: OrgInfo | null; path: OrgInfo[]; orgId: string | number | null; channelOrgId: string | number | null; agentOrgId: string | number | null }]
loaded: [data: OrgInfo[]]
}>()
const rawTree = ref<OrgInfo[]>([])
const loading = ref(false)
function normType(type?: string): string {
return (type || '').toUpperCase().trim()
}
function typeLabel(type?: string): string {
const m: Record<string, string> = typeLabelMap
return m[normType(type)] || type || '-'
}
function filterHideTypes(nodes: OrgInfo[], hideSet: Set<string>): OrgInfo[] {
if (!nodes?.length) return []
const out: OrgInfo[] = []
for (const n of nodes) {
const t = normType(n.type)
if (hideSet.has(t)) {
out.push(...filterHideTypes(n.children || [], hideSet))
continue
}
const children = filterHideTypes(n.children || [], hideSet)
out.push({
...n,
children: children.length ? children : undefined
})
}
return out
}
const displayTree = computed(() => {
const hideSet = new Set((props.hideNodeTypes || []).map(normType))
return filterHideTypes(rawTree.value, hideSet)
})
function mapTreeSelectNodes(nodes: OrgInfo[]): any[] {
if (!nodes?.length) return []
return nodes.map((n) => {
const childList = n.children?.length ? mapTreeSelectNodes(n.children) : undefined
return {
...n,
label: `${n.name || '-'}(${typeLabel(n.type)})`,
children: childList
}
})
}
const treeSelectData = computed(() => mapTreeSelectNodes(displayTree.value))
const inputPlaceholder = computed(() => {
if (loading.value) return '加载中…'
if (treeSelectData.value.length === 0) return props.emptyText
return props.placeholder
})
function findPath(nodes: OrgInfo[], targetId: string | number, path: OrgInfo[] = []): OrgInfo[] | null {
if (!nodes?.length) return null
const sid = String(targetId)
for (const n of nodes) {
const next = [...path, n]
if (String(n.id) === sid) return next
const sub = findPath(n.children || [], targetId, next)
if (sub) return sub
}
return null
}
function deriveBindings(path: OrgInfo[]) {
if (!path?.length) {
return { orgId: null as string | number | null, channelOrgId: null as string | number | null, agentOrgId: null as string | number | null }
}
let channelOrgId: string | number | null = null
let agentOrgId: string | number | null = null
for (const n of path) {
const t = normType(n.type)
if (t === 'CHANNEL') channelOrgId = n.id
if (t === 'SERVICE_PROVIDER') agentOrgId = n.id
}
const last = path[path.length - 1]
const orgId = last.id
const lt = normType(last.type)
if (lt === 'CHANNEL') {
channelOrgId = last.id
agentOrgId = null
} else if (lt === 'SERVICE_PROVIDER') {
agentOrgId = last.id
}
return { orgId, channelOrgId, agentOrgId }
}
function onUpdateModelValue(val: string | number | null) {
emit('update:modelValue', val)
if (val == null || val === '') {
emit('change', {
node: null,
path: [],
orgId: null,
channelOrgId: null,
agentOrgId: null
})
return
}
const path = findPath(displayTree.value, val)
const pathArr = path ?? []
const node = pathArr.length ? pathArr[pathArr.length - 1] : null
if (node) {
emit('change', {
node,
path: pathArr,
...deriveBindings(pathArr)
})
} else {
emit('change', {
node: null,
path: [],
orgId: val,
channelOrgId: null,
agentOrgId: null
})
}
}
function parseTreeResponse(data: any): OrgInfo[] {
let list: any = []
if (Array.isArray(data)) {
list = data
} else if (data && typeof data === 'object') {
const d = data.data
if (Array.isArray(d)) list = d
else if (d && typeof d === 'object') list = d.tree || d.list || []
else list = data.tree || data.list || []
}
if (!Array.isArray(list)) list = []
if (list.length > 0 && list[0].children !== undefined) {
return list as OrgInfo[]
}
return treeDataTranslate(list, 'id', 'parentId') as OrgInfo[]
}
function loadTree(): Promise<void> {
if (props.treeData != null && Array.isArray(props.treeData)) {
rawTree.value = props.treeData
emit('loaded', rawTree.value)
return Promise.resolve()
}
if (!props.autoLoad) {
rawTree.value = []
return Promise.resolve()
}
loading.value = true
return getOrgTree()
.then(({ data }) => {
rawTree.value = Array.isArray(parseTreeResponse(data)) ? parseTreeResponse(data) : []
emit('loaded', rawTree.value)
})
.catch(() => {
rawTree.value = []
emit('loaded', [])
})
.finally(() => {
loading.value = false
})
}
function onFocus() {
if (props.autoLoad && props.treeData == null && rawTree.value.length === 0) {
loadTree()
}
}
watch(
() => props.treeData,
(v) => {
if (v != null && Array.isArray(v)) {
rawTree.value = v
}
},
{ deep: true }
)
function refresh(): Promise<void> {
rawTree.value = []
return loadTree()
}
defineExpose({
refresh,
loadTree,
getDisplayTree: () => rawTree.value
})
onMounted(() => {
if (props.autoLoad) {
loadTree()
}
})
</script>
<style scoped lang="scss">
.org-tree-select-wrap {
width: 100%;
}
</style>