2025-08-19 10:19:29 +08:00
|
|
|
|
<template>
|
|
|
|
|
<div class="p-6 bg-gray-50">
|
2025-08-19 14:56:48 +08:00
|
|
|
|
<div class="appointment mx-auto bg-white rounded-xl shadow-sm overflow-hidden transition-all duration-300 hover:shadow-md">
|
2025-08-19 10:19:29 +08:00
|
|
|
|
<!-- 表单标题区域 -->
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
<!-- 表头 -->
|
|
|
|
|
<el-row :gutter="20" class="mb-3 font-medium text-gray-700">
|
|
|
|
|
<el-col :span="6" :xs="24" :sm="8">专业</el-col>
|
|
|
|
|
<el-col :span="9" :xs="24" :sm="8">设计人员(可多选)</el-col>
|
|
|
|
|
<el-col :span="9" :xs="24" :sm="8">校审人员(可多选)</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<!-- 分割线 -->
|
|
|
|
|
<el-divider class="my-4" />
|
|
|
|
|
|
|
|
|
|
<!-- 专业配置行:专业(左)+ 设计人员(中)+ 校审人员(右) 横向排列 -->
|
|
|
|
|
<div
|
|
|
|
|
v-for="(majorConfig, configIndex) in combinedConfigs"
|
|
|
|
|
:key="configIndex"
|
|
|
|
|
style="background: aliceblue; border-radius: 10px"
|
|
|
|
|
class="mb-5 animate-fadeIn"
|
|
|
|
|
>
|
|
|
|
|
<el-row :gutter="20" class="items-top">
|
|
|
|
|
<!-- 左侧:专业选择 -->
|
|
|
|
|
<el-col :span="6" :xs="24" :sm="8" class="mb-4 sm:mb-0" style="margin-top: 8px">
|
|
|
|
|
<el-form-item
|
|
|
|
|
:prop="`designers.${configIndex}.userMajor`"
|
|
|
|
|
:rules="{ required: true, message: '请选择专业', trigger: 'change' }"
|
|
|
|
|
class="mb-0"
|
|
|
|
|
label-width="80px"
|
|
|
|
|
label="专业"
|
|
|
|
|
>
|
|
|
|
|
<!-- 专业选择下拉框 -->
|
|
|
|
|
<el-select
|
|
|
|
|
v-model="form.designers[configIndex].userMajor"
|
|
|
|
|
placeholder="请选择专业"
|
|
|
|
|
class="w-full transition-all duration-300 border-gray-300"
|
|
|
|
|
@change="(val) => handleMajorChange(val, configIndex)"
|
|
|
|
|
>
|
|
|
|
|
<!-- 临时添加调试显示 -->
|
|
|
|
|
<template v-if="des_user_major && des_user_major.length > 0">
|
|
|
|
|
<el-option v-for="item in des_user_major" :key="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>
|
|
|
|
|
|
|
|
|
|
<!-- 中间:设计人员 -->
|
|
|
|
|
<el-col :span="9" :xs="24" :sm="8" class="mb-4 sm:mb-0">
|
|
|
|
|
<div class="pl-0 sm:pl-4 border-l-0 sm:border-l-2 border-blue-200 py-0 sm:py-2">
|
|
|
|
|
<!-- 设计人员列表 -->
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
<div v-for="(person, personIndex) in majorConfig.designPersons" :key="personIndex" class="flex items-center">
|
|
|
|
|
<el-form-item
|
|
|
|
|
:prop="`designers.${configIndex}.persons.${personIndex}.userId`"
|
|
|
|
|
:rules="{ required: true, message: '请选择人员', trigger: 'change' }"
|
|
|
|
|
class="flex-1 mr-3 mb-0"
|
|
|
|
|
label="设计人员"
|
|
|
|
|
label-width="80px"
|
|
|
|
|
>
|
|
|
|
|
<el-select
|
|
|
|
|
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="item.userId" :label="item.nickName" :value="item.userId" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<div>
|
|
|
|
|
<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="16">
|
|
|
|
|
<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="!majorConfig.userMajor || disabledForm"
|
|
|
|
|
>
|
|
|
|
|
<el-icon :size="16">
|
|
|
|
|
<Plus />
|
|
|
|
|
</el-icon>
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 空状态提示 -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="majorConfig.designPersons.length == 0"
|
|
|
|
|
class="text-gray-500 text-sm py-2 bg-gray-100 rounded-lg border border-dashed border-gray-200 mt-1"
|
|
|
|
|
>
|
|
|
|
|
暂无设计人员,请点击"添加设计人员"
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
|
|
|
|
|
<!-- 右侧:校审人员 -->
|
|
|
|
|
<el-col :span="9" :xs="24" :sm="8">
|
|
|
|
|
<div class="pl-0 sm:pl-4 border-l-0 sm:border-l-2 border-green-200 py-0 sm:py-2">
|
|
|
|
|
<!-- 校审人员列表 -->
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
<div v-for="(person, personIndex) in majorConfig.reviewPersons" :key="personIndex" class="flex items-center">
|
|
|
|
|
<el-form-item
|
|
|
|
|
:prop="`reviewers.${configIndex}.persons.${personIndex}.userId`"
|
|
|
|
|
:rules="{ required: true, message: '请选择人员', trigger: 'change' }"
|
|
|
|
|
class="flex-1 mr-3 mb-0"
|
|
|
|
|
label="校审人员"
|
|
|
|
|
label-width="80px"
|
|
|
|
|
>
|
|
|
|
|
<el-select
|
|
|
|
|
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="item.userId" :label="item.nickName" :value="item.userId" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<div>
|
|
|
|
|
<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="16">
|
|
|
|
|
<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="!majorConfig.userMajor || disabledForm"
|
|
|
|
|
>
|
|
|
|
|
<el-icon :size="16">
|
|
|
|
|
<Plus />
|
|
|
|
|
</el-icon>
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 空状态提示 -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="majorConfig.reviewPersons.length == 0"
|
|
|
|
|
class="text-gray-500 text-sm py-2 bg-gray-100 rounded-lg border border-dashed border-gray-200 mt-1"
|
|
|
|
|
>
|
|
|
|
|
暂无校审人员,请点击"添加校审人员"
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<!-- 删除专业配置行 -->
|
|
|
|
|
<el-row class="mt-2">
|
|
|
|
|
<el-col :span="24" 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>
|
|
|
|
|
|
|
|
|
|
<!-- 提交按钮区域 -->
|
2025-08-21 12:03:34 +08:00
|
|
|
|
<div v-if="!disabledForm" class="flex justify-center space-x-6 mt-8 pt-6 border-t border-gray-100">
|
2025-08-19 10:19:29 +08:00
|
|
|
|
<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>
|
2025-08-21 12:03:34 +08:00
|
|
|
|
<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">
|
2025-08-19 10:19:29 +08:00
|
|
|
|
<i class="el-icon-refresh mr-2"></i>重置
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</el-form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup name="PersonnelForm" lang="ts">
|
|
|
|
|
import { ref, reactive, computed, onMounted, toRefs, watch, WatchStopHandle } from 'vue';
|
|
|
|
|
import { getCurrentInstance } from 'vue';
|
|
|
|
|
import type { ComponentInternalInstance } from 'vue';
|
|
|
|
|
import { useUserStoreHook } from '@/store/modules/user';
|
|
|
|
|
import { ElMessage, ElLoading } from 'element-plus';
|
|
|
|
|
import { Delete, Plus } from '@element-plus/icons-vue'; // 修复:添加Plus图标导入
|
|
|
|
|
import { designUserAdd, designUserList, systemUserList } from '@/api/design/appointment';
|
|
|
|
|
|
|
|
|
|
// 获取当前实例
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
|
|
// 获取用户 store
|
|
|
|
|
const userStore = useUserStoreHook();
|
|
|
|
|
// 从 store 中获取当前选中的项目
|
|
|
|
|
const currentProject = computed(() => userStore.selectedProject);
|
|
|
|
|
// 专业字典数据 - 增加默认空数组避免undefined
|
|
|
|
|
const { des_user_major = ref([]) } = toRefs<any>(proxy?.useDict('des_user_major') || {});
|
|
|
|
|
|
|
|
|
|
// 调试:打印专业数据
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
console.log('专业数据:', des_user_major.value);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 表单数据:保持原有数据结构不变
|
|
|
|
|
interface MajorGroup {
|
|
|
|
|
userMajor: string | null; // 专业
|
|
|
|
|
persons: Array<{ userId: number | null }>; // 该专业下的多个人员
|
|
|
|
|
}
|
|
|
|
|
const form = reactive({
|
|
|
|
|
projectId: currentProject.value?.id,
|
|
|
|
|
designLeader: null, // 设计负责人
|
|
|
|
|
designers: [] as MajorGroup[], // 设计人员:按专业分组,每组含多个人员
|
|
|
|
|
reviewers: [] as MajorGroup[] // 校审人员:按专业分组,每组含多个人员
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 组合配置用于视图展示(专业+设计人员+校审人员)
|
|
|
|
|
const combinedConfigs = computed(() => {
|
|
|
|
|
// 确保designers和reviewers数组长度一致
|
|
|
|
|
const maxLength = Math.max(form.designers.length, form.reviewers.length);
|
|
|
|
|
while (form.designers.length < maxLength) {
|
|
|
|
|
form.designers.push(createEmptyMajorGroup());
|
|
|
|
|
}
|
|
|
|
|
while (form.reviewers.length < maxLength) {
|
|
|
|
|
form.reviewers.push(createEmptyMajorGroup());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 组合数据用于视图展示
|
|
|
|
|
return form.designers.map((designerGroup, index) => ({
|
|
|
|
|
userMajor: designerGroup.userMajor,
|
|
|
|
|
designPersons: designerGroup.persons,
|
|
|
|
|
reviewPersons: form.reviewers[index].persons
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 表单验证规则
|
|
|
|
|
const rules = reactive({
|
|
|
|
|
designLeader: [{ required: true, message: '请选择设计负责人', trigger: 'change' }]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 用户列表
|
|
|
|
|
const userList = ref([]);
|
|
|
|
|
|
|
|
|
|
// 表单引用
|
|
|
|
|
const leaveFormRef = ref();
|
|
|
|
|
const disabledForm = ref(false); //控制提交按钮状态
|
|
|
|
|
|
|
|
|
|
/** 查询当前部门的所有用户 */
|
|
|
|
|
const getDeptAllUser = async (deptId: any) => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await systemUserList({ deptId });
|
|
|
|
|
userList.value = res.rows;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error('获取用户列表失败');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 查询当前表单数据并回显 */
|
|
|
|
|
const designUser = async () => {
|
|
|
|
|
if (!currentProject.value?.id) return;
|
|
|
|
|
|
|
|
|
|
const loading = ElLoading.service({
|
|
|
|
|
lock: true,
|
|
|
|
|
text: '加载配置数据中...',
|
|
|
|
|
background: 'rgba(255, 255, 255, 0.7)'
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
const res = await designUserList({ projectId: currentProject.value?.id });
|
|
|
|
|
// 清空现有数据
|
|
|
|
|
form.designLeader = null;
|
|
|
|
|
form.designers = [];
|
|
|
|
|
form.reviewers = [];
|
|
|
|
|
|
|
|
|
|
if (res.code == 200 && res.rows && res.rows.length > 0) {
|
|
|
|
|
disabledForm.value = true;
|
|
|
|
|
// 1. 分类整理数据(按用户类型)
|
|
|
|
|
const designLeader = res.rows.find((item) => item.userType == 1);
|
|
|
|
|
const designerItems = res.rows.filter((item) => item.userType == 2);
|
|
|
|
|
const reviewerItems = res.rows.filter((item) => item.userType == 3);
|
|
|
|
|
|
|
|
|
|
// 2. 回显设计负责人
|
|
|
|
|
if (designLeader) form.designLeader = designLeader.userId;
|
|
|
|
|
|
|
|
|
|
// 3. 回显设计人员(按专业分组)
|
|
|
|
|
form.designers = groupPersonByMajor(designerItems);
|
|
|
|
|
// 4. 回显校审人员(按专业分组)
|
|
|
|
|
form.reviewers = groupPersonByMajor(reviewerItems);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 补全默认空项(至少1个专业分组,每组至少1个人员)
|
|
|
|
|
if (form.designers.length == 0) form.designers.push(createEmptyMajorGroup());
|
|
|
|
|
if (form.reviewers.length == 0) form.reviewers.push(createEmptyMajorGroup());
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error('获取配置数据失败');
|
|
|
|
|
// 异常时初始化默认空项
|
|
|
|
|
form.designers = [createEmptyMajorGroup()];
|
|
|
|
|
form.reviewers = [createEmptyMajorGroup()];
|
|
|
|
|
} finally {
|
|
|
|
|
loading.close();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 辅助函数:创建空的专业分组(含1个空人员) */
|
|
|
|
|
const createEmptyMajorGroup = (): MajorGroup => ({
|
|
|
|
|
userMajor: null,
|
|
|
|
|
persons: [{ userId: null }]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/** 辅助函数:按专业分组整理人员数据(用于回显) */
|
|
|
|
|
const groupPersonByMajor = (items: any[]): MajorGroup[] => {
|
|
|
|
|
const groupMap: Record<string, MajorGroup> = {};
|
|
|
|
|
items.forEach((item) => {
|
|
|
|
|
const major = item.userMajor || '未分类';
|
|
|
|
|
// 不存在该专业分组则创建
|
|
|
|
|
if (!groupMap[major]) {
|
|
|
|
|
groupMap[major] = { userMajor: item.userMajor, persons: [] };
|
|
|
|
|
}
|
|
|
|
|
// 添加当前人员到专业分组
|
|
|
|
|
groupMap[major].persons.push({ userId: item.userId });
|
|
|
|
|
});
|
|
|
|
|
// 处理空分组(确保每组至少1个人员)
|
|
|
|
|
Object.values(groupMap).forEach((group) => {
|
|
|
|
|
if (group.persons.length == 0) group.persons.push({ userId: null });
|
|
|
|
|
});
|
|
|
|
|
return Object.values(groupMap);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 新增专业配置行 */
|
|
|
|
|
const addMajor = () => {
|
|
|
|
|
form.designers.push(createEmptyMajorGroup());
|
|
|
|
|
form.reviewers.push(createEmptyMajorGroup());
|
|
|
|
|
|
|
|
|
|
// 滚动到新增的专业配置行
|
|
|
|
|
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);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 给指定专业配置行添加人员 */
|
|
|
|
|
const addPerson = (type: 'designers' | 'reviewers', configIndex: number) => {
|
|
|
|
|
form[type][configIndex].persons.push({ userId: null });
|
|
|
|
|
|
|
|
|
|
// 滚动到新增的人员选择框
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const personSelects = document.querySelectorAll(`[data-v-${proxy?.$options.__scopeId}] .el-select`);
|
|
|
|
|
if (personSelects.length > 0) {
|
|
|
|
|
personSelects[personSelects.length - 1].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
|
|
|
}
|
|
|
|
|
}, 100);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 从指定专业配置行删除人员 */
|
|
|
|
|
const removePerson = (type: 'designers' | 'reviewers', configIndex: number, personIndex: number) => {
|
|
|
|
|
const targetGroup = form[type][configIndex];
|
|
|
|
|
if (targetGroup.persons.length <= 1) {
|
|
|
|
|
ElMessage.warning(`该专业至少保留一个${type == 'designers' ? '设计' : '校审'}人员`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
targetGroup.persons.splice(personIndex, 1);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 专业变更时:清空当前专业下的人员(避免专业与人员不匹配) */
|
|
|
|
|
const handleMajorChange = (newMajor: string, configIndex: number) => {
|
|
|
|
|
// 直接修改原始数据源,确保响应式生效
|
|
|
|
|
form.designers[configIndex].userMajor = newMajor;
|
|
|
|
|
form.reviewers[configIndex].userMajor = newMajor;
|
|
|
|
|
form.designers[configIndex].persons = [{ userId: null }];
|
|
|
|
|
form.reviewers[configIndex].persons = [{ userId: null }];
|
|
|
|
|
// ElMessage.info(`已重置「${getMajorLabel(newMajor)}」专业下的人员,请重新选择`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ========== 核心:重复校验逻辑 ==========
|
|
|
|
|
/**
|
|
|
|
|
* 校验同一角色内(设计/校审)的「专业+人员」组合唯一性
|
|
|
|
|
*/
|
|
|
|
|
const checkDuplicate = (current: { userId: number | null }, role: 'designers' | 'reviewers', configIndex: number, personIndex: number) => {
|
|
|
|
|
console.log(`校验触发 - 角色: ${role}, 专业索引: ${configIndex}, 人员索引: ${personIndex}, 人员ID: ${current.userId}`);
|
|
|
|
|
console.log(form);
|
|
|
|
|
|
|
|
|
|
const currentGroup = form[role][configIndex];
|
|
|
|
|
// 未选专业/人员时不校验
|
|
|
|
|
if (!currentGroup.userMajor || !current.userId) return;
|
|
|
|
|
|
|
|
|
|
// 生成当前「专业+人员」唯一标识
|
|
|
|
|
const currentKey = `${currentGroup.userMajor}-${current.userId}`;
|
|
|
|
|
let duplicateItem = null;
|
|
|
|
|
|
|
|
|
|
// 1. 检查当前专业配置行内是否有重复人员
|
|
|
|
|
duplicateItem = currentGroup.persons.find((item, idx) => {
|
|
|
|
|
return idx !== personIndex && item.userId == current.userId;
|
|
|
|
|
});
|
|
|
|
|
if (duplicateItem) {
|
|
|
|
|
ElMessage.warning(`当前专业下「${getUserName(current.userId)}」已存在,请重新选择`);
|
|
|
|
|
current.userId = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 检查同一角色内其他专业配置行是否有重复(专业+人员唯一)
|
|
|
|
|
form[role].forEach((group, gIdx) => {
|
|
|
|
|
if (gIdx == configIndex) return; // 跳过当前配置行
|
|
|
|
|
group.persons.forEach((item) => {
|
|
|
|
|
if (`${group.userMajor}-${item.userId}` == currentKey) {
|
|
|
|
|
duplicateItem = item;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (duplicateItem) {
|
|
|
|
|
ElMessage.warning(`「${getMajorLabel(currentGroup.userMajor)}+${getUserName(current.userId)}」组合已存在,请重新选择`);
|
|
|
|
|
current.userId = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 辅助函数:通过专业值获取专业名称 */
|
|
|
|
|
const getMajorLabel = (majorValue: string | null) => {
|
|
|
|
|
if (!majorValue || !des_user_major.value) return '';
|
|
|
|
|
const major = des_user_major.value.find((item: any) => item.value == majorValue);
|
|
|
|
|
return major ? major.label : majorValue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 辅助函数:通过用户ID获取用户名 */
|
|
|
|
|
const getUserName = (userId: number | null) => {
|
|
|
|
|
if (!userId || !userList.value.length) return '';
|
|
|
|
|
const user = userList.value.find((item: any) => item.userId == userId);
|
|
|
|
|
return user ? user.nickName : userId;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 提交表单(保持原有数据结构) */
|
|
|
|
|
const submitForm = async () => {
|
|
|
|
|
if (!leaveFormRef.value) return;
|
|
|
|
|
try {
|
|
|
|
|
// 1. 基础表单验证
|
|
|
|
|
await leaveFormRef.value.validate();
|
|
|
|
|
// 2. 提交前二次校验:「专业+人员」组合唯一性
|
|
|
|
|
let hasDuplicate = false;
|
|
|
|
|
const allKeys: string[] = [];
|
|
|
|
|
|
|
|
|
|
// 收集所有「专业+人员」组合(设计+校审分开校验)
|
|
|
|
|
const collectKeys = (roleGroups: MajorGroup[], roleName: string) => {
|
|
|
|
|
roleGroups.forEach((group) => {
|
|
|
|
|
if (!group.userMajor) return;
|
|
|
|
|
group.persons.forEach((person) => {
|
|
|
|
|
if (!person.userId) return;
|
|
|
|
|
const key = `${group.userMajor}-${person.userId}`;
|
|
|
|
|
if (allKeys.includes(key)) {
|
|
|
|
|
hasDuplicate = true;
|
|
|
|
|
ElMessage.error(`${roleName}中存在重复的「专业+人员」组合,请检查`);
|
|
|
|
|
}
|
|
|
|
|
allKeys.push(key);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 校验设计人员
|
|
|
|
|
collectKeys(form.designers, '设计人员');
|
|
|
|
|
if (hasDuplicate) return;
|
|
|
|
|
|
|
|
|
|
// 清空临时数组,校验校审人员(不校验设计与校审之间)
|
|
|
|
|
allKeys.length = 0;
|
|
|
|
|
collectKeys(form.reviewers, '校审人员');
|
|
|
|
|
if (hasDuplicate) return;
|
|
|
|
|
|
|
|
|
|
// 3. 构建提交数据(适配后端原有数据格式)
|
|
|
|
|
const submitData = {
|
|
|
|
|
projectId: form.projectId,
|
|
|
|
|
personnel: [
|
|
|
|
|
// 设计负责人
|
|
|
|
|
{
|
|
|
|
|
userId: form.designLeader,
|
|
|
|
|
userType: 'designLeader',
|
|
|
|
|
userMajor: null
|
|
|
|
|
},
|
|
|
|
|
// 设计人员:展开专业分组,每个人员单独作为一条数据
|
|
|
|
|
...form.designers.flatMap((group) =>
|
|
|
|
|
group.persons.map((person) => ({
|
|
|
|
|
userId: person.userId,
|
|
|
|
|
userType: 'designer',
|
|
|
|
|
userMajor: group.userMajor
|
|
|
|
|
}))
|
|
|
|
|
),
|
|
|
|
|
// 校审人员:展开专业分组,每个人员单独作为一条数据
|
|
|
|
|
...form.reviewers.flatMap((group) =>
|
|
|
|
|
group.persons.map((person) => ({
|
|
|
|
|
userId: person.userId,
|
|
|
|
|
userType: 'reviewer',
|
|
|
|
|
userMajor: group.userMajor
|
|
|
|
|
}))
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
// 4. 数据处理(保持原有逻辑不变)
|
|
|
|
|
const arr = [];
|
|
|
|
|
userList.value.forEach((item) => {
|
|
|
|
|
submitData.personnel.forEach((item1) => {
|
|
|
|
|
if (item1.userId == item.userId) {
|
|
|
|
|
let userType = 1;
|
|
|
|
|
if (item1.userType == 'designer') userType = 2;
|
|
|
|
|
else if (item1.userType == 'reviewer') userType = 3;
|
|
|
|
|
arr.push({
|
|
|
|
|
userName: item.nickName,
|
|
|
|
|
projectId: submitData.projectId,
|
|
|
|
|
userId: item1.userId,
|
|
|
|
|
userType: userType,
|
|
|
|
|
userMajor: item1.userMajor
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 5. 提交到后端(保持原有逻辑不变)
|
|
|
|
|
const loading = ElLoading.service({ text: '提交中...', background: 'rgba(255,255,255,0.7)' });
|
|
|
|
|
const res = await designUserAdd({
|
|
|
|
|
list: arr,
|
|
|
|
|
projectId: currentProject.value?.id
|
|
|
|
|
});
|
|
|
|
|
if (res.code == 200) {
|
|
|
|
|
disabledForm.value = true;
|
2025-08-19 20:01:29 +08:00
|
|
|
|
loading.close();
|
2025-08-19 10:19:29 +08:00
|
|
|
|
ElMessage.success('提交成功');
|
|
|
|
|
} else {
|
|
|
|
|
ElMessage.error(res.msg || '提交失败');
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error('请完善表单信息后再提交');
|
|
|
|
|
} finally {
|
2025-08-19 20:01:29 +08:00
|
|
|
|
// ElLoading.service().close();
|
2025-08-19 10:19:29 +08:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 重置表单(适配新数据结构) */
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
if (leaveFormRef.value) {
|
|
|
|
|
leaveFormRef.value.resetFields();
|
|
|
|
|
// 重置为默认空状态(1个专业分组,每组1个空人员)
|
|
|
|
|
form.designers = [createEmptyMajorGroup()];
|
|
|
|
|
form.reviewers = [createEmptyMajorGroup()];
|
|
|
|
|
ElMessage.info('表单已重置');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 监听项目ID刷新数据
|
|
|
|
|
const listeningProject: WatchStopHandle = watch(
|
|
|
|
|
() => currentProject.value?.id,
|
|
|
|
|
() => {
|
|
|
|
|
getDeptAllUser(userStore.deptId).then(() => {
|
|
|
|
|
designUser();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 页面生命周期
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
listeningProject();
|
|
|
|
|
});
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
getDeptAllUser(userStore.deptId).then(() => {
|
|
|
|
|
designUser();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
2025-08-19 14:56:48 +08:00
|
|
|
|
.appointment {
|
2025-08-19 10:19:29 +08:00
|
|
|
|
width: 70vw;
|
|
|
|
|
max-width: 1600px;
|
|
|
|
|
|
|
|
|
|
.el-select__wrapper {
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-button--small {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.fonts {
|
|
|
|
|
.el-form-item--default .el-form-item__label {
|
|
|
|
|
font-size: 18px !important;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 自定义动画
|
|
|
|
|
@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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&__content {
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::v-deep .el-select {
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
|
|
.el-input__inner {
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&: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: 6px;
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
|
|
|
|
&--primary {
|
|
|
|
|
background-color: #409eff;
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background-color: #66b1ff;
|
|
|
|
|
border-color: #66b1ff;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&--success {
|
|
|
|
|
background-color: #67c23a;
|
|
|
|
|
border-color: #67c23a;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background-color: #85ce61;
|
|
|
|
|
border-color: #85ce61;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&:disabled {
|
|
|
|
|
background-color: #b3e099;
|
|
|
|
|
border-color: #b3e099;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&--danger {
|
|
|
|
|
background-color: #f56c6c;
|
|
|
|
|
border-color: #f56c6c;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background-color: #f78989;
|
|
|
|
|
border-color: #f78989;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&:disabled {
|
|
|
|
|
background-color: #ffcccc;
|
|
|
|
|
border-color: #ffbbbb;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&--text {
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
color: #f78989;
|
|
|
|
|
background-color: rgba(245, 108, 108, 0.05);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 响应式网格布局
|
|
|
|
|
.grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.grid-cols-1 {
|
|
|
|
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.md\:grid-cols-2 {
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gap-4 {
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 适配小屏幕(小于768px时,垂直排列)
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.appWidth {
|
|
|
|
|
width: 95vw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::v-deep .el-form {
|
|
|
|
|
padding: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::v-deep .el-form-item__label {
|
|
|
|
|
width: 100px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 小屏幕下各列上下间距
|
|
|
|
|
::v-deep .el-col-xs-24 + .el-col-xs-24 {
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|