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

477 lines
13 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="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>