OrganizationTree.vue 9.63 KB
<template>
  <div class="space-y-1">
    <div 
      v-for="org in organizations" 
      :key="org.id"
      class="space-y-1"
    >
      <div
        :class="[
          'flex items-center gap-3 py-2 pl-2 pr-3 rounded-lg transition-all border cursor-pointer',
          !isSelectable(org)
            ? 'opacity-40 cursor-not-allowed border-transparent bg-transparent'
            : selectedId === org.id
              ? 'border-blue-500 bg-white shadow-[0_0_0_2px_rgba(59,130,246,0.12)]'
              : 'border-transparent hover:border-neutral-200 hover:bg-neutral-50'
        ]"
        @click="handleSelect(org)"
      > 
        <!-- 展开/收起按钮 -->
        <button
          v-if="hasChildren(org)"
          @click.stop="toggleExpand(org.id)"
          class="p-1 hover:bg-neutral-200 rounded-md transition-colors"
        >
          <ChevronDown 
            v-if="expandedIds.has(org.id)"
            class="h-4 w-4 text-neutral-600" 
          />
          <ChevronRight 
            v-else
            class="h-4 w-4 text-neutral-600" 
          />
        </button>
        <div v-else class="w-6" />
        
        <div
          :class="[
            'w-4 h-4 rounded-sm border flex items-center justify-center transition-colors',
            selectedId === org.id
              ? 'border-blue-500 bg-blue-500 ring-2 ring-blue-100'
              : isSelectable(org)
                ? 'border-neutral-300 bg-white'
                : 'border-dashed border-neutral-300 bg-neutral-100'
          ]"
        >
          <span
            v-if="selectedId === org.id"
            class="inline-block w-2 h-2 rounded-sm bg-white"
          />
        </div>
        <!-- 组织图标 -->
        <Building2 
          :class="[
            'h-4 w-4 transition-colors',
            !isSelectable(org) 
              ? 'text-neutral-300' 
              : selectedId === org.id 
                ? 'text-blue-600' 
                : 'text-neutral-500'
          ]" 
        />
        
        <!-- 组织信息 -->
        <div class="flex-1">
          <div class="flex items-center gap-2">
            <span 
              :class="[
                !isSelectable(org) 
                  ? 'text-neutral-400' 
                  : selectedId === org.id 
                    ? 'text-blue-600 font-medium'
                    : 'text-neutral-900'
              ]"
            >
              {{ org.name }}
            </span>
            <el-tag
              size="small"
              effect="plain"
              class="text-neutral-600 border-neutral-200 !bg-[#f5f7fb]"
            >
              {{ getOrgTypeLabel(org.type) }}
            </el-tag>
            <span 
              v-if="!isSelectable(org)" 
              class="text-xs text-neutral-400"
            >
              (不可选)
            </span>
          </div>
        </div>
      </div>
      
      <!-- 子组织 -->
      <div
        v-if="hasChildren(org) && expandedIds.has(org.id)"
        class="ml-8 pl-2 border-l border-dashed border-neutral-200 space-y-1"
      >
        <OrganizationTree
          :organizations="org.children!"
          :selected-id="selectedId"
          :role-id="roleId"
          :expanded-ids="expandedIds"
          :account-type="accountType"
          @select="$emit('select', $event)"
          @toggle-expand="handleToggleExpand"
        />
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Building2, ChevronDown, ChevronRight } from 'lucide-vue-next'
interface Organization {
  id: string
  name: string
  type: '地市' | '区县' | '客户经理团队'
  parentId?: string
  children?: Organization[]
}
interface Role {
  id: string
  name: string
  level: '地市级' | '区县级' | '一线人员'
}
interface Props {
  organizations: Organization[]
  selectedId?: string
  roleId?: string
  roles?: Role[]
  expandedIds?: Set<string>
  accountType?: '地市级' | '区县级' | '一线人员'
}
const props = withDefaults(defineProps<Props>(), {
  selectedId: '',
  roleId: '',
  roles: () => [],
  expandedIds: () => new Set(),
  accountType: undefined
})
const emit = defineEmits<{
  select: [orgId: string]
  'toggle-expand': [orgId: string]
}>()
// 如果没有传入 expandedIds,使用本地状态
const localExpandedIds = ref(new Set<string>())
const expandedIds = computed(() => 
  props.expandedIds && props.expandedIds.size > 0 
    ? props.expandedIds 
    : localExpandedIds.value
)
// 工具函数
const hasChildren = (org: Organization): boolean => {
  return !!(org.children && org.children.length > 0)
}
const isSelectable = (org: Organization): boolean => {
  console.log('检查组织可选性:', org.name, org.type, '账号类型:', props.accountType)
  
  // 只根据账号类型判断,不根据角色层级判断
  if (props.accountType) {
    switch (props.accountType) {
      case '地市级':
        // 地市级只能选择地市级组织
        console.log('地市级账号,检查组织类型是否为地市:', org.type === '地市')
        return org.type === '地市'
      case '区县级':
        // 区县级只能选择区县级组织
        console.log('区县级账号,检查组织类型是否为区县:', org.type === '区县')
        return org.type === '区县'
      case '一线人员':
       // 区县级只能选择区县级组织
        console.log('区县级账号,检查组织类型是否为区县:', org.type === '区县')
        return org.type === '区县'
      default:
        return true
    }
  }
  
  // 如果没有账号类型,可以选择所有组织
  return true
}
// 检查组织是否属于区县下
const isUnderCounty = (org: Organization): boolean => {
  console.log('OrganizationTree.vue:202 检查组织是否属于区县下:', org.name, org.type, org.id)
  
  // 从根节点开始构建完整的组织映射,确保包含所有组织
  const buildOrgMap = (organizations: Organization[], orgMap: Map<string, Organization> = new Map()) => {
    organizations.forEach(o => {
      orgMap.set(o.id, o)
      console.log('OrganizationTree.vue:210 添加组织到映射:', o.id, o.name, o.type)
      if (o.children) {
        buildOrgMap(o.children, orgMap)
      }
    })
    return orgMap
  }
  
  // 使用完整的组织数据构建映射
  const orgMap = buildOrgMap(props.organizations)
  console.log('OrganizationTree.vue:218 组织映射大小:', orgMap.size)
  
  // 从当前组织开始向上查找
  let currentOrg = org
  while (currentOrg) {
    console.log('OrganizationTree.vue:224 查找父组织ID:', currentOrg.parentId)
    const parentOrg = currentOrg.parentId ? orgMap.get(currentOrg.parentId) : undefined
    console.log('OrganizationTree.vue:226 找到父组织:', parentOrg?.name, parentOrg?.type)
    
    if (parentOrg) {
      if (parentOrg.type === '区县') {
        console.log('OrganizationTree.vue:230 找到区县父组织:', parentOrg.name)
        return true
      }
      currentOrg = parentOrg
    } else {
      console.log('OrganizationTree.vue:236 未找到父组织,退出循环')
      break
    }
  }
  
  console.log('OrganizationTree.vue:241 未找到区县父组织')
  return false
}
const getOrgTypeLabel = (type: Organization['type']) => {
  switch (type) {
    case '地市':
      return '地市'
    case '区县':
      return '区县'
    case '客户经理团队':
      return '客户经理团队'
    default:
      return type
  }
}
const getRoleLevel = (roleId: string): string => {
  if (!props.roles || props.roles.length === 0) return '区县级'
  
  const role = props.roles.find(r => r.id === roleId)
  return role ? role.level : '区县级'
}
// 事件处理
const handleSelect = (org: Organization) => {
  if (isSelectable(org)) {
    emit('select', org.id)
  }
}
const toggleExpand = (orgId: string) => {
  if (props.expandedIds && props.expandedIds.size > 0) {
    // 使用父组件的展开状态
    emit('toggle-expand', orgId)
  } else {
    // 使用本地展开状态
    if (localExpandedIds.value.has(orgId)) {
      localExpandedIds.value.delete(orgId)
    } else {
      localExpandedIds.value.add(orgId)
    }
  }
}
const handleToggleExpand = (orgId: string) => {
  emit('toggle-expand', orgId)
}
// 初始化展开顶级组织
if (props.expandedIds && props.expandedIds.size === 0) {
  props.organizations.forEach(org => {
    if (!org.parentId) {
      localExpandedIds.value.add(org.id)
    }
  })
}


