index.vue 9.94 KB
<script setup lang="ts">
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { OrgApi, type OrgListReqVO, type OrgSaveReqVO, type OrgVO } from '@/api/system/org'
import { formatDateTime } from '@/utils'

/**
 * 系统管理 - 组织管理
 *
 * 接口:/admin-api/system/org(权限点 system:org:*)
 *
 * 组织是树形数据(层级结构),`/list` 一次下发整棵树,所以这里**没有分页**:
 * el-table 用 row-key + children 递归渲染,分页会把层级切断。
 *
 * ⚠️ 本项目用 `system_org` 而不是 `system_dept`,但语义就是「部门」:
 *    用户的 `orgId` 与登录信息里的 `deptId` 是同一个值。
 *
 * ⚠️ 后端删除前会挡两种情况(`OrgServiceImpl.deleteOrg`):
 *    有子组织(ORG_EXISTS_CHILDREN)、组织下还有用户(ORG_EXISTS_USER)。
 *    前者前端先拦一道,后者只能靠后端报错——前端不知道每个组织下有多少用户。
 */
defineOptions({ name: 'SystemOrg' })

const loading = ref(false)
const list = ref<OrgVO[]>([])

const queryParams = reactive<OrgListReqVO>({
  name: ''
})

async function loadList() {
  loading.value = true
  try {
    list.value = (await OrgApi.getOrgList(queryParams)) || []
  } finally {
    loading.value = false
  }
}

function handleSearch() {
  loadList()
}

function handleReset() {
  queryParams.name = ''
  loadList()
}

// ---------------------------------------------------------------------------
// 新增 / 修改
// ---------------------------------------------------------------------------

const dialogVisible = ref(false)
const submitting = ref(false)
const formRef = ref<FormInstance>()

function emptyForm(): OrgSaveReqVO {
  return {
    parentId: 0,
    name: '',
    sort: 0,
    status: 0,
    leader: '',
    mobile: '',
    email: '',
    remark: ''
  }
}

const form = reactive<OrgSaveReqVO>(emptyForm())

const isUpdate = computed(() => !!form.id)

/**
 * 编辑时把自己及其子孙从「上级组织」候选里剔掉:
 * 后端只挡了「parentId === 自己」,把自己设成自己的子孙的父节点并不会被拒绝,
 * 但那样会在树里形成一个闭环,整棵子树从界面上消失。
 */
function excludeSubtree(nodes: OrgVO[], excludeId?: number): OrgVO[] {
  return nodes
    .filter((node) => node.id !== excludeId)
    .map((node) => ({ ...node, children: node.children ? excludeSubtree(node.children, excludeId) : [] }))
}

const parentTree = computed(() => excludeSubtree(list.value, form.id))

const rules: FormRules = {
  name: [
    { required: true, message: '请输入组织名称', trigger: 'blur' },
    { max: 30, message: '组织名称长度不能超过 30 个字符', trigger: 'blur' }
  ],
  sort: [{ required: true, message: '请输入显示顺序', trigger: 'blur' }],
  status: [{ required: true, message: '请选择状态', trigger: 'change' }],
  email: [
    { type: 'email', message: '邮箱格式不正确', trigger: 'blur' },
    { max: 50, message: '邮箱长度不能超过 50 个字符', trigger: 'blur' }
  ],
  leader: [{ max: 30, message: '负责人姓名长度不能超过 30 个字符', trigger: 'blur' }],
  remark: [{ max: 500, message: '备注长度不能超过 500 个字符', trigger: 'blur' }]
}

function handleCreate(parentId = 0) {
  Object.assign(form, emptyForm(), { parentId })
  dialogVisible.value = true
  nextTick(() => formRef.value?.clearValidate())
}

async function handleUpdate(row: OrgVO) {
  const detail = await OrgApi.getOrg(row.id)
  Object.assign(form, emptyForm(), {
    id: detail.id,
    parentId: detail.parentId ?? 0,
    name: detail.name,
    sort: detail.sort ?? 0,
    status: detail.status ?? 0,
    leader: detail.leader || '',
    mobile: detail.mobile || '',
    email: detail.email || '',
    // remark 必须回显:OrgSaveReqVO 带这个字段,提交时 updateById 会整体覆盖该列,
    // 若这里不回填,保存一次就把库里的原备注清成空串。
    remark: detail.remark || ''
  })
  dialogVisible.value = true
  nextTick(() => formRef.value?.clearValidate())
}

