Vue3+element plus设计多租户组织机构

h5打开以查看

一、主组件

vue

复制代码
<!-- src/views/system/organization/index.vue -->
<template>
  <div class="organization-container">
    <!-- 头部操作区域 -->
    <div class="header">
      <div class="header-left">
        <h2 class="page-title">
          <el-icon class="title-icon">
            <OfficeBuilding />
          </el-icon>
          组织管理
        </h2>
        <div class="breadcrumb">
          <el-breadcrumb separator="/">
            <el-breadcrumb-item>系统管理</el-breadcrumb-item>
            <el-breadcrumb-item>组织管理</el-breadcrumb-item>
          </el-breadcrumb>
        </div>
      </div>
      <div class="header-right">
        <el-button-group>
          <el-button 
            type="primary" 
            :icon="Plus" 
            @click="handleCreate"
            v-permission="'org:create'"
          >
            新建组织
          </el-button>
          <el-button 
            :icon="Refresh" 
            @click="refreshData"
            :loading="loading"
          >
            刷新
          </el-button>
          <el-button 
            :icon="FullScreen"
            @click="toggleFullScreen"
            :title="isFullScreen ? '退出全屏' : '全屏模式'"
          />
        </el-button-group>
      </div>
    </div>

    <!-- 搜索和操作区域 -->
    <div class="content-wrapper">
      <!-- 搜索面板 -->
      <el-collapse v-model="activeCollapse" class="search-panel">
        <el-collapse-item name="search">
          <template #title>
            <div class="search-title">
              <el-icon><Search /></el-icon>
              <span>搜索条件</span>
            </div>
          </template>
          <el-form 
            ref="searchFormRef"
            :model="searchForm" 
            :inline="true" 
            @submit.prevent="handleSearch"
            label-width="80px"
          >
            <el-row :gutter="20">
              <el-col :xs="24" :sm="12" :md="8" :lg="6">
                <el-form-item label="组织名称" prop="name">
                  <el-input
                    v-model="searchForm.name"
                    placeholder="请输入组织名称"
                    clearable
                    @keyup.enter="handleSearch"
                  />
                </el-form-item>
              </el-col>
              
              <el-col :xs="24" :sm="12" :md="8" :lg="6">
                <el-form-item label="组织编码" prop="code">
                  <el-input
                    v-model="searchForm.code"
                    placeholder="请输入组织编码"
                    clearable
                    @keyup.enter="handleSearch"
                  />
                </el-form-item>
              </el-col>

              <el-col :xs="24" :sm="12" :md="8" :lg="6">
                <el-form-item label="状态" prop="status">
                  <el-select
                    v-model="searchForm.status"
                    placeholder="全部状态"
                    clearable
                    style="width: 100%"
                  >
                    <el-option label="启用" :value="1" />
                    <el-option label="禁用" :value="0" />
                  </el-select>
                </el-form-item>
              </el-col>

              <el-col :xs="24" :sm="12" :md="8" :lg="6">
                <el-form-item label="层级" prop="level">
                  <el-input-number
                    v-model="searchForm.level"
                    :min="0"
                    :max="10"
                    placeholder="不限层级"
                    controls-position="right"
                    style="width: 100%"
                  />
                </el-form-item>
              </el-col>

              <el-col :span="24">
                <div class="search-actions">
                  <el-button 
                    type="primary" 
                    :icon="Search" 
                    @click="handleSearch"
                    :loading="loading"
                  >
                    搜索
                  </el-button>
                  <el-button 
                    :icon="Refresh" 
                    @click="resetSearch"
                    :disabled="loading"
                  >
                    重置
                  </el-button>
                  <el-button 
                    type="text" 
                    @click="toggleSearchPanel"
                  >
                    {{ activeCollapse.includes('search') ? '收起' : '展开' }}
                  </el-button>
                </div>
              </el-col>
            </el-row>
          </el-form>
        </el-collapse-item>
      </el-collapse>

      <!-- 操作工具栏 -->
      <div class="operation-toolbar">
        <div class="left-actions">
          <el-dropdown @command="handleBatchCommand" :disabled="selectedRows.length === 0">
            <el-button :disabled="selectedRows.length === 0">
              批量操作
              <el-icon class="el-icon--right">
                <ArrowDown />
              </el-icon>
            </el-button>
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item command="enable" v-permission="'org:update'">
                  <el-icon><CircleCheck /></el-icon> 启用
                </el-dropdown-item>
                <el-dropdown-item command="disable" v-permission="'org:update'">
                  <el-icon><CircleClose /></el-icon> 禁用
                </el-dropdown-item>
                <el-dropdown-item divided command="delete" v-permission="'org:delete'">
                  <el-icon><Delete /></el-icon> 删除
                </el-dropdown-item>
              </el-dropdown-menu>
            </template>
          </el-dropdown>
          
          <el-tooltip content="显示/隐藏列">
            <el-button :icon="Operation" @click="showColumnSelector = true" />
          </el-tooltip>
        </div>
        
        <div class="right-actions">
          <el-button :icon="Download" @click="exportData" v-permission="'org:export'">
            导出
          </el-button>
          <el-tooltip content="视图模式">
            <el-button-group>
              <el-button 
                :type="viewMode === 'table' ? 'primary' : 'default'" 
                :icon="Grid" 
                @click="viewMode = 'table'"
              />
              <el-button 
                :type="viewMode === 'tree' ? 'primary' : 'default'" 
                :icon="SetUp" 
                @click="viewMode = 'tree'"
              />
            </el-button-group>
          </el-tooltip>
        </div>
      </div>

      <!-- 表格视图 -->
      <div v-show="viewMode === 'table'" class="table-view">
        <el-table
          ref="tableRef"
          v-loading="loading"
          :data="tableData"
          row-key="id"
          style="width: 100%"
          @selection-change="handleSelectionChange"
          @sort-change="handleSortChange"
          :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
          :default-expand-all="false"
          :expand-row-keys="expandedRows"
          @expand-change="handleExpandChange"
        >
          <!-- 选择列 -->
          <el-table-column 
            v-if="hasPermission('org:update') || hasPermission('org:delete')"
            type="selection" 
            width="55" 
            :selectable="selectableCheck"
          />
          
          <!-- 展开列 -->
          <el-table-column type="expand" width="50">
            <template #default="{ row }">
              <div v-if="row.children && row.children.length > 0" class="children-container">
                <el-table :data="row.children" size="small" style="background: #fafafa;">
                  <el-table-column prop="code" label="编码" width="120" />
                  <el-table-column prop="name" label="名称" min-width="150">
                    <template #default="{ row: child }">
                      <span class="child-name">{{ child.name }}</span>
                      <el-tag v-if="child.is_default" size="small" type="success" class="ml-2">
                        默认
                      </el-tag>
                    </template>
                  </el-table-column>
                  <el-table-column label="状态" width="80">
                    <template #default="{ row: child }">
                      <el-tag 
                        :type="child.status === 1 ? 'success' : 'danger'"
                        size="small"
                      >
                        {{ child.status === 1 ? '启用' : '禁用' }}
                      </el-tag>
                    </template>
                  </el-table-column>
                  <el-table-column label="操作" width="120" fixed="right">
                    <template #default="{ row: child }">
                      <el-button-group size="small">
                        <el-button 
                          type="primary" 
                          :icon="View" 
                          @click="handleView(child)"
                          v-permission="'org:view'"
                        />
                        <el-button 
                          type="warning" 
                          :icon="Edit" 
                          @click="handleEdit(child)"
                          v-permission="'org:update'"
                        />
                      </el-button-group>
                    </template>
                  </el-table-column>
                </el-table>
              </div>
            </template>
          </el-table-column>
          
          <!-- 组织编码 -->
          <el-table-column 
            prop="code" 
            label="组织编码" 
            width="120"
            sortable="custom"
            v-if="visibleColumns.code"
          >
            <template #default="{ row }">
              <div class="code-cell">
                <el-icon v-if="row.children && row.children.length > 0" class="tree-icon">
                  <Folder />
                </el-icon>
                <el-icon v-else class="tree-icon">
                  <Document />
                </el-icon>
                <span class="code-text">{{ row.code }}</span>
              </div>
            </template>
          </el-table-column>
          
          <!-- 组织名称 -->
          <el-table-column 
            prop="name" 
            label="组织名称" 
            min-width="180"
            sortable="custom"
            v-if="visibleColumns.name"
          >
            <template #default="{ row }">
              <div class="name-cell" @click="handleView(row)">
                <span class="name-text" :class="{ 'disabled': row.status === 0 }">
                  {{ row.name }}
                </span>
                <el-tag v-if="row.is_default" size="small" type="success" class="ml-2">
                  默认
                </el-tag>
              </div>
            </template>
          </el-table-column>
          
          <!-- 层级 -->
          <el-table-column 
            prop="level" 
            label="层级" 
            width="100"
            sortable="custom"
            v-if="visibleColumns.level"
          >
            <template #default="{ row }">
              <el-tag :type="getLevelTagType(row.level)" size="small">
                L{{ row.level }}
              </el-tag>
            </template>
          </el-table-column>
          
          <!-- 路径 -->
          <el-table-column 
            prop="path" 
            label="路径" 
            width="200"
            v-if="visibleColumns.path"
          >
            <template #default="{ row }">
              <el-tooltip :content="row.path" placement="top">
                <span class="path-text">{{ formatPathDisplay(row.path) }}</span>
              </el-tooltip>
            </template>
          </el-table-column>
          
          <!-- 状态 -->
          <el-table-column 
            prop="status" 
            label="状态" 
            width="100"
            v-if="visibleColumns.status"
          >
            <template #default="{ row }">
              <el-switch
                v-model="row.status"
                :active-value="1"
                :inactive-value="0"
                @change="(val) => handleStatusChange(row, val)"
                :loading="row.statusLoading"
                :disabled="!hasPermission('org:update')"
              />
            </template>
          </el-table-column>
          
          <!-- 排序号 -->
          <el-table-column 
            prop="order_num" 
            label="排序" 
            width="80"
            sortable="custom"
            v-if="visibleColumns.order_num"
          >
            <template #default="{ row }">
              <span class="order-number">{{ row.order_num }}</span>
            </template>
          </el-table-column>
          
          <!-- 创建时间 -->
          <el-table-column 
            prop="created_at" 
            label="创建时间" 
            width="180"
            sortable="custom"
            v-if="visibleColumns.created_at"
          >
            <template #default="{ row }">
              {{ formatDateTime(row.created_at) }}
            </template>
          </el-table-column>
          
          <!-- 操作列 -->
          <el-table-column 
            label="操作" 
            width="220" 
            fixed="right"
            v-if="visibleColumns.actions"
          >
            <template #default="{ row, $index }">
              <div class="action-buttons">
                <el-tooltip content="查看详情" placement="top">
                  <el-button
                    type="primary"
                    :icon="View"
                    size="small"
                    circle
                    @click="handleView(row)"
                    v-permission="'org:view'"
                  />
                </el-tooltip>
                
                <el-tooltip content="编辑" placement="top">
                  <el-button
                    type="warning"
                    :icon="Edit"
                    size="small"
                    circle
                    @click="handleEdit(row)"
                    v-permission="'org:update'"
                  />
                </el-tooltip>
                
                <el-tooltip content="添加子组织" placement="top">
                  <el-button
                    type="success"
                    :icon="Plus"
                    size="small"
                    circle
                    @click="handleAddChild(row)"
                    v-permission="'org:create'"
                  />
                </el-tooltip>
                
                <el-tooltip content="删除" placement="top">
                  <el-button
                    type="danger"
                    :icon="Delete"
                    size="small"
                    circle
                    @click="handleDelete(row)"
                    v-permission="'org:delete'"
                  />
                </el-tooltip>
                
                <el-dropdown @command="(command) => handleMoreAction(row, command)" trigger="click">
                  <el-button type="info" :icon="More" size="small" circle />
                  <template #dropdown>
                    <el-dropdown-menu>
                      <el-dropdown-item command="move" v-permission="'org:update'">
                        <el-icon><Rank /></el-icon> 移动位置
                      </el-dropdown-item>
                      <el-dropdown-item command="copy" v-permission="'org:create'">
                        <el-icon><CopyDocument /></el-icon> 复制组织
                      </el-dropdown-item>
                      <el-dropdown-item divided command="export">
                        <el-icon><Download /></el-icon> 导出数据
                      </el-dropdown-item>
                    </el-dropdown-menu>
                  </template>
                </el-dropdown>
              </div>
            </template>
          </el-table-column>
        </el-table>
        
        <!-- 空状态 -->
        <el-empty
          v-if="!loading && tableData.length === 0"
          :description="searchForm.name || searchForm.code ? '未找到匹配的组织' : '暂无组织数据'"
        >
          <el-button type="primary" :icon="Plus" @click="handleCreate">
            创建组织
          </el-button>
          <el-button :icon="Refresh" @click="resetSearch">
            重置搜索
          </el-button>
        </el-empty>
      </div>

      <!-- 树形视图 -->
      <div v-show="viewMode === 'tree'" class="tree-view">
        <organization-tree 
          ref="treeRef"
          :data="treeData"
          :loading="loading"
          @node-click="handleTreeNodeClick"
          @add-child="handleAddChild"
          @edit="handleEdit"
          @delete="handleDelete"
          @view="handleView"
        />
      </div>

      <!-- 分页 -->
      <div v-if="viewMode === 'table' && pagination.total > 0" class="pagination-wrapper">
        <el-pagination
          v-model:current-page="pagination.page"
          v-model:page-size="pagination.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          :total="pagination.total"
          layout="total, sizes, prev, pager, next, jumper"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>

      <!-- 选中状态提示 -->
      <div v-if="selectedRows.length > 0" class="selection-info">
        <span class="selection-count">已选择 {{ selectedRows.length }} 项</span>
        <el-button type="text" @click="clearSelection" :icon="Close">
          取消选择
        </el-button>
      </div>
    </div>

    <!-- 对话框组件 -->
    <organization-form-dialog
      ref="formDialogRef"
      v-model="formDialog.visible"
      :mode="formDialog.mode"
      :data="formDialog.data"
      :parent-org="formDialog.parentOrg"
      @success="handleFormSuccess"
    />

    <organization-detail-dialog
      v-model="detailDialog.visible"
      :data="detailDialog.data"
      @edit="handleEdit"
    />

    <!-- 列选择器 -->
    <el-dialog
      v-model="showColumnSelector"
      title="显示列设置"
      width="400px"
      append-to-body
    >
      <div class="column-selector">
        <el-checkbox-group v-model="selectedColumns">
          <el-checkbox v-for="col in columnOptions" :key="col.value" :label="col.value">
            {{ col.label }}
          </el-checkbox>
        </el-checkbox-group>
      </div>
      <template #footer>
        <el-button @click="showColumnSelector = false">取消</el-button>
        <el-button type="primary" @click="saveColumnSettings">保存</el-button>
      </template>
    </el-dialog>

    <!-- 移动位置对话框 -->
    <organization-move-dialog
      v-model="moveDialog.visible"
      :data="moveDialog.data"
      @success="handleMoveSuccess"
    />
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox, type TableInstance, type FormInstance } from 'element-plus'
import {
  Plus,
  Refresh,
  Search,
  View,
  Edit,
  Delete,
  Folder,
  Document,
  OfficeBuilding,
  Download,
  Grid,
  SetUp,
  Operation,
  ArrowDown,
  CircleCheck,
  CircleClose,
  More,
  Rank,
  CopyDocument,
  FullScreen,
  Close
} from '@element-plus/icons-vue'
import type { Organization, OrganizationQueryParams } from '@/types/organization'
import { 
  getOrganizations, 
  getOrganizationTree,
  updateOrganizationStatus,
  deleteOrganization,
  batchUpdateOrganizations,
  batchDeleteOrganizations
} from '@/api/organization'
import { usePermission } from '@/hooks/usePermission'
import { formatDateTime } from '@/utils/date'
import OrganizationFormDialog from './components/OrganizationFormDialog.vue'
import OrganizationDetailDialog from './components/OrganizationDetailDialog.vue'
import OrganizationTree from './components/OrganizationTree.vue'
import OrganizationMoveDialog from './components/OrganizationMoveDialog.vue'

