You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

640 lines
20 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="split-package-form">
<el-form ref="basicForm" :model="basicInfo" :rules="basicRules" label-width="120px">
<!-- 基本信息 -->
<div class="section-title">
<i class="el-icon-edit-outline"></i>
基本信息
</div>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="预算名称">
<el-input
:value="packageData.name"
placeholder="预算名称"
readonly
disabled
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="分配金额">
<el-input
:value="formatCurrency(packageData.allocated_amount)"
placeholder="分配金额"
readonly
disabled
>
<template slot="append">万元</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="申请依据" prop="basis">
<el-input
v-model="basicInfo.basis"
type="textarea"
:rows="3"
placeholder="请输入申请依据"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="申请目的" prop="purpose">
<el-input
v-model="basicInfo.purpose"
type="textarea"
:rows="3"
placeholder="请输入申请目的"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="申请理由" prop="reason">
<el-input
v-model="basicInfo.reason"
type="textarea"
:rows="3"
placeholder="请输入申请理由"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="项目内容" prop="content">
<el-input
v-model="basicInfo.content"
type="textarea"
:rows="4"
placeholder="请详细描述项目内容"
maxlength="1000"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 执行科室选择与金额分配 -->
<div class="section-title">
<i class="el-icon-s-cooperation"></i>
执行科室分配
</div>
<div class="department-allocation">
<div class="add-department">
<el-select
v-model="selectedDepartment"
placeholder="选择执行科室"
style="width: 200px; margin-right: 10px;"
clearable
>
<el-option
v-for="dept in availableDepartments"
:key="dept.id"
:label="dept.name"
:value="dept.id"
/>
</el-select>
<el-button type="primary" @click="addDepartment" :disabled="!selectedDepartment">
添加科室
</el-button>
</div>
<el-table
:data="departmentAllocations"
style="width: 100%; margin-top: 15px;"
border
:summary-method="getSummaries"
show-summary
>
<el-table-column type="index" label="序号" width="60" align="center" />
<!-- 隐含department_id显示名称 -->
<el-table-column label="执行科室" width="220">
<template slot-scope="scope">
<div style="display: flex; align-items: center;">
<el-tag type="info" style="margin-right: 8px;">{{ scope.row.departmentName }}</el-tag>
<el-tooltip content="科室ID" placement="top">
<span style="color: #bbb; font-size: 12px;">[ID: {{ scope.row.departmentId || scope.row.department_id }}]</span>
</el-tooltip>
<!-- 隐藏域确保department_id在表格数据中 -->
<input type="hidden" :value="scope.row.departmentId || scope.row.department_id" />
<input type="hidden" :value="scope.row.packageId || scope.row.package_id" />
</div>
</template>
</el-table-column>
<el-table-column label="分配金额" width="200" align="center">
<template slot-scope="scope">
<el-input
v-model.number="scope.row.allocatedAmount"
type="number"
:min="0.1"
:step="0.1"
placeholder="请输入分配金额"
@input="validateAllocation"
>
<template slot="append">万元</template>
</el-input>
</template>
</el-table-column>
<el-table-column prop="childPackageName" label="执行科室包名称" width="250">
<template slot-scope="scope">
<el-input
v-model="scope.row.childPackageName"
placeholder="请输入执行科室包名称"
maxlength="200"
/>
</template>
</el-table-column>
<el-table-column prop="description" label="说明">
<template slot-scope="scope">
<el-input
v-model="scope.row.description"
placeholder="请输入说明(可选)"
maxlength="300"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template slot-scope="scope">
<el-button
type="danger"
size="mini"
@click="removeDepartment(scope.$index)"
icon="el-icon-delete"
circle
/>
</template>
</el-table-column>
</el-table>
<div class="allocation-summary" v-if="departmentAllocations.length > 0">
<div class="summary-item" :class="{ 'error': totalAllocated > packageData.allocated_amount }">
<span>已分配总额:{{ formatCurrency(totalAllocated) }} 万元</span>
</div>
<div class="summary-item">
<span>待分配金额:{{ formatCurrency(packageData.allocated_amount - totalAllocated) }} 万元</span>
</div>
<div class="summary-item" v-if="totalAllocated !== packageData.allocated_amount" style="color: #F56C6C;">
<i class="el-icon-warning"></i>
分配金额必须等于预算分配金额
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="form-actions">
<el-button @click="handleCancel">取 消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
{{ packageData.isEditMode ? '' : '' }}
</el-button>
</div>
</div>
</template>
<script>
import { listdeptNoAuth } from '@/api/system/department.js'
export default {
name: 'SplitPackageForm',
props: {
packageData: {
type: Object,
required: true
}
},
data() {
return {
submitting: false,
selectedDepartment: '',
departmentOptions: [],
basicInfo: {
basis: '',
purpose: '',
reason: '',
content: ''
},
submissionData: null,
basicRules: {
basis: [
{ required: true, message: '请输入申请依据', trigger: 'blur' },
{ min: 10, max: 500, message: '申请依据长度在10到500个字符', trigger: 'blur' }
],
purpose: [
{ required: true, message: '请输入申请目的', trigger: 'blur' },
{ min: 10, max: 500, message: '申请目的长度在10到500个字符', trigger: 'blur' }
],
reason: [
{ required: true, message: '请输入申请理由', trigger: 'blur' },
{ min: 10, max: 500, message: '申请理由长度在10到500个字符', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入项目内容', trigger: 'blur' },
{ min: 20, max: 1000, message: '项目内容长度在20到1000个字符', trigger: 'blur' }
]
},
departmentAllocations: []
}
},
computed: {
availableAmount() {
return this.packageData.allocated_amount
},
availableDepartments() {
const allocatedDeptIds = this.departmentAllocations.map(item => item.departmentId)
return this.departmentOptions.filter(dept => !allocatedDeptIds.includes(dept.id))
},
totalAllocated() {
return this.departmentAllocations.reduce((sum, item) => {
return sum + (parseFloat(item.allocatedAmount) || 0)
}, 0)
}
},
async created() {
await this.loadDepartments()
await this.loadSubmissionData()
// 如果是编辑模式,加载现有分配数据
if (this.packageData.isEditMode) {
await this.loadExistingAllocations()
}
},
methods: {
async loadDepartments() {
try {
const response = await listdeptNoAuth()
if (response && Array.isArray(response)) {
this.departmentOptions = response
}
} catch (error) {
console.error('加载科室失败:', error)
this.$message.error('加载科室列表失败')
}
},
async loadSubmissionData() {
// 加载申请依据等基本信息
try {
console.log('Loading submission data for package:', this.packageData)
// 1. 优先从父级预算包的申请信息字段中加载
if (this.packageData.application_basis ||
this.packageData.application_purpose ||
this.packageData.application_reason ||
this.packageData.project_content) {
this.basicInfo = {
basis: this.packageData.application_basis || '',
purpose: this.packageData.application_purpose || '',
reason: this.packageData.application_reason || '',
content: this.packageData.project_content || ''
}
console.log('从父级预算包加载申请信息:', this.basicInfo)
return
}
// 2. 尝试从submission中加载
if (this.packageData.submission) {
this.basicInfo = {
basis: this.packageData.submission.application_basis || '',
purpose: this.packageData.submission.application_purpose || '',
reason: this.packageData.submission.application_reason || '',
content: this.packageData.submission.project_content || ''
}
console.log('从submission加载申请信息:', this.basicInfo)
return
}
// 3. 如果是编辑模式但没有数据尝试从API获取
if (this.packageData.isEditMode) {
try {
// 这里可以调用API获取BudgetSubmission数据
// 暂时使用默认值,避免表单为空
this.basicInfo = {
basis: '修改分配',
purpose: '调整预算分配',
reason: '根据实际需求调整执行科室分配',
content: '对预算包进行重新分配'
}
console.log('使用默认申请信息:', this.basicInfo)
} catch (error) {
console.error('从API加载申请信息失败:', error)
}
}
} catch (error) {
console.error('加载提交数据失败:', error)
}
},
async loadExistingAllocations() {
// 编辑模式直接使用API返回的children数据
try {
if (this.packageData.children && this.packageData.children.length > 0) {
this.departmentAllocations = this.packageData.children.map(child => ({
packageId: child.id,
departmentId: child.department.id,
departmentName: child.department.name,
allocatedAmount: child.allocated_amount,
childPackageName: child.name,
description: child.description || ''
}))
}
// 加载申请依据信息
if (this.packageData.submission) {
this.basicInfo = {
basis: this.packageData.submission.application_basis || '',
purpose: this.packageData.submission.application_purpose || '',
reason: this.packageData.submission.application_reason || '',
content: this.packageData.submission.project_content || ''
}
}
} catch (error) {
console.error('加载现有分配数据失败:', error)
this.$message.error('加载现有分配数据失败')
}
},
addDepartment() {
if (!this.selectedDepartment) return
const dept = this.departmentOptions.find(d => d.id === this.selectedDepartment)
if (!dept) return
this.departmentAllocations.push({
departmentId: dept.id,
departmentName: dept.name,
allocatedAmount: 0,
childPackageName: `${this.packageData.name}-${dept.name}`,
description: ''
})
this.selectedDepartment = ''
},
removeDepartment(index) {
this.departmentAllocations.splice(index, 1)
this.validateAllocation()
},
validateAllocation() {
this.$nextTick(() => {
// 触发金额分配验证
})
},
getSummaries(param) {
const { columns } = param
const sums = []
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = '合计'
} else if (index === 2) { // 分配金额列
sums[index] = this.formatCurrency(this.totalAllocated) + ' 万元'
} else {
sums[index] = ''
}
})
return sums
},
async handleSubmit() {
// 验证基本信息表单
const basicValid = await this.$refs.basicForm.validate().catch(() => false)
if (!basicValid) {
this.$message.error('请完善基本信息')
return
}
// 验证科室分配
if (this.departmentAllocations.length === 0) {
this.$message.error('请至少选择一个执行科室')
return
}
// 验证金额分配
if (Math.abs(this.totalAllocated - this.packageData.allocated_amount) > 0.0001) {
this.$message.error('分配金额总和必须等于预算分配金额')
return
}
// 验证执行科室包名称和数据完整性
for (let i = 0; i < this.departmentAllocations.length; i++) {
const item = this.departmentAllocations[i]
// 验证科室ID
if (item.departmentId === null || item.departmentId === undefined || item.departmentId === '') {
this.$message.error(`${i+1}执行科室ID不能为空请重新选择科室`)
return
}
// 验证科室名称
if (!item.departmentName || !item.departmentName.trim()) {
this.$message.error(`${i+1}行:执行科室名称不能为空`)
return
}
// 验证包名称
if (!item.childPackageName || !item.childPackageName.trim()) {
this.$message.error(`请输入${item.departmentName}的执行科室包名称`)
return
}
// 验证分配金额
if (!item.allocatedAmount || item.allocatedAmount <= 0) {
this.$message.error(`请为${item.departmentName}输入有效的分配金额`)
return
}
// 验证金额格式
if (isNaN(parseFloat(item.allocatedAmount))) {
this.$message.error(`${item.departmentName}的分配金额格式不正确`)
return
}
}
// 验证科室是否重复
const departmentIds = this.departmentAllocations.map(item => item.departmentId)
const uniqueDepartmentIds = [...new Set(departmentIds)]
if (departmentIds.length !== uniqueDepartmentIds.length) {
this.$message.error('存在重复的执行科室,请检查')
return
}
this.submitting = true
try {
// 编辑模式下,需要提供原有子包信息用于后端差异对比
const originalChildren = this.packageData.isEditMode && this.packageData.children
? this.packageData.children.map(child => child.id)
: []
const splitData = {
package_id: this.packageData.id,
is_edit_mode: this.packageData.isEditMode || false,
original_child_package_ids: originalChildren, // 原有子包ID列表用于后端删除判断
budget_submission: {
application_basis: this.basicInfo.basis,
application_purpose: this.basicInfo.purpose,
application_reason: this.basicInfo.reason,
project_content: this.basicInfo.content
},
department_allocations: this.departmentAllocations.map(item => {
// 确保 department_id 不为空
if (item.departmentId === null || item.departmentId === undefined || item.departmentId === '') {
throw new Error(`科室 ${item.departmentName} 的department_id为空`)
}
const allocation = {
department_id: item.departmentId,
package_id: item.packageId,
allocated_amount: parseFloat(item.allocatedAmount),
package_name: item.childPackageName,
description: item.description || ''
}
// 编辑模式下查找对应的原有子包ID
if (this.packageData.isEditMode && this.packageData.children) {
const existingChild = this.packageData.children.find(
child => child.department_id === item.departmentId
)
if (existingChild) {
allocation.existing_package_id = existingChild.id // 用于更新现有子包
allocation.action = 'update' // 标记为更新操作
} else {
allocation.action = 'create' // 标记为新增操作
}
} else {
allocation.action = 'create' // 新建模式都是创建
}
return allocation
})
}
console.log('提交的分配数据:', splitData)
this.$emit('submit', splitData)
} catch (error) {
console.error('提交失败:', error)
// 根据错误类型显示不同的错误信息
if (error.message && error.message.includes('department_id为空')) {
this.$message.error(error.message)
} else if (error.response && error.response.data && error.response.data.message) {
this.$message.error(error.response.data.message)
} else if (error.message) {
this.$message.error(error.message)
} else {
this.$message.error('拆包操作失败,请检查数据完整性')
}
} finally {
this.submitting = false
}
},
handleCancel() {
this.$emit('cancel')
},
formatCurrency(amount) {
if (amount === null || amount === undefined || amount === '') return '0.0000'
const num = Number(amount)
return isFinite(num)
? num.toLocaleString('zh-CN', { minimumFractionDigits: 4, maximumFractionDigits: 4 })
: '0.0000'
}
}
}
</script>
<style scoped>
.split-package-form {
padding: 20px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #303133;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #409EFF;
}
.section-title i {
margin-right: 8px;
color: #409EFF;
}
.amount-hint {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
.department-allocation {
margin: 30px 0;
}
.add-department {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.allocation-summary {
margin-top: 20px;
padding: 15px;
background: #f5f7fa;
border-radius: 4px;
border-left: 4px solid #409EFF;
}
.summary-item {
margin-bottom: 8px;
font-weight: bold;
}
.summary-item:last-child {
margin-bottom: 0;
}
.summary-item.error {
color: #F56C6C;
}
.form-actions {
text-align: right;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #EBEEF5;
}
.form-actions .el-button {
margin-left: 10px;
}
</style>