Files
td_official/src/views/design/appointment/index copy 2.vue
2025-08-28 23:32:17 +08:00

805 lines
28 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="appointment mx-auto bg-white rounded-xl shadow-sm overflow-hidden transition-all duration-300 hover:shadow-md">
<!-- 表单标题区域 -->
<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>
<!-- 提交按钮区域 -->
<div 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"
:disabled="disabledForm"
>
<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"
:disabled="disabledForm"
>
<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;
loading.close();
ElMessage.success('提交成功');
} else {
ElMessage.error(res.msg || '提交失败');
}
} catch (error) {
ElMessage.error('请完善表单信息后再提交');
} finally {
// ElLoading.service().close();
}
};
/** 重置表单(适配新数据结构) */
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>
.appointment {
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>