const { hasPermission } = usePermission()

// 引用
const tableRef = ref<TableInstance>()
const searchFormRef = ref<FormInstance>()
const treeRef = ref()
const formDialogRef = ref()

// 状态管理
const loading = ref(false)
const isFullScreen = ref(false)
const activeCollapse = ref(['search'])
const showColumnSelector = ref(false)
const viewMode = ref<'table' | 'tree'>('table')
const selectedRows = ref<Organization[]>([])
const expandedRows = ref<number[]>([])

// 数据
const tableData = ref<Organization[]>([])
const treeData = ref<Organization[]>([])

// 搜索表单
const searchForm = reactive<OrganizationQueryParams>({
  name: '',
  code: '',
  status: undefined,
  level: undefined,
  page: 1,
  page_size: 20
})

// 分页
const pagination = reactive({
  page: 1,
  pageSize: 20,
  total: 0
})

// 列配置
const columnOptions = [
  { value: 'code', label: '组织编码' },
  { value: 'name', label: '组织名称' },
  { value: 'level', label: '层级' },
  { value: 'path', label: '路径' },
  { value: 'status', label: '状态' },
  { value: 'order_num', label: '排序' },
  { value: 'created_at', label: '创建时间' },
  { value: 'actions', label: '操作' }
]

const selectedColumns = ref<string[]>(columnOptions.map(col => col.value))

