Files
td_official/src/views/design/appointment/index.vue
2025-09-01 09:18:21 +08:00

1097 lines
42 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="p-6 bg-gray-50">
<!-- 外层容器添加横向滚动支持 -->
<div class="overflow-x-auto">
<div class="appointment mx-auto bg-white rounded-xl shadow-sm overflow-hidden transition-all duration-300 hover:shadow-md min-w-[1200px]">
<!-- 表单标题区域 -->
<div class="bg-gradient-to-r from-blue-500 to-blue-600 text-white p-6">
<h2 class="text-2xl font-bold flex items-center"><i class="el-icon-user-circle mr-3"></i>人员配置</h2>
<p class="text-blue-100 mt-2 opacity-90">请配置项目相关负责人员信息</p>
<el-button @click="disabledForm = false" class="px-8 py-2.5 transition-all duration-300 font-medium" v-if="disabledForm">
点击编辑
</el-button>
</div>
<!-- 表单内容区域 -->
<el-form ref="leaveFormRef" :model="form" :disabled="disabledForm" :rules="rules" label-width="120px" class="p-6 space-y-6">
<!-- 设计负责人 -->
<div class="fonts">
<el-row>
<el-col :span="8">
<el-form-item label="设计负责人" prop="designLeader" class="mb-4">
<el-select
v-model="form.designLeader"
placeholder="请选择设计负责人"
class="w-full transition-all duration-300 border-gray-300 focus:border-blue-400 focus:ring-1 focus:ring-blue-400"
>
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 专业人员配置专业 + 设计 + 校审 + 审定 + 审核 横向排列 -->
<div class="border border-gray-200 rounded-lg p-5 transition-all duration-300 hover:shadow-md bg-gray-50">
<div class="flex justify-between items-center mb-5">
<h3 class="text-lg font-semibold text-gray-700 flex items-center"><i class="el-icon-users mr-2 text-blue-500"></i>专业人员配置</h3>
<div class="flex gap-3">
<!-- 新增专业按钮 -->
<el-button type="primary" size="small" :disabled="disabledForm" @click="addMajor">
<i class="el-icon-plus mr-1"></i>新增专业
</el-button>
</div>
</div>
<!-- 表头总和24确保一行显示 -->
<el-row :gutter="8" class="mb-3 font-medium text-gray-700 whitespace-nowrap">
<el-col :span="4">专业</el-col>
<el-col :span="5">设计人员可多选</el-col>
<el-col :span="5">校审人员</el-col>
<el-col :span="5">审定人员</el-col>
<el-col :span="4">审核人员</el-col>
<el-col :span="3"></el-col>
</el-row>
<!-- 分割线 -->
<el-divider class="my-4" />
<!-- 专业配置行循环渲染 + 专业同步逻辑 -->
<div
v-for="(majorConfig, configIndex) in combinedConfigs"
:key="`major-${configIndex}`"
style="background: aliceblue; border-radius: 10px; padding: 12px 0"
class="mb-5 animate-fadeIn"
>
<el-row :gutter="8" class="items-top">
<!-- 1. 专业选择核心统一所有角色的专业来源 -->
<el-col :span="3" class="mb-4 sm:mb-0 pl-4" style="margin-top:8px;">
<el-form-item
:prop="`designers.${configIndex}.userMajor`"
:rules="[
{ required: true, message: '请选择专业', trigger: 'change' },
{ validator: validateMajor, trigger: 'change' }
]"
class="mb-0"
label-width="60px"
label="专业"
>
<el-select filterable
v-model="form.designers[configIndex].userMajor"
placeholder="请选择专业"
class="w-full transition-all duration-300 border-gray-300"
@change="(val) => handleMajorChange(val, configIndex)"
clearable
>
<template v-if="des_user_major && des_user_major.length > 0">
<el-option v-for="item in des_user_major" :key="`major-item-${item.value}`" :label="item.label" :value="item.value" />
</template>
<template v-else>
<el-option label="无专业数据" value="" disabled />
</template>
</el-select>
</el-form-item>
</el-col>
<!-- 2. 设计人员 -->
<el-col :span="4" class="mb-4 sm:mb-0">
<div class="pl-2 border-l-2 border-blue-200 py-2">
<div class="space-y-3">
<div
v-for="(person, personIndex) in majorConfig.designPersons"
:key="`design-${configIndex}-${personIndex}`"
class="flex items-center"
>
<el-form-item
:prop="`designers.${configIndex}.persons.${personIndex}.userId`"
:rules="{ required: true, message: '请选择设计人员', trigger: 'change' }"
class="flex-1 mr-2 mb-0"
label="设计"
label-width="50px"
>
<el-select filterable
v-model="person.userId"
placeholder="选择人员"
class="w-full transition-all duration-300 border-gray-300"
@change="() => checkDuplicate(person, 'designers', configIndex, personIndex)"
>
<el-option v-for="item in userList" :key="`user-${item.userId}`" :label="item.nickName" :value="item.userId" />
</el-select>
</el-form-item>
<div class="flex gap-1">
<el-button
type="danger"
size="small"
@click="removePerson('designers', configIndex, personIndex)"
class="transition-all duration-300 hover:bg-red-600"
:disabled="majorConfig.designPersons.length <= 1 || disabledForm"
>
<el-icon :size="14"><Delete /></el-icon>
</el-button>
<el-button
type="success"
size="small"
@click="addPerson('designers', configIndex)"
class="transition-all duration-300 transform hover:scale-105"
:disabled="!form.designers[configIndex].userMajor || disabledForm"
>
<el-icon :size="14"><Plus /></el-icon>
</el-button>
</div>
</div>
</div>
<div
v-if="majorConfig.designPersons.length == 0"
class="text-gray-500 text-xs py-2 bg-gray-100 rounded border border-dashed border-gray-200"
>
点击"添加"
</div>
</div>
</el-col>
<!-- 3. 校审人员 -->
<el-col :span="5" class="mb-4 sm:mb-0">
<div class="pl-2 border-l-2 border-green-200 py-2">
<div class="space-y-3">
<div
v-for="(person, personIndex) in majorConfig.reviewPersons"
:key="`review-${configIndex}-${personIndex}`"
class="flex items-center"
>
<el-form-item
:prop="`reviewers.${configIndex}.persons.${personIndex}.userId`"
:rules="{ required: true, message: '请选择校审人员', trigger: 'change' }"
class="flex-1 mr-2 mb-0"
label="校审"
label-width="50px"
>
<el-select filterable
v-model="person.userId"
placeholder="选择人员"
class="w-full transition-all duration-300 border-gray-300"
@change="() => checkDuplicate(person, 'reviewers', configIndex, personIndex)"
>
<el-option v-for="item in userList" :key="`user-${item.userId}`" :label="item.nickName" :value="item.userId" />
</el-select>
</el-form-item>
<!-- <div class="flex gap-1">
<el-button
type="danger"
size="small"
@click="removePerson('reviewers', configIndex, personIndex)"
class="transition-all duration-300 hover:bg-red-600"
:disabled="majorConfig.reviewPersons.length <= 1 || disabledForm"
>
<el-icon :size="14"><Delete /></el-icon>
</el-button>
<el-button
type="success"
size="small"
@click="addPerson('reviewers', configIndex)"
class="transition-all duration-300 transform hover:scale-105"
:disabled="!form.designers[configIndex].userMajor || disabledForm"
>
<el-icon :size="14"><Plus /></el-icon>
</el-button>
</div> -->
</div>
</div>
<div
v-if="majorConfig.reviewPersons.length == 0"
class="text-gray-500 text-xs py-2 bg-gray-100 rounded border border-dashed border-gray-200"
>
点击"添加"
</div>
</div>
</el-col>
<!-- 4. 审定人员 -->
<el-col :span="5" class="mb-4 sm:mb-0">
<div class="pl-2 border-l-2 border-orange-200 py-2">
<div class="space-y-3">
<div
v-for="(person, personIndex) in majorConfig.approvedPersons"
:key="`approved-${configIndex}-${personIndex}`"
class="flex items-center"
>
<el-form-item
:prop="`approved.${configIndex}.persons.${personIndex}.userId`"
:rules="{ required: true, message: '请选择审定人员', trigger: 'change' }"
class="flex-1 mr-2 mb-0"
label="审定"
label-width="50px"
>
<el-select filterable
v-model="person.userId"
placeholder="选择人员"
class="w-full transition-all duration-300 border-gray-300"
@change="() => checkDuplicate(person, 'approved', configIndex, personIndex)"
>
<el-option v-for="item in userList" :key="`user-${item.userId}`" :label="item.nickName" :value="item.userId" />
</el-select>
</el-form-item>
<!-- <div class="flex gap-1">
<el-button
type="danger"
size="small"
@click="removePerson('approved', configIndex, personIndex)"
class="transition-all duration-300 hover:bg-red-600"
:disabled="majorConfig.approvedPersons.length <= 1 || disabledForm"
>
<el-icon :size="14"><Delete /></el-icon>
</el-button>
<el-button
type="success"
size="small"
@click="addPerson('approved', configIndex)"
class="transition-all duration-300 transform hover:scale-105"
:disabled="!form.designers[configIndex].userMajor || disabledForm"
>
<el-icon :size="14"><Plus /></el-icon>
</el-button>
</div> -->
</div>
</div>
<div
v-if="majorConfig.approvedPersons.length == 0"
class="text-gray-500 text-xs py-2 bg-gray-100 rounded border border-dashed border-gray-200"
>
点击"添加"
</div>
</div>
</el-col>
<!-- 5. 审核人员 -->
<el-col :span="5" class="mb-4 sm:mb-0">
<div class="pl-2 border-l-2 border-purple-200 py-2">
<div class="space-y-3">
<div
v-for="(person, personIndex) in majorConfig.auditorPersons"
:key="`auditor-${configIndex}-${personIndex}`"
class="flex items-center"
>
<el-form-item
:prop="`auditor.${configIndex}.persons.${personIndex}.userId`"
:rules="{ required: true, message: '请选择审核人员', trigger: 'change' }"
class="flex-1 mr-2 mb-0"
label="审核"
label-width="50px"
>
<el-select filterable
v-model="person.userId"
placeholder="选择人员"
class="w-full transition-all duration-300 border-gray-300"
@change="() => checkDuplicate(person, 'auditor', configIndex, personIndex)"
>
<el-option v-for="item in userList" :key="`user-${item.userId}`" :label="item.nickName" :value="item.userId" />
</el-select>
</el-form-item>
<!-- <div class="flex gap-1">
<el-button
type="danger"
size="small"
@click="removePerson('auditor', configIndex, personIndex)"
class="transition-all duration-300 hover:bg-red-600"
:disabled="majorConfig.auditorPersons.length <= 1 || disabledForm"
>
<el-icon :size="14"><Delete /></el-icon>
</el-button>
<el-button
type="success"
size="small"
@click="addPerson('auditor', configIndex)"
class="transition-all duration-300 transform hover:scale-105"
:disabled="!form.designers[configIndex].userMajor || disabledForm"
>
<el-icon :size="14"><Plus /></el-icon>
</el-button>
</div> -->
</div>
</div>
<div
v-if="majorConfig.auditorPersons.length == 0"
class="text-gray-500 text-xs py-2 bg-gray-100 rounded border border-dashed border-gray-200"
>
点击"添加"
</div>
</div>
</el-col>
<!-- 操作列 -->
<el-col :span="2" class="text-right pr-4">
<el-button
type="text"
class="text-red-500 hover:text-red-700 transition-colors"
@click="removeMajor(configIndex)"
:disabled="combinedConfigs.length <= 1 || disabledForm"
>
<i class="el-icon-delete mr-1"></i>删除专业
</el-button>
</el-col>
</el-row>
</div>
</div>
<!-- 提交按钮区域 -->
<div v-if="!disabledForm" class="flex justify-center space-x-6 mt-8 pt-6 border-t border-gray-100">
<el-button
type="primary"
size="large"
v-hasPermi="['design:user:batch']"
@click="submitForm"
class="px-8 py-2.5 transition-all duration-300 transform hover:scale-105 bg-blue-500 hover:bg-blue-600 text-white font-medium"
>
<i class="el-icon-check mr-2"></i>确认提交
</el-button>
<el-button size="large" @click="resetForm" class="px-8 py-2.5 transition-all duration-300 border-gray-300 hover:bg-gray-100 font-medium">
<i class="el-icon-refresh mr-2"></i>重置
</el-button>
</div>
</el-form>
</div>
</div>
</div>
</template>
<script setup name="PersonnelForm" lang="ts">
import { ref, reactive, computed, onMounted, toRefs, watch, WatchStopHandle, type FormItemRule } from 'vue';
import { getCurrentInstance } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { useUserStoreHook } from '@/store/modules/user';
import { ElMessage, ElLoading, ElForm } from 'element-plus';
import { Delete, Plus } from '@element-plus/icons-vue';
import { designUserAdd, designUserList, systemUserList } from '@/api/design/appointment';
// 获取当前实例
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
// 专业字典数据(确保默认空数组)
const { des_user_major = ref<Array<{ label: string; value: string }>>([]) } = toRefs<any>(proxy?.useDict('des_user_major') || {});
// 表单数据定义(明确类型)
interface Person {
userId: number | null;
}
interface MajorGroup {
userMajor: string | null; // 专业值与字典value对应
persons: Person[]; // 该专业下的人员
}
const form = reactive({
projectId: currentProject.value?.id || null,
designLeader: null as number | null, // 设计负责人
designers: [] as MajorGroup[], // 设计人员userType=2
reviewers: [] as MajorGroup[], // 校审人员userType=3
approved: [] as MajorGroup[], // 审定人员userType=4
auditor: [] as MajorGroup[] // 审核人员userType=5
});
// 组合配置:统一所有角色的专业和人员(确保长度一致)
const combinedConfigs = computed(() => {
// 取所有角色的最大长度,确保数组长度统一
const maxLength = Math.max(form.designers.length, form.reviewers.length, form.approved.length, form.auditor.length);
// 补全空分组(确保每个角色数组长度相同,避免渲染异常)
while (form.designers.length < maxLength) form.designers.push(createEmptyMajorGroup());
while (form.reviewers.length < maxLength) form.reviewers.push(createEmptyMajorGroup());
while (form.approved.length < maxLength) form.approved.push(createEmptyMajorGroup());
while (form.auditor.length < maxLength) form.auditor.push(createEmptyMajorGroup());
// 组合数据所有角色共用designers的专业值确保同步
return form.designers.map((designerGroup, index) => ({
userMajor: designerGroup.userMajor, // 统一专业来源
designPersons: designerGroup.persons,
reviewPersons: form.reviewers[index].persons,
approvedPersons: form.approved[index].persons,
auditorPersons: form.auditor[index].persons
}));
});
// 表单验证规则(新增专业有效性校验)
const rules = reactive({
designLeader: [{ required: true, message: '请选择设计负责人', trigger: 'change' }]
});
// 自定义校验:确保专业值在字典中存在(避免无效值)
const validateMajor: FormItemRule = (rule, value, callback) => {
if (!value) {
callback(new Error('请选择专业'));
} else {
// 检查专业值是否在字典中存在
const majorExists = des_user_major.value.some((item) => item.value == value);
if (majorExists) {
callback();
} else {
callback(new Error('选择的专业无效,请重新选择'));
}
}
};
// 其他基础变量
const userList = ref<Array<{ userId: number; nickName: string }>>([]);
const leaveFormRef = ref<InstanceType<typeof ElForm> | null>(null);
const disabledForm = ref(true); // 初始禁用表单(编辑时开启)
/** 查询当前部门的所有用户(确保用户列表有效) */
const getDeptAllUser = async (deptId: number | undefined) => {
console.log(1111111111111);
if (!deptId) {
ElMessage.warning('请先选择部门');
return;
}
try {
const res = await systemUserList({ deptId });
userList.value = res.rows || [];
if (userList.value.length == 0) {
ElMessage.info('当前部门暂无用户数据,请先添加用户');
}
} catch (error) {
ElMessage.error('获取用户列表失败,请刷新重试');
console.error('获取用户列表异常:', error);
}
};
/** 查询当前表单数据并回显(确保专业值正确赋值) */
const designUser = async () => {
const projectId = currentProject.value?.id;
if (!projectId) {
ElMessage.warning('请先选择项目');
return;
}
const loading = ElLoading.service({
lock: true,
text: '加载配置数据中...',
background: 'rgba(255, 255, 255, 0.7)'
});
try {
const res = await designUserList({ projectId });
// 清空现有数据(避免残留旧值)
form.designLeader = null;
form.designers = [];
form.reviewers = [];
form.approved = [];
form.auditor = [];
disabledForm.value = false;
if (res.code == 200 && Array.isArray(res.rows) && res.rows.length > 0) {
disabledForm.value = true;
// 1. 按用户类型分类数据(明确类型)
const designLeader = res.rows.find((item: any) => item.userType == 1);
const designerItems = res.rows.filter((item: any) => item.userType == 2);
const reviewerItems = res.rows.filter((item: any) => item.userType == 3);
const approvedItems = res.rows.filter((item: any) => item.userType == 4);
const auditorItems = res.rows.filter((item: any) => item.userType == 5);
// 2. 回显设计负责人
if (designLeader) form.designLeader = designLeader.userId;
// 3. 按专业分组回显各角色人员(确保专业值非空)
form.designers = groupPersonByMajor(designerItems);
form.reviewers = groupPersonByMajor(reviewerItems);
form.approved = groupPersonByMajor(approvedItems);
form.auditor = groupPersonByMajor(auditorItems);
// 4. 补全空分组(确保至少有一个专业)
if (form.designers.length == 0) form.designers.push(createEmptyMajorGroup());
// 同步其他角色的分组长度
while (form.reviewers.length < form.designers.length) form.reviewers.push(createEmptyMajorGroup());
while (form.approved.length < form.designers.length) form.approved.push(createEmptyMajorGroup());
while (form.auditor.length < form.designers.length) form.auditor.push(createEmptyMajorGroup());
// 5. 确保所有角色的专业值与designers同步回显时可能存在差异
form.designers.forEach((designerGroup, index) => {
if (designerGroup.userMajor) {
form.reviewers[index].userMajor = designerGroup.userMajor;
form.approved[index].userMajor = designerGroup.userMajor;
form.auditor[index].userMajor = designerGroup.userMajor;
}
});
} else {
// 无数据时初始化默认分组
form.designers = [createEmptyMajorGroup()];
form.reviewers = [createEmptyMajorGroup()];
form.approved = [createEmptyMajorGroup()];
form.auditor = [createEmptyMajorGroup()];
}
} catch (error) {
ElMessage.error('获取配置数据失败,请刷新重试');
console.error('获取配置数据异常:', error);
// 异常时初始化默认分组(避免页面空白)
form.designers = [createEmptyMajorGroup()];
form.reviewers = [createEmptyMajorGroup()];
form.approved = [createEmptyMajorGroup()];
form.auditor = [createEmptyMajorGroup()];
} finally {
loading.close();
}
};
/** 辅助函数:创建空的专业分组(确保初始结构正确) */
const createEmptyMajorGroup = (): MajorGroup => ({
userMajor: null,
persons: [{ userId: null }] // 初始一个空人员
});
/** 辅助函数:按专业分组整理人员数据(确保专业值非空) */
const groupPersonByMajor = (items: any[]): MajorGroup[] => {
const groupMap: Record<string, MajorGroup> = {};
// 过滤无效数据确保item有userMajor和userId
const validItems = items.filter((item) => item?.userMajor && item?.userId);
if (validItems.length == 0) {
return [createEmptyMajorGroup()];
}
validItems.forEach((item) => {
const major = item.userMajor;
// 初始化分组(确保不重复)
if (!groupMap[major]) {
groupMap[major] = { userMajor: major, persons: [] };
}
// 添加人员(避免重复人员)
const personExists = groupMap[major].persons.some((p) => p.userId == item.userId);
if (!personExists) {
groupMap[major].persons.push({ userId: item.userId });
}
});
// 确保每个分组至少有一个人员
Object.values(groupMap).forEach((group) => {
if (group.persons.length == 0) {
group.persons.push({ userId: null });
}
});
return Object.values(groupMap);
};
/** 新增专业配置行(确保所有角色同步新增) */
const addMajor = () => {
const newGroup = createEmptyMajorGroup();
form.designers.push(newGroup);
form.reviewers.push({ ...newGroup }); // 深拷贝避免引用问题
form.approved.push({ ...newGroup });
form.auditor.push({ ...newGroup });
// 滚动到新增行(提升用户体验)
setTimeout(() => {
const groups = document.querySelectorAll(`[data-v-${proxy?.$options.__scopeId}] .animate-fadeIn`);
if (groups.length > 0) {
groups[groups.length - 1].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, 100);
};
/** 删除专业配置行(确保所有角色同步删除) */
const removeMajor = (configIndex: number) => {
if (form.designers.length <= 1) {
ElMessage.warning('至少保留一个专业配置');
return;
}
// 同步删除所有角色的对应分组
form.designers.splice(configIndex, 1);
form.reviewers.splice(configIndex, 1);
form.approved.splice(configIndex, 1);
form.auditor.splice(configIndex, 1);
};
/** 给指定专业配置行添加人员(支持所有角色) */
const addPerson = (type: 'designers' | 'reviewers' | 'approved' | 'auditor', configIndex: number) => {
// 确保分组存在
if (form[type].length <= configIndex) {
ElMessage.warning('专业配置不存在');
return;
}
// 添加空人员(后续选择)
form[type][configIndex].persons.push({ userId: null });
// 滚动到新增的人员选择框
setTimeout(() => {
const selects = document.querySelectorAll(`[data-v-${proxy?.$options.__scopeId}] .el-select`);
if (selects.length > 0) {
selects[selects.length - 1].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, 100);
};
/** 从指定专业配置行删除人员(支持所有角色) */
const removePerson = (type: 'designers' | 'reviewers' | 'approved' | 'auditor', configIndex: number, personIndex: number) => {
const targetGroup = form[type][configIndex];
if (!targetGroup) {
ElMessage.warning('专业配置不存在');
return;
}
if (targetGroup.persons.length <= 1) {
const roleNames = {
designers: '设计',
reviewers: '校审',
approved: '审定',
auditor: '审核'
};
ElMessage.warning(`该专业至少保留一个${roleNames[type]}人员`);
return;
}
targetGroup.persons.splice(personIndex, 1);
};
/** 专业变更时:同步所有角色的专业值并重置人员(核心修复) */
const handleMajorChange = (newMajor: string | null, configIndex: number) => {
// 过滤无效专业值
if (!newMajor || !des_user_major.value.some((item) => item.value == newMajor)) {
ElMessage.warning('选择的专业无效,请重新选择');
return;
}
// 1. 同步所有角色的专业值(确保统一)
form.designers[configIndex].userMajor = newMajor;
form.reviewers[configIndex].userMajor = newMajor;
form.approved[configIndex].userMajor = newMajor;
form.auditor[configIndex].userMajor = newMajor;
// 2. 重置当前专业下的所有人员(避免专业与人员不匹配)
form.designers[configIndex].persons = [{ userId: null }];
form.reviewers[configIndex].persons = [{ userId: null }];
form.approved[configIndex].persons = [{ userId: null }];
form.auditor[configIndex].persons = [{ userId: null }];
ElMessage.success(`已切换为「${getMajorLabel(newMajor)}」专业,请重新选择人员`);
};
/** 重复校验:同一角色内「专业+人员」组合唯一 */
const checkDuplicate = (
currentPerson: Person,
role: 'designers' | 'reviewers' | 'approved' | 'auditor',
configIndex: number,
personIndex: number
) => {
const currentGroup = form[role][configIndex];
const currentMajor = form.designers[configIndex].userMajor; // 统一专业来源
// 未选专业/人员时不校验
if (!currentMajor || !currentPerson.userId) return;
const currentKey = `${currentMajor}-${currentPerson.userId}`;
let duplicateItem: Person | null = null;
// 1. 检查当前专业行内是否有重复人员
duplicateItem = currentGroup.persons.find((item, idx) => idx !== personIndex && item.userId == currentPerson.userId);
if (duplicateItem) {
ElMessage.warning(`当前专业下「${getUserName(currentPerson.userId)}」已存在,请重新选择`);
currentPerson.userId = null;
return;
}
// 2. 检查同一角色其他专业行是否有重复
form[role].forEach((group, gIdx) => {
if (gIdx == configIndex) return;
const groupMajor = form.designers[gIdx].userMajor;
if (!groupMajor) return;
group.persons.forEach((item) => {
if (item.userId && `${groupMajor}-${item.userId}` == currentKey) {
duplicateItem = item;
}
});
});
if (duplicateItem) {
ElMessage.warning(`${getMajorLabel(currentMajor)}+${getUserName(currentPerson.userId)}」组合已存在,请重新选择`);
currentPerson.userId = null;
}
};
/** 辅助函数通过专业值获取专业名称避免显示value */
const getMajorLabel = (majorValue: string | null) => {
if (!majorValue) return '';
const major = des_user_major.value.find((item) => item.value == majorValue);
return major ? major.label : majorValue;
};
/** 辅助函数通过用户ID获取用户名 */
const getUserName = (userId: number | null) => {
if (!userId) return '';
const user = userList.value.find((item) => item.userId == userId);
return user ? user.nickName : `${userId}`;
};
/** 提交表单(核心:确保专业值正确传递) */
const submitForm = async () => {
if (!leaveFormRef.value) {
ElMessage.warning('表单实例未加载,请刷新重试');
return;
}
try {
// 1. 基础表单验证(确保所有必填项已填)
await leaveFormRef.value.validate();
// 2. 提交前二次校验:专业值非空(双重保障)
const hasEmptyMajor = form.designers.some((group) => !group.userMajor);
if (hasEmptyMajor) {
ElMessage.error('存在未选择专业的配置,请完善后提交');
return;
}
// 3. 提交前二次校验:「专业+人员」组合唯一(所有角色)
let hasDuplicate = false;
const validateRoleDuplicate = (roleGroups: MajorGroup[], roleName: string) => {
const keys: string[] = [];
roleGroups.forEach((group, gIdx) => {
const major = form.designers[gIdx].userMajor;
if (!major) return;
group.persons.forEach((person) => {
if (!person.userId) return;
const key = `${major}-${person.userId}`;
if (keys.includes(key)) {
hasDuplicate = true;
ElMessage.error(`${roleName}中存在重复的「专业+人员」组合,请检查`);
}
keys.push(key);
});
});
};
validateRoleDuplicate(form.designers, '设计人员');
if (hasDuplicate) return;
validateRoleDuplicate(form.reviewers, '校审人员');
if (hasDuplicate) return;
validateRoleDuplicate(form.approved, '审定人员');
if (hasDuplicate) return;
validateRoleDuplicate(form.auditor, '审核人员');
if (hasDuplicate) return;
// 4. 构建提交数据(确保专业值正确传递)
const submitData = {
projectId: form.projectId,
personnel: [
// 设计负责人userType=1
form.designLeader
? {
userId: form.designLeader,
userType: 'designLeader',
userMajor: null
}
: null,
// 设计人员userType=2
...form.designers.flatMap((group, gIdx) => {
const major = form.designers[gIdx].userMajor; // 统一专业来源
return group.persons
.filter((person) => person.userId) // 过滤空人员
.map((person) => ({
userId: person.userId,
userType: 'designer',
userMajor: major // 确保专业值非空
}));
}),
// 校审人员userType=3
...form.reviewers.flatMap((group, gIdx) => {
const major = form.designers[gIdx].userMajor;
return group.persons
.filter((person) => person.userId)
.map((person) => ({
userId: person.userId,
userType: 'reviewer',
userMajor: major
}));
}),
// 审定人员userType=4
...form.approved.flatMap((group, gIdx) => {
const major = form.designers[gIdx].userMajor;
return group.persons
.filter((person) => person.userId)
.map((person) => ({
userId: person.userId,
userType: 'approved',
userMajor: major
}));
}),
// 审核人员userType=5
...form.auditor.flatMap((group, gIdx) => {
const major = form.designers[gIdx].userMajor;
return group.persons
.filter((person) => person.userId)
.map((person) => ({
userId: person.userId,
userType: 'auditor',
userMajor: major
}));
})
].filter(Boolean) // 过滤null值如未选设计负责人
};
// 5. 数据格式转换适配后端userType定义
const submitList = submitData.personnel.map((item: any) => {
let userType = 1; // 默认设计负责人
if (item.userType == 'designer') userType = 2;
else if (item.userType == 'reviewer') userType = 3;
else if (item.userType == 'approved') userType = 4;
else if (item.userType == 'auditor') userType = 5;
// 查找用户名确保userName非空
const user = userList.value.find((u) => u.userId == item.userId);
const userName = user ? user.nickName : `用户${item.userId}`;
return {
userName,
projectId: submitData.projectId,
userId: item.userId,
userType,
userMajor: item.userMajor // 最终传递的专业值(确保非空)
};
});
// 6. 提交到后端(确保参数正确)
const loading = ElLoading.service({ text: '提交中...', background: 'rgba(255,255,255,0.7)' });
const res = await designUserAdd({
list: submitList,
projectId: form.projectId
});
if (res.code == 200) {
disabledForm.value = true;
ElMessage.success('提交成功,已保存人员配置');
// 提交成功后重新加载数据(确保回显正确)
await designUser();
} else {
ElMessage.error(res.msg || '提交失败,请重试');
}
} catch (error: any) {
// 捕获表单验证错误或其他异常
if (error.name == 'ValidationError') {
ElMessage.error('请完善表单必填项后再提交');
} else {
ElMessage.error('提交过程异常,请刷新页面重试');
console.error('表单提交异常:', error);
}
} finally {
ElLoading.service().close();
}
};
/** 重置表单(确保恢复初始状态) */
const resetForm = async () => {
if (!leaveFormRef.value) return;
// 重置表单字段
leaveFormRef.value.resetFields();
// 重置数据结构(恢复为初始空分组)
form.designers = [createEmptyMajorGroup()];
form.reviewers = [createEmptyMajorGroup()];
form.approved = [createEmptyMajorGroup()];
form.auditor = [createEmptyMajorGroup()];
form.designLeader = null;
ElMessage.info('表单已重置,请重新配置人员');
};
/** 监听项目ID变化刷新用户列表和表单数据 */
const listeningProject: WatchStopHandle = watch(
() => currentProject.value?.id,
async (newProjectId) => {
if (newProjectId) {
form.projectId = newProjectId;
// 先获取用户列表,再加载表单数据
await getDeptAllUser(userStore.deptId);
await designUser();
}
},
{ immediate: true } // 初始加载时触发
);
/** 监听专业字典变化:确保专业下拉框数据最新 */
watch(
() => des_user_major.value,
(newMajorList) => {
if (newMajorList.length == 0) {
ElMessage.warning('专业字典数据为空,请联系管理员配置');
}
},
{ deep: true }
);
// 页面生命周期
onUnmounted(() => {
// 清除监听
listeningProject();
});
onMounted(async () => {
// 初始加载:先获取部门用户,再加载表单数据
await getDeptAllUser(userStore.deptId);
await designUser();
// 打印初始数据(调试用)
console.log('初始专业字典:', des_user_major.value);
console.log('初始用户列表:', userList.value);
});
</script>
<style lang="scss" scoped>
.appointment {
width: 100%;
max-width: 1800px;
.el-select__wrapper {
width: 100% !important;
}
.fonts {
.el-form-item--default .el-form-item__label {
font-size: 18px !important;
font-weight: 600;
}
}
}
// 自定义动画:新增专业行时的过渡效果
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fadeIn {
animation: fadeIn 0.3s ease-out forwards;
}
// 表单样式优化:统一间距和字体
::v-deep .el-form {
--el-form-item-margin-bottom: 0;
}
::v-deep .el-form-item {
margin-bottom: 0;
&__label {
font-weight: 500;
color: #4e5969;
font-size: 14px;
}
&__content {
padding: 0;
font-size: 14px;
}
// 表单验证错误提示样式
&__error {
font-size: 12px;
margin-top: 4px;
}
}
// 选择器样式:统一高度和边框
::v-deep .el-select {
width: 100%;
.el-input__inner {
border-radius: 6px;
transition: all 0.3s ease;
padding: 0 12px;
height: 32px; // 统一高度,避免布局错乱
font-size: 14px;
}
&:hover .el-input__inner {
border-color: #66b1ff;
}
&.el-select-focus .el-input__inner {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
}
// 按钮样式:统一大小和颜色
::v-deep .el-button {
border-radius: 4px;
padding: 4px 8px;
font-size: 12px;
&--small {
padding: 2px 6px;
font-size: 12px;
}
// 角色区分色(提升视觉识别)
&.el-button--success {
background-color: #67c23a;
border-color: #67c23a;
&:hover {
background-color: #85ce61;
border-color: #85ce61;
}
&:disabled {
background-color: #b3e099;
border-color: #b3e099;
}
}
&.el-button--danger {
background-color: #f56c6c;
border-color: #f56c6c;
&:hover {
background-color: #f78989;
border-color: #f78989;
}
&:disabled {
background-color: #ffcccc;
border-color: #ffbbbb;
}
}
&.el-button--text {
color: #f56c6c;
&:hover {
color: #f78989;
background-color: rgba(245, 108, 108, 0.05);
}
}
}
// 确保表头不换行
.whitespace-nowrap {
white-space: nowrap;
}
// 响应式处理:小屏幕横向滚动,避免挤压
@media (max-width: 1200px) {
::v-deep .el-row {
flex-wrap: nowrap !important; // 强制不换行
}
::v-deep .el-col {
flex-shrink: 0 !important; // 禁止列收缩
}
.appointment {
width: 98vw;
}
// 小屏幕下调整标签宽度,节省空间
::v-deep .el-form-item--default .el-form-item__label {
width: 50px !important;
}
}
// 空状态样式优化:统一视觉效果
::v-deep .text-gray-500.text-xs {
text-align: center;
background-color: #f9fafb;
border-color: #e5e7eb;
}
</style>