Files
td_official/src/views/design/appointment/index.vue

1097 lines
42 KiB
Vue
Raw Normal View History

2025-08-19 10:19:29 +08:00
<template>
<div class="p-6 bg-gray-50">
2025-08-28 23:32:17 +08:00
<!-- 外层容器添加横向滚动支持 -->
<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>
2025-08-19 10:19:29 +08:00
</div>
2025-08-28 23:32:17 +08:00
<!-- 表单内容区域 -->
<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">
2025-08-19 10:19:29 +08:00
<el-select
2025-08-28 23:32:17 +08:00
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"
2025-08-19 10:19:29 +08:00
>
2025-08-28 23:32:17 +08:00
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" />
2025-08-19 10:19:29 +08:00
</el-select>
</el-form-item>
</el-col>
2025-08-28 23:32:17 +08:00
</el-row>
</div>
2025-08-19 10:19:29 +08:00
2025-08-28 23:32:17 +08:00
<!-- 专业人员配置专业 + 设计 + 校审 + 审定 + 审核 横向排列 -->
<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>
2025-08-29 16:50:24 +08:00
<el-col :span="5">校审人员</el-col>
<el-col :span="5">审定人员</el-col>
<el-col :span="4">审核人员</el-col>
2025-08-28 23:32:17 +08:00
<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. 专业选择核心统一所有角色的专业来源 -->
2025-08-29 16:50:24 +08:00
<el-col :span="3" class="mb-4 sm:mb-0 pl-4" style="margin-top:8px;">
2025-08-28 23:32:17 +08:00
<el-form-item
:prop="`designers.${configIndex}.userMajor`"
:rules="[
{ required: true, message: '请选择专业', trigger: 'change' },
{ validator: validateMajor, trigger: 'change' }
]"
class="mb-0"
label-width="60px"
label="专业"
>
2025-08-29 20:57:49 +08:00
<el-select filterable
2025-08-28 23:32:17 +08:00
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"
2025-08-19 10:19:29 +08:00
>
2025-08-28 23:32:17 +08:00
<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"
2025-08-19 10:19:29 +08:00
>
2025-08-29 20:57:49 +08:00
<el-select filterable
2025-08-28 23:32:17 +08:00
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>
2025-08-19 10:19:29 +08:00
</div>
</div>
2025-08-28 23:32:17 +08:00
<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>
2025-08-19 10:19:29 +08:00
</div>
2025-08-28 23:32:17 +08:00
</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"
2025-08-19 10:19:29 +08:00
>
2025-08-28 23:32:17 +08:00
<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"
2025-08-19 10:19:29 +08:00
>
2025-08-29 20:57:49 +08:00
<el-select filterable
2025-08-28 23:32:17 +08:00
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>
2025-08-29 16:50:24 +08:00
<!-- <div class="flex gap-1">
2025-08-28 23:32:17 +08:00
<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>
2025-08-29 16:50:24 +08:00
</div> -->
2025-08-28 23:32:17 +08:00
</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"
2025-08-19 10:19:29 +08:00
>
2025-08-29 20:57:49 +08:00
<el-select filterable
2025-08-28 23:32:17 +08:00
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>
2025-08-29 16:50:24 +08:00
<!-- <div class="flex gap-1">
2025-08-28 23:32:17 +08:00
<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>
2025-08-29 16:50:24 +08:00
</div> -->
2025-08-28 23:32:17 +08:00
</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"
2025-08-19 10:19:29 +08:00
>
2025-08-29 20:57:49 +08:00
<el-select filterable
2025-08-28 23:32:17 +08:00
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>
2025-08-29 16:50:24 +08:00
<!-- <div class="flex gap-1">
2025-08-28 23:32:17 +08:00
<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>
2025-08-29 16:50:24 +08:00
</div> -->
2025-08-19 10:19:29 +08:00
</div>
</div>
2025-08-28 23:32:17 +08:00
<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>
2025-08-19 10:19:29 +08:00
</div>
2025-08-28 23:32:17 +08:00
</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"
2025-08-19 10:19:29 +08:00
>
2025-08-28 23:32:17 +08:00
<i class="el-icon-delete mr-1"></i>删除专业
</el-button>
</el-col>
</el-row>
</div>
2025-08-19 10:19:29 +08:00
</div>
2025-08-28 23:32:17 +08:00
<!-- 提交按钮区域 -->
<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>
2025-08-19 10:19:29 +08:00
</div>
</div>
</template>
<script setup name="PersonnelForm" lang="ts">
2025-08-28 23:32:17 +08:00
import { ref, reactive, computed, onMounted, toRefs, watch, WatchStopHandle, type FormItemRule } from 'vue';
2025-08-19 10:19:29 +08:00
import { getCurrentInstance } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { useUserStoreHook } from '@/store/modules/user';
2025-08-28 23:32:17 +08:00
import { ElMessage, ElLoading, ElForm } from 'element-plus';
import { Delete, Plus } from '@element-plus/icons-vue';
2025-08-19 10:19:29 +08:00
import { designUserAdd, designUserList, systemUserList } from '@/api/design/appointment';
// 获取当前实例
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
2025-08-28 23:32:17 +08:00
// 专业字典数据(确保默认空数组)
const { des_user_major = ref<Array<{ label: string; value: string }>>([]) } = toRefs<any>(proxy?.useDict('des_user_major') || {});
2025-08-19 10:19:29 +08:00
2025-08-28 23:32:17 +08:00
// 表单数据定义(明确类型)
interface Person {
userId: number | null;
}
2025-08-19 10:19:29 +08:00
interface MajorGroup {
2025-08-28 23:32:17 +08:00
userMajor: string | null; // 专业值与字典value对应
persons: Person[]; // 该专业下的人员
2025-08-19 10:19:29 +08:00
}
const form = reactive({
2025-08-28 23:32:17 +08:00
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
2025-08-19 10:19:29 +08:00
});
2025-08-28 23:32:17 +08:00
// 组合配置:统一所有角色的专业和人员(确保长度一致)
2025-08-19 10:19:29 +08:00
const combinedConfigs = computed(() => {
2025-08-28 23:32:17 +08:00
// 取所有角色的最大长度,确保数组长度统一
const maxLength = Math.max(form.designers.length, form.reviewers.length, form.approved.length, form.auditor.length);
2025-08-19 10:19:29 +08:00
2025-08-28 23:32:17 +08:00
// 补全空分组(确保每个角色数组长度相同,避免渲染异常)
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的专业值确保同步
2025-08-19 10:19:29 +08:00
return form.designers.map((designerGroup, index) => ({
2025-08-28 23:32:17 +08:00
userMajor: designerGroup.userMajor, // 统一专业来源
2025-08-19 10:19:29 +08:00
designPersons: designerGroup.persons,
2025-08-28 23:32:17 +08:00
reviewPersons: form.reviewers[index].persons,
approvedPersons: form.approved[index].persons,
auditorPersons: form.auditor[index].persons
2025-08-19 10:19:29 +08:00
}));
});
2025-08-28 23:32:17 +08:00
// 表单验证规则(新增专业有效性校验)
2025-08-19 10:19:29 +08:00
const rules = reactive({
designLeader: [{ required: true, message: '请选择设计负责人', trigger: 'change' }]
});
2025-08-28 23:32:17 +08:00
// 自定义校验:确保专业值在字典中存在(避免无效值)
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('选择的专业无效,请重新选择'));
}
}
};
2025-08-19 10:19:29 +08:00
2025-08-28 23:32:17 +08:00
// 其他基础变量
const userList = ref<Array<{ userId: number; nickName: string }>>([]);
const leaveFormRef = ref<InstanceType<typeof ElForm> | null>(null);
const disabledForm = ref(true); // 初始禁用表单(编辑时开启)
2025-08-19 10:19:29 +08:00
2025-08-28 23:32:17 +08:00
/** 查询当前部门的所有用户(确保用户列表有效) */
const getDeptAllUser = async (deptId: number | undefined) => {
2025-08-29 16:12:14 +08:00
console.log(1111111111111);
2025-08-28 23:32:17 +08:00
if (!deptId) {
ElMessage.warning('请先选择部门');
return;
}
2025-08-19 10:19:29 +08:00
try {
const res = await systemUserList({ deptId });
2025-08-28 23:32:17 +08:00
userList.value = res.rows || [];
if (userList.value.length == 0) {
ElMessage.info('当前部门暂无用户数据,请先添加用户');
}
2025-08-19 10:19:29 +08:00
} catch (error) {
2025-08-28 23:32:17 +08:00
ElMessage.error('获取用户列表失败,请刷新重试');
console.error('获取用户列表异常:', error);
2025-08-19 10:19:29 +08:00
}
};
2025-08-28 23:32:17 +08:00
/** 查询当前表单数据并回显(确保专业值正确赋值) */
2025-08-19 10:19:29 +08:00
const designUser = async () => {
2025-08-28 23:32:17 +08:00
const projectId = currentProject.value?.id;
if (!projectId) {
ElMessage.warning('请先选择项目');
return;
}
2025-08-19 10:19:29 +08:00
const loading = ElLoading.service({
lock: true,
text: '加载配置数据中...',
background: 'rgba(255, 255, 255, 0.7)'
});
2025-08-28 23:32:17 +08:00
2025-08-19 10:19:29 +08:00
try {
2025-08-28 23:32:17 +08:00
const res = await designUserList({ projectId });
// 清空现有数据(避免残留旧值)
2025-08-19 10:19:29 +08:00
form.designLeader = null;
form.designers = [];
form.reviewers = [];
2025-08-28 23:32:17 +08:00
form.approved = [];
form.auditor = [];
2025-09-01 09:15:27 +08:00
disabledForm.value = false;
2025-08-28 23:32:17 +08:00
if (res.code == 200 && Array.isArray(res.rows) && res.rows.length > 0) {
2025-08-19 10:19:29 +08:00
disabledForm.value = true;
2025-08-28 23:32:17 +08:00
// 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);
2025-08-19 10:19:29 +08:00
// 2. 回显设计负责人
if (designLeader) form.designLeader = designLeader.userId;
2025-08-28 23:32:17 +08:00
// 3. 按专业分组回显各角色人员(确保专业值非空)
2025-08-19 10:19:29 +08:00
form.designers = groupPersonByMajor(designerItems);
form.reviewers = groupPersonByMajor(reviewerItems);
2025-08-28 23:32:17 +08:00
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()];
2025-08-19 10:19:29 +08:00
}
} catch (error) {
2025-08-28 23:32:17 +08:00
ElMessage.error('获取配置数据失败,请刷新重试');
console.error('获取配置数据异常:', error);
// 异常时初始化默认分组(避免页面空白)
2025-08-19 10:19:29 +08:00
form.designers = [createEmptyMajorGroup()];
form.reviewers = [createEmptyMajorGroup()];
2025-08-28 23:32:17 +08:00
form.approved = [createEmptyMajorGroup()];
form.auditor = [createEmptyMajorGroup()];
2025-08-19 10:19:29 +08:00
} finally {
loading.close();
}
};
2025-08-28 23:32:17 +08:00
/** 辅助函数:创建空的专业分组(确保初始结构正确) */
2025-08-19 10:19:29 +08:00
const createEmptyMajorGroup = (): MajorGroup => ({
userMajor: null,
2025-08-28 23:32:17 +08:00
persons: [{ userId: null }] // 初始一个空人员
2025-08-19 10:19:29 +08:00
});
2025-08-28 23:32:17 +08:00
/** 辅助函数:按专业分组整理人员数据(确保专业值非空) */
2025-08-19 10:19:29 +08:00
const groupPersonByMajor = (items: any[]): MajorGroup[] => {
const groupMap: Record<string, MajorGroup> = {};
2025-08-28 23:32:17 +08:00
// 过滤无效数据确保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;
// 初始化分组(确保不重复)
2025-08-19 10:19:29 +08:00
if (!groupMap[major]) {
2025-08-28 23:32:17 +08:00
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 });
2025-08-19 10:19:29 +08:00
}
});
2025-08-28 23:32:17 +08:00
// 确保每个分组至少有一个人员
2025-08-19 10:19:29 +08:00
Object.values(groupMap).forEach((group) => {
2025-08-28 23:32:17 +08:00
if (group.persons.length == 0) {
group.persons.push({ userId: null });
}
2025-08-19 10:19:29 +08:00
});
2025-08-28 23:32:17 +08:00
2025-08-19 10:19:29 +08:00
return Object.values(groupMap);
};
2025-08-28 23:32:17 +08:00
/** 新增专业配置行(确保所有角色同步新增) */
2025-08-19 10:19:29 +08:00
const addMajor = () => {
2025-08-28 23:32:17 +08:00
const newGroup = createEmptyMajorGroup();
form.designers.push(newGroup);
form.reviewers.push({ ...newGroup }); // 深拷贝避免引用问题
form.approved.push({ ...newGroup });
form.auditor.push({ ...newGroup });
2025-08-19 10:19:29 +08:00
2025-08-28 23:32:17 +08:00
// 滚动到新增行(提升用户体验)
2025-08-19 10:19:29 +08:00
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);
};
2025-08-28 23:32:17 +08:00
/** 删除专业配置行(确保所有角色同步删除) */
2025-08-19 10:19:29 +08:00
const removeMajor = (configIndex: number) => {
if (form.designers.length <= 1) {
ElMessage.warning('至少保留一个专业配置');
return;
}
2025-08-28 23:32:17 +08:00
// 同步删除所有角色的对应分组
2025-08-19 10:19:29 +08:00
form.designers.splice(configIndex, 1);
form.reviewers.splice(configIndex, 1);
2025-08-28 23:32:17 +08:00
form.approved.splice(configIndex, 1);
form.auditor.splice(configIndex, 1);
2025-08-19 10:19:29 +08:00
};
2025-08-28 23:32:17 +08:00
/** 给指定专业配置行添加人员(支持所有角色) */
const addPerson = (type: 'designers' | 'reviewers' | 'approved' | 'auditor', configIndex: number) => {
// 确保分组存在
if (form[type].length <= configIndex) {
ElMessage.warning('专业配置不存在');
return;
}
// 添加空人员(后续选择)
2025-08-19 10:19:29 +08:00
form[type][configIndex].persons.push({ userId: null });
// 滚动到新增的人员选择框
setTimeout(() => {
2025-08-28 23:32:17 +08:00
const selects = document.querySelectorAll(`[data-v-${proxy?.$options.__scopeId}] .el-select`);
if (selects.length > 0) {
selects[selects.length - 1].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
2025-08-19 10:19:29 +08:00
}
}, 100);
};
2025-08-28 23:32:17 +08:00
/** 从指定专业配置行删除人员(支持所有角色) */
const removePerson = (type: 'designers' | 'reviewers' | 'approved' | 'auditor', configIndex: number, personIndex: number) => {
2025-08-19 10:19:29 +08:00
const targetGroup = form[type][configIndex];
2025-08-28 23:32:17 +08:00
if (!targetGroup) {
ElMessage.warning('专业配置不存在');
return;
}
2025-08-19 10:19:29 +08:00
if (targetGroup.persons.length <= 1) {
2025-08-28 23:32:17 +08:00
const roleNames = {
designers: '设计',
reviewers: '校审',
approved: '审定',
auditor: '审核'
};
ElMessage.warning(`该专业至少保留一个${roleNames[type]}人员`);
2025-08-19 10:19:29 +08:00
return;
}
targetGroup.persons.splice(personIndex, 1);
};
2025-08-28 23:32:17 +08:00
/** 专业变更时:同步所有角色的专业值并重置人员(核心修复) */
const handleMajorChange = (newMajor: string | null, configIndex: number) => {
// 过滤无效专业值
if (!newMajor || !des_user_major.value.some((item) => item.value == newMajor)) {
ElMessage.warning('选择的专业无效,请重新选择');
return;
}
// 1. 同步所有角色的专业值(确保统一)
2025-08-19 10:19:29 +08:00
form.designers[configIndex].userMajor = newMajor;
form.reviewers[configIndex].userMajor = newMajor;
2025-08-28 23:32:17 +08:00
form.approved[configIndex].userMajor = newMajor;
form.auditor[configIndex].userMajor = newMajor;
// 2. 重置当前专业下的所有人员(避免专业与人员不匹配)
2025-08-19 10:19:29 +08:00
form.designers[configIndex].persons = [{ userId: null }];
form.reviewers[configIndex].persons = [{ userId: null }];
2025-08-28 23:32:17 +08:00
form.approved[configIndex].persons = [{ userId: null }];
form.auditor[configIndex].persons = [{ userId: null }];
2025-08-19 10:19:29 +08:00
2025-08-28 23:32:17 +08:00
ElMessage.success(`已切换为「${getMajorLabel(newMajor)}」专业,请重新选择人员`);
};
2025-08-19 10:19:29 +08:00
2025-08-28 23:32:17 +08:00
/** 重复校验:同一角色内「专业+人员」组合唯一 */
const checkDuplicate = (
currentPerson: Person,
role: 'designers' | 'reviewers' | 'approved' | 'auditor',
configIndex: number,
personIndex: number
) => {
2025-08-19 10:19:29 +08:00
const currentGroup = form[role][configIndex];
2025-08-28 23:32:17 +08:00
const currentMajor = form.designers[configIndex].userMajor; // 统一专业来源
2025-08-19 10:19:29 +08:00
// 未选专业/人员时不校验
2025-08-28 23:32:17 +08:00
if (!currentMajor || !currentPerson.userId) return;
2025-08-19 10:19:29 +08:00
2025-08-28 23:32:17 +08:00
const currentKey = `${currentMajor}-${currentPerson.userId}`;
let duplicateItem: Person | null = null;
2025-08-19 10:19:29 +08:00
2025-08-28 23:32:17 +08:00
// 1. 检查当前专业行内是否有重复人员
duplicateItem = currentGroup.persons.find((item, idx) => idx !== personIndex && item.userId == currentPerson.userId);
2025-08-19 10:19:29 +08:00
if (duplicateItem) {
2025-08-28 23:32:17 +08:00
ElMessage.warning(`当前专业下「${getUserName(currentPerson.userId)}」已存在,请重新选择`);
currentPerson.userId = null;
2025-08-19 10:19:29 +08:00
return;
}
2025-08-28 23:32:17 +08:00
// 2. 检查同一角色其他专业行是否有重复
2025-08-19 10:19:29 +08:00
form[role].forEach((group, gIdx) => {
2025-08-28 23:32:17 +08:00
if (gIdx == configIndex) return;
const groupMajor = form.designers[gIdx].userMajor;
if (!groupMajor) return;
2025-08-19 10:19:29 +08:00
group.persons.forEach((item) => {
2025-08-28 23:32:17 +08:00
if (item.userId && `${groupMajor}-${item.userId}` == currentKey) {
2025-08-19 10:19:29 +08:00
duplicateItem = item;
}
});
});
if (duplicateItem) {
2025-08-28 23:32:17 +08:00
ElMessage.warning(`${getMajorLabel(currentMajor)}+${getUserName(currentPerson.userId)}」组合已存在,请重新选择`);
currentPerson.userId = null;
2025-08-19 10:19:29 +08:00
}
};
2025-08-28 23:32:17 +08:00
/** 辅助函数通过专业值获取专业名称避免显示value */
2025-08-19 10:19:29 +08:00
const getMajorLabel = (majorValue: string | null) => {
2025-08-28 23:32:17 +08:00
if (!majorValue) return '';
const major = des_user_major.value.find((item) => item.value == majorValue);
2025-08-19 10:19:29 +08:00
return major ? major.label : majorValue;
};
/** 辅助函数通过用户ID获取用户名 */
const getUserName = (userId: number | null) => {
2025-08-28 23:32:17 +08:00
if (!userId) return '';
const user = userList.value.find((item) => item.userId == userId);
return user ? user.nickName : `${userId}`;
2025-08-19 10:19:29 +08:00
};
2025-08-28 23:32:17 +08:00
/** 提交表单(核心:确保专业值正确传递) */
2025-08-19 10:19:29 +08:00
const submitForm = async () => {
2025-08-28 23:32:17 +08:00
if (!leaveFormRef.value) {
ElMessage.warning('表单实例未加载,请刷新重试');
return;
}
2025-08-19 10:19:29 +08:00
try {
2025-08-28 23:32:17 +08:00
// 1. 基础表单验证(确保所有必填项已填)
2025-08-19 10:19:29 +08:00
await leaveFormRef.value.validate();
2025-08-28 23:32:17 +08:00
// 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;
2025-08-19 10:19:29 +08:00
group.persons.forEach((person) => {
if (!person.userId) return;
2025-08-28 23:32:17 +08:00
const key = `${major}-${person.userId}`;
if (keys.includes(key)) {
2025-08-19 10:19:29 +08:00
hasDuplicate = true;
ElMessage.error(`${roleName}中存在重复的「专业+人员」组合,请检查`);
}
2025-08-28 23:32:17 +08:00
keys.push(key);
2025-08-19 10:19:29 +08:00
});
});
};
2025-08-28 23:32:17 +08:00
validateRoleDuplicate(form.designers, '设计人员');
2025-08-19 10:19:29 +08:00
if (hasDuplicate) return;
2025-08-28 23:32:17 +08:00
validateRoleDuplicate(form.reviewers, '校审人员');
if (hasDuplicate) return;
validateRoleDuplicate(form.approved, '审定人员');
if (hasDuplicate) return;
validateRoleDuplicate(form.auditor, '审核人员');
2025-08-19 10:19:29 +08:00
if (hasDuplicate) return;
2025-08-28 23:32:17 +08:00
// 4. 构建提交数据(确保专业值正确传递)
2025-08-19 10:19:29 +08:00
const submitData = {
projectId: form.projectId,
personnel: [
2025-08-28 23:32:17 +08:00
// 设计负责人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值如未选设计负责人
2025-08-19 10:19:29 +08:00
};
2025-08-28 23:32:17 +08:00
// 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 // 最终传递的专业值(确保非空)
};
2025-08-19 10:19:29 +08:00
});
2025-08-28 23:32:17 +08:00
// 6. 提交到后端(确保参数正确)
2025-08-19 10:19:29 +08:00
const loading = ElLoading.service({ text: '提交中...', background: 'rgba(255,255,255,0.7)' });
const res = await designUserAdd({
2025-08-28 23:32:17 +08:00
list: submitList,
projectId: form.projectId
2025-08-19 10:19:29 +08:00
});
2025-08-28 23:32:17 +08:00
2025-08-19 10:19:29 +08:00
if (res.code == 200) {
disabledForm.value = true;
2025-08-28 23:32:17 +08:00
ElMessage.success('提交成功,已保存人员配置');
// 提交成功后重新加载数据(确保回显正确)
await designUser();
2025-08-19 10:19:29 +08:00
} else {
2025-08-28 23:32:17 +08:00
ElMessage.error(res.msg || '提交失败,请重试');
}
} catch (error: any) {
// 捕获表单验证错误或其他异常
if (error.name == 'ValidationError') {
ElMessage.error('请完善表单必填项后再提交');
} else {
ElMessage.error('提交过程异常,请刷新页面重试');
console.error('表单提交异常:', error);
2025-08-19 10:19:29 +08:00
}
} finally {
2025-08-28 23:32:17 +08:00
ElLoading.service().close();
2025-08-19 10:19:29 +08:00
}
};
2025-08-28 23:32:17 +08:00
/** 重置表单(确保恢复初始状态) */
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('表单已重置,请重新配置人员');
2025-08-19 10:19:29 +08:00
};
2025-08-28 23:32:17 +08:00
/** 监听项目ID变化刷新用户列表和表单数据 */
2025-08-19 10:19:29 +08:00
const listeningProject: WatchStopHandle = watch(
() => currentProject.value?.id,
2025-08-28 23:32:17 +08:00
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 }
2025-08-19 10:19:29 +08:00
);
// 页面生命周期
onUnmounted(() => {
2025-08-28 23:32:17 +08:00
// 清除监听
2025-08-19 10:19:29 +08:00
listeningProject();
});
2025-08-28 23:32:17 +08:00
onMounted(async () => {
// 初始加载:先获取部门用户,再加载表单数据
await getDeptAllUser(userStore.deptId);
await designUser();
// 打印初始数据(调试用)
console.log('初始专业字典:', des_user_major.value);
console.log('初始用户列表:', userList.value);
2025-08-19 10:19:29 +08:00
});
</script>
<style lang="scss" scoped>
2025-08-19 14:56:48 +08:00
.appointment {
2025-08-28 23:32:17 +08:00
width: 100%;
max-width: 1800px;
2025-08-19 10:19:29 +08:00
.el-select__wrapper {
width: 100% !important;
}
.fonts {
.el-form-item--default .el-form-item__label {
font-size: 18px !important;
2025-08-28 23:32:17 +08:00
font-weight: 600;
2025-08-19 10:19:29 +08:00
}
}
}
2025-08-28 23:32:17 +08:00
// 自定义动画:新增专业行时的过渡效果
2025-08-19 10:19:29 +08:00
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fadeIn {
animation: fadeIn 0.3s ease-out forwards;
}
2025-08-28 23:32:17 +08:00
// 表单样式优化:统一间距和字体
2025-08-19 10:19:29 +08:00
::v-deep .el-form {
--el-form-item-margin-bottom: 0;
}
::v-deep .el-form-item {
margin-bottom: 0;
&__label {
font-weight: 500;
color: #4e5969;
2025-08-28 23:32:17 +08:00
font-size: 14px;
2025-08-19 10:19:29 +08:00
}
&__content {
padding: 0;
2025-08-28 23:32:17 +08:00
font-size: 14px;
}
// 表单验证错误提示样式
&__error {
font-size: 12px;
margin-top: 4px;
2025-08-19 10:19:29 +08:00
}
}
2025-08-28 23:32:17 +08:00
// 选择器样式:统一高度和边框
2025-08-19 10:19:29 +08:00
::v-deep .el-select {
width: 100%;
.el-input__inner {
border-radius: 6px;
transition: all 0.3s ease;
2025-08-28 23:32:17 +08:00
padding: 0 12px;
height: 32px; // 统一高度,避免布局错乱
font-size: 14px;
2025-08-19 10:19:29 +08:00
}
&: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);
}
}
2025-08-28 23:32:17 +08:00
// 按钮样式:统一大小和颜色
2025-08-19 10:19:29 +08:00
::v-deep .el-button {
2025-08-28 23:32:17 +08:00
border-radius: 4px;
padding: 4px 8px;
font-size: 12px;
2025-08-19 10:19:29 +08:00
2025-08-28 23:32:17 +08:00
&--small {
padding: 2px 6px;
font-size: 12px;
2025-08-19 10:19:29 +08:00
}
2025-08-28 23:32:17 +08:00
// 角色区分色(提升视觉识别)
&.el-button--success {
2025-08-19 10:19:29 +08:00
background-color: #67c23a;
border-color: #67c23a;
&:hover {
background-color: #85ce61;
border-color: #85ce61;
}
&:disabled {
background-color: #b3e099;
border-color: #b3e099;
}
}
2025-08-28 23:32:17 +08:00
&.el-button--danger {
2025-08-19 10:19:29 +08:00
background-color: #f56c6c;
border-color: #f56c6c;
&:hover {
background-color: #f78989;
border-color: #f78989;
}
&:disabled {
background-color: #ffcccc;
border-color: #ffbbbb;
}
}
2025-08-28 23:32:17 +08:00
&.el-button--text {
2025-08-19 10:19:29 +08:00
color: #f56c6c;
&:hover {
color: #f78989;
background-color: rgba(245, 108, 108, 0.05);
}
}
}
2025-08-28 23:32:17 +08:00
// 确保表头不换行
.whitespace-nowrap {
white-space: nowrap;
2025-08-19 10:19:29 +08:00
}
2025-08-28 23:32:17 +08:00
// 响应式处理:小屏幕横向滚动,避免挤压
@media (max-width: 1200px) {
::v-deep .el-row {
flex-wrap: nowrap !important; // 强制不换行
2025-08-19 10:19:29 +08:00
}
2025-08-28 23:32:17 +08:00
::v-deep .el-col {
flex-shrink: 0 !important; // 禁止列收缩
2025-08-19 10:19:29 +08:00
}
2025-08-28 23:32:17 +08:00
.appointment {
width: 98vw;
2025-08-19 10:19:29 +08:00
}
2025-08-28 23:32:17 +08:00
// 小屏幕下调整标签宽度,节省空间
::v-deep .el-form-item--default .el-form-item__label {
width: 50px !important;
2025-08-19 10:19:29 +08:00
}
}
2025-08-28 23:32:17 +08:00
// 空状态样式优化:统一视觉效果
::v-deep .text-gray-500.text-xs {
text-align: center;
background-color: #f9fafb;
border-color: #e5e7eb;
}
2025-08-19 10:19:29 +08:00
</style>