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.

644 lines
19 KiB

<template>
<div class="container">
<div ref="lxHeader">
<LxHeader :custom="true" icon="el-icon-s-check" text="执行科室分解审核" style="margin-bottom: 10px; border: 0;">
<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="draft" />
<el-option label="已提交" value="submitted" />
<el-option label="审核通过" value="approved" />
</el-select>
</div>
<div class="actions">
<el-button type="primary" @click="load">查询</el-button>
</div>
</div>
</div>
</LxHeader>
</div>
<el-table
ref="reviewTable"
v-loading="loading"
:data="tableData"
:height="tableHeight"
class="v-table"
style="width: 100%; margin-bottom: 20px;"
border
>
<el-table-column type="index" align="center" label="序号" width="60" />
<el-table-column label="预算年度" width="100" align="center">
<template slot-scope="scope">
<el-tag type="primary">{{ getBudgetYear(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="牵头项目" min-width="180">
<template slot-scope="scope">
<span class="package-name">{{ getParentPackageName(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column label="执行科室包" min-width="180">
<template slot-scope="scope">
<span class="package-name">{{ getPackageName(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column label="执行科室" width="140" align="center">
<template slot-scope="scope">
<el-tag type="info">{{ getDepartmentName(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="分配金额" width="140" align="right">
<template slot-scope="scope">
<span class="amount-text">{{ formatCurrency(getAllocatedAmount(scope.row)) }} 万元</span>
</template>
</el-table-column>
<el-table-column label="提交状态" width="120" align="center">
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="提交人" width="120" align="center">
<template slot-scope="scope">
{{ scope.row.submitter ? scope.row.submitter.name : '-' }}
</template>
</el-table-column>
<el-table-column label="提交时间" width="170" align="center">
<template slot-scope="scope">
{{ formatDate(scope.row.submitted_at) }}
</template>
</el-table-column>
<el-table-column label="最新审核意见" min-width="220">
<template slot-scope="scope">
<span>{{ scope.row.approval_notes || '-' }}</span>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="300" align="center">
<template slot-scope="scope">
<div class="action-group">
<el-button type="primary" size="mini" @click="viewDetail(scope.row)">查看详情</el-button>
<el-button
v-if="scope.row.status === 'submitted'"
type="warning"
size="mini"
@click="openReviewDialog(scope.row)"
>
审核
</el-button>
<el-button size="mini" @click="openHistoryDialog(scope.row)">审核记录</el-button>
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="total > 0"
:current-page="currentPage"
:page-sizes="[10, 15, 20, 50]"
:page-size="pageSize"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 20px; text-align: right;"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<el-dialog
title="执行科室分解详情"
:visible.sync="detailDialogVisible"
width="95%"
:close-on-click-modal="false"
top="5vh"
custom-class="submission-dialog"
>
<budget-submission-form
v-if="detailDialogVisible && currentPackage"
:package-data="currentPackage"
mode="view"
@cancel="detailDialogVisible = false"
/>
</el-dialog>
<el-dialog
title="执行科室分解审核"
:visible.sync="reviewDialogVisible"
width="96%"
:close-on-click-modal="false"
top="3vh"
custom-class="submission-dialog review-dialog"
>
<div v-if="reviewDialogVisible && currentReviewRow && currentPackage" class="review-layout">
<div class="review-detail-panel">
<budget-submission-form
:package-data="currentPackage"
mode="view"
@cancel="reviewDialogVisible = false"
/>
</div>
<div class="review-action-panel">
<div class="panel-title">审核操作</div>
<el-descriptions :column="1" border size="small" class="review-summary">
<el-descriptions-item label="牵头项目">{{ getParentPackageName(currentReviewRow) }}</el-descriptions-item>
<el-descriptions-item label="执行科室包">{{ getPackageName(currentReviewRow) }}</el-descriptions-item>
<el-descriptions-item label="执行科室">{{ getDepartmentName(currentReviewRow) }}</el-descriptions-item>
<el-descriptions-item label="提交状态">{{ getStatusText(currentReviewRow.status) }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ formatDate(currentReviewRow.submitted_at) }}</el-descriptions-item>
</el-descriptions>
<el-form label-position="top" class="review-form">
<el-form-item label="审核结论">
<el-radio-group v-model="reviewForm.action">
<el-radio label="approve">通过</el-radio>
<el-radio label="return">退回</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="reviewForm.action === 'return' ? '退回意见' : '审核备注(选填)'">
<el-input
v-model="reviewForm.approval_notes"
type="textarea"
:rows="8"
:maxlength="1000"
:placeholder="reviewForm.action === 'return' ? '请输入退回修改意见' : '可填写审核说明'"
show-word-limit
/>
</el-form-item>
</el-form>
<div class="panel-title panel-title-spaced">最近审核记录</div>
<div v-if="reviewHistories(currentReviewRow).length > 0" class="inline-history-list">
<div
v-for="history in reviewHistories(currentReviewRow).slice(0, 5)"
:key="history.id"
class="inline-history-item"
>
<div class="inline-history-head">
<span>{{ getHistoryActionText(history) }}</span>
<span>{{ formatDate(history.operated_at) }}</span>
</div>
<div class="inline-history-meta">
{{ history.operator ? history.operator.name : '-' }} · {{ getHistoryStatusFlow(history) }}
</div>
<div v-if="history.notes" class="inline-history-notes">{{ history.notes }}</div>
</div>
</div>
<el-empty v-else :image-size="60" description="暂无审核记录" />
<div class="review-action-footer">
<el-button @click="reviewDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="reviewSubmitting" @click="submitReview">确认审核</el-button>
</div>
</div>
</div>
</el-dialog>
<el-dialog
title="审核记录"
:visible.sync="historyDialogVisible"
width="760px"
:close-on-click-modal="false"
>
<div v-if="currentHistoryRow">
<el-descriptions :column="2" border size="small" class="history-summary">
<el-descriptions-item label="牵头项目">{{ getParentPackageName(currentHistoryRow) }}</el-descriptions-item>
<el-descriptions-item label="执行科室包">{{ getPackageName(currentHistoryRow) }}</el-descriptions-item>
<el-descriptions-item label="执行科室">{{ getDepartmentName(currentHistoryRow) }}</el-descriptions-item>
<el-descriptions-item label="当前状态">{{ getStatusText(currentHistoryRow.status) }}</el-descriptions-item>
</el-descriptions>
<el-timeline v-if="reviewHistories(currentHistoryRow).length > 0" class="history-timeline">
<el-timeline-item
v-for="history in reviewHistories(currentHistoryRow)"
: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 { formatMoney } from '@/utils/budgetMoney'
import LxHeader from '@/components/LxHeader/index.vue'
import BudgetSubmissionForm from '@/views/budget/collection/components/BudgetSubmissionForm.vue'
import { getBudgetCollectionYearOptions } from '@/api/budget/budgetCollectionYear.js'
import { getDepartmentSubmissions, reviewDepartmentSubmission } from '@/api/budget/leadDepartment.js'
export default {
name: 'DepartmentReviewManagement',
components: {
LxHeader,
BudgetSubmissionForm
},
data() {
return {
loading: false,
tableHeight: 0,
searchFields: {
year_id: '',
status: ''
},
tableData: [],
total: 0,
currentPage: 1,
pageSize: 15,
yearOptions: [],
detailDialogVisible: false,
reviewDialogVisible: false,
historyDialogVisible: false,
currentPackage: null,
currentReviewRow: null,
currentHistoryRow: null,
reviewSubmitting: false,
reviewForm: {
action: 'approve',
approval_notes: ''
}
}
},
created() {
this.initLoad()
this.loadYearOptions()
},
methods: {
initLoad() {
const clientHeight = document.documentElement.clientHeight
const lxHeaderHeight = 140
const alertHeight = 70
const paginationHeight = 60
const topHeight = 50
this.tableHeight = clientHeight - lxHeaderHeight - alertHeight - topHeight - paginationHeight - 20
},
async loadYearOptions() {
try {
const response = await getBudgetCollectionYearOptions()
if (response && response.length) {
this.yearOptions = response
if (!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.tableData = []
this.total = 0
return
}
this.loading = true
try {
const response = await getDepartmentSubmissions({
...this.searchFields,
page_size: this.pageSize,
page: this.currentPage
})
this.tableData = response && response.data ? response.data : []
this.total = response ? response.total || 0 : 0
this.currentPage = response ? response.current_page || 1 : 1
} catch (error) {
console.error('获取执行科室分解审核列表失败:', error)
this.$message.error(error.message || '获取执行科室分解审核列表失败')
this.tableData = []
this.total = 0
} finally {
this.loading = false
}
},
viewDetail(row) {
if (!row.package) {
this.$message.warning('预算包数据不存在')
return
}
this.currentPackage = { ...row.package }
this.detailDialogVisible = true
},
openReviewDialog(row) {
if (!row.package) {
this.$message.warning('预算包数据不存在')
return
}
this.currentReviewRow = row
this.currentPackage = { ...row.package }
this.reviewForm = {
action: 'approve',
approval_notes: ''
}
this.reviewDialogVisible = true
},
openHistoryDialog(row) {
this.currentHistoryRow = row
this.historyDialogVisible = true
},
async submitReview() {
if (!this.currentReviewRow) {
return
}
if (this.reviewForm.action === 'return' && !this.reviewForm.approval_notes.trim()) {
this.$message.warning('请输入退回修改意见')
return
}
this.reviewSubmitting = true
try {
await reviewDepartmentSubmission(this.currentReviewRow.id, {
action: this.reviewForm.action,
approval_notes: this.reviewForm.approval_notes.trim()
})
this.$message.success(this.reviewForm.action === 'approve' ? '审核通过' : '已退回执行科室修改')
this.reviewDialogVisible = false
await this.load()
} catch (error) {
console.error('执行科室分解审核失败:', error)
} finally {
this.reviewSubmitting = false
}
},
handleSizeChange(newSize) {
this.pageSize = newSize
this.currentPage = 1
this.load()
},
handleCurrentChange(newPage) {
this.currentPage = newPage
this.load()
},
formatCurrency(amount) {
return formatMoney(amount)
},
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'
})
},
getBudgetYear(row) {
return row.package && row.package.budget_year ? `${row.package.budget_year.year}` : '-'
},
getParentPackageName(row) {
return row.package && row.package.parent ? row.package.parent.name : '-'
},
getPackageName(row) {
return row.package ? row.package.name : '-'
},
getDepartmentName(row) {
return row.package && row.package.department ? row.package.department.name : '-'
},
getAllocatedAmount(row) {
return row.package ? row.package.allocated_amount : '0.0000'
},
getStatusType(status) {
const typeMap = {
draft: 'info',
submitted: 'warning',
approved: 'success'
}
return typeMap[status] || 'info'
},
getStatusText(status) {
const textMap = {
draft: '草稿',
submitted: '已提交',
approved: '审核通过'
}
return textMap[status] || '未知'
},
reviewHistories(row) {
return (row.review_histories || []).slice().sort((a, b) => {
return new Date(b.operated_at || 0).getTime() - new Date(a.operated_at || 0).getTime()
})
},
getHistoryActionText(history) {
const actionMap = {
submit: '提交审核',
resubmit: '重新提交',
approve: '审核通过',
return: '退回修改'
}
return actionMap[history.action] || history.action || '未知操作'
},
getHistoryStatusFlow(history) {
const fromText = this.getStatusText(history.from_status)
const toText = this.getStatusText(history.to_status)
return `${fromText} -> ${toText}`
}
}
}
</script>
<style scoped>
.container {
padding: 12px 16px 16px;
}
.search-panel {
display: flex;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.filters,
.actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.action-group {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
flex-wrap: nowrap;
white-space: nowrap;
}
.review-layout {
display: flex;
gap: 16px;
align-items: flex-start;
}
.review-detail-panel {
min-width: 0;
flex: 1 1 auto;
}
.review-action-panel {
width: 360px;
padding: 16px;
border: 1px solid #ebeef5;
border-radius: 8px;
background: #fafbfd;
}
.panel-title {
margin-bottom: 12px;
color: #303133;
font-size: 16px;
font-weight: 600;
}
.panel-title-spaced {
margin-top: 20px;
}
.review-summary,
.history-summary {
margin-bottom: 16px;
}
.review-form {
margin-top: 16px;
}
.inline-history-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.inline-history-item,
.timeline-card {
padding: 12px;
border: 1px solid #ebeef5;
border-radius: 6px;
background: #fff;
}
.inline-history-head {
display: flex;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
color: #303133;
font-weight: 600;
}
.inline-history-meta,
.timeline-meta {
color: #909399;
font-size: 12px;
}
.inline-history-notes,
.timeline-notes {
margin-top: 8px;
color: #606266;
line-height: 1.6;
word-break: break-all;
}
.timeline-title {
margin-bottom: 6px;
color: #303133;
font-weight: 600;
}
.history-timeline {
margin-top: 20px;
}
.review-action-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
}
.package-name {
font-weight: 600;
color: #303133;
}
.amount-text {
color: #409EFF;
font-weight: 600;
}
@media (max-width: 1400px) {
.review-layout {
flex-direction: column;
}
.review-action-panel {
width: 100%;
}
}
</style>