Files
td_official/src/views/design/appointment/index.vue
2025-08-15 23:14:13 +08:00

591 lines
19 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="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>
<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-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">
<!-- 设计人员专业选择绑定重复校验 -->
<el-select
v-model="designer.userMajor"
placeholder="请选择专业"
class="transition-all duration-300 border-gray-300"
:rules="{ required: true, message: '请选择专业', trigger: 'change' }"
@change="() => checkDuplicate(designer, 'designers', index)"
>
<el-option v-for="item in des_user_major" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<!-- 设计人员选择绑定重复校验 -->
<el-select
v-model="designer.userId"
placeholder="请选择设计人员"
class="transition-all duration-300 border-gray-300"
@change="() => checkDuplicate(designer, 'designers', index)"
>
<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">
<!-- 校审人员专业选择绑定重复校验 -->
<el-select
v-model="reviewer.userMajor"
placeholder="请选择专业"
class="transition-all duration-300 border-gray-300"
:rules="{ required: true, message: '请选择专业', trigger: 'change' }"
@change="() => checkDuplicate(reviewer, 'reviewers', index)"
>
<el-option v-for="item in des_user_major" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<!-- 校审人员选择绑定重复校验 -->
<el-select
v-model="reviewer.userId"
placeholder="请选择校审人员"
class="transition-all duration-300 border-gray-300"
@change="() => checkDuplicate(reviewer, 'reviewers', index)"
>
<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"
v-hasPermi="['design:user:batch']"
@click="submitForm"
class="px-8 py-2.5 transition-all duration-300 transform hover:scale-105 bg-blue-500 hover:bg-blue-600 text-white font-medium"
>
<i class="el-icon-check mr-2"></i>确认提交
</el-button>
<el-button size="large" @click="resetForm" class="px-8 py-2.5 transition-all duration-300 border-gray-300 hover:bg-gray-100 font-medium">
<i class="el-icon-refresh mr-2"></i>重置
</el-button>
</div>
</el-form>
</div>
</div>
</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 } 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();
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 });
if (res.code == 200 && res.rows) {
// 清空现有数据
form.designLeader = null;
form.designers = [];
form.reviewers = [];
if (res.rows.length > 0) {
disabledForm.value = true;
}
// 处理返回的数据,进行回显
res.rows.forEach((item: any) => {
if (item.userType == 1) {
form.designLeader = item.userId;
} else if (item.userType == 2) {
form.designers.push({
userId: item.userId,
userMajor: item.userMajor || null
});
} else if (item.userType == 3) {
form.reviewers.push({
userId: item.userId,
userMajor: item.userMajor || null
});
}
});
// 补全默认空项
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 });
// 滚动到最后一个新增元素
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);
};
// ===================== 核心修改:重复校验逻辑 =====================
/**
* 校验同一角色内(设计/校审)的「专业+人员」组合唯一性
* @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;
};
/** 提交表单(补充提交前重复校验) */
const submitForm = async () => {
if (!leaveFormRef.value) return;
try {
// 1. 先做基础表单验证
await leaveFormRef.value.validate();
// 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. 构建提交数据(原有逻辑不变)
const submitData = {
projectId: form.projectId,
personnel: [
// 设计负责人
{
userId: form.designLeader,
userType: 'designLeader',
userMajor: null
},
// 设计人员
...form.designers.map((designer) => ({
userId: designer.userId,
userType: 'designer',
userMajor: designer.userMajor
})),
// 校审人员
...form.reviewers.map((reviewer) => ({
userId: reviewer.userId,
userType: 'reviewer',
userMajor: reviewer.userMajor
}))
]
};
// 数据处理(原有逻辑不变)
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
});
}
});
});
// 4. 提交到后端(原有逻辑不变)
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;
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('表单已重置');
}
};
// 监听项目ID刷新数据
const listeningProject: WatchStopHandle = watch(
() => currentProject.value?.id,
() => {
getDeptAllUser(userStore.deptId).then(() => {
designUser();
});
}
);
// 页面生命周期
onUnmounted(() => {
listeningProject(); // 修复原代码watchStopHandle调用问题
});
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>