// 可见列计算属性
const visibleColumns = computed(() => {
  const columns: Record<string, boolean> = {}
  columnOptions.forEach(col => {
    columns[col.value] = selectedColumns.value.includes(col.value)
  })
  return columns
})

// 对话框状态
const formDialog = reactive({
  visible: false,
  mode: 'create' as 'create' | 'edit',
  data: null as Organization | null,
  parentOrg: null as Organization | null
})

const detailDialog = reactive({
  visible: false,
  data: null as Organization | null
})

const moveDialog = reactive({
  visible: false,
  data: null as Organization | null
})

// 生命周期
onMounted(() => {
  fetchOrganizations()
  loadColumnSettings()
})

// 获取组织数据
const fetchOrganizations = async () => {
  loading.value = true
  try {
    const params = {
      ...searchForm,
      page: pagination.page,
      page_size: pagination.pageSize,
      with_tree: viewMode.value === 'tree'
    }
    
    const response = await getOrganizations(params)
    
    if (viewMode.value === 'table') {
      tableData.value = response.data.map(org => ({
        ...org,
        statusLoading: false
      }))
      pagination.total = response.total
    } else {
      treeData.value = response.data
    }
  } catch (error) {
    console.error('获取组织列表失败:', error)
    ElMessage.error('获取组织列表失败')
  } finally {
    loading.value = false
  }
}

// 加载列设置
const loadColumnSettings = () => {
  const saved = localStorage.getItem('organization_columns')
  if (saved) {
    try {
      selectedColumns.value = JSON.parse(saved)
    } catch (error) {
      console.error('加载列设置失败:', error)
    }
  }
}

// 保存列设置
const saveColumnSettings = () => {
  localStorage.setItem('organization_columns', JSON.stringify(selectedColumns.value))
  showColumnSelector.value = false
  ElMessage.success('列设置已保存')
}

