255 lines
6.5 KiB
Vue
Executable File
255 lines
6.5 KiB
Vue
Executable File
<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>
|