</script>
<style scoped>
.space-y-1 > * + * {
  margin-top: 0.25rem;
}
.flex {
  display: flex;
}
.items-center {
  align-items: center;
}
.gap-2 {
  gap: 0.5rem;
}
.p-1 {
  padding: 0.25rem;
}
.p-2 {
  padding: 0.5rem;
}
.rounded {
  border-radius: 0.25rem;
}
.cursor-pointer {
  cursor: pointer;
}
.cursor-not-allowed {
  cursor: not-allowed;
}
.transition-colors {
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}
.opacity-40 {
  opacity: 0.4;
}
.bg-blue-50 {
  background-color: #eff6ff;
}
.border {
  border-width: 1px;
}
.border-blue-500 {
  border-color: #3b82f6;
}
.hover\:bg-neutral-50:hover {
  background-color: #f9fafb;
}
.hover\:bg-neutral-200:hover {
  background-color: #e5e7eb;
}
.h-4 {
  height: 1rem;
}
.w-4 {
  width: 1rem;
}
.w-6 {
  width: 1.5rem;
}
.text-neutral-300 {
  color: #d1d5db;
}
.text-neutral-400 {
  color: #9ca3af;
}
.text-neutral-500 {
  color: #6b7280;
}
.text-neutral-600 {
  color: #4b5563;
}
.text-neutral-900 {
  color: #111827;
}
.text-blue-600 {
  color: #2563eb;
}
.text-xs {
  font-size: 0.75rem;
  line-height: 1rem;
}
.flex-1 {
  flex: 1 1 0%;
}
.ml-4 {
  margin-left: 1rem;
}
.border-l-2 {
  border-left-width: 2px;
}
.border-neutral-200 {
  border-color: #e5e7eb;
}
</style>