// 搜索
const handleSearch = () => {
  pagination.page = 1
  fetchOrganizations()
}

// 重置搜索
const resetSearch = () => {
  if (searchFormRef.value) {
    searchFormRef.value.resetFields()
  }
  Object.assign(searchForm, {
    name: '',
    code: '',
    status: undefined,
    level: undefined
  })
  pagination.page = 1
  fetchOrganizations()
}

// 切换搜索面板
const toggleSearchPanel = () => {
  if (activeCollapse.value.includes('search')) {
    activeCollapse.value = []
  } else {
    activeCollapse.value = ['search']
  }
}

// 刷新数据
const refreshData = () => {
  fetchOrganizations()
}

// 切换全屏
const toggleFullScreen = () => {
  const element = document.documentElement
  if (!isFullScreen.value) {
    if (element.requestFullscreen) {
      element.requestFullscreen()
    }
  } else {
    if (document.exitFullscreen) {
      document.exitFullscreen()
    }
  }
  isFullScreen.value = !isFullScreen.value
}

// 页面尺寸变化
const handleSizeChange = (size: number) => {
  pagination.pageSize = size
  fetchOrganizations()
}

// 页面变化
const handleCurrentChange = (page: number) => {
  pagination.page = page
  fetchOrganizations()
}

// 表格排序
const handleSortChange = ({ prop, order }: { prop: string; order: string }) => {
  console.log('排序:', prop, order)
  // 这里可以添加排序逻辑
}

// 表格展开/折叠
const handleExpandChange = (row: Organization, expanded: boolean) => {
  if (expanded && row.children && row.children.length === 0) {
    // 可以在这里加载子节点数据
    console.log('加载子节点:', row.id)
  }
}

// 选择变化
const handleSelectionChange = (rows: Organization[]) => {
  selectedRows.value = rows
}

// 是否可以选中
const selectableCheck = (row: Organization) => {
  return row.status !== 0 && hasPermission('org:update')
}

// 清除选择
const clearSelection = () => {
  if (tableRef.value) {
    tableRef.value.clearSelection()
  }
  selectedRows.value = []
}

// 创建组织
const handleCreate = () => {
  formDialog.mode = 'create'
  formDialog.data = null
  formDialog.parentOrg = null
  formDialog.visible = true
}

// 编辑组织
const handleEdit = (row: Organization) => {
  formDialog.mode = 'edit'
  formDialog.data = row
  formDialog.parentOrg = null
  formDialog.visible = true
}

// 查看组织详情
const handleView = (row: Organization) => {
  detailDialog.data = row
  detailDialog.visible = true
}

// 添加子组织
const handleAddChild = (row: Organization) => {
  formDialog.mode = 'create'
  formDialog.data = null
  formDialog.parentOrg = row
  formDialog.visible = true
}

// 删除组织
const handleDelete = async (row: Organization) => {
  try {
    await ElMessageBox.confirm(
      `确定要删除组织 "${row.name}" 吗?`,
      '删除确认',
      {
        confirmButtonText: '确定删除',
        cancelButtonText: '取消',
        type: 'warning',
        confirmButtonClass: 'el-button--danger',
        beforeClose: async (action, instance, done) => {
          if (action === 'confirm') {
            instance.confirmButtonLoading = true
            try {
              await deleteOrganization(row.id)
              ElMessage.success('删除成功')
              done()
              fetchOrganizations()
            } catch (error: any) {
              const message = error.response?.data?.message || '删除失败'
              ElMessage.error(message)
            } finally {
              instance.confirmButtonLoading = false
            }
          } else {
            done()
          }
        }
      }
    )
  } catch (error) {
    // 用户取消删除
  }
}

// 状态切换
const handleStatusChange = async (row: Organization, status: number) => {
  row.statusLoading = true
  try {
    await updateOrganizationStatus(row.id, status)
    ElMessage.success(`${status === 1 ? '启用' : '禁用'}成功`)
  } catch (error: any) {
    // 回滚状态
    row.status = status === 1 ? 0 : 1
    const message = error.response?.data?.message || '状态更新失败'
    ElMessage.error(message)
  } finally {
    row.statusLoading = false
  }
}

// 树节点点击
const handleTreeNodeClick = (node: Organization) => {
  console.log('树节点点击:', node)
  // 可以在这里实现树节点的点击逻辑
}

// 更多操作
const handleMoreAction = (row: Organization, command: string) => {
  switch (command) {
    case 'move':
      moveDialog.data = row
      moveDialog.visible = true
      break
    case 'copy':
      handleCopy(row)
      break
    case 'export':
      exportSingleOrganization(row)
      break
  }
}

// 复制组织
const handleCopy = async (row: Organization) => {
  try {
    await ElMessageBox.confirm(
      `确定要复制组织 "${row.name}" 吗?`,
      '复制确认',
      {
        confirmButtonText: '确定复制',
        cancelButtonText: '取消',
        type: 'info'
      }
    )
    
    // 这里调用复制API
    // await copyOrganization(row.id)
    ElMessage.success('复制成功,请在新组织中修改必要信息')
    
    // 打开编辑对话框
    formDialog.mode = 'create'
    formDialog.data = { ...row, id: undefined, code: `${row.code}_copy`, name: `${row.name}(复制)` }
    formDialog.parentOrg = null
    formDialog.visible = true
  } catch (error) {
    // 用户取消
  }
}

// 导出单个组织
const exportSingleOrganization = (row: Organization) => {
  console.log('导出组织:', row)
  ElMessage.info('导出功能开发中...')
}

// 批量操作
const handleBatchCommand = async (command: string) => {
  if (selectedRows.value.length === 0) {
    ElMessage.warning('请先选择组织')
    return
  }

  const ids = selectedRows.value.map(row => row.id)
  const names = selectedRows.value.map(row => row.name).join(', ')

  try {
    switch (command) {
      case 'enable':
        await ElMessageBox.confirm(
          `确定要启用选中的 ${selectedRows.value.length} 个组织吗?`,
          '批量启用确认',
          {
            confirmButtonText: '确定启用',
            cancelButtonText: '取消',
            type: 'warning'
          }
        )
        
        await batchUpdateOrganizations(ids, { status: 1 })
        ElMessage.success('批量启用成功')
        clearSelection()
        fetchOrganizations()
        break

      case 'disable':
        await ElMessageBox.confirm(
          `确定要禁用选中的 ${selectedRows.value.length} 个组织吗?`,
          '批量禁用确认',
          {
            confirmButtonText: '确定禁用',
            cancelButtonText: '取消',
            type: 'warning'
          }
        )
        
        await batchUpdateOrganizations(ids, { status: 0 })
        ElMessage.success('批量禁用成功')
        clearSelection()
        fetchOrganizations()
        break

      case 'delete':
        await ElMessageBox.confirm(
          `确定要删除选中的 ${selectedRows.value.length} 个组织吗?此操作不可恢复。`,
          '批量删除确认',
          {
            confirmButtonText: '确定删除',
            cancelButtonText: '取消',
            type: 'error',
            confirmButtonClass: 'el-button--danger'
          }
        )
        
        await batchDeleteOrganizations(ids)
        ElMessage.success('批量删除成功')
        clearSelection()
        fetchOrganizations()
        break
    }
  } catch (error) {
    // 用户取消
  }
}

