From 3082e3ac30a563461b16b812cc79d6f067850110 Mon Sep 17 00:00:00 2001 From: weizong song Date: Wed, 12 Aug 2026 02:32:44 +0800 Subject: [PATCH] up --- .../components/BudgetSubmissionForm.vue | 17 +- .../components/LeadDepartmentSummaryForm.vue | 332 ++++++++++++------ .../budget/collection/leadDepartment.vue | 17 +- .../components/BudgetSubmissionForm.spec.js | 32 ++ .../LeadDepartmentSummaryForm.spec.js | 135 +++++++ 5 files changed, 415 insertions(+), 118 deletions(-) create mode 100644 tests/unit/components/BudgetSubmissionForm.spec.js create mode 100644 tests/unit/components/LeadDepartmentSummaryForm.spec.js diff --git a/src/views/budget/collection/components/BudgetSubmissionForm.vue b/src/views/budget/collection/components/BudgetSubmissionForm.vue index fc3285e..4e7abbb 100644 --- a/src/views/budget/collection/components/BudgetSubmissionForm.vue +++ b/src/views/budget/collection/components/BudgetSubmissionForm.vue @@ -2014,12 +2014,9 @@ export default { }, handleUploadRemove(file, fileList, row) { - // 从文件列表中移除 - const index = fileList.findIndex(f => f.uid === file.uid) - if (index !== -1) { - fileList.splice(index, 1) - row.fileList = fileList - } + // Element UI 传入的 fileList 已经移除了当前文件,直接同步即可。 + // 不能再从 fileList 中查找并 splice,否则编辑态的旧附件列表不会更新。 + row.fileList = fileList.slice() }, handleGeneralUploadSuccess(response, file, fileList) { @@ -2036,12 +2033,8 @@ export default { }, handleGeneralUploadRemove(file, fileList) { - // 从文件列表中移除 - const index = fileList.findIndex(f => f.uid === file.uid) - if (index !== -1) { - fileList.splice(index, 1) - this.formData.generalFiles = fileList - } + // Element UI 传入的 fileList 已经移除了当前文件,直接同步即可。 + this.formData.generalFiles = fileList.slice() }, handleUploadError(err, file, fileList) { diff --git a/src/views/budget/collection/components/LeadDepartmentSummaryForm.vue b/src/views/budget/collection/components/LeadDepartmentSummaryForm.vue index 5b562cc..cb078cf 100644 --- a/src/views/budget/collection/components/LeadDepartmentSummaryForm.vue +++ b/src/views/budget/collection/components/LeadDepartmentSummaryForm.vue @@ -271,7 +271,7 @@ :on-remove="(file, fileList) => handleEconomicUploadRemove(fileList, scope.row)" :on-error="handleUploadError" multiple - :limit="5" + :limit="99" > 上传附件 @@ -334,31 +334,101 @@ 申请资金汇总 - - - - - - - - - - - - - - - - 新项目包含 - 旧项目包含 - - - +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
item.departmentName) }, - fundAmountFields() { - const fund = this.formData.fundApplication - return [ - { label: '新项目总额(万元)', target: fund.newProject, key: 'totalAmount' }, - { label: '新项目本年(万元)', target: fund.newProject, key: 'yearCurrent' }, - { label: '新项目下年(万元)', target: fund.newProject, key: 'yearNext' }, - { label: '旧项目合同总额(万元)', target: fund.oldProject, key: 'contractTotal' }, - { label: '旧项目前年累计(万元)', target: fund.oldProject, key: 'previousYearsTotal' }, - { label: '旧项目前年余额(万元)', target: fund.oldProject, key: 'previousYearBalance' }, - { label: '旧项目本年(万元)', target: fund.oldProject, key: 'yearCurrent' } - ] + fundApplicationRows() { + return [this.formData.fundApplication] + }, + + nextBudgetYearLabel() { + const currentYear = parseInt(this.formData.budgetYear, 10) + return Number.isFinite(currentYear) ? `${currentYear + 1}年` : '下一年' }, currentYearMonthlyTotal() { @@ -1090,7 +1156,9 @@ export default { submissionDate: submission.submissionDate, economicItems: submission.economicItems || [], performanceIndicators: submission.performanceIndicators || [], - implementationSchedule: submission.implementationSchedule || '' + implementationSchedule: submission.implementationSchedule || '', + implementationScheduleDetail: submission.implementationScheduleDetail || {}, + fundApplication: submission.fundApplication || {} }))) // 检查并提示缺少绩效指标数据 @@ -1103,22 +1171,9 @@ export default { } this.formData.executeDepartmentCount = this.formData.departmentSubmissions.length - - // 使用后端返回的经济分类汇总数据,如果没有则自动生成 - if (data.economicSummary && data.economicSummary.length > 0) { - this.$set(this.formData, 'economicSummary', data.economicSummary.map(item => ({ - categoryId: item.categoryId, - categoryName: item.categoryName, - totalAmount: item.totalAmount, - departmentCount: item.departmentCount, - summaryCalculationBasis: '', // 默认为空,需要用户填写 - fileList: [], - basisList: item.departments ? item.departments.map(dept => ({ department: dept, basis: '' })) : [] - }))) - } else { - // 自动生成经济分类汇总(向后兼容) - this.generateEconomicSummary(this.formData.departmentSubmissions) - } + this.generateEconomicSummary(this.formData.departmentSubmissions) + this.aggregateFundApplication(this.formData.departmentSubmissions) + this.aggregateImplementationScheduleAmounts(this.formData.departmentSubmissions) } } catch (error) { console.error('获取执行科室填报数据失败:', error) @@ -1140,16 +1195,17 @@ export default { if (economicMap.has(key)) { const existing = economicMap.get(key) existing.totalAmount = addMoney(existing.totalAmount, amount) - // 检查是否已存在该科室的记录 const existingDept = existing.basisList.find(dept => dept.department === submission.departmentName) if (existingDept) { existingDept.amount = addMoney(existingDept.amount, amount) - existingDept.basis = item.calculationBasis // 更新测算依据 + existingDept.basis = [existingDept.basis, item.calculationBasis].filter(Boolean).join('\n') + existingDept.files.push(...this.normalizeFileList(item.files || [])) } else { existing.basisList.push({ department: submission.departmentName, amount: amount, - basis: item.calculationBasis + basis: item.calculationBasis, + files: this.normalizeFileList(item.files || []) }) existing.departmentCount += 1 } @@ -1159,13 +1215,14 @@ export default { categoryName: item.categoryName, totalAmount: amount, departmentCount: 1, - summaryCalculationBasis: '', // 默认为空,需要用户填写 + summaryCalculationBasis: '', fileList: [], expanded: false, // 初始状态为收起 basisList: [{ department: submission.departmentName, amount: amount, - basis: item.calculationBasis + basis: item.calculationBasis, + files: this.normalizeFileList(item.files || []) }] }) } @@ -1173,8 +1230,81 @@ export default { } }) - // 转换为数组格式并确保响应式更新 - this.$set(this.formData, 'economicSummary', Array.from(economicMap.values())) + const summary = Array.from(economicMap.values()).map(item => ({ + ...item, + summaryCalculationBasis: item.basisList + .map(detail => `${detail.department}:${detail.basis || '-'}`) + .join('\n\n'), + fileList: this.mergeFileLists(...item.basisList.map(detail => detail.files)) + })) + this.$set(this.formData, 'economicSummary', summary) + }, + + aggregateFundApplication(submissions) { + const values = (section, key) => submissions.map(submission => { + const application = submission.fundApplication || {} + const source = application[section] || {} + return source[key] || '0.0000' + }) + const fund = this.formData.fundApplication + fund.newProject.totalAmount = sumMoney(values('newProject', 'totalAmount')) + fund.newProject.yearCurrent = sumMoney(values('newProject', 'yearCurrent')) + fund.newProject.yearNext = sumMoney(values('newProject', 'yearNext')) + fund.oldProject.contractTotal = sumMoney(values('oldProject', 'contractTotal')) + fund.oldProject.previousYearsTotal = sumMoney(values('oldProject', 'previousYearsTotal')) + fund.oldProject.previousYearBalance = sumMoney(values('oldProject', 'previousYearBalance')) + fund.oldProject.yearCurrent = sumMoney(values('oldProject', 'yearCurrent')) + + // 固定资产不是可求和字段:只有一个执行科室时才继承其选择,多科室时由牵头科室自行选择。 + if (submissions.length === 1) { + const application = submissions[0].fundApplication || {} + const newProject = application.newProject || {} + const oldProject = application.oldProject || {} + fund.newProject.hasFixedAssets = Boolean(newProject.hasFixedAssets) + fund.oldProject.hasFixedAssets = Boolean(oldProject.hasFixedAssets) + } + }, + + handleFixedAssetOptionChange(project, hasFixedAssets, checked) { + // 两个勾选框互斥;点击当前已选项时维持选择,避免出现“是/否”都未选的状态。 + if (checked) { + this.$set(project, 'hasFixedAssets', hasFixedAssets) + } + }, + + aggregateImplementationScheduleAmounts(submissions) { + const detail = this.formData.implementationScheduleDetail + const values = key => submissions.map(submission => + (submission.implementationScheduleDetail || {})[key] || '0.0000' + ) + detail.currentYearAmount = sumMoney(values('currentYearAmount')) + for (let month = 1; month <= 12; month++) { + const key = `currentYearMonth${month}` + detail[key] = sumMoney(values(key)) + } + }, + + mergeFileLists(...fileLists) { + const seen = new Set() + return fileLists.reduce((files, fileList) => files.concat(fileList || []), []).filter(file => { + const fileId = file.fileId || file.id + if (!fileId || seen.has(fileId)) return false + seen.add(fileId) + return true + }) + }, + + mergeSavedEconomicSummary(savedItems) { + const items = savedItems || [] + items.forEach(savedItem => { + const economicItem = this.formData.economicSummary.find(item => item.categoryId === savedItem.categoryId) + if (economicItem) { + this.$set(economicItem, 'summaryCalculationBasis', savedItem.summaryCalculationBasis || savedItem.summary_calculation_basis || '') + if (Object.prototype.hasOwnProperty.call(savedItem, 'files') || Object.prototype.hasOwnProperty.call(savedItem, 'file_list')) { + this.$set(economicItem, 'fileList', this.normalizeFileList(savedItem.files || savedItem.file_list || [])) + } + } + }) }, normalizeSummaryMoney(value) { @@ -1249,14 +1379,7 @@ export default { // 填充经济分类汇总数据(如果有传递) if (data.economicSummary && data.economicSummary.length > 0) { - // 将已有的汇总测算依据应用到当前的经济分类汇总中 - data.economicSummary.forEach(savedItem => { - const economicItem = this.formData.economicSummary.find(item => item.categoryId === savedItem.categoryId) - if (economicItem) { - this.$set(economicItem, 'summaryCalculationBasis', savedItem.summaryCalculationBasis || '') - this.$set(economicItem, 'fileList', this.normalizeFileList(savedItem.files || [])) - } - }) + this.mergeSavedEconomicSummary(data.economicSummary) } // 如果经济分类汇总数据不完整,或需要更详细的数据,再调用API补充 @@ -1270,16 +1393,7 @@ export default { // 补充经济分类汇总数据(如果lead_submission中没有完整数据) if (apiData.economicSummary && apiData.economicSummary.length > 0) { - apiData.economicSummary.forEach(savedItem => { - const economicItem = this.formData.economicSummary.find(item => item.categoryId === savedItem.categoryId) - if (economicItem && !economicItem.summaryCalculationBasis) { - // 只在没有数据时补充 - this.$set(economicItem, 'summaryCalculationBasis', savedItem.summaryCalculationBasis || '') - } - if (economicItem) { - this.$set(economicItem, 'fileList', this.normalizeFileList(savedItem.files || [])) - } - }) + this.mergeSavedEconomicSummary(apiData.economicSummary) } // 补充绩效指标数据(如果lead_submission中没有) @@ -1325,23 +1439,13 @@ export default { // 填充经济分类汇总数据 if (data.economicSummary && data.economicSummary.length > 0) { - // 使用新格式的数据 - data.economicSummary.forEach(savedItem => { - const economicItem = this.formData.economicSummary.find(item => item.categoryId === savedItem.categoryId) - if (economicItem) { - this.$set(economicItem, 'summaryCalculationBasis', savedItem.summaryCalculationBasis || '') - this.$set(economicItem, 'fileList', this.normalizeFileList(savedItem.files || [])) - } - }) + this.mergeSavedEconomicSummary(data.economicSummary) } else if (data.economic_summary && data.economic_summary.length > 0) { - // 向后兼容老格式 - data.economic_summary.forEach(savedItem => { - const economicItem = this.formData.economicSummary.find(item => item.categoryId === savedItem.categoryId) - if (economicItem) { - this.$set(economicItem, 'summaryCalculationBasis', savedItem.summary_calculation_basis || '') - this.$set(economicItem, 'fileList', this.normalizeFileList(savedItem.files || savedItem.file_list || [])) - } - }) + this.mergeSavedEconomicSummary(data.economic_summary.map(item => ({ + ...item, + categoryId: item.categoryId || item.category_id, + files: item.files || item.file_list || [] + }))) } } else if (response && !response.errcode) { // 处理老格式的响应(向后兼容) @@ -1363,12 +1467,11 @@ export default { } if (data.economic_summary && data.economic_summary.length > 0) { - data.economic_summary.forEach(savedItem => { - const economicItem = this.formData.economicSummary.find(item => item.categoryId === savedItem.categoryId) - if (economicItem) { - this.$set(economicItem, 'summaryCalculationBasis', savedItem.summary_calculation_basis || '') - } - }) + this.mergeSavedEconomicSummary(data.economic_summary.map(item => ({ + ...item, + categoryId: item.categoryId || item.category_id, + files: item.files || item.file_list || [] + }))) } } else if (response && response.errcode) { // 如果是新建模式,不显示错误 @@ -1845,6 +1948,29 @@ export default { margin: 15px 0; } +.fund-summary-table-container { + margin: 15px 0; + overflow-x: auto; +} + +.fund-summary-table { + min-width: 1440px; +} + +.fund-summary-table >>> .el-table__header th { + background-color: #f5f7fa; + color: #606266; + font-weight: 600; +} + +.fund-summary-table >>> .el-table__cell { + padding: 8px 0; +} + +.fund-summary-table >>> .el-input__inner { + text-align: right; +} + .economic-table, .performance-table { font-size: 14px; diff --git a/src/views/budget/collection/leadDepartment.vue b/src/views/budget/collection/leadDepartment.vue index 698426d..1f1d9ad 100644 --- a/src/views/budget/collection/leadDepartment.vue +++ b/src/views/budget/collection/leadDepartment.vue @@ -151,7 +151,7 @@ :disabled="!hasChildren(scope.row) && !getLeadSubmission(scope.row)" @click="openSummarySubmit(scope.row)" > - {{ getLeadSubmission(scope.row) && getLeadSubmission(scope.row).status !== 'draft' ? '查看汇总' : '汇总提交' }} + {{ summaryActionLabel(scope.row) }} @@ -436,6 +436,17 @@ export default { return row.submission || row.lead_submission || null }, + hasStartedLeadSummary(row) { + const submission = this.getLeadSubmission(row) + return Boolean(submission && (submission.summary_started_at || submission.summaryStartedAt)) + }, + + summaryActionLabel(row) { + const submission = this.getLeadSubmission(row) + if (!this.hasStartedLeadSummary(row)) return '汇总提交' + return submission && submission.status === 'draft' ? '修改汇总' : '查看汇总' + }, + canAllocate(row) { const year = row.budget_year || {} return year.status === 'ACTIVE' @@ -462,12 +473,12 @@ export default { openSummarySubmit(row) { // 打开汇总提交对话框 this.currentSummaryPackage = { ...row } - // 根据是否已有提交记录判断模式 + // 小包分解会预建 lead 草稿保存基础申请信息,不能用记录是否存在判断是否首次汇总。 const submission = this.getLeadSubmission(row) const isActiveYear = row.budget_year && row.budget_year.status === 'ACTIVE' this.summaryMode = !isActiveYear ? 'view' - : !submission ? 'create' : submission.status === 'draft' ? 'edit' : 'view' + : !this.hasStartedLeadSummary(row) ? 'create' : submission.status === 'draft' ? 'edit' : 'view' this.summarySubmitVisible = true }, diff --git a/tests/unit/components/BudgetSubmissionForm.spec.js b/tests/unit/components/BudgetSubmissionForm.spec.js new file mode 100644 index 0000000..55dbd0a --- /dev/null +++ b/tests/unit/components/BudgetSubmissionForm.spec.js @@ -0,0 +1,32 @@ +jest.mock('@/api/budget/departmentSubmission.js', () => ({})) + +import BudgetSubmissionForm from '@/views/budget/collection/components/BudgetSubmissionForm.vue' + +describe('BudgetSubmissionForm attachment removal', () => { + const removedFile = { uid: 1, fileId: 101 } + const remainingFile = { uid: 2, fileId: 102 } + + it('uses Element UI’s post-removal list for economic-item attachments', () => { + const row = { fileList: [removedFile, remainingFile] } + const remainingFiles = [remainingFile] + + BudgetSubmissionForm.methods.handleUploadRemove(removedFile, remainingFiles, row) + + expect(row.fileList).toEqual([remainingFile]) + expect(row.fileList).not.toBe(remainingFiles) + }) + + it('uses Element UI’s post-removal list for general attachments', () => { + const context = { + formData: { + generalFiles: [removedFile, remainingFile] + } + } + const remainingFiles = [remainingFile] + + BudgetSubmissionForm.methods.handleGeneralUploadRemove.call(context, removedFile, remainingFiles) + + expect(context.formData.generalFiles).toEqual([remainingFile]) + expect(context.formData.generalFiles).not.toBe(remainingFiles) + }) +}) diff --git a/tests/unit/components/LeadDepartmentSummaryForm.spec.js b/tests/unit/components/LeadDepartmentSummaryForm.spec.js new file mode 100644 index 0000000..003d9af --- /dev/null +++ b/tests/unit/components/LeadDepartmentSummaryForm.spec.js @@ -0,0 +1,135 @@ +jest.mock('@/api/budget/leadDepartment.js', () => ({})) +jest.mock('@/views/budget/collection/components/BudgetSubmissionForm.vue', () => ({})) + +import LeadDepartmentSummaryForm from '@/views/budget/collection/components/LeadDepartmentSummaryForm.vue' + +function createContext() { + const methods = LeadDepartmentSummaryForm.methods + const context = { + formData: { + economicSummary: [], + fundApplication: { + newProject: {}, + oldProject: {} + }, + implementationScheduleDetail: {} + }, + $set(target, key, value) { + target[key] = value + } + } + context.normalizeFileList = methods.normalizeFileList.bind(context) + context.mergeFileLists = methods.mergeFileLists.bind(context) + return context +} + +describe('LeadDepartmentSummaryForm automatic aggregation', () => { + const submissions = [ + { + departmentName: '监测一科', + economicItems: [{ + categoryId: 1, + categoryName: '物业管理费', + amount: '10.0000', + calculationBasis: '10个月 × 1万元', + files: [{ id: 101, name: '一科.pdf' }] + }], + fundApplication: { + newProject: { totalAmount: '3.0000', yearCurrent: '2.0000', yearNext: '1.0000' }, + oldProject: { contractTotal: '8.0000', previousYearsTotal: '4.0000', previousYearBalance: '1.0000', yearCurrent: '2.0000' } + }, + implementationScheduleDetail: { currentYearAmount: '4.0000', currentYearMonth1: '1.0000', currentYearMonth2: '3.0000' } + }, + { + departmentName: '监测二科', + economicItems: [{ + categoryId: 1, + categoryName: '物业管理费', + amount: '20.0000', + calculationBasis: '20个月 × 1万元', + files: [{ id: 102, name: '二科.pdf' }] + }], + fundApplication: { + newProject: { totalAmount: '5.0000', yearCurrent: '3.0000', yearNext: '2.0000' }, + oldProject: { contractTotal: '12.0000', previousYearsTotal: '6.0000', previousYearBalance: '2.0000', yearCurrent: '4.0000' } + }, + implementationScheduleDetail: { currentYearAmount: '6.0000', currentYearMonth1: '2.0000', currentYearMonth2: '4.0000' } + } + ] + + it('combines economic amounts, department-labelled bases, and every attachment', () => { + const context = createContext() + + LeadDepartmentSummaryForm.methods.generateEconomicSummary.call(context, submissions) + + expect(context.formData.economicSummary).toEqual([expect.objectContaining({ + categoryId: 1, + totalAmount: '30.0000', + departmentCount: 2, + summaryCalculationBasis: '监测一科:10个月 × 1万元\n\n监测二科:20个月 × 1万元', + fileList: [ + expect.objectContaining({ fileId: 101 }), + expect.objectContaining({ fileId: 102 }) + ] + })]) + }) + + it('adds all seven fund fields and all thirteen schedule amount fields', () => { + const context = createContext() + + LeadDepartmentSummaryForm.methods.aggregateFundApplication.call(context, submissions) + LeadDepartmentSummaryForm.methods.aggregateImplementationScheduleAmounts.call(context, submissions) + + expect(context.formData.fundApplication).toEqual({ + newProject: { totalAmount: '8.0000', yearCurrent: '5.0000', yearNext: '3.0000' }, + oldProject: { contractTotal: '20.0000', previousYearsTotal: '10.0000', previousYearBalance: '3.0000', yearCurrent: '6.0000' } + }) + expect(context.formData.implementationScheduleDetail.currentYearAmount).toBe('10.0000') + expect(context.formData.implementationScheduleDetail.currentYearMonth1).toBe('3.0000') + expect(context.formData.implementationScheduleDetail.currentYearMonth2).toBe('7.0000') + expect(context.formData.implementationScheduleDetail.currentYearMonth12).toBe('0.0000') + }) + + it('inherits fixed-asset choices only when there is one department submission', () => { + const context = createContext() + context.formData.fundApplication = { + newProject: { hasFixedAssets: null }, + oldProject: { hasFixedAssets: null } + } + + LeadDepartmentSummaryForm.methods.aggregateFundApplication.call(context, submissions) + + expect(context.formData.fundApplication.newProject.hasFixedAssets).toBeNull() + expect(context.formData.fundApplication.oldProject.hasFixedAssets).toBeNull() + + LeadDepartmentSummaryForm.methods.aggregateFundApplication.call(context, [{ + fundApplication: { + newProject: { hasFixedAssets: true }, + oldProject: { hasFixedAssets: false } + } + }]) + + expect(context.formData.fundApplication.newProject.hasFixedAssets).toBe(true) + expect(context.formData.fundApplication.oldProject.hasFixedAssets).toBe(false) + }) + + it('restores saved economic summary content instead of recalculating it in edit mode', () => { + const context = createContext() + context.formData.economicSummary = [{ + categoryId: 1, + summaryCalculationBasis: '自动生成的依据', + fileList: [{ fileId: 101, name: '自动附件.pdf' }] + }] + + LeadDepartmentSummaryForm.methods.mergeSavedEconomicSummary.call(context, [{ + categoryId: 1, + summaryCalculationBasis: '已保存的汇总依据', + files: [{ id: 201, name: '已保存附件.pdf' }] + }]) + + expect(context.formData.economicSummary[0]).toEqual(expect.objectContaining({ + summaryCalculationBasis: '已保存的汇总依据', + fileList: [expect.objectContaining({ fileId: 201 })] + })) + }) +})