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

591 lines
19 KiB
Vue
Raw Normal View History

2025-08-12 19:01:04 +08:00
<template>
<div class="p-6 bg-gray-50">
<div class="appWidth 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>
2025-08-13 19:11:51 +08:00
<el-button @click="disabledForm = false" class="px-8 py-2.5 transition-all duration-300 font-medium" v-if="disabledForm">
点击编辑
</el-button>
2025-08-12 19:01:04 +08:00
</div>
<!-- 表单内容区域 -->
2025-08-13 19:11:51 +08:00
<el-form ref="leaveFormRef" :model="form" :disabled="disabledForm" :rules="rules" label-width="120px" class="p-6 space-y-6">
2025-08-12 19:01:04 +08:00
<!-- 设计负责人 -->
<div class="fonts">
<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>
</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-user mr-2 text-blue-500"></i>设计人员</h3>
<el-button
type="primary"
size="small"
@click="addPerson('designers')"
class="transition-all duration-300 transform hover:scale-105 bg-blue-500 hover:bg-blue-600"
>
<i class="el-icon-plus mr-1"></i>新增设计人员
</el-button>
</div>
<div v-for="(designer, index) in form.designers" :key="index" class="flex items-center mb-4 animate-fadeIn">
<el-form-item
:prop="`designers.${index}.userId`"
:rules="{ required: true, message: '请选择设计人员', trigger: 'change' }"
class="flex-1 mr-3"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
2025-08-15 16:34:59 +08:00
<!-- 设计人员专业选择绑定重复校验 -->
2025-08-12 19:01:04 +08:00
<el-select
v-model="designer.userMajor"
placeholder="请选择专业"
class="transition-all duration-300 border-gray-300"
:rules="{ required: true, message: '请选择专业', trigger: 'change' }"
2025-08-15 16:34:59 +08:00
@change="() => checkDuplicate(designer, 'designers', index)"
2025-08-12 19:01:04 +08:00
>
<el-option v-for="item in des_user_major" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
2025-08-15 16:34:59 +08:00
<!-- 设计人员选择绑定重复校验 -->
<el-select
v-model="designer.userId"
placeholder="请选择设计人员"
class="transition-all duration-300 border-gray-300"
@change="() => checkDuplicate(designer, 'designers', index)"
>
2025-08-12 19:01:04 +08:00
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" />
</el-select>
</div>
</el-form-item>
<el-button
type="danger"
size="small"
@click="removePerson('designers', index)"
class="transition-all duration-300 hover:bg-red-600"
:disabled="form.designers.length <= 1"
>
<el-icon :size="16">
<Delete />
</el-icon>
</el-button>
</div>
<div v-if="form.designers.length === 0" class="text-gray-500 text-center py-4 bg-gray-100 rounded-lg border border-dashed border-gray-200">
暂无设计人员请点击"新增设计人员"添加
</div>
</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-check-circle mr-2 text-green-500"></i>校审人员</h3>
<el-button
type="primary"
size="small"
@click="addPerson('reviewers')"
class="transition-all duration-300 transform hover:scale-105 bg-blue-500 hover:bg-blue-600"
>
<i class="el-icon-plus mr-1"></i>新增校审人员
</el-button>
</div>
<div v-for="(reviewer, index) in form.reviewers" :key="index" class="flex items-center mb-4 animate-fadeIn">
<el-form-item
:prop="`reviewers.${index}.userId`"
:rules="{ required: true, message: '请选择校审人员', trigger: 'change' }"
class="flex-1 mr-3"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
2025-08-15 16:34:59 +08:00
<!-- 校审人员专业选择绑定重复校验 -->
2025-08-12 19:01:04 +08:00
<el-select
v-model="reviewer.userMajor"
placeholder="请选择专业"
class="transition-all duration-300 border-gray-300"
:rules="{ required: true, message: '请选择专业', trigger: 'change' }"
2025-08-15 16:34:59 +08:00
@change="() => checkDuplicate(reviewer, 'reviewers', index)"
2025-08-12 19:01:04 +08:00
>
<el-option v-for="item in des_user_major" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
2025-08-15 16:34:59 +08:00
<!-- 校审人员选择绑定重复校验 -->
<el-select
v-model="reviewer.userId"
placeholder="请选择校审人员"
class="transition-all duration-300 border-gray-300"
@change="() => checkDuplicate(reviewer, 'reviewers', index)"
>
2025-08-12 19:01:04 +08:00
<el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" />
</el-select>
</div>
</el-form-item>
<el-button
type="danger"
size="small"
@click="removePerson('reviewers', index)"
class="transition-all duration-300 hover:bg-red-600"
:disabled="form.reviewers.length <= 1"
>
<el-icon :size="16">
<Delete />
</el-icon>
</el-button>
</div>
<div v-if="form.reviewers.length === 0" class="text-gray-500 text-center py-4 bg-gray-100 rounded-lg border border-dashed border-gray-200">
暂无校审人员请点击"新增校审人员"添加
</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"
2025-08-15 23:14:13 +08:00
v-hasPermi="['design:user:batch']"
2025-08-12 19:01:04 +08:00
@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>
</template>
<script setup name="PersonnelForm" lang="ts">
2025-08-15 16:34:59 +08:00
import { ref, reactive, computed, onMounted, toRefs, watch, WatchStopHandle } from 'vue';
2025-08-12 19:01:04 +08:00
import { getCurrentInstance } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { useUserStoreHook } from '@/store/modules/user';
import { ElMessage, ElLoading } from 'element-plus';
import { Delete } 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 } = toRefs<any>(proxy?.useDict('des_user_major'));
// 表单数据
const form = reactive({
projectId: currentProject.value?.id,
designLeader: null, // 设计负责人
// 设计人员列表包含用户ID和专业
designers: [] as Array<{ userId: number | null; userMajor: string | null }>,
// 校审人员列表包含用户ID和专业
reviewers: [] as Array<{ userId: number | null; userMajor: string | null }>
});
// 表单验证规则
const rules = reactive({
designLeader: [{ required: true, message: '请选择设计负责人', trigger: 'change' }]
});
// 用户列表
const userList = ref([]);
// 表单引用
const leaveFormRef = ref();
2025-08-13 19:11:51 +08:00
const disabledForm = ref(false); //控制提交按钮状态
2025-08-15 16:34:59 +08:00
2025-08-12 19:01:04 +08:00
/** 查询当前部门的所有用户 */
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 });
if (res.code == 200 && res.rows) {
// 清空现有数据
form.designLeader = null;
form.designers = [];
form.reviewers = [];
2025-08-13 19:11:51 +08:00
if (res.rows.length > 0) {
disabledForm.value = true;
}
2025-08-12 19:01:04 +08:00
// 处理返回的数据,进行回显
res.rows.forEach((item: any) => {
if (item.userType == 1) {
2025-08-15 16:34:59 +08:00
form.designLeader = item.userId;
2025-08-12 19:01:04 +08:00
} else if (item.userType == 2) {
2025-08-15 16:34:59 +08:00
form.designers.push({
userId: item.userId,
userMajor: item.userMajor || null
});
2025-08-12 19:01:04 +08:00
} else if (item.userType == 3) {
2025-08-15 16:34:59 +08:00
form.reviewers.push({
userId: item.userId,
userMajor: item.userMajor || null
});
2025-08-12 19:01:04 +08:00
}
});
2025-08-15 16:34:59 +08:00
// 补全默认空项
2025-08-12 19:01:04 +08:00
if (form.designers.length === 0) {
form.designers.push({ userId: null, userMajor: null });
}
if (form.reviewers.length === 0) {
form.reviewers.push({ userId: null, userMajor: null });
}
} else {
// 添加默认空项
form.designers.push({ userId: null, userMajor: null });
form.reviewers.push({ userId: null, userMajor: null });
}
} catch (error) {
ElMessage.error('获取配置数据失败');
form.designers.push({ userId: null, userMajor: null });
form.reviewers.push({ userId: null, userMajor: null });
} finally {
loading.close();
}
};
/** 添加人员 */
const addPerson = (type: 'designers' | 'reviewers') => {
form[type].push({ userId: null, userMajor: null });
2025-08-15 16:34:59 +08:00
// 滚动到最后一个新增元素
2025-08-12 19:01:04 +08:00
setTimeout(() => {
const elements = document.querySelectorAll(`[data-v-${proxy?.$options.__scopeId}] .el-select`);
if (elements.length > 0) {
elements[elements.length - 1].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, 100);
};
/** 移除人员 */
const removePerson = (type: 'designers' | 'reviewers', index: number) => {
if (form[type].length <= 1) {
ElMessage.warning('至少保留一个人员');
return;
}
form[type].splice(index, 1);
};
2025-08-15 16:34:59 +08:00
// ===================== 核心修改:重复校验逻辑 =====================
/**
* 校验同一角色内设计/校审专业+人员组合唯一性
* @param current 当前操作的人员对象
* @param role 角色类型designers/reviewers
* @param currentIndex 当前操作的索引
*/
const checkDuplicate = (current: { userId: number | null; userMajor: string | null }, role: 'designers' | 'reviewers', currentIndex: number) => {
// 未选完专业/人员时不校验
if (!current.userId || !current.userMajor) return;
// 只获取当前角色的所有人员(设计只查设计,校审只查校审)
const targetList = form[role];
// 生成当前「专业+人员」唯一标识
const currentKey = `${current.userMajor}-${current.userId}`;
// 检查当前角色内是否有重复
const duplicateItem = targetList.find((item, index) => {
// 排除当前操作项本身
if (index === currentIndex) return false;
// 对比「专业+人员」组合
return `${item.userMajor}-${item.userId}` === currentKey;
});
// 存在重复时提示并清空当前选择
if (duplicateItem) {
ElMessage.warning(`当前「${getMajorLabel(current.userMajor)}+${getUserName(current.userId)}」组合已存在,请重新选择`);
// 清空当前项的选择
current.userId = null;
current.userMajor = 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;
};
/** 提交表单(补充提交前重复校验) */
2025-08-12 19:01:04 +08:00
const submitForm = async () => {
if (!leaveFormRef.value) return;
try {
2025-08-15 16:34:59 +08:00
// 1. 先做基础表单验证
2025-08-12 19:01:04 +08:00
await leaveFormRef.value.validate();
2025-08-15 16:34:59 +08:00
// 2. 提交前二次校验:确保当前角色内无重复(防止手动修改数据绕过实时校验)
let hasDuplicate = false;
// 校验设计人员
const designKeys = form.designers.map((item) => `${item.userMajor}-${item.userId}`);
if (new Set(designKeys).size !== designKeys.length) {
hasDuplicate = true;
ElMessage.error('设计人员中存在重复的「专业+人员」组合,请检查');
}
// 校验校审人员(不校验设计与校审之间)
if (!hasDuplicate) {
const reviewKeys = form.reviewers.map((item) => `${item.userMajor}-${item.userId}`);
if (new Set(reviewKeys).size !== reviewKeys.length) {
hasDuplicate = true;
ElMessage.error('校审人员中存在重复的「专业+人员」组合,请检查');
}
}
// 有重复则终止提交
if (hasDuplicate) return;
// 3. 构建提交数据(原有逻辑不变)
2025-08-12 19:01:04 +08:00
const submitData = {
projectId: form.projectId,
personnel: [
// 设计负责人
{
userId: form.designLeader,
2025-08-15 16:34:59 +08:00
userType: 'designLeader',
userMajor: null
2025-08-12 19:01:04 +08:00
},
// 设计人员
...form.designers.map((designer) => ({
userId: designer.userId,
2025-08-15 16:34:59 +08:00
userType: 'designer',
userMajor: designer.userMajor
2025-08-12 19:01:04 +08:00
})),
// 校审人员
...form.reviewers.map((reviewer) => ({
userId: reviewer.userId,
2025-08-15 16:34:59 +08:00
userType: 'reviewer',
userMajor: reviewer.userMajor
2025-08-12 19:01:04 +08:00
}))
]
};
2025-08-15 16:34:59 +08:00
// 数据处理(原有逻辑不变)
2025-08-12 19:01:04 +08:00
const arr = [];
userList.value.forEach((item) => {
submitData.personnel.forEach((item1) => {
if (item1.userId === item.userId) {
2025-08-15 16:34:59 +08:00
let userType = 1;
if (item1.userType === 'designer') userType = 2;
else if (item1.userType === 'reviewer') userType = 3;
2025-08-12 19:01:04 +08:00
arr.push({
userName: item.nickName,
projectId: submitData.projectId,
userId: item1.userId,
userType: userType,
userMajor: item1.userMajor
});
}
});
});
2025-08-15 16:34:59 +08:00
// 4. 提交到后端(原有逻辑不变)
const loading = ElLoading.service({ text: '提交中...', background: 'rgba(255,255,255,0.7)' });
2025-08-12 19:01:04 +08:00
const res = await designUserAdd({
list: arr,
projectId: currentProject.value?.id
});
if (res.code === 200) {
2025-08-13 19:11:51 +08:00
disabledForm.value = true;
2025-08-12 19:01:04 +08:00
ElMessage.success('提交成功');
} else {
ElMessage.error(res.msg || '提交失败');
}
} catch (error) {
ElMessage.error('请完善表单信息后再提交');
} finally {
ElLoading.service().close();
}
};
/** 重置表单 */
const resetForm = () => {
if (leaveFormRef.value) {
leaveFormRef.value.resetFields();
form.designers = [{ userId: null, userMajor: null }];
form.reviewers = [{ userId: null, userMajor: null }];
ElMessage.info('表单已重置');
}
};
2025-08-15 16:34:59 +08:00
// 监听项目ID刷新数据
const listeningProject: WatchStopHandle = watch(
2025-08-14 16:40:52 +08:00
() => currentProject.value?.id,
2025-08-15 16:34:59 +08:00
() => {
2025-08-14 16:40:52 +08:00
getDeptAllUser(userStore.deptId).then(() => {
designUser();
});
}
);
2025-08-12 19:01:04 +08:00
2025-08-15 16:34:59 +08:00
// 页面生命周期
2025-08-14 16:40:52 +08:00
onUnmounted(() => {
2025-08-15 16:34:59 +08:00
listeningProject(); // 修复原代码watchStopHandle调用问题
2025-08-14 16:40:52 +08:00
});
2025-08-12 19:01:04 +08:00
onMounted(() => {
getDeptAllUser(userStore.deptId).then(() => {
designUser();
});
});
</script>
<style lang="scss">
.appWidth {
width: 50vw;
max-width: 1200px;
.el-select__wrapper {
width: 16vw !important;
}
.el-button--small {
margin-bottom: 10px;
}
.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;
padding: 0 0 8px 0;
}
&__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;
}
}
&--danger {
background-color: #f56c6c;
border-color: #f56c6c;
&:hover {
background-color: #f78989;
border-color: #f78989;
}
&:disabled {
background-color: #ffcccc;
border-color: #ffbbbb;
cursor: not-allowed;
}
}
}
// 响应式网格布局
.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;
}
// 适配小屏幕
@media (max-width: 768px) {
.appWidth {
width: 95vw;
}
::v-deep .el-form {
padding: 4px;
}
::v-deep .el-form-item__label {
width: 100px;
}
}
</style>