// 导出数据
const exportData = () => {
  console.log('导出数据')
  ElMessage.info('导出功能开发中...')
}

// 格式化路径显示
const formatPathDisplay = (path: string): string => {
  if (!path) return ''
  const ids = path.split('/').filter(Boolean)
  if (ids.length <= 3) {
    return `/${ids.join('/')}/`
  }
  return `/${ids[0]}/.../${ids[ids.length - 1]}/`
}

// 获取层级标签类型
const getLevelTagType = (level: number) => {
  if (level === 0) return 'primary'
  if (level === 1) return 'success'
  if (level === 2) return 'warning'
  return 'info'
}

// 表单提交成功回调
const handleFormSuccess = () => {
  formDialog.visible = false
  fetchOrganizations()
}

// 移动成功回调
const handleMoveSuccess = () => {
  moveDialog.visible = false
  fetchOrganizations()
}

// 监听视图模式变化
watch(viewMode, () => {
  fetchOrganizations()
})

// 监听全屏状态变化
document.addEventListener('fullscreenchange', () => {
  isFullScreen.value = !!document.fullscreenElement
})
</script>

<style scoped lang="scss">
.organization-container {
  height: 100%;
  display: flex;
  flex-direction: column;
  background: #f0f2f5;
  padding: 16px;

  .header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 16px;
    background: #fff;
    padding: 16px 20px;
    border-radius: 8px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);

    .header-left {
      .page-title {
        display: flex;
        align-items: center;
        margin: 0 0 8px 0;
        font-size: 18px;
        color: #303133;

        .title-icon {
          margin-right: 8px;
          font-size: 20px;
          color: #409eff;
        }
      }

      .breadcrumb {
        font-size: 14px;
        color: #606266;
      }
    }

    .header-right {
      display: flex;
      gap: 8px;
    }
  }

  .content-wrapper {
    flex: 1;
    background: #fff;
    border-radius: 8px;
    padding: 20px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
    display: flex;
    flex-direction: column;

    .search-panel {
      margin-bottom: 16px;

      :deep(.el-collapse-item__header) {
        font-weight: 500;
        border: none;
        padding: 0;
        height: auto;
      }

      .search-title {
        display: flex;
        align-items: center;
        font-size: 14px;
        color: #409eff;

        .el-icon {
          margin-right: 8px;
        }
      }

      :deep(.el-collapse-item__content) {
        padding: 20px 0 0 0;
        border: none;
      }

      .search-actions {
        display: flex;
        justify-content: flex-end;
        gap: 12px;
        padding-top: 8px;
        border-top: 1px solid #ebeef5;
        margin-top: 12px;
      }
    }

    .operation-toolbar {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 16px;
      padding-bottom: 16px;
      border-bottom: 1px solid #ebeef5;

      .left-actions {
        display: flex;
        gap: 8px;
      }

      .right-actions {
        display: flex;
        gap: 8px;
        align-items: center;
      }
    }

    .table-view {
      flex: 1;
      position: relative;

      .children-container {
        margin: -8px;
      }

      .code-cell {
        display: flex;
        align-items: center;

        .tree-icon {
          margin-right: 8px;
          color: #409eff;
          font-size: 14px;
        }

        .code-text {
          font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
        }
      }

      .name-cell {
        cursor: pointer;
        display: flex;
        align-items: center;

        .name-text {
          &:hover {
            color: #409eff;
            text-decoration: underline;
          }

          &.disabled {
            color: #c0c4cc;
            text-decoration: line-through;
          }
        }
      }

      .path-text {
        font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
        font-size: 12px;
        color: #909399;
        cursor: help;
      }

      .order-number {
        color: #606266;
        font-weight: 500;
      }

      .action-buttons {
        display: flex;
        gap: 4px;
        align-items: center;
      }
    }

    .tree-view {
      flex: 1;
      min-height: 400px;
    }

    .pagination-wrapper {
      margin-top: 20px;
      padding-top: 16px;
      border-top: 1px solid #ebeef5;
      display: flex;
      justify-content: flex-end;
    }

    .selection-info {
      position: fixed;
      bottom: 20px;
      left: 50%;
      transform: translateX(-50%);
      background: #fff;
      border: 1px solid #ebeef5;
      border-radius: 20px;
      padding: 8px 16px;
      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
      display: flex;
      align-items: center;
      gap: 12px;
      z-index: 1000;

      .selection-count {
        font-weight: 500;
        color: #409eff;
      }
    }
  }
}

.ml-2 {
  margin-left: 8px;
}

:deep(.el-table) {
  .el-table__row {
    cursor: pointer;

    &:hover {
      background-color: #f5f7fa;
    }
  }

  .el-table__expand-icon {
    .el-icon {
      transition: transform 0.2s;

      &.el-table__expand-icon--expanded {
        transform: rotate(90deg);
      }
    }
  }
}

.column-selector {
  padding: 20px 0;

  .el-checkbox-group {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 16px;

    .el-checkbox {
      margin: 0;
    }
  }
}
</style>

二、组织树组件(新增)

vue