async function handleSubmit() {
  try {
    await formRef.value?.validate()
  } catch {
    return
  }
  submitting.value = true
  try {
    const data: OrgSaveReqVO = { ...form }
    if (data.id) {
      await OrgApi.updateOrg(data)
    } else {
      await OrgApi.createOrg(data)
    }
    ElMessage.success(data.id ? '修改成功' : '新增成功')
    dialogVisible.value = false
    loadList()
  } finally {
    submitting.value = false
  }
}

// ---------------------------------------------------------------------------
// 删除
// ---------------------------------------------------------------------------

function handleDelete(row: OrgVO) {
  if (row.children && row.children.length > 0) {
    ElMessage.warning('该组织下还有子组织,请先删除子组织')
    return
  }
  ElMessageBox.confirm(`确认删除组织「${row.name}」吗?`, '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  })
    .then(async () => {
      await OrgApi.deleteOrg(row.id)
      ElMessage.success('删除成功')
      loadList()
    })
    .catch(() => {})
}

async function handleExport() {
  await OrgApi.exportOrg(queryParams)
}

onMounted(loadList)
</script>

<template>
  <div class="app-container">
    <el-card shadow="never" class="mb-3">
      <el-form :model="queryParams" inline class="search-form">
        <el-form-item label="组织名称">
          <el-input v-model="queryParams.name" placeholder="请输入组织名称" clearable style="width: 180px" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <el-card shadow="never">
      <div class="mb-3">
        <el-button v-hasPermi="'system:org:create'" type="primary" @click="handleCreate(0)">新增</el-button>
        <el-button v-hasPermi="'system:org:export'" plain @click="handleExport">导出</el-button>
        <el-button plain @click="loadList">刷新</el-button>
      </div>

      <el-table
        v-loading="loading"
        :data="list"
        row-key="id"
        default-expand-all
        border
        stripe
        size="small"
      >
        <el-table-column type="index" label="#" width="50" align="center" />
        <el-table-column prop="name" label="组织名称" min-width="200" />
        <el-table-column prop="sort" label="排序" width="80" align="center" />
        <el-table-column prop="leader" label="负责人" min-width="120" />
        <el-table-column prop="mobile" label="联系电话" min-width="130" />
        <el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
        <el-table-column label="状态" width="90" align="center">
          <template #default="{ row }">
            <DictTag dict-type="system_common_status" :value="row.status" />
          </template>
        </el-table-column>
        <el-table-column label="创建时间" width="170">
          <template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
        </el-table-column>
        <el-table-column label="操作" width="190" fixed="right" align="center">
          <template #default="{ row }">
            <el-button v-hasPermi="'system:org:create'" link type="primary" @click="handleCreate(row.id)">
              新增下级
            </el-button>
            <el-button v-hasPermi="'system:org:update'" link type="primary" @click="handleUpdate(row)">
              编辑
            </el-button>
            <el-button v-hasPermi="'system:org:delete'" link type="danger" @click="handleDelete(row)">
              删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>

    <el-dialog v-model="dialogVisible" :title="isUpdate ? '修改组织' : '新增组织'" width="640px">
      <el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
        <el-form-item label="上级组织" prop="parentId">
          <el-tree-select
            v-model="form.parentId"
            :data="parentTree"
            :props="{ label: 'name', children: 'children' }"
            node-key="id"
            check-strictly
            default-expand-all
            :render-after-expand="false"
            placeholder="不选则为顶级组织"
            class="w-full"
          />
        </el-form-item>
        <el-form-item label="组织名称" prop="name">
          <el-input v-model="form.name" placeholder="请输入组织名称" maxlength="30" />
        </el-form-item>
        <el-form-item label="显示顺序" prop="sort">
          <el-input-number v-model="form.sort" :min="0" controls-position="right" />
        </el-form-item>
        <el-form-item label="负责人" prop="leader">
          <el-input v-model="form.leader" placeholder="请输入负责人姓名" maxlength="30" />
        </el-form-item>
        <el-form-item label="联系电话" prop="mobile">
          <el-input v-model="form.mobile" placeholder="请输入联系电话" maxlength="20" />
        </el-form-item>
        <el-form-item label="邮箱" prop="email">
          <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
        </el-form-item>
        <el-form-item label="状态" prop="status">
          <el-radio-group v-model="form.status">
            <el-radio :label="0">开启</el-radio>
            <el-radio :label="1">停用</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" placeholder="请输入备注" />
        </el-form-item>
      </el-form>

      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>