设计出图

This commit is contained in:
2025-08-18 20:02:07 +08:00
parent 12e80c57d3
commit 9e2cf9c73a
5 changed files with 573 additions and 610 deletions

View File

@ -49,7 +49,7 @@
@click.stop
>
<li
style="margin-bottom: 10px"
style="margin-top: 10px;"
v-for="(file, index) in fileList"
:key="file.uid"
class="el-upload-list__item ele-upload-list__item-content"

View File

@ -13,7 +13,9 @@
<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-row>
<el-col :span="8"
><el-form-item label="设计负责人" prop="designLeader" class="mb-4">
<el-select
v-model="form.designLeader"
placeholder="请选择设计负责人"
@ -22,125 +24,201 @@
<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-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>新增设计人员
<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>
<div v-for="(designer, index) in form.designers" :key="index" class="flex items-center mb-4 animate-fadeIn">
<!-- 表头 -->
<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.${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"
:prop="`designers.${configIndex}.userMajor`"
:rules="{ required: true, message: '请选择专业', trigger: 'change' }"
@change="() => checkDuplicate(designer, 'designers', index)"
class="mb-0"
label-width="80px"
label="专业"
>
<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"
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="transition-all duration-300 border-gray-300"
@change="() => checkDuplicate(designer, 'designers', index)"
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>
</div>
</el-form-item>
<div>
<el-button
type="danger"
size="small"
@click="removePerson('designers', index)"
@click="removePerson('designers', configIndex, personIndex)"
class="transition-all duration-300 hover:bg-red-600"
:disabled="form.designers.length <= 1"
:disabled="majorConfig.designPersons.length <= 1 || disabledForm"
>
<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"
type="success"
size="small"
@click="addPerson('reviewers')"
class="transition-all duration-300 transform hover:scale-105 bg-blue-500 hover:bg-blue-600"
@click="addPerson('designers', configIndex)"
class="transition-all duration-300 transform hover:scale-105"
:disabled="!majorConfig.userMajor || disabledForm"
>
<i class="el-icon-plus mr-1"></i>新增校审人员
<el-icon :size="16">
<Plus />
</el-icon>
</el-button>
</div>
</div>
</div>
<div v-for="(reviewer, index) in form.reviewers" :key="index" class="flex items-center mb-4 animate-fadeIn">
<!-- 空状态提示 -->
<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.${index}.userId`"
:rules="{ required: true, message: '请选择校审人员', trigger: 'change' }"
class="flex-1 mr-3"
:prop="`reviewers.${configIndex}.persons.${personIndex}.userId`"
:rules="{ required: true, message: '请选择人员', trigger: 'change' }"
class="flex-1 mr-3 mb-0"
label="校审人员"
label-width="80px"
>
<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"
v-model="person.userId"
placeholder="请选择校审人员"
class="transition-all duration-300 border-gray-300"
@change="() => checkDuplicate(reviewer, 'reviewers', index)"
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>
</div>
</el-form-item>
<div>
<el-button
type="danger"
size="small"
@click="removePerson('reviewers', index)"
@click="removePerson('reviewers', configIndex, personIndex)"
class="transition-all duration-300 hover:bg-red-600"
:disabled="form.reviewers.length <= 1"
:disabled="majorConfig.reviewPersons.length <= 1 || disabledForm"
>
<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">
暂无校审人员请点击"新增校审人员"添加
<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
@ -149,10 +227,16 @@
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">
<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>
@ -167,7 +251,7 @@ 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 { Delete, Plus } from '@element-plus/icons-vue'; // 修复添加Plus图标导入
import { designUserAdd, designUserList, systemUserList } from '@/api/design/appointment';
// 获取当前实例
@ -176,17 +260,43 @@ const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userStore = useUserStoreHook();
// 从 store 中获取当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
// 专业字典数据
const { des_user_major } = toRefs<any>(proxy?.useDict('des_user_major'));
// 专业字典数据 - 增加默认空数组避免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, // 设计负责人
// 设计人员列表包含用户ID和专业
designers: [] as Array<{ userId: number | null; userMajor: string | null }>,
// 校审人员列表包含用户ID和专业
reviewers: [] as Array<{ userId: number | null; userMajor: string | 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
}));
});
// 表单验证规则
@ -222,146 +332,215 @@ const designUser = async () => {
});
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) {
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);
}
// 处理返回的数据,进行回显
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 });
}
// 补全默认空项至少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.push({ userId: null, userMajor: null });
form.reviewers.push({ userId: null, userMajor: null });
// 异常时初始化默认空项
form.designers = [createEmptyMajorGroup()];
form.reviewers = [createEmptyMajorGroup()];
} finally {
loading.close();
}
};
/** 添加人员 */
const addPerson = (type: 'designers' | 'reviewers') => {
form[type].push({ userId: null, userMajor: null });
// 滚动到最后一个新增元素
/** 辅助函数创建空的专业分组含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 elements = document.querySelectorAll(`[data-v-${proxy?.$options.__scopeId}] .el-select`);
if (elements.length > 0) {
elements[elements.length - 1].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
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 removePerson = (type: 'designers' | 'reviewers', index: number) => {
if (form[type].length <= 1) {
ElMessage.warning('至少保留一个人员');
/** 删除专业配置行 */
const removeMajor = (configIndex: number) => {
if (form.designers.length <= 1) {
ElMessage.warning('至少保留一个专业配置');
return;
}
form[type].splice(index, 1);
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)}」专业下的人员,请重新选择`);
};
// ========== 核心:重复校验逻辑 ==========
/**
* 校验同一角色内(设计/校审)的「专业+人员」组合唯一性
* @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 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 targetList = form[role];
// 生成当前「专业+人员」唯一标识
const currentKey = `${current.userMajor}-${current.userId}`;
const currentKey = `${currentGroup.userMajor}-${current.userId}`;
let duplicateItem = null;
// 检查当前角色内是否有重复
const duplicateItem = targetList.find((item, index) => {
// 排除当前操作项本身
if (index === currentIndex) return false;
// 对比「专业+人员」组合
return `${item.userMajor}-${item.userId}` === currentKey;
// 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(current.userMajor)}+${getUserName(current.userId)}」组合已存在,请重新选择`);
// 清空当前项的选择
ElMessage.warning(`${getMajorLabel(currentGroup.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);
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);
const user = userList.value.find((item: any) => item.userId == userId);
return user ? user.nickName : userId;
};
/** 提交表单(补充提交前重复校验 */
/** 提交表单(保持原有数据结构 */
const submitForm = async () => {
if (!leaveFormRef.value) return;
try {
// 1. 先做基础表单验证
// 1. 基础表单验证
await leaveFormRef.value.validate();
// 2. 提交前二次校验:确保当前角色内无重复(防止手动修改数据绕过实时校验)
// 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);
});
});
};
// 校验设计人员
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('校审人员中存在重复的「专业+人员」组合,请检查');
}
}
// 有重复则终止提交
collectKeys(form.designers, '设计人员');
if (hasDuplicate) return;
// 3. 构建提交数据(原有逻辑不变
// 清空临时数组,校验校审人员(不校验设计与校审之间
allKeys.length = 0;
collectKeys(form.reviewers, '校审人员');
if (hasDuplicate) return;
// 3. 构建提交数据(适配后端原有数据格式)
const submitData = {
projectId: form.projectId,
personnel: [
@ -371,29 +550,33 @@ const submitForm = async () => {
userType: 'designLeader',
userMajor: null
},
// 设计人员
...form.designers.map((designer) => ({
userId: designer.userId,
// 设计人员:展开专业分组,每个人员单独作为一条数据
...form.designers.flatMap((group) =>
group.persons.map((person) => ({
userId: person.userId,
userType: 'designer',
userMajor: designer.userMajor
})),
// 校审人员
...form.reviewers.map((reviewer) => ({
userId: reviewer.userId,
userType: 'reviewer',
userMajor: reviewer.userMajor
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) {
if (item1.userId == item.userId) {
let userType = 1;
if (item1.userType === 'designer') userType = 2;
else if (item1.userType === 'reviewer') userType = 3;
if (item1.userType == 'designer') userType = 2;
else if (item1.userType == 'reviewer') userType = 3;
arr.push({
userName: item.nickName,
projectId: submitData.projectId,
@ -405,13 +588,13 @@ const submitForm = async () => {
});
});
// 4. 提交到后端(原有逻辑不变)
// 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) {
if (res.code == 200) {
disabledForm.value = true;
ElMessage.success('提交成功');
} else {
@ -424,12 +607,13 @@ const submitForm = async () => {
}
};
/** 重置表单 */
/** 重置表单(适配新数据结构) */
const resetForm = () => {
if (leaveFormRef.value) {
leaveFormRef.value.resetFields();
form.designers = [{ userId: null, userMajor: null }];
form.reviewers = [{ userId: null, userMajor: null }];
// 重置为默认空状态1个专业分组每组1个空人员
form.designers = [createEmptyMajorGroup()];
form.reviewers = [createEmptyMajorGroup()];
ElMessage.info('表单已重置');
}
};
@ -446,7 +630,7 @@ const listeningProject: WatchStopHandle = watch(
// 页面生命周期
onUnmounted(() => {
listeningProject(); // 修复原代码watchStopHandle调用问题
listeningProject();
});
onMounted(() => {
getDeptAllUser(userStore.deptId).then(() => {
@ -455,16 +639,19 @@ onMounted(() => {
});
</script>
<style lang="scss">
<style lang="scss" scoped>
.appWidth {
width: 50vw;
max-width: 1200px;
width: 70vw;
max-width: 1600px;
.el-select__wrapper {
width: 16vw !important;
width: 100% !important;
}
.el-button--small {
margin-bottom: 10px;
margin-bottom: 0;
}
.fonts {
.el-form-item--default .el-form-item__label {
font-size: 18px !important;
@ -499,7 +686,6 @@ onMounted(() => {
&__label {
font-weight: 500;
color: #4e5969;
padding: 0 0 8px 0;
}
&__content {
@ -539,6 +725,21 @@ onMounted(() => {
}
}
&--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;
@ -554,6 +755,15 @@ onMounted(() => {
cursor: not-allowed;
}
}
&--text {
color: #f56c6c;
&:hover {
color: #f78989;
background-color: rgba(245, 108, 108, 0.05);
}
}
}
// 响应式网格布局
@ -573,7 +783,7 @@ onMounted(() => {
gap: 1rem;
}
// 适配小屏幕
// 适配小屏幕小于768px时垂直排列
@media (max-width: 768px) {
.appWidth {
width: 95vw;
@ -586,5 +796,10 @@ onMounted(() => {
::v-deep .el-form-item__label {
width: 100px;
}
// 小屏幕下各列上下间距
::v-deep .el-col-xs-24 + .el-col-xs-24 {
margin-top: 12px;
}
}
</style>

View File

@ -1,321 +0,0 @@
<template>
<div class="p-2 bg-gray-50" style="padding: 20px;">
<!-- <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="文件名称" prop="fileName">
<el-input v-model="queryParams.fileName" placeholder="请输入文件名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="原文件名" prop="originalName">
<el-input v-model="queryParams.originalName" placeholder="请输入原文件名" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="是否最新" prop="newest">
<el-select v-model="queryParams.newest" placeholder="请选择" clearable>
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition> -->
<div class="drawing">
<div class="file-category">
<span style="color: #757575; display: inline-block; margin-bottom: 15px">卷册目录</span>
<div
v-for="(item, i) of volumeCatalogList"
:key="i"
:class="{ active: currentActive === i }"
@click="handleClick(item, i)"
class="category-item"
>
<el-icon :size="20" class="folder-icon">
<Folder />
</el-icon>
<span class="folder-name">
{{ item.volumeNumber }}
</span>
</div>
</div>
<div class="boxs">
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<DrawingTable
:drawingList="drawingList"
:loading="loading"
:drawing_file_type="drawing_file_type"
:wf_business_status="wf_business_status"
@selection-change="handleSelectionChange"
@view="handleView"
@update="handleUpdate"
@delete="handleDelete"
@view-info="handleViewInfo"
@cancel-process-apply="handleCancelProcessApply"
/>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
</div>
</div>
</div>
</template>
<script setup name="Drawing" lang="ts">
import { listDrawing, delDrawing } from '@/api/design/drawing';
import { DrawingVO } from '@/api/design/drawing/types';
import { useUserStoreHook } from '@/store/modules/user';
import { LeaveVO } from '@/api/workflow/leave/types';
import { cancelProcessApply } from '@/api/workflow/instance';
import DrawingTable from './DrawingTable.vue';
import { listVolumeCatalog } from '@/api/design/volumeCatalog';
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { drawing_file_type, wf_business_status } = toRefs<any>(proxy?.useDict('drawing_file_type', 'wf_business_status'));
console.log(drawing_file_type);
// 当前激活项的索引
const currentActive = ref(0);
const volumeCatalogList = ref([]);
const drawingList = ref<DrawingVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const activeName = ref('1');
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: currentProject.value?.id,
fileName: undefined,
fileSuffix: undefined,
fileStatus: undefined,
originalName: undefined,
newest: undefined,
volumeCatalogId: undefined, //卷册目录id
params: {}
}
});
const { queryParams } = toRefs(data);
/** 查询图纸管理列表 */
const getList = async () => {
loading.value = true;
const res = await listDrawing(queryParams.value);
drawingList.value = res.rows;
total.value = res.total;
loading.value = false;
};
// 点击事件处理函数
const handleClick = (item, index) => {
console.log(item);
currentActive.value = index;
queryParams.value.volumeCatalogId = item.design;
getList();
};
/** 查询卷册目录列表 */
const getlistVolume = async () => {
const res = await listVolumeCatalog({ projectId: currentProject.value?.id });
volumeCatalogList.value = res.rows;
if (res.rows.length) {
queryParams.value.volumeCatalogId = res.rows[0].id;
getList();
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: DrawingVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 修改按钮操作 */
const handleUpdate = async (row?: DrawingVO) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/design-management/drawing/indexEdit`,
query: {
id: row.id,
type: 'update'
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: DrawingVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除图纸管理编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
await delDrawing(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
};
const handleView = (row) => {
var url = row.file.url;
window.open(url, '_blank');
};
/** 查看按钮操作 */
const handleViewInfo = (row?: LeaveVO) => {
proxy.$tab.closePage(proxy.$route);
proxy.$router.push({
path: `/design-management/drawing/indexEdit`,
query: {
id: row.id,
type: 'view'
}
});
};
/** 撤销按钮操作 */
const handleCancelProcessApply = async (id: string) => {
await proxy?.$modal.confirm('是否确认撤销当前单据?');
loading.value = true;
const data = {
businessId: id,
message: '申请人撤销流程!'
};
await cancelProcessApply(data).finally(() => (loading.value = false));
await getList();
proxy?.$modal.msgSuccess('撤销成功');
};
onMounted(() => {
getlistVolume();
});
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value?.id,
(nid, oid) => {
queryParams.value.projectId = nid;
getlistVolume();
}
);
onUnmounted(() => {
listeningProject();
});
</script>
<style lang="scss">
.drawing {
display: flex;
.el-tabs__header {
height: 90vh !important;
}
.file-category {
width: 200px;
display: flex;
flex-direction: column;
background-color: #f7f7f79e;
padding: 10px;
border-radius: 6px;
margin-right: 10px;
height: 84vh;
}
.file-category > div {
cursor: pointer;
padding: 8px 12px;
margin-bottom: 4px;
border-radius: 4px;
display: flex;
white-space: nowrap;
transition: all 0.2s ease;
> span {
margin-left: 6px;
}
}
.file-category {
width: 200px;
display: flex;
flex-direction: column;
background-color: #f7f7f79e;
padding: 10px;
border-radius: 8px;
margin-right: 10px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
// 分类项样式优化
.category-item {
cursor: pointer;
padding: 10px 12px;
margin-bottom: 4px;
border-radius: 6px;
display: inline-flex;
align-items: center; /* 垂直居中对齐 */
white-space: nowrap;
transition: all 0.25s ease;
font-size: 14px;
color: #334155;
line-height: 1; /* 确保行高一致 */
&:hover {
background-color: #f1f5f9;
transform: translateX(2px);
}
}
// 图标样式
.folder-icon {
color: #94a3b8;
transition: color 0.25s ease;
}
// 文件夹名称样式
.folder-name {
margin-left: 8px; /* 增加图标与文字间距 */
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - 30px); /* 限制最大宽度,防止溢出 */
}
// 活跃状态样式
.category-item.active {
background-color: #eff6ff;
color: #2563eb;
font-weight: 500;
.folder-icon {
color: #2563eb;
}
}
.boxs {
width: calc(100% - 220px);
}
}
</style>

View File

@ -44,6 +44,7 @@
target="_blank"
:type="scope.row.status == '1' ? 'primary' : 'info'"
:underline="false"
@click="handleBookFile(scope.row)"
>
{{ scope.row.fileName }}
</el-link>
@ -207,6 +208,9 @@ const handleDownload = (row: any) => {
getCheck(row);
proxy?.$download.oss(row.fileId);
};
const handleBookFile = (row: any) => {
getCheck(row);
};
// 调用查阅接口
const getCheck = async (row) => {
volumeFileViewer({ volumeFileId: row.fileId });

View File

@ -25,7 +25,7 @@
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['design:volumeCatalog:add']">新增</el-button>
</el-col>
<!-- <el-col :span="1.5">
<el-col :span="1.5">
<file-upload
v-model="form.file"
isImportInfo
@ -41,7 +41,7 @@
>
<el-button type="warning" plain icon="Upload">导入</el-button>
</file-upload>
</el-col> -->
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
@ -94,7 +94,7 @@
v-hasPermi="['design:volumeFile:add']"
>上传图纸</el-button
>
<el-button
<!-- <el-button
link
type="primary"
icon="edit"
@ -112,7 +112,7 @@
v-if="scope.row.auditType == 'back' || scope.row.auditStatus == 'termination'"
@click="handleViewHistory(scope.row)"
>查看单据</el-button
>
> -->
</template>
</el-table-column>
</el-table>
@ -159,20 +159,31 @@
</div>
</template>
</el-dialog>
<el-dialog draggable title="上传图纸卷册文件" v-model="uploadVisible" width="500px" append-to-body>
<el-dialog draggable title="上传图纸文件" v-model="uploadVisible" width="500px" append-to-body>
<el-form :model="uploadForm" label-width="80px" :inline="false">
<el-form-item label="上传文件" prop="fileId">
<file-upload :fileType="['pdf']" :fileSize="''" v-model="uploadForm.fileId"></file-upload>
<el-form-item label="蓝图" prop="type">
<el-select v-model="uploadForm.type" placeholder="请选择图纸类型"
><el-option label="过程图纸" value="1" /><el-option label="蓝图" value="3"
/></el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit" :loading="buttonLoading">确定</el-button>
<el-button @click="uploadVisible = false">取消</el-button>
<el-form-item v-if="uploadForm.type == '3'" label="蓝图" prop="fileIds">
<file-upload :fileType="['pdf']" :isShowTip="false" :fileSize="100" v-model="uploadForm.fileIds"></file-upload>
</el-form-item>
<el-form-item v-if="uploadForm.type == '1'" label="过程图纸" prop="cancellationIds">
<file-upload :fileType="['pdf']" :isShowTip="false" :fileSize="100" v-model="uploadForm.cancellationIds"></file-upload>
</el-form-item>
</el-form>
<span style="font-size: 12px; color: #999999">注意:请上传pdf格式文件</span>
<div style="display: flex; justify-content: flex-end">
<el-button type="primary" @click="onSubmit" :loading="buttonLoading">确定</el-button>
<el-button @click="uploadVisible = false">取消</el-button>
</div>
</el-dialog>
<!-- 查看文件列表 -->
<el-dialog draggable title="文件列表" v-model="viewVisible" width="45%">
<el-table v-if="fileList.length > 0" :data="fileList" style="width: 100%" border>
<el-tabs type="border-card" v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tab-pane label="蓝图" name="first"
><el-table :data="fileList" style="width: 100%" border>
<el-table-column prop="fileName" label="文件" align="center">
<template #default="scope">
<el-link
@ -201,11 +212,45 @@
<el-button type="danger" link icon="Download" @click="handleDownload(scope.row)"> 下载 </el-button>
</template>
</el-table-column>
</el-table>
<div v-else class="empty-list text-center">暂无文件</div>
</el-table></el-tab-pane
>
<el-tab-pane label="过程图纸 " name="second"
><el-table :data="fileList" style="width: 100%" border>
<el-table-column prop="fileName" label="文件" align="center">
<template #default="scope">
<el-link
:key="scope.row.fileId"
:href="scope.row.fileUrl"
target="_blank"
:type="scope.row.status == '1' ? 'primary' : 'info'"
:underline="false"
>
{{ scope.row.fileName }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="size" label="是否变更" align="center" width="120">
<template #default="scope">
<el-tag :type="scope.row.type == 1 ? 'success' : 'info'">{{ scope.row.type == 1 ? '否' : '是' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="size" label="状态" width="120" align="center">
<template #default="scope">
<el-tag :type="scope.row.status == 1 ? 'success' : 'info'">{{ scope.row.status == 1 ? '使用中' : '已作废' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="170" align="center">
<template #default="scope">
<el-button type="primary" v-if="activeName === 'second'" @click="handleAuditInfo(scope.row)">审核过程图纸</el-button>
<el-button type="danger" link icon="Download" @click="handleDownload(scope.row)"> 下载 </el-button>
</template>
</el-table-column>
</el-table></el-tab-pane
>
</el-tabs>
<template #footer>
<span>
<el-button type="primary" @click="viewVisible = false">关闭</el-button>
<el-button type="danger" @click="viewVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
@ -242,6 +287,7 @@ const ids = ref<Array<string | number>>([]);
const histroyRef = ref<InstanceType<typeof histroy>>();
const single = ref(true);
const multiple = ref(true);
const activeName = ref('first');
const total = ref(0);
const dialogHistory = ref(false);
// 获取用户 store
@ -259,9 +305,11 @@ const dialog = reactive<DialogOption>({
const uploadForm = reactive({
userIds: [],
volumeCatalogId: undefined,
fileId: undefined,
fileIds: undefined,
cancellationIds: undefined,
explainText: '',
fileList: [],
type: '1',
cancellationIds: [] // 用于存储已作废的文件ID
});
const examineForm = ref({
@ -419,10 +467,12 @@ const handleUpload = async (row?: any) => {
const resetUploadForm = () => {
uploadForm.userIds = [];
uploadForm.volumeCatalogId = undefined;
uploadForm.fileId = undefined;
uploadForm.fileIds = undefined;
uploadForm.cancellationIds = undefined;
uploadForm.explainText = '';
uploadForm.fileList = [];
uploadForm.cancellationIds = []; // 重置作废文件ID列表
uploadForm.type = '1';
uploadForm.cancellationIds = [];
};
/** 提交按钮 */
@ -449,10 +499,19 @@ const handleDownload = (row: any) => {
/** 上传文件提交 */
const onSubmit = async () => {
buttonLoading.value = true;
let cancellationIds = [];
let fileIds = [];
if (uploadForm.cancellationIds && uploadForm.cancellationIds.length > 0) {
cancellationIds = uploadForm.cancellationIds.split(',');
}
if (uploadForm.fileIds && uploadForm.fileIds.length > 0) {
fileIds = uploadForm.fileIds.split(',');
}
let obj = {
volumeCatalogId: uploadForm.volumeCatalogId,
fileIds: uploadForm.fileId.split(','),
explainText: ''
fileIds: fileIds,
cancellationIds: cancellationIds,
type: uploadForm.type
};
try {
await uploadVolumeFile(obj);
@ -511,7 +570,13 @@ const handleUpdate = async (row?: VolumeCatalogVO) => {
dialog.visible = true;
dialog.title = '修改设计出图计划';
};
// 过程图纸
const handleClick = () => {
//
};
const handleAuditInfo = (row) => {
// 审核图纸
};
onMounted(() => {
getUserAppList();
getList();