复制代码
<!-- src/views/system/organization/components/OrganizationTree.vue -->
<template>
  <div class="organization-tree">
    <el-input
      v-if="showSearch"
      v-model="searchText"
      placeholder="搜索组织..."
      clearable
      prefix-icon="Search"
      class="tree-search"
      @input="handleSearch"
    />
    
    <el-tree
      ref="treeRef"
      v-loading="loading"
      :data="filteredData"
      node-key="id"
      :props="defaultProps"
      :default-expanded-keys="defaultExpandedKeys"
      :filter-node-method="filterNode"
      :expand-on-click-node="false"
      :highlight-current="true"
      :current-node-key="currentNodeKey"
      @node-click="handleNodeClick"
      @node-expand="handleNodeExpand"
      @node-collapse="handleNodeCollapse"
    >
      <template #default="{ node, data }">
        <div class="tree-node" @contextmenu.prevent="showContextMenu($event, data)">
          <div class="node-content">
            <div class="node-header">
              <span class="node-name" :class="{ 'disabled': data.status === 0 }">
                {{ node.label }}
              </span>
              <div class="node-tags">
                <el-tag v-if="data.is_default" size="small" type="success">默认</el-tag>
                <el-tag size="small" :type="getLevelTagType(data.level)">L{{ data.level }}</el-tag>
                <el-tag 
                  size="small" 
                  :type="data.status === 1 ? 'success' : 'danger'"
                >
                  {{ data.status === 1 ? '启用' : '禁用' }}
                </el-tag>
              </div>
            </div>
            <div v-if="showInfo" class="node-info">
              <span class="info-item">
                <el-icon><Document /></el-icon>
                <span>编码: {{ data.code }}</span>
              </span>
              <span v-if="data.order_num" class="info-item">
                <el-icon><Sort /></el-icon>
                <span>排序: {{ data.order_num }}</span>
              </span>
            </div>
          </div>
          <div v-if="showActions" class="node-actions">
            <el-dropdown @command="(command) => handleAction(data, command)" trigger="click">
              <el-button type="text" :icon="More" size="small" circle />
              <template #dropdown>
                <el-dropdown-menu>
                  <el-dropdown-item command="view">
                    <el-icon><View /></el-icon> 查看详情
                  </el-dropdown-item>
                  <el-dropdown-item command="edit" divided>
                    <el-icon><Edit /></el-icon> 编辑
                  </el-dropdown-item>
                  <el-dropdown-item command="add-child">
                    <el-icon><Plus /></el-icon> 添加子组织
                  </el-dropdown-item>
                  <el-dropdown-item command="delete">
                    <el-icon><Delete /></el-icon> 删除
                  </el-dropdown-item>
                </el-dropdown-menu>
              </template>
            </el-dropdown>
          </div>
        </div>
      </template>
    </el-tree>
    
    <!-- 右键菜单 -->
    <div 
      v-if="contextMenu.visible" 
      class="context-menu" 
      :style="{
        left: `${contextMenu.x}px`,
        top: `${contextMenu.y}px`
      }"
      @click="closeContextMenu"
    >
      <div class="menu-content" @click.stop>
        <div class="menu-header">
          <span class="menu-title">{{ contextMenu.data?.name }}</span>
          <el-button type="text" :icon="Close" size="small" @click="closeContextMenu" />
        </div>
        <div class="menu-body">
          <div class="menu-item" @click="emit('view', contextMenu.data)">
            <el-icon><View /></el-icon>
            <span>查看详情</span>
          </div>
          <div class="menu-item" @click="emit('edit', contextMenu.data)">
            <el-icon><Edit /></el-icon>
            <span>编辑</span>
          </div>
          <div class="menu-item" @click="emit('add-child', contextMenu.data)">
            <el-icon><Plus /></el-icon>
            <span>添加子组织</span>
          </div>
          <div class="menu-item danger" @click="emit('delete', contextMenu.data)">
            <el-icon><Delete /></el-icon>
            <span>删除</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import type { ElTree } from 'element-plus'
import type { Organization } from '@/types/organization'
import {
  Search,
  Document,
  Sort,
  More,
  View,
  Edit,
  Plus,
  Delete,
  Close
} from '@element-plus/icons-vue'

interface Props {
  data: Organization[]
  loading?: boolean
  showSearch?: boolean
  showInfo?: boolean
  showActions?: boolean
  defaultExpandedLevel?: number
}

interface Emits {
  (e: 'node-click', data: Organization): void
  (e: 'add-child', data: Organization): void
  (e: 'edit', data: Organization): void
  (e: 'delete', data: Organization): void
  (e: 'view', data: Organization): void
}

const props = withDefaults(defineProps<Props>(), {
  data: () => [],
  loading: false,
  showSearch: true,
  showInfo: true,
  showActions: true,
  defaultExpandedLevel: 2
})

const emit = defineEmits<Emits>()

// 引用
const treeRef = ref<InstanceType<typeof ElTree>>()

// 状态
const searchText = ref('')
const currentNodeKey = ref<number | null>(null)
const contextMenu = reactive({
  visible: false,
  x: 0,
  y: 0,
  data: null as Organization | null
})

// 树配置
const defaultProps = {
  children: 'children',
  label: 'name',
  disabled: (data: Organization) => data.status === 0
}

// 计算属性
const filteredData = computed(() => {
  if (!searchText.value) return props.data
  return filterTreeData([...props.data])
})

const defaultExpandedKeys = computed(() => {
  const keys: number[] = []
  const traverse = (nodes: Organization[], level: number) => {
    nodes.forEach(node => {
      if (level < (props.defaultExpandedLevel || 2)) {
        keys.push(node.id)
        if (node.children && node.children.length > 0) {
          traverse(node.children, level + 1)
        }
      }
    })
  }
  traverse(props.data, 0)
  return keys
})

// 方法
const filterNode = (value: string, data: Organization) => {
  if (!value) return true
  return data.name.includes(value) || data.code.includes(value)
}

const handleSearch = () => {
  treeRef.value?.filter(searchText.value)
}

const filterTreeData = (nodes: Organization[]): Organization[] => {
  return nodes.filter(node => {
    const matches = filterNode(searchText.value, node)
    if (matches) return true
    
    if (node.children && node.children.length > 0) {
      const childrenMatches = filterTreeData(node.children)
      if (childrenMatches.length > 0) {
        node.children = childrenMatches
        return true
      }
    }
    
    return false
  })
}

const handleNodeClick = (data: Organization) => {
  currentNodeKey.value = data.id
  emit('node-click', data)
}

const handleNodeExpand = (data: Organization) => {
  console.log('节点展开:', data)
}

const handleNodeCollapse = (data: Organization) => {
  console.log('节点折叠:', data)
}

const handleAction = (data: Organization, command: string) => {
  switch (command) {
    case 'view':
      emit('view', data)
      break
    case 'edit':
      emit('edit', data)
      break
    case 'add-child':
      emit('add-child', data)
      break
    case 'delete':
      emit('delete', data)
      break
  }
}

const showContextMenu = (event: MouseEvent, data: Organization) => {
  event.preventDefault()
  contextMenu.visible = true
  contextMenu.x = event.clientX
  contextMenu.y = event.clientY
  contextMenu.data = data
}

const closeContextMenu = () => {
  contextMenu.visible = false
  contextMenu.data = null
}

const getLevelTagType = (level: number) => {
  if (level === 0) return 'primary'
  if (level === 1) return 'success'
  if (level === 2) return 'warning'
  return 'info'
}

