增加组织架构

This commit is contained in:
2026-08-27 21:25:39 +08:00
parent 74ab6c4c6d
commit d6bba226d0
41 changed files with 6128 additions and 5218 deletions
@@ -0,0 +1,241 @@
<template>
<el-dialog
v-model="visible"
:title="isEdit ? '编辑组织' : '新建组织'"
width="600px"
:close-on-click-modal="false"
@closed="handleClosed"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
<el-form-item label="上级组织" prop="parent_id">
<el-tree-select
v-model="form.parent_id"
:data="parentOptions"
:props="treeSelectProps"
check-strictly
clearable
placeholder="顶级组织请留空"
style="width: 100%"
/>
<div class="form-tip">留空表示顶级组织(企业单位)</div>
</el-form-item>
<el-form-item label="组织名称" prop="org_name">
<el-input v-model="form.org_name" maxlength="50" show-word-limit placeholder="例如:连云港云泽集团" />
</el-form-item>
<el-form-item label="组织编码" prop="org_code">
<el-input v-model="form.org_code" maxlength="20" show-word-limit placeholder="留空时按组织设置自动生成" />
<div class="form-tip">同一租户内不允许重复,可留空由系统生成</div>
</el-form-item>
<el-form-item label="组织类型" prop="is_company">
<el-radio-group v-model="form.is_company">
<el-radio :value="1">企业单位</el-radio>
<el-radio :value="0">部门</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="负责人" prop="leader_id">
<el-select v-model="form.leader_id" filterable clearable placeholder="请选择负责人" style="width: 100%">
<el-option
v-for="item in employeeOptions"
:key="item.id"
:label="item.name || item.account"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="9999" controls-position="right" style="width: 100%" />
<div class="form-tip">数字越小越靠前</div>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmit">
{{ isEdit ? '保存' : '创建' }}
</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { computed, reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import { buildOrgTree, collectSubtreeIds, unwrap, useOrganizationApi } from '../composables';
const props = defineProps({
/** 所属模块:erp(进销存)或 oa(办公自动化) */
module: { type: String, default: 'erp' },
});
const emit = defineEmits(['success']);
const api = computed(() => useOrganizationApi(props.module));
const visible = ref(false);
const loading = ref(false);
const isEdit = ref(false);
const formRef = ref();
const orgTree = ref([]);
const employeeOptions = ref([]);
const editingId = ref(0);
const treeSelectProps = { value: 'id', label: 'org_name', children: 'children' };
const emptyForm = () => ({
parent_id: null,
org_name: '',
org_code: '',
leader_id: null,
sort: 0,
is_company: 0,
status: 1,
remark: '',
});
const form = reactive(emptyForm());
const rules = {
org_name: [
{ required: true, message: '请输入组织名称', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
],
org_code: [
{ pattern: /^[a-zA-Z0-9_-]*$/, message: '只能包含英文、数字、下划线和横线', trigger: 'blur' },
],
};
// 编辑时排除自身及其子树,避免把组织挂到自己的下级从而形成环路
const parentOptions = computed(() => {
if (!isEdit.value || !editingId.value) return orgTree.value;
const excluded = new Set();
const walk = (nodes) => {
nodes.forEach((node) => {
if (Number(node.id) === Number(editingId.value)) {
collectSubtreeIds(node).forEach((id) => excluded.add(id));
return;
}
walk(node.children || []);
});
};
walk(orgTree.value);
const filter = (nodes) =>
nodes
.filter((node) => !excluded.has(Number(node.id)))
.map((node) => ({ ...node, children: filter(node.children || []) }));
return filter(orgTree.value);
});
const loadOptions = async () => {
try {
const [orgList, employees] = await Promise.all([
api.value.getOrganizationList().then(unwrap),
api.value.getEmployeeList().then(unwrap),
]);
orgTree.value = buildOrgTree(orgList);
employeeOptions.value = (employees || []).map((item) => ({
id: item.id,
name: item.name,
account: item.account,
}));
} catch (error) {
ElMessage.error(error?.message || '加载选项失败');
}
};
const open = (row, parentId) => {
Object.assign(form, emptyForm());
isEdit.value = Boolean(row);
editingId.value = row?.id || 0;
if (row) {
Object.assign(form, {
parent_id: row.parent_id ? Number(row.parent_id) : null,
org_name: row.org_name || '',
org_code: row.org_code || '',
leader_id: row.leader_id ? Number(row.leader_id) : null,
sort: Number(row.sort || 0),
is_company: Number(row.is_company || 0),
status: Number(row.status ?? 1),
remark: row.remark || '',
});
} else if (parentId) {
form.parent_id = Number(parentId);
form.is_company = 0;
} else {
form.is_company = 1;
}
visible.value = true;
loadOptions();
};
const handleSubmit = async () => {
if (!formRef.value) return;
try {
await formRef.value.validate();
} catch {
return;
}
const payload = {
parent_id: form.parent_id || 0,
org_name: form.org_name.trim(),
org_code: form.org_code.trim(),
leader_id: form.leader_id || 0,
sort: form.sort,
is_company: form.is_company,
status: form.status,
remark: form.remark || '',
};
loading.value = true;
try {
if (isEdit.value) {
unwrap(await api.value.updateOrganization(editingId.value, payload));
ElMessage.success('保存成功');
} else {
unwrap(await api.value.createOrganization(payload));
ElMessage.success('创建成功');
}
visible.value = false;
emit('success');
} catch (error) {
ElMessage.error(error?.message || '保存失败');
} finally {
loading.value = false;
}
};
const handleClosed = () => {
formRef.value?.clearValidate();
Object.assign(form, emptyForm());
};
defineExpose({ open });
</script>
<style lang="less" scoped>
.form-tip {
margin-top: 4px;
font-size: 12px;
color: #909399;
line-height: 1.4;
}
</style>