增加erp组织架构

This commit is contained in:
李志强 2026-02-24 16:52:28 +08:00
parent 186315d82c
commit 6c904ef3cd
6 changed files with 626 additions and 0 deletions

65
src/api/erp.js Normal file
View File

@ -0,0 +1,65 @@
import request from "@/utils/request";
/*************************************************
****************** 组织机构相关接口 ******************
*************************************************/
/**
* 获取组织机构列表
* @returns {Promise}
*/
export function getOrganizationList() {
return request({
url: '/admin/erp/organization',
method: 'get'
});
}
/**
* 获取组织机构详情
* @param {number} id 组织机构ID
* @returns {Promise}
*/
export function getOrganizationDetail(id) {
return request({
url: `/admin/erp/getOrganizationDetail/${id}`,
method: "get",
});
}
/**
* 创建组织机构数据
* @param {Object} data 组织机构数据
* @returns {Promise}
*/
export function createOrganization(data) {
return request({
url: "/admin/erp/organization",
method: "post",
data: data,
headers: {
"Content-Type": "multipart/form-data"
}
});
}
// 更新组织机构信息
export function editOrganization(id, data) {
return request({
url: `/admin/erp/organization/${id}`,
method: 'post',
data: data
});
}
/**
* 删除组织机构数据
* @param {number} id 组织机构ID
* @returns {Promise}
*/
export function deleteOrganization(id) {
return request({
url: `/admin/erp/organization/${id}`,
method: "delete",
});
}

View File

