smart-bid-web/src/views/template/templateInfo/components/TemplateForm.vue

477 lines
13 KiB
Vue
Raw Normal View History

<template>
<div class="app-container" :class="{ 'no-pointer-events': showUploadAnimation || showSaveAnimation }">
<!-- 全局上传动画 -->
<div v-if="showUploadAnimation" class="global-upload-animation">
<div class="animation-mask"></div>
<div class="animation-content">
<div class="spinner"></div>
<div class="animation-text">{{ animationText }}</div>
<div class="animation-subtext">正在处理文件请稍候...</div>
</div>
</div>
<!-- 保存动画 -->
<div v-if="showSaveAnimation" class="global-upload-animation">
<div class="animation-mask"></div>
<div class="animation-content">
<div class="spinner"></div>
<div class="animation-text">数据上传中</div>
<div class="animation-subtext">请稍后正在保存数据...</div>
</div>
</div>
<div class="content-header">
<el-breadcrumb separator="/" class="breadcrumb">
<!-- 面包屑可根据实际需求添加 -->
</el-breadcrumb>
<div class="header-actions">
<el-button class="reset-btn" @click="handleBack()" :disabled="showSaveAnimation">返回</el-button>
<el-button class="search-btn" @click="handleSave()" :loading="showSaveAnimation">
{{ type === 'add' ? '新增' : '保存' }}
</el-button>
</div>
</div>
<div class="content-body">
<el-row :gutter="24" class="content-row">
<el-col :span="24" class="form-pane">
<BasicInfo
ref="basicInfoRef"
:industry-type-options="industryTypeOptions"
:type="type"
/>
</el-col>
<el-col :span="12" class="form-pane">
<ProjectFile
ref="projectFileRef"
title="项目文件"
/>
</el-col>
<el-col :span="12" class="form-pane">
<SectionFile
ref="sectionFileRef"
title="标段/标包文件"
/>
</el-col>
<el-col :span="24" class="form-pane">
<AnalysisLabel
ref="analysisLabelRef"
title="解析标签组"
/>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
import { decryptWithSM4, encryptWithSM4 } from '@/utils/sm'
import BasicInfo from './child/BasicInfo.vue'
import ProjectFile from './child/ProjectFile.vue'
import SectionFile from './child/SectionFile.vue'
import AnalysisLabel from './child/AnalysisLabel.vue'
import {
addTemplateInfo,
updateTemplateInfo,
checkTemplateNameUnique,
getTemplateInfoDetail
} from "@/api/template/templateInfo/templateInfo";
export default {
name: 'TemplateForm',
components: {
BasicInfo,
ProjectFile,
SectionFile,
AnalysisLabel
},
props: {
// 仅保留必要的props移除type和rowData从路由获取
visible: {
type: Boolean,
default: false
},
industryTypeOptions: {
type: Array,
default: () => []
}
},
data() {
return {
type: 'add', // 默认为新增,将从路由参数覆盖
templateId: null, // 从路由参数解析的模板ID
showUploadAnimation: false,
showSaveAnimation: false,
uploadQueue: 0,
animationText: '识别中'
}
},
methods: {
// 返回列表页
handleBack() {
const obj = { path: "/templateInfo/index" };
this.$tab.closeOpenPage(obj);
},
// 处理文件数据提取fileRes中的属性到顶层
processFile(file, source) {
const baseProps = {
compositionName: file.compositionName,
businessType: file.businessType,
compositionType: file.compositionType,
compositionFileType: file.compositionFileType,
source: source,
fileName: file.response?.fileRes?.fileName || file.name || '',
filePath: file.response?.fileRes?.filePath || file.filePath || '',
fileSize: file.response?.fileRes?.fileSize || file.size || 0,
fileType: file.response?.fileRes?.fileType || '',
sourceFileName: file.response?.fileRes?.sourceFileName || file.name || '',
suffixName: file.response?.fileRes?.suffixName || '',
percentage: file.percentage || 0,
status: file.status || ''
};
delete baseProps.response;
return baseProps;
},
// 保存模板信息
async handleSave() {
if (this.showSaveAnimation) return;
this.showSaveAnimation = true;
try {
// 1. 校验并获取所有子组件数据
const [basicData, projectFileData, sectionFileData, analysisLabelData] = await Promise.all([
this.$refs.basicInfoRef.validate(),
this.$refs.projectFileRef.validate(),
this.$refs.sectionFileRef.validate(),
this.$refs.analysisLabelRef.validate()
]);
console.log("project",projectFileData)
console.log("section",sectionFileData)
// 2. 处理文件列表
const projectFiles = projectFileData.flatMap(tab =>
tab.fileList.map(file => this.processFile(file, 'project'))
);
const sectionFiles = sectionFileData.flatMap(tab =>
tab.fileList.map(file => this.processFile(file, 'section'))
);
const allFiles = [...projectFiles, ...sectionFiles];
if (allFiles.length === 0) {
throw new Error('请至少上传一个文件');
}
// 3. 处理删除的文件路径
const processDelFiles = (delFileList) => {
return delFileList.map(path => path.includes('mainDatabase/') ? path : path);
};
// 4. 合并表单数据
const formData = {
...basicData,
...analysisLabelData,
templateId: this.type === 'edit' ? this.templateId : null,
useState: '0',
publishedStatus: '1',
files: allFiles,
delFiles: [
...processDelFiles(projectFileData.flatMap(item => item.delFileList || [])),
...processDelFiles(sectionFileData.flatMap(item => item.delFileList || []))
]
};
console.log('666',formData)
// 5. 校验模板名称唯一性(仅编辑时)
if (this.type === 'edit') {
const uniqueRes = await checkTemplateNameUnique({
templateName: formData.templateName,
templateId: formData.templateId
});
if (uniqueRes.data > 0) {
throw new Error('同一行业下模板名称已存在,请更换');
}
}
// 6. 调用接口保存
if (this.type === 'add') {
await addTemplateInfo(formData);
} else {
await updateTemplateInfo(formData);
}
this.$message.success(`${this.type === 'add' ? '新增' : '编辑'}模板成功`);
this.$emit('save-success');
this.handleBack();
} catch (error) {
this.$message.error(error.message || '请完善表单信息');
} finally {
this.showSaveAnimation = false;
}
},
// 上传动画控制
handleStartUpload(text) {
this.animationText = text || '处理中';
this.uploadQueue++;
this.showUploadAnimation = true;
},
handleEndUpload() {
this.uploadQueue--;
if (this.uploadQueue <= 0) {
this.showUploadAnimation = false;
this.uploadQueue = 0;
}
},
// 加载模板详情(编辑模式)
async loadTemplateData() {
if (!this.templateId) return;
try {
const res = await getTemplateInfoDetail({ templateId: this.templateId });
const detail = res.data || {};
console.log('111',detail)
// 向子组件传递数据
this.$refs.basicInfoRef.setFormData(detail);
this.$refs.projectFileRef.setFormData(detail.projectFiles || []);
this.$refs.sectionFileRef.setFormData(detail.sectionFiles || []);
this.$refs.analysisLabelRef.setFormData(detail);
} catch (error) {
this.$message.error('加载模板详情失败:' + error.message);
}
}
},
mounted() {
// 监听上传动画事件
this.$bus.$on('startUpload', this.handleStartUpload);
this.$bus.$off('endUpload', this.handleEndUpload);
this.$bus.$on('endUpload', this.handleEndUpload);
// 从路由参数中解析type和templateId核心逻辑
const { type: encryptedType, templateId: encryptedTemplateId } = this.$route.query;
// 解析并验证type
if (encryptedType) {
try {
const decryptedType = decryptWithSM4(encryptedType);
// 确保type只能是add或edit非法值默认add
this.type = ['add', 'edit'].includes(decryptedType) ? decryptedType : 'add';
} catch (e) {
console.error('解析type失败', e);
this.type = 'add'; // 解析失败默认新增
}
}
// 解析templateId
if (encryptedTemplateId) {
try {
this.templateId = decryptWithSM4(encryptedTemplateId);
} catch (e) {
console.error('解析templateId失败', e);
this.templateId = null; // 解析失败置空
}
}
// 编辑模式且有有效ID时加载详情数据
if (this.type === 'edit' && this.templateId) {
this.loadTemplateData();
}
},
beforeDestroy() {
// 移除事件监听,避免内存泄漏
this.$bus.$off('startUpload', this.handleStartUpload);
this.$bus.$off('endUpload', this.handleEndUpload);
}
}
</script>
<style scoped lang="scss">
.app-container {
padding: 24px;
background: linear-gradient(180deg, #F1F6FF 20%, #E5EFFF 100%);
min-height: 100vh;
overflow-y: auto;
position: relative;
&.no-pointer-events {
pointer-events: none;
* { pointer-events: none; }
}
}
.global-upload-animation {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
pointer-events: auto;
.animation-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(4px);
}
.animation-content {
position: relative;
z-index: 10000;
text-align: center;
background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
padding: 40px 50px;
border-radius: 20px;
box-shadow: 0 10px 40px rgba(31, 114, 234, 0.15), 0 0 0 1px rgba(31, 114, 234, 0.1);
min-width: 280px;
.spinner {
width: 60px;
height: 60px;
border: 4px solid #f3f7ff;
border-top: 4px solid #409EFF;
border-radius: 50%;
animation: spin 1.2s linear infinite;
margin: 0 auto 20px;
position: relative;
&::after {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border: 4px solid transparent;
border-top: 4px solid #66b1ff;
border-radius: 50%;
animation: spin 0.8s linear infinite reverse;
}
}
.animation-text {
font-size: 20px;
font-weight: 600;
color: #409EFF;
margin-bottom: 8px;
background: linear-gradient(135deg, #409EFF 0%, #66b1ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.animation-subtext {
font-size: 14px;
color: #666;
opacity: 0.8;
}
}
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.breadcrumb {
font-size: 14px;
color: #606266;
::v-deep .el-breadcrumb__item:last-child .el-breadcrumb__inner {
color: #303133;
font-weight: 500;
}
}
.header-actions {
display: flex;
gap: 12px;
}
.content-body {
margin-top: 10px;
}
.content-row {
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.form-pane {
background: #fff;
border-radius: 16px;
box-shadow: 0px 4px 20px 0px rgba(31, 35, 55, 0.1);
padding: 20px;
margin-bottom: 20px;
flex: 1;
min-width: 300px;
}
.search-btn {
background: #409EFF;
border-color: #409EFF;
color: #fff;
padding: 12px 24px;
border-radius: 6px;
box-shadow: 0px 4px 12px 0px rgba(64, 158, 255, 0.4);
transition: all 0.3s ease;
&:hover {
background: #66b1ff;
box-shadow: 0px 6px 16px 0px rgba(64, 158, 255, 0.5);
}
}
.reset-btn {
background: #fff;
border: 1px solid #dcdfe6;
color: #606266;
padding: 12px 24px;
border-radius: 6px;
box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&:hover {
background: #f5f7fa;
color: #409EFF;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 992px) {
.content-row {
gap: 16px;
}
.form-pane {
flex: 0 0 100%;
max-width: 100%;
}
}
</style>