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.

613 lines
18 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="container">
<!-- 查询配置 -->
<div>
<div ref="lxHeader">
<LxHeader :custom="true" icon="el-icon-s-order" text="执行科室预算分解表提交" style="margin-bottom: 10px; border: 0px;">
<div>
<div class="search-panel">
<div class="filters">
<el-select
v-model="searchFields.year_id"
placeholder="选择预算年度"
style="width: 150px"
@change="load"
>
<el-option
v-for="item in yearOptions"
:key="item.value"
:label="item.year + '年'"
:value="item.value"
/>
</el-select>
<el-select
v-model="searchFields.status"
placeholder="选择状态"
style="width: 150px"
clearable
@change="load"
>
<el-option label="待提交" value="pending" />
<el-option label="草稿/已退回" value="draft" />
<el-option label="待科室负责人审核" value="submitted" />
<el-option label="待分管领导审核" value="manager_approved" />
<el-option label="终审通过" value="approved" />
</el-select>
</div>
<div class="actions">
<Button type="primary" @click="load">查询</Button>
</div>
</div>
</div>
</LxHeader>
</div>
<!-- 分配给本科室的预算包列表 -->
<el-table
ref="packageTable"
v-loading="loading"
:data="packageData"
:height="tableHeight"
class="v-table"
style="width: 100%; margin-bottom: 20px;"
border
row-key="id"
>
<el-table-column type="index" align="center" label="序号" width="60" />
<el-table-column prop="budget_year" label="预算年度" width="100" align="center">
<template slot-scope="scope">
<el-tag type="primary">
{{ scope.row.budget_year ? scope.row.budget_year.year + '年' : '-' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="parent_name" label="上级包名称" min-width="200">
<template slot-scope="scope">
<span style="color: #909399; font-size: 12px;">
{{ scope.row.parent_name || '-' }}
</span>
</template>
</el-table-column>
<el-table-column prop="executing_department" label="执行部门" min-width="150">
<template slot-scope="scope">
<span style="color: #606266; font-size: 13px;">
{{ scope.row.department.name || '-' }}
</span>
</template>
</el-table-column>
<el-table-column prop="allocated_amount" label="分配金额" width="160" align="right">
<template slot-scope="scope">
<span style="color: #409EFF; font-weight: bold;">
{{ formatCurrency(scope.row.allocated_amount) }} 万元
</span>
</template>
</el-table-column>
<el-table-column prop="submission_status" label="提交状态" width="220" align="center">
<template slot-scope="scope">
<el-tag :type="getStatusTagType(scope.row.submission_status)">
{{ getStatusText(scope.row.submission_status) }}
</el-tag>
<div
v-if="scope.row.submission_status === 'draft' && scope.row.submission && scope.row.submission.approval_notes"
class="return-note-wrapper"
>
<span class="return-note-text">退回意见:{{ scope.row.submission.approval_notes }}</span>
<el-button type="text" class="return-note-link" @click="showApprovalNotes(scope.row.submission.approval_notes)">
查看完整
</el-button>
</div>
</template>
</el-table-column>
<el-table-column prop="submission_date" label="提交时间" width="180" align="center">
<template slot-scope="scope">
{{ scope.row.submission_date ? formatDate(scope.row.submission_date) : '-' }}
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="280">
<template slot-scope="scope">
<div class="action-group">
<Button
v-if="!scope.row.submission && canWrite(scope.row)"
type="primary"
size="small"
@click="openSubmissionForm(scope.row)"
>
填写分解表
</Button>
<Button
v-if="scope.row.submission && scope.row.submission.status === 'draft' && canWrite(scope.row)"
type="warning"
size="small"
@click="editSubmissionForm(scope.row)"
>
修改分解表
</Button>
<Button
v-if="scope.row.submission"
type="info"
size="small"
@click="viewSubmissionForm(scope.row)"
>
查看详情
</Button>
<Button
v-if="scope.row.submission"
size="small"
@click="openReviewHistory(scope.row)"
>
审核记录
</Button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分解表填写对话框 -->
<el-dialog
:title="dialogTitle"
:visible.sync="submissionDialogVisible"
width="95%"
:close-on-click-modal="false"
top="5vh"
custom-class="submission-dialog"
>
<budget-submission-form
v-if="submissionDialogVisible"
:package-data="currentPackage"
:mode="submissionMode"
@save-draft="handleSubmissionDraft"
@submit="handleSubmissionSubmit"
@cancel="submissionDialogVisible = false"
/>
</el-dialog>
<el-dialog
title="审核记录"
:visible.sync="historyDialogVisible"
width="760px"
:close-on-click-modal="false"
>
<div v-if="currentHistoryPackage">
<el-descriptions :column="2" border size="small" class="history-summary">
<el-descriptions-item label="上级包名称">{{ currentHistoryPackage.parent_name || '-' }}</el-descriptions-item>
<el-descriptions-item label="执行部门">{{ currentHistoryPackage.department ? currentHistoryPackage.department.name : '-' }}</el-descriptions-item>
<el-descriptions-item label="提交状态">{{ getStatusText(currentHistoryPackage.submission_status) }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ currentHistoryPackage.submission_date ? formatDate(currentHistoryPackage.submission_date) : '-' }}</el-descriptions-item>
</el-descriptions>
<el-timeline v-if="reviewHistories(currentHistoryPackage).length > 0" class="history-timeline">
<el-timeline-item
v-for="history in reviewHistories(currentHistoryPackage)"
:key="history.id"
:timestamp="formatDate(history.operated_at)"
placement="top"
>
<div class="timeline-card">
<div class="timeline-title">{{ getHistoryActionText(history) }}</div>
<div class="timeline-meta">{{ history.operator ? history.operator.name : '-' }} · {{ getHistoryStatusFlow(history) }}</div>
<div v-if="history.notes" class="timeline-notes">{{ history.notes }}</div>
</div>
</el-timeline-item>
</el-timeline>
<el-empty v-else :image-size="80" description="暂无审核记录" />
</div>
</el-dialog>
</div>
</template>
<script>
import LxHeader from '@/components/LxHeader/index.vue'
import { getBudgetCollectionYearOptions } from '@/api/budget/budgetCollectionYear.js'
import {
getDepartmentPackages,
getPackageDecomposition,
saveDepartmentSubmissionDraft,
submitDepartmentSubmission
} from '@/api/budget/departmentSubmission.js'
import BudgetSubmissionForm from './components/BudgetSubmissionForm.vue'
import { formatMoney } from '@/utils/budgetMoney'
export default {
name: 'DepartmentSubmission',
components: {
LxHeader,
BudgetSubmissionForm
},
data() {
return {
loading: false,
tableHeight: 0,
searchFields: {
year_id: '',
status: ''
},
packageData: [],
yearOptions: [],
submissionDialogVisible: false,
currentPackage: null,
currentHistoryPackage: null,
historyDialogVisible: false,
submissionMode: 'create' // create, edit, view
}
},
computed: {
dialogTitle() {
const titles = {
create: '填写预算分解表',
edit: '修改预算分解表',
view: '查看预算分解表'
}
return titles[this.submissionMode] || '预算分解表'
}
},
created() {
this.initLoad()
this.loadYearOptions()
},
methods: {
initLoad() {
const clientHeight = document.documentElement.clientHeight
const lxHeaderHeight = 140
const paginationHeight = 60
const topHeight = 50
const tableHeight = clientHeight - lxHeaderHeight - topHeight - paginationHeight - 20
this.tableHeight = tableHeight
},
async loadYearOptions() {
try {
const response = await getBudgetCollectionYearOptions()
if (response && response.length) {
this.yearOptions = response
if (this.yearOptions.length > 0 && !this.searchFields.year_id) {
const activeYear = this.yearOptions.find(item => item.status === 'ACTIVE')
this.searchFields.year_id = (activeYear || this.yearOptions[0]).value
this.$nextTick(() => {
this.load()
})
}
} else {
this.$message.warning('未找到可用的预算年度,请联系管理员')
}
} catch (error) {
console.error('加载年度选项失败:', error)
this.$message.error('加载年度选项失败,请刷新页面重试')
}
},
async load() {
if (!this.searchFields.year_id) {
this.$message.warning('请选择预算年度')
this.packageData = []
return
}
this.loading = true
try {
const params = {
...this.searchFields
}
const response = await getDepartmentPackages(params)
// 根据后端ApiResponse设计判断无errcode表示成功
if (response && !response.errcode) {
this.packageData = Array.isArray(response) ? response : []
} else if (response && response.errcode) {
this.packageData = []
this.$message.warning(response.errmsg || '获取预算包列表失败')
}
} catch (error) {
console.error('获取数据失败:', error)
if (error.response && error.response.data) {
const errorMessage = error.response.data.message || error.response.data.error || '获取数据失败'
this.$message.error(errorMessage)
} else {
this.$message.error('获取数据失败,请检查网络连接')
}
this.packageData = []
} finally {
this.loading = false
}
},
async openSubmissionForm(packageItem) {
await this.openSubmissionDialog(packageItem, 'create')
},
async editSubmissionForm(packageItem) {
if (!packageItem.submission || packageItem.submission.status !== 'draft' || !this.canWrite(packageItem)) {
this.$message.warning('当前申报状态不可修改')
return
}
await this.openSubmissionDialog(packageItem, 'edit')
},
async viewSubmissionForm(packageItem) {
await this.openSubmissionDialog(packageItem, 'view')
},
openReviewHistory(packageItem) {
this.currentHistoryPackage = packageItem
this.historyDialogVisible = true
},
async openSubmissionDialog(packageItem, mode) {
try {
const packageDetail = await getPackageDecomposition(packageItem.id)
this.currentPackage = {
...packageItem,
...packageDetail
}
} catch (error) {
console.warn('加载预算分解详情失败,回退到列表数据:', error.message)
this.currentPackage = { ...packageItem }
}
this.submissionMode = mode
this.submissionDialogVisible = true
},
async handleSubmissionSubmit(submissionData) {
try {
await submitDepartmentSubmission(this.currentPackage.id, submissionData)
this.$message.success('提交成功')
this.submissionDialogVisible = false
this.load()
} catch (error) {
console.error('提交失败:', error)
if (error.response && error.response.data && error.response.data.message) {
this.$message.error(error.response.data.message)
} else {
this.$message.error('提交失败')
}
}
},
async handleSubmissionDraft(submissionData) {
try {
await saveDepartmentSubmissionDraft(this.currentPackage.id, submissionData)
this.$message.success('草稿已保存')
this.submissionDialogVisible = false
this.load()
} catch (error) {
this.$message.error(error.message || '草稿保存失败')
}
},
formatCurrency(amount) {
return formatMoney(amount)
},
canWrite(packageItem) {
const year = packageItem.budget_year || {}
return year.status === 'ACTIVE'
},
formatDate(dateStr) {
if (!dateStr) return '-'
const date = new Date(dateStr)
return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN', {
hour12: false,
hour: '2-digit',
minute: '2-digit'
})
},
getStatusTagType(status) {
const typeMap = {
pending: 'warning',
draft: 'warning',
submitted: 'primary',
manager_approved: 'primary',
approved: 'success',
rejected: 'danger'
}
return typeMap[status] || 'info'
},
getStatusText(status) {
const textMap = {
pending: '待提交',
draft: '草稿/已退回',
submitted: '待科室负责人审核',
manager_approved: '待分管领导审核',
approved: '终审通过',
rejected: '已拒绝'
}
return textMap[status] || '未知'
},
showApprovalNotes(notes) {
this.$alert(notes || '无退回意见', '完整退回意见', {
confirmButtonText: '关闭'
})
},
reviewHistories(packageItem) {
const histories = packageItem && packageItem.submission && packageItem.submission.review_histories
? packageItem.submission.review_histories
: []
return histories.slice().sort((a, b) => new Date(b.operated_at || 0).getTime() - new Date(a.operated_at || 0).getTime())
},
getHistoryActionText(history) {
const actionMap = {
submit: '提交审核',
resubmit: '重新提交',
manager_approve: '科室负责人初审通过',
manager_return: '科室负责人退回修改',
leader_approve: '分管领导终审通过',
leader_return: '分管领导退回修改',
approve: '审核通过',
return: '退回修改'
}
return actionMap[history.action] || history.action || '未知操作'
},
getHistoryStatusFlow(history) {
return `${this.getStatusText(history.from_status)} -> ${this.getStatusText(history.to_status)}`
}
}
}
</script>
<style scoped>
.return-note-wrapper {
margin-top: 4px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.return-note-text {
font-size: 12px;
color: #e6a23c;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.return-note-link {
padding: 0;
font-size: 12px;
flex-shrink: 0;
}
.container {
padding: 12px 16px 16px 16px;
}
.v-table {
border: 1px solid #e6e6e6;
}
.search-panel {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 0;
}
.filters {
display: flex;
align-items: center;
gap: 12px;
}
.actions {
display: flex;
align-items: center;
gap: 10px;
}
.action-group {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
flex-wrap: nowrap;
white-space: nowrap;
}
/* 统一表格行高 */
.v-table >>> .el-table__body tr {
height: 50px !important;
}
.v-table >>> .el-table__body tr td {
height: 50px !important;
max-height: 50px !important;
vertical-align: middle !important;
padding: 8px 12px !important;
box-sizing: border-box !important;
}
/* 所有单元格内容的统一处理 */
.v-table >>> .el-table__body tr td .cell {
height: 34px !important;
max-height: 34px !important;
line-height: 18px !important;
overflow: hidden !important;
display: flex !important;
align-items: center !important;
}
/* 确保标签和按钮不会撑开行高 */
.v-table >>> .el-table__body tr td .el-tag {
height: 24px !important;
line-height: 22px !important;
}
.v-table >>> .el-table__body tr td .el-button {
height: 28px !important;
padding: 4px 8px !important;
}
.history-summary {
margin-bottom: 16px;
}
.history-timeline {
margin-top: 20px;
}
.timeline-card {
padding: 12px;
border: 1px solid #ebeef5;
border-radius: 6px;
background: #fff;
}
.timeline-title {
margin-bottom: 6px;
color: #303133;
font-weight: 600;
}
.timeline-meta {
color: #909399;
font-size: 12px;
}
.timeline-notes {
margin-top: 8px;
color: #606266;
line-height: 1.6;
word-break: break-all;
}
</style>
<style>
/* 分解表对话框样式 */
.submission-dialog {
margin-top: 5vh !important;
}
.submission-dialog .el-dialog__body {
padding: 10px 20px !important;
max-height: 80vh;
overflow-y: auto;
}
</style>