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 })]
+ }))
+ })
+})