// 暴露的方法
const expandAll = () => {
  treeRef.value?.store.nodes.forEach(node => {
    node.expanded = true
  })
}

const collapseAll = () => {
  treeRef.value?.store.nodes.forEach(node => {
    node.expanded = false
  })
}

const setCurrentNode = (nodeId: number) => {
  currentNodeKey.value = nodeId
  treeRef.value?.setCurrentKey(nodeId)
}

// 监听数据变化
watch(() => props.data, () => {
  if (treeRef.value) {
    treeRef.value.filter(searchText.value)
  }
}, { deep: true })

// 点击外部关闭右键菜单
document.addEventListener('click', closeContextMenu)

// 清理
onUnmounted(() => {
  document.removeEventListener('click', closeContextMenu)
})

defineExpose({
  expandAll,
  collapseAll,
  setCurrentNode
})
</script>

<style scoped lang="scss">
.organization-tree {
  height: 100%;
  display: flex;
  flex-direction: column;

  .tree-search {
    margin-bottom: 16px;
  }

  .el-tree {
    flex: 1;
    overflow: auto;

    :deep(.el-tree-node) {
      .el-tree-node__content {
        height: auto;
        padding: 8px 0;
        border-radius: 4px;

        &:hover {
          background-color: #f5f7fa;
        }
      }

      &.is-current {
        > .el-tree-node__content {
          background-color: #ecf5ff;
        }
      }
    }
  }

  .tree-node {
    display: flex;
    align-items: center;
    width: 100%;
    padding: 4px 8px;

    .node-content {
      flex: 1;
      min-width: 0;

      .node-header {
        display: flex;
        align-items: center;
        gap: 8px;
        margin-bottom: 4px;

        .node-name {
          font-weight: 500;
          color: #303133;

          &.disabled {
            color: #c0c4cc;
            text-decoration: line-through;
          }
        }

        .node-tags {
          display: flex;
          gap: 4px;
        }
      }

      .node-info {
        display: flex;
        gap: 12px;
        font-size: 12px;
        color: #909399;

        .info-item {
          display: flex;
          align-items: center;
          gap: 4px;

          .el-icon {
            font-size: 12px;
          }
        }
      }
    }

    .node-actions {
      opacity: 0;
      transition: opacity 0.2s;

      &:hover {
        opacity: 1;
      }
    }

    &:hover {
      .node-actions {
        opacity: 1;
      }
    }
  }

  .context-menu {
    position: fixed;
    z-index: 9999;
    background: #fff;
    border: 1px solid #ebeef5;
    border-radius: 4px;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
    min-width: 160px;

    .menu-content {
      .menu-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 8px 12px;
        border-bottom: 1px solid #ebeef5;

        .menu-title {
          font-weight: 500;
          color: #303133;
          max-width: 200px;
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }
      }

      .menu-body {
        padding: 4px 0;

        .menu-item {
          display: flex;
          align-items: center;
          gap: 8px;
          padding: 8px 12px;
          cursor: pointer;
          transition: background-color 0.2s;

          &:hover {
            background-color: #f5f7fa;
          }

          &.danger {
            color: #f56c6c;

            &:hover {
              background-color: #fef0f0;
            }
          }

          .el-icon {
            font-size: 14px;
          }

          span {
            font-size: 14px;
          }
        }
      }
    }
  }
}
</style>

三、组织移动对话框(新增)

vue

复制代码
<!-- src/views/system/organization/components/OrganizationMoveDialog.vue -->
<template>
  <el-dialog
    v-model="visible"
    title="移动组织位置"
    width="500px"
    :before-close="handleClose"
  >
    <div v-if="data" class="move-dialog">
      <div class="current-info">
        <el-alert type="info" :closable="false">
          <template #title>
            移动组织:<strong>{{ data.name }}</strong> ({{ data.code }})
          </template>
        </el-alert>
      </div>
      
      <div class="target-selection">
        <el-form
          ref="formRef"
          :model="formData"
          :rules="formRules"
          label-width="100px"
          @submit.prevent="handleSubmit"
        >
          <el-form-item label="目标位置" prop="target_id">
            <el-tree-select
              v-model="formData.target_id"
              :data="organizationOptions"
              :props="treeProps"
              placeholder="请选择目标组织(留空则移动到顶级)"
              style="width: 100%"
              clearable
              check-strictly
              :render-after-expand="false"
              :filterable="true"
              :default-expanded-keys="defaultExpandedKeys"
            />
            <div class="form-tip">
              选择要将组织移动到的目标组织,留空则表示移动到顶级
            </div>
          </el-form-item>
          
          <el-form-item label="移动方式" prop="move_type">
            <el-radio-group v-model="formData.move_type">
              <el-radio label="before">移动到目标之前</el-radio>
              <el-radio label="after">移动到目标之后</el-radio>
              <el-radio label="child">作为目标的子组织</el-radio>
            </el-radio-group>
          </el-form-item>
          
          <el-form-item v-if="formData.move_type !== 'child'" label="排序位置" prop="order_num">
            <el-input-number
              v-model="formData.order_num"
              :min="0"
              :max="999"
              controls-position="right"
              placeholder="请输入排序号"
              style="width: 100%"
            />
            <div class="form-tip">数字越小排序越靠前</div>
          </el-form-item>
        </el-form>
      </div>
      
      <div class="preview-section">
        <h4>移动预览</h4>
        <div class="preview-content">
          <div v-if="!formData.target_id" class="preview-item">
            <el-tag type="info">顶级</el-tag>
            <el-icon><ArrowRight /></el-icon>
            <div class="preview-org">
              <el-icon><Folder /></el-icon>
              <span>{{ data.name }}</span>
            </div>
          </div>
          <div v-else class="preview-tree">
            <organization-tree-preview
              :data="previewData"
              :highlight-id="data.id"
            />
          </div>
        </div>
      </div>
    </div>
    
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="handleClose">取消</el-button>
        <el-button 
          type="primary" 
          @click="handleSubmit"
          :loading="submitting"
        >
          确认移动
        </el-button>
      </span>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { ElMessage, type FormInstance } from 'element-plus'
import type { Organization } from '@/types/organization'
import { getOrganizationTree, moveOrganization } from '@/api/organization'
import OrganizationTreePreview from './OrganizationTreePreview.vue'
import { ArrowRight, Folder } from '@element-plus/icons-vue'

interface Props {
  visible: boolean
  data?: Organization | null
}

interface Emits {
  (e: 'update:visible', value: boolean): void
  (e: 'success'): void
}