@ -7,6 +7,14 @@ export function getAllUsers() {
method: 'get', method: 'get',
}); });
} }
//获取租户用户
export function getTenantUsers(tenantId) {
return request({
url: `/admin/getTenantUsers/${tenantId}`,
method: 'get',
});
}
// 获取用户信息 // 获取用户信息
export function getUserInfo(userId) { export function getUserInfo(userId) {

View File

@ -40,6 +40,7 @@ export const useAuthStore = defineStore('auth', () => {
account: userInfo.account || '', account: userInfo.account || '',
name: userInfo.name || '', name: userInfo.name || '',
group_id: userInfo.group_id || '', group_id: userInfo.group_id || '',
tenant_id: userInfo.tenant_id || '',
avatar: userInfo.avatar || '' avatar: userInfo.avatar || ''
} }

View File

@ -0,0 +1,202 @@
<template>
<el-dialog
v-model="visible"
:title="isEdit ? '编辑部门' : '添加部门'"
width="500px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-form-item label="上级部门" prop="parent_id">
<el-tree-select
v-model="formData.parent_id"
:data="treeData"
:props="treeProps"
check-strictly
placeholder="请选择上级部门"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item label="部门名称" prop="org_name">
<el-input v-model="formData.org_name" placeholder="请输入部门名称,例如:连云港云泽集团" />
</el-form-item>
<el-form-item label="部门编码" prop="org_code">
<el-input v-model="formData.org_code" placeholder="请输入部门编码,例如LYG-YZ-GROUP" />
</el-form-item>
<el-form-item label="负责人" prop="leader_id">
<el-select v-model="formData.leader_id" placeholder="请选择负责人">
<el-option v-for="item in leaderList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" :max="999" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { createOrganization, editOrganization } from '@/api/erp'
import { useAuthStore } from '@/stores/auth'
import { getTenantUsers } from '@/api/user'
interface TreeNode {
id: number
org_name: string
org_code?: string
parent_id?: number
parent_name?: string
leader_id?: number
sort?: number
status?: number
remark?: string
children?: TreeNode[]
}
const props = defineProps<{
treeData: TreeNode[]
treeProps: { label: string; children: string; value: string }
}>()
const emit = defineEmits<{
success: []
}>()
const visible = ref(false)
const loading = ref(false)
const formRef = ref<FormInstance>()
const isEdit = ref(false)
const authStore = useAuthStore()
const tenantId = (authStore.user as any)?.tenant_id
const leaderList = ref([])
const formData = reactive({
id: 0,
parent_id: undefined as number | undefined,
org_name: '',
org_code: '',
leader_id: undefined as number | undefined,
sort: 0,
status: 1,
remark: ''
})
const formRules: FormRules = {
org_name: [{ required: true, message: '请输入部门名称', trigger: 'blur' }],
org_code: [{ required: true, message: '请输入部门编码', trigger: 'blur' }]
}
onMounted(() => {
getLeaderList()
})
const getLeaderList = async () => {
const res = await getTenantUsers(tenantId)
if (res.code === 200) {
leaderList.value = res.data.list
}
}
//
const open = (data?: TreeNode, parentId?: number) => {
if (data) {
//
isEdit.value = true
Object.assign(formData, data)
} else {
//
isEdit.value = false
formData.id = 0
formData.parent_id = parentId
formData.org_name = ''
formData.org_code = ''
formData.leader_id = undefined
formData.sort = 0
formData.status = 1
formData.remark = ''
}
visible.value = true
}
//
const handleClose = () => {
visible.value = false
formRef.value?.resetFields()
}
//
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate()
loading.value = true
try {
let res
if (isEdit.value && formData.id) {
// 使
const submitData: Record<string, any> = {}
Object.keys(formData).forEach(key => {
if (
key !== 'id' &&
(formData as any)[key] !== undefined &&
(formData as any)[key] !== null
) {
submitData[key] = (formData as any)[key]
}
})
if (tenantId) {
submitData.tenant_id = tenantId
}
res = await editOrganization(formData.id, submitData)
} else {
// 使 FormData multipart/form-data
const submitData = new FormData()
Object.keys(formData).forEach(key => {
const value = (formData as any)[key]
if (key !== 'id' && value !== undefined && value !== null) {
submitData.append(key, String(value))
}
})
if (tenantId) {
submitData.append('tenant_id', String(tenantId))
}
res = await createOrganization(submitData)
}
if (res.code === 200) {
ElMessage.success(isEdit.value ? '编辑成功' : '添加成功')
emit('success')
handleClose()
} else {
ElMessage.error(res.msg || (isEdit.value ? '编辑失败' : '添加失败'))
}
} catch (error) {
console.error('提交失败:', error)
ElMessage.error('提交失败')
} finally {
loading.value = false
}
}
onMounted(() => {
getLeaderList()
})
defineExpose({ open })
</script>

View File

@ -0,0 +1,350 @@
<template>
<div class="organization-container">
<el-row :gutter="16">
<!-- 左侧组织架构树 -->
<el-col :span="8">
<el-card shadow="hover" class="tree-card" v-loading="loading">
<template #header>
<div class="card-header">
<span>组织架构</span>
<el-button type="primary" link @click="handleAddRoot">
<el-icon><Plus /></el-icon>
添加
</el-button>
</div>
</template>
<el-tree
ref="treeRef"
:data="treeData"
:props="treeProps"
node-key="id"
default-expand-all
:expand-on-click-node="false"
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<div class="tree-node">
<span class="node-label">{{ node.label }}</span>
<div class="node-actions" @click.stop>
<el-button link type="primary" size="small" @click="handleAdd(data)">
<el-icon><Plus /></el-icon>
</el-button>
<el-button link type="primary" size="small" @click="handleEdit(data)">
<el-icon><Edit /></el-icon>
</el-button>
<el-button link type="danger" size="small" @click="handleDelete(data)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</template>
</el-tree>
</el-card>
</el-col>
<!-- 右侧详情面板 -->
<el-col :span="16">
<el-card shadow="hover" class="detail-card">
<template #header>
<div class="card-header">
<span>{{ detailTitle }}</span>
</div>
</template>
<div v-if="selectedNode" class="detail-content">
<el-descriptions :column="2" border>
<el-descriptions-item label="部门名称">
{{ selectedNode.org_name }}
</el-descriptions-item>
<el-descriptions-item label="部门编码">
{{ selectedNode.org_code || '-' }}
</el-descriptions-item>
<el-descriptions-item label="上级部门">
{{ selectedNode.parent_name || '顶级部门' }}
</el-descriptions-item>
<el-descriptions-item label="负责人">
{{ selectedNode.leader_name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="状态" :span="2">
<el-tag :type="selectedNode.status === 1 ? 'success' : 'danger'">
{{ selectedNode.status === 1 ? '启用' : '禁用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">
{{ selectedNode.remark || '-' }}
</el-descriptions-item>
</el-descriptions>
<div class="detail-actions">
<el-button type="primary" @click="handleEdit(selectedNode)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button type="danger" @click="handleDelete(selectedNode)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</div>
<el-empty v-else description="请选择左侧组织节点查看详情" />
</el-card>
</el-col>
</el-row>
<!-- 添加/编辑对话框 -->
<OrganizationEdit
ref="editRef"
:tree-data="treeData"
:tree-props="treeProps"
@success="handleSuccess"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Edit, Delete } from '@element-plus/icons-vue'
import OrganizationEdit from './components/edit.vue'
import { getOrganizationList, getOrganizationDetail, deleteOrganization } from '@/api/erp'
//
interface TreeNode {
id: number
tenant_id?: number
org_name: string
org_code?: string
parent_id?: number
parent_name?: string
leader_id?: number
leader_name?: string
sort?: number
status?: number
remark?: string
children?: TreeNode[]
}
//
const treeData = ref<TreeNode[]>([])
const treeProps = {
label: 'org_name',
children: 'children',
value: 'id'
}
const treeRef = ref()
const editRef = ref()
const selectedNode = ref<TreeNode | null>(null)
const loading = ref(false)
//
const buildTree = (list: any[]): TreeNode[] => {
const nodeMap = new Map<number, TreeNode>()
const roots: TreeNode[] = []
list.forEach(item => {
const node: TreeNode = {
id: item.id,
tenant_id: item.tenant_id,
org_name: item.org_name,
org_code: item.org_code,
parent_id: item.parent_id,
parent_name: item.parent_name,
leader_id: item.leader_id,
leader_name: item.leader_name,
sort: item.sort,
status: item.status,
remark: item.remark,
children: []
}
nodeMap.set(node.id, node)
})
nodeMap.forEach(node => {
if (node.parent_id && nodeMap.has(node.parent_id)) {
const parent = nodeMap.get(node.parent_id)!
if (!parent.children) {
parent.children = []
}
parent.children.push(node)
} else {
roots.push(node)
}
})
return roots
}
//
const loadOrganizationDetail = async (id: number) => {
try {
const res = await getOrganizationDetail(id)
if (res.code === 200) {
const data = res.data || null
if (data) {
selectedNode.value = {
id: data.id,
tenant_id: data.tenant_id,
org_name: data.org_name,
org_code: data.org_code,
parent_id: data.parent_id,
parent_name: data.parent_name,
leader_id: data.leader_id,
leader_name: data.leader_name,
sort: data.sort,
status: data.status,
remark: data.remark,
children: []
}
} else {
selectedNode.value = null
}
} else {
ElMessage.error(res.msg || '获取组织机构详情失败')
}
} catch (error) {
console.error('获取组织机构详情失败:', error)
ElMessage.error('获取组织机构详情失败')
}
}
//
const fetchOrganizationList = async () => {
loading.value = true
try {
const res = await getOrganizationList()
if (res.code === 200) {
const list = res.data || []
treeData.value = buildTree(list)
//
if (!selectedNode.value && list.length) {
await loadOrganizationDetail(list[0].id)
}
} else {
ElMessage.error(res.msg || '获取组织机构列表失败')
}
} catch (error) {
console.error('获取组织机构列表失败:', error)
ElMessage.error('获取组织机构列表失败')
} finally {
loading.value = false
}
}
//
const detailTitle = computed(() => {
return selectedNode.value ? `部门详情 - ${selectedNode.value.org_name}` : '部门详情'
})
//
const handleNodeClick = (data: TreeNode) => {
loadOrganizationDetail(data.id)
}
//
const handleAddRoot = () => {
editRef.value?.open(undefined, undefined)
}
//
const handleAdd = (data: TreeNode) => {
editRef.value?.open(undefined, data.id)
}
//
const handleEdit = (data: TreeNode) => {
editRef.value?.open(data)
}
//
const handleSuccess = () => {
fetchOrganizationList()
}
//
const handleDelete = (data: TreeNode) => {
ElMessageBox.confirm(`确定要删除部门「${data.org_name}」吗?`, '提示', {
type: 'warning'
})
.then(async () => {
try {
loading.value = true
const res = await deleteOrganization(data.id)
if (res.code === 200) {
ElMessage.success('删除成功')
fetchOrganizationList()
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
} finally {
loading.value = false
}
})
.catch(() => {})
}
onMounted(() => {
fetchOrganizationList()
})
</script>
<style lang="less" scoped>
.organization-container {
// padding: 16px;
}
.tree-card {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
}
.tree-node {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding-right: 8px;
.node-label {
flex: 1;
}
.node-actions {
display: none;
gap: 4px;
}
&:hover .node-actions {
display: flex;
}
}
.detail-card {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-content {
.el-descriptions {
margin-bottom: 20px;
}
.detail-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
}
}
</style>