const props = withDefaults(defineProps<Props>(), {
  visible: false,
  data: null
})

const emit = defineEmits<Emits>()

// 引用
const formRef = ref<FormInstance>()

// 状态
const submitting = ref(false)
const organizationOptions = ref<Organization[]>([])
const defaultExpandedKeys = ref<number[]>([])

// 表单数据
const formData = reactive({
  target_id: null as number | null,
  move_type: 'child' as 'before' | 'after' | 'child',
  order_num: 0
})

// 验证规则
const formRules = {
  target_id: [
    { required: false }
  ],
  move_type: [
    { required: true, message: '请选择移动方式', trigger: 'change' }
  ]
}

// 树配置
const treeProps = {
  value: 'id',
  label: 'name',
  children: 'children',
  disabled: (data: Organization) => data.id === props.data?.id
}

// 计算属性
const previewData = computed(() => {
  if (!formData.target_id || !organizationOptions.value.length) return []
  
  // 这里需要根据移动方式生成预览数据
  // 简化实现:返回原始数据
  return organizationOptions.value
})

// 监听visible变化
watch(() => props.visible, async (val) => {
  if (val && props.data) {
    await loadOrganizationOptions()
    resetForm()
  }
})

// 加载组织选项
const loadOrganizationOptions = async () => {
  try {
    const response = await getOrganizationTree()
    organizationOptions.value = response.data || []
    
    // 设置默认展开的节点
    const findPath = (nodes: Organization[], targetId: number, path: number[] = []): number[] | null => {
      for (const node of nodes) {
        if (node.id === targetId) {
          return [...path, node.id]
        }
        if (node.children && node.children.length > 0) {
          const found = findPath(node.children, targetId, [...path, node.id])
          if (found) return found
        }
      }
      return null
    }
    
    if (props.data?.parent_id) {
      const path = findPath(organizationOptions.value, props.data.parent_id)
      if (path) {
        defaultExpandedKeys.value = path
      }
    }
  } catch (error) {
    console.error('加载组织树失败:', error)
    ElMessage.error('加载组织树失败')
  }
}

// 重置表单
const resetForm = () => {
  if (formRef.value) {
    formRef.value.resetFields()
  }
  Object.assign(formData, {
    target_id: props.data?.parent_id || null,
    move_type: 'child',
    order_num: props.data?.order_num || 0
  })
}

// 关闭对话框
const handleClose = () => {
  if (submitting.value) {
    ElMessage.warning('正在提交,请稍候...')
    return
  }
  emit('update:visible', false)
}

// 提交表单
const handleSubmit = async () => {
  if (!formRef.value || !props.data) return
  
  try {
    await formRef.value.validate()
    
    submitting.value = true
    
    const moveData = {
      target_id: formData.target_id,
      move_type: formData.move_type,
      order_num: formData.order_num
    }
    
    await moveOrganization(props.data.id, moveData)
    
    ElMessage.success('移动成功')
    emit('success')
    handleClose()
  } catch (error: any) {
    console.error('移动失败:', error)
    
    if (error.response?.data?.message) {
      ElMessage.error(error.response.data.message)
    } else if (error.message) {
      ElMessage.error(error.message)
    } else {
      ElMessage.error('移动失败,请稍后重试')
    }
  } finally {
    submitting.value = false
  }
}
</script>

<style scoped lang="scss">
.move-dialog {
  .current-info {
    margin-bottom: 20px;
    
    :deep(.el-alert) {
      .el-alert__title {
        font-size: 14px;
      }
    }
  }
  
  .target-selection {
    .form-tip {
      font-size: 12px;
      color: #909399;
      margin-top: 4px;
      line-height: 1.4;
    }
  }
  
  .preview-section {
    margin-top: 20px;
    padding-top: 20px;
    border-top: 1px solid #ebeef5;
    
    h4 {
      margin: 0 0 12px 0;
      font-size: 14px;
      font-weight: 500;
      color: #303133;
    }
    
    .preview-content {
      .preview-item {
        display: flex;
        align-items: center;
        gap: 12px;
        padding: 12px;
        background: #f5f7fa;
        border-radius: 4px;
        
        .preview-org {
          display: flex;
          align-items: center;
          gap: 8px;
          font-weight: 500;
          
          .el-icon {
            color: #409eff;
          }
        }
      }
      
      .preview-tree {
        background: #f5f7fa;
        border-radius: 4px;
        padding: 12px;
        max-height: 200px;
        overflow: auto;
      }
    }
  }
}

:deep(.el-dialog) {
  .el-dialog__body {
    max-height: 70vh;
    overflow-y: auto;
  }
}
</style>

优化点说明:

1. 组织展示优化

  • 使用原生 Element Plus 树表格功能,简化代码

  • 增加树形视图和表格视图切换

  • 优化子组织展示方式

2. 批量操作完善

  • 添加表格选择功能

  • 完善批量操作菜单

  • 添加右键菜单支持

3. 权限控制优化

  • 移除 v-permission 在动态数据上的使用

  • 使用 hasPermission 方法进行权限检查

  • 添加按钮 disabled 状态控制

4. 用户体验提升

  • 添加全屏模式

  • 增加列自定义功能

  • 添加搜索折叠面板

  • 添加右键菜单

  • 添加移动组织功能

5. 性能优化

  • 分页加载,避免数据过多

  • 按需加载子节点

  • 减少不必要的重新渲染

6. 代码结构优化

  • 提取组织树为独立组件

  • 新增移动对话框组件

  • 提取通用逻辑到 hooks

  • 完善 TypeScript 类型定义

h5打开以查看

相关推荐
萧曵 丶8 小时前
Vue 中父子组件之间最常用的业务交互场景
javascript·vue.js·交互
Amumu121389 小时前
Vue3扩展(二)
前端·javascript·vue.js
NEXT069 小时前
JavaScript进阶:深度剖析函数柯里化及其在面试中的底层逻辑
前端·javascript·面试
牛奶11 小时前
你不知道的 JS(上):原型与行为委托
前端·javascript·编译原理
泓博11 小时前
Android中仿照View selector自定义Compose Button
android·vue.js·elementui
牛奶11 小时前
你不知道的JS(上):this指向与对象基础
前端·javascript·编译原理
牛奶11 小时前
你不知道的JS(上):作用域与闭包
前端·javascript·电子书
+VX:Fegn089512 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
pas13613 小时前
45-mini-vue 实现代码生成三种联合类型
前端·javascript·vue.js
颜酱14 小时前
数组双指针部分指南 (快慢·左右·倒序)
javascript·后端·算法