科室资金预算

master
lion 5 months ago
parent 51b5c0a3df
commit 746cda70dd

@ -0,0 +1,53 @@
import request from "@/utils/request";
function customParamsSerializer(params) {
let result = '';
for (let key in params) {
if (params.hasOwnProperty(key)) {
if (Array.isArray(params[key])) {
params[key].forEach((item, index) => {
if (item instanceof Array) {
item.forEach((item1, index1) => {
result += `${key}[${index}][${index1}]=${item1}&`
})
} else if (typeof item === 'object') {
for (let key1 in item) {
result += `${key}[${index}][${key1}]=${item[key1]}&`
}
}
// result += `${key}[${index}][key]=${item.key}&${key}[${index}][op]=${item.op}&${key}[${index}][value]=${item.value}&`;
});
} else {
result += `${key}=${params[key]}&`;
}
}
}
return result.slice(0, -1);
}
export function endIndex(params, noloading = false) {
return request({
method: 'get',
url: '/api/ht/contract/end-index',
params,
noloading,
paramsSerializer: customParamsSerializer
})
}
export function partIndex(params, noloading = false) {
return request({
method: 'get',
url: '/api/ht/contract/part-index',
params,
noloading,
paramsSerializer: customParamsSerializer
})
}
export function statisticDepartment(params) {
return request({
method: 'get',
url: '/api/ht/notice/statistic-department-v2',
params
})
}

@ -0,0 +1,330 @@
<template>
<div>
<Modal fullscreen class-name="oa-modal" footer-hide title="科室资金执行率明细" :mask-closable="false" v-model="isShowModal">
<div class="department-progress-detail">
<el-container>
<el-main>
<!-- 科室标题 -->
<div class="department-title">
<h3>{{row?(row.plan_department?row.plan_department.name:''):''}} - 资金执行率明细</h3>
</div>
<!-- 执行率统计卡片 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="8">
<el-card class="summary-card" shadow="hover">
<div class="card-title">当前执行率</div>
<div class="execution-rate">{{ getCompletionRate(row.rate) }}%</div>
<div class="card-desc">已财务确认的资金比例</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="summary-card" shadow="hover">
<div class="card-title">预计执行率</div>
<div class="execution-rate">{{ getCompletionRate(row.rate_end) }}%</div>
<div class="card-desc">全部付款确认后预计的执行率</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="summary-card" shadow="hover">
<div class="card-title">预算金额</div>
<div class="execution-rate">{{ formatToWan(row.contract_plan_sum) }}</div>
<div class="card-desc">科室预算金额</div>
</el-card>
</el-col>
</el-row>
<!-- 已执行项目表格 -->
<el-card class="table-container mb-4">
<div class="section-title">已执行项目财务付款确认
</div>
<xy-table ref="xyTable" :list="endList" :table-item="table" :height='300'>
<template v-slot:status>
<el-table-column header-align="center" label="当前状态" width="120">
<template slot-scope="scope">
<el-tag type='success'>已完成</el-tag>
</template>
</el-table-column>
</template>
<template v-slot:btns>
<el-table-column :fixed="$store.getters.device === 'mobile'?false:'right'" header-align="center"
label="操作" width="120">
<template slot-scope="scope">
<el-button type="primary" size="small" @click="getDetail(scope.row,'end')"></el-button>
</template>
</el-table-column>
</template>
</xy-table>
<div style="display: flex; justify-content: flex-end">
<Page :total="endTotal" show-elevator @on-change="endPageChange" />
</div>
</el-card>
<!-- 未执行完项目表格 -->
<el-card class="table-container">
<div class="section-title">未执行完项目流程中/预算流转中
</div>
<xy-table ref="xyTable" :list="partList" :table-item="table" :height='300'>
<template v-slot:status>
<el-table-column header-align="center" label="当前状态" width="120" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.funds_count>0" type='warning'>部分支付中</el-tag>
<el-tag v-else type='warning'>oa流程中</el-tag>
</template>
</el-table-column>
</template>
<template v-slot:btns>
<el-table-column :fixed="$store.getters.device === 'mobile'?false:'right'" header-align="center"
label="操作" width="120" align="center">
<template slot-scope="scope">
<el-button type="primary" size="small" @click="getDetail(scope.row,'part')"></el-button>
</template>
</el-table-column>
</template>
</xy-table>
<div style="display: flex; justify-content: flex-end">
<Page :total="partTotal" show-elevator @on-change="partPageChange" />
</div>
</el-card>
</el-main>
</el-container>
</div>
</Modal>
<detail ref="detail"></detail>
</div>
</template>
<script>
import {
endIndex,
partIndex,
} from "@/api/departmentProgress"
import detail from "../components/detail.vue"
export default {
name: 'DepartmentProgressDetail',
components: {
detail
},
data() {
return {
isShowModal: false,
row: {},
endTotal: 0,
partTotal: 0,
endSelect: {
page: 1,
page_size: 10,
},
partSelect: {
page: 1,
page_size: 10,
},
endList: [],
partList: [],
table: [{
prop: 'name',
label: '项目名称',
// fixed: 'left',
// width: 240,
align: 'left'
}, {
prop: 'department.name',
label: '科室',
width: 240,
}, {
prop: 'admin.name',
label: '经办人',
width: 100,
}, {
prop: 'contract_plan_sum',
label: '预算金额',
width: 100,
formatter(v1, v2, value){
return value ? value: '0.00'
}
}, {
prop: 'funds_count',
label: '发起支付笔数',
width: 100,
}, {
prop: 'contract_plan_act_sum',
label: '已确认付款金额',
width: 120,
formatter(v1, v2, value){
return value ? value: '0.00'
}
}, {
prop: 'status',
label: '当前状态',
width: 120,
}]
}
},
watch: {
isShowModal(newval) {
if (newval) {
} else {
this.row = {}
}
},
row(newval) {
if (newval) {
this.getEndIndex(newval.plan_department_id)
this.getPartIndex(newval.plan_department_id)
}
}
},
methods: {
partPageChange(e) {
this.partSelect.page = e
this.getPartIndex(this.row.plan_department_id)
},
endPageChange(e) {
this.endSelect.page = e
this.getEndIndex(this.row.plan_department_id)
},
async getEndIndex(id) {
const res = await endIndex({
department_id: id,
page: this.endSelect.page,
page_size: this.endSelect.page_size
})
this.endList = res.list.data
this.endTotal = res.list.total
},
async getPartIndex(id) {
const res = await partIndex({
department_id: id,
page: this.partSelect.page,
page_size: this.partSelect.page_size
})
this.partList = res.list.data
this.partTotal = res.list.total
},
getDetail(row,type) {
this.$refs.detail.type = type
this.$refs.detail.detailRow = row
this.$refs.detail.detailDrawerVisible = true
},
getCompletionRate(dept) {
return dept ? Number(dept.toFixed(2)) : 0
},
formatToWan(num) {
if (num === null || num === undefined) return '0元';
num = Number(num);
if (isNaN(num)) return '0元';
if (Math.abs(num) < 10000) {
return num.toString() + '元'; // 1
} else {
return (num / 10000).toFixed(2) + '万元'; // 1
}
},
}
}
</script>
<style scoped lang="scss">
::v-deep .ivu-modal-body {
max-height: none !important;
}
.department-progress-detail {
min-height: 100vh;
background: #f4f6fa;
}
.header-bg {
background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
color: white;
padding: 0;
height: 70px;
display: flex;
align-items: center;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0 30px;
}
.header-title {
font-size: 20px;
font-weight: bold;
}
.header-user {
font-size: 14px;
}
.sidebar {
background-color: #f8f9fa;
min-height: calc(100vh - 70px);
border-right: 1px solid #dee2e6;
}
.department-title {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
text-align: center;
}
.action-buttons {
margin-bottom: 20px;
text-align: right;
}
.summary-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
text-align: center;
}
.card-title {
font-size: 16px;
margin-bottom: 10px;
}
.execution-rate {
font-size: 32px;
font-weight: bold;
margin-bottom: 5px;
}
.card-desc {
font-size: 13px;
}
.section-title {
background-color: #f8f9fa;
padding: 15px;
margin: 20px 0 15px 0;
border-left: 4px solid #007bff;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
}
.table-container {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.detail-table {
font-size: 14px;
}
</style>

@ -0,0 +1,249 @@
<template>
<div>
<el-drawer :title="`${detailRow.name}`" :visible.sync="detailDrawerVisible" direction="rtl" size="800px">
<div class="detail">
<div>
<div class="section-title">列支信息</div>
<div class="list">
<div>
<span>项目名称</span>
<div>{{ detailRow.name }}</div>
</div>
<div><span>所属科室</span>
<div>{{ detailRow.department?detailRow.department.name:'' }}</div>
</div>
<div><span>经办人</span>
<div>{{ detailRow.admin?detailRow.admin.name:'' }}</div>
</div>
<div>
<span>预算科目</span>
<div>
<div v-for="item in detailRow.plan_link">
<div>{{item.plan.name}}</div>
</div>
</div>
</div>
<div><span>预算金额</span>
<div>{{ detailRow.contract_plan_sum?detailRow.contract_plan_sum:'0.00' }}</div>
</div>
<div><span>已确认金额</span>
<div>{{ detailRow.contract_plan_act_sum?detailRow.contract_plan_act_sum:'0.00' }}
</div>
</div>
<div><span>当前状态</span>
<div>
<el-tag size="small" v-if="type==='part'" type="warning">
{{detailRow.funds_count>0?'部分支付中':'oa流程中'}}
</el-tag>
<el-tag size="small" v-if="type==='end'" type="success">
已完成
</el-tag>
</div>
</div>
</div>
</div>
<div>
<div class="section-title">事前流程</div>
<div class="flows">
<div v-for="item in flowLinks">
<div class="flows-name">{{item.custom_model_name}}流程</div>
<div class="flows-title">{{item.flow_title}}</div>
<div class="flows-btn" @click="toOaDetail(item.id)"></div>
</div>
</div>
</div>
<div>
<div class="section-title">付款申请</div>
<div class="pay">
<div v-for="item in signPlan">
<div style="justify-content: space-between;">
<span>{{item.act_money}}</span>
<div style="text-align: right">
<el-tag size="small" :type="item.status?'success':'warning'">{{item.status?'已审核':'待审核'}}</el-tag>
</div>
</div>
<div>
<span>发起人</span>
<div>{{item.admin.name}}</div>
</div>
<div>
<span>申请日期</span>
<div>{{item.created_at}}</div>
</div>
<div>
<span>说明</span>
<div>{{item.remark}}</div>
</div>
</div>
</div>
</div>
</div>
</el-drawer>
<Modal :width="86" class-name="oa-modal" title="流程办理" fullscreen :mask-closable="false" v-model="isShowOaModal"
footer-hide>
<div style="width: 100%;height: 100%;">
<iframe style="width: 100%;height: 100%;border-radius: 0 0 6px 6px;" :src="oaUrl" frameborder="0"></iframe>
</div>
</Modal>
</div>
</template>
<script>
import {
detailContract
} from "@/api/contract/contract";
import {
getFundLog,
} from "@/api/paymentRegistration/fundLog.js";
export default {
data() {
return {
detailDrawerVisible: false,
detailRow: {},
type: '',
flowLinks: [],
signPlan: [],
oaUrl: '',
isShowOaModal:false
}
},
watch: {
detailDrawerVisible(newval) {
if (newval) {
this.getContract(this.detailRow.id)
this.getFundLog(this.detailRow.id)
} else {
this.detailRow = {}
this.type = ""
this.flowLinks = []
this.signPlan = []
this.oaUrl = ''
this.isShowOaModal = false
}
},
// detailRow(newval) {
// if (newval) {
// this.getContract(newval.id)
// this.getFundLog(newval.id)
// }
// }
},
methods: {
async getContract(id) {
const res = await detailContract({
id: id,
});
// this.detail = res;
this.flowLinks = res.contract_flow_links
},
async getFundLog(id) {
const plan = await getFundLog({
contract_id: id,
});
this.signPlan = plan.data;
},
toOaDetail(id) {
let url =
`${process.env.VUE_APP_OUT_URL}/#/flow/detail?auth_token=${window.encodeURIComponent(getToken())}&isSinglePage=1&flow_id=${id}`
this.oaUrl = url
this.isShowOaModal = true
}
}
}
</script>
<style scoped lang="scss">
.detail {
padding: 20px;
padding-top: 0;
overflow: scroll;
}
.section-title {
background-color: #f8f9fa;
padding: 15px;
margin: 20px 0 15px 0;
border-left: 4px solid #007bff;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
}
.list {
font-size: 16px;
&>div {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
&>span:first-child {
color: #909399;
width: 150px;
font-weight: bold;
}
&>div {
width: calc(100% - 150px);
}
}
}
.flows {
font-size: 16px;
&>div {
background-color: #f8f9fa;
border-radius: 10px;
font-size: 16px;
padding: 10px 20px;
margin-bottom: 20px;
}
&-name {
color: #007bff;
font-weight: bold;
margin-bottom: 10px;
}
&-title {
margin-bottom: 10px;
}
&-btn {
color: #007bff;
cursor: pointer;
font-weight: bold;
}
}
.pay {
&>div {
background-color: #f8f9fa;
border-radius: 10px;
font-size: 16px;
padding: 10px 20px;
margin-bottom: 20px;
&>div {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
&>span:first-child {
color: #909399;
width: 150px;
font-weight: bold;
}
&>div {
width: calc(100% - 150px);
}
}
}
}
</style>

@ -0,0 +1,205 @@
<template>
<div>
<el-main style="padding-top:0px">
<!-- 页面标题和操作 -->
<div class="d-flex justify-between align-center mb-4" style="margin: 0px 0 16px 0;">
<h3 style="display: inline-block;"><i class="el-icon-data-analysis"></i> 科室资金执行率</h3>
<div>
<el-date-picker size="small" v-model="select.year"
value-format="yyyy" format="yyyy" type="year" placeholder="选择年">
</el-date-picker>
<el-button style="margin-left:10px" type="primary" size="small" @click="getProgress"></el-button>
</div>
</div>
<!-- 科室执行率卡片 -->
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8" v-for="(dept, idx) in list" :key="dept.idx">
<el-card class="department-card" shadow="hover">
<div>
<h4 class="card-title">{{ dept.plan_department?dept.plan_department.name:'' }}</h4>
<div class="mb-2">
<div class="d-flex justify-between mb-1">
<span class="fw-bold">当前执行率</span>
<span class="execution-rate text-primary">{{ getCompletionRate(dept.rate) }}%</span>
</div>
<el-progress :show-text='false' :percentage="getCompletionRate(dept.rate)" :stroke-width="8" color="#409EFF" />
</div>
<div class="mb-2">
<div class="d-flex justify-between mb-1">
<span class="fw-bold">完成后执行率</span>
<span class="execution-rate text-success">{{ getCompletionRate(dept.rate_end) }}%</span>
</div>
<el-progress :show-text='false' :percentage="getCompletionRate(dept.rate_end)" :stroke-width="8" color="#67C23A" />
</div>
<el-row class="text-center" :gutter="0">
<el-col :span="8">
<div class="text-muted">预算总额</div>
<div class="text-muted fw-bold">{{ formatToWan(dept.contract_plan_sum) }}</div>
</el-col>
<el-col :span="8">
<div class="text-muted">已执行</div>
<div class="text-muted fw-bold text-success">{{ formatToWan(dept.use_money_total) }}</div>
</el-col>
<el-col :span="8">
<div class="text-muted">进行中</div>
<div class="text-muted fw-bold text-warning">{{ getDoingMoneyTotal(dept.contract_plan_sum,dept.use_money_total) }}</div>
</el-col>
</el-row>
<div class="mt-3 detail" @click="goToDetail(dept)">
<el-button type="text">
<i class="el-icon-view"></i> 查看详情
</el-button>
</div>
</div>
</el-card>
</el-col>
</el-row>
</el-main>
</el-container>
<departmentProgressDetail ref="departmentProgressDetail"></departmentProgressDetail>
</div>
</template>
<script>
import {
statisticDepartment
} from "@/api/departmentProgress"
import departmentProgressDetail from './components/departmentProgressDetail.vue'
export default {
name: 'DepartmentProgress',
components:{
departmentProgressDetail
},
data() {
return {
select: {
year: new Date().getFullYear()+''
},
list:[]
}
},
created(){
this.getProgress()
},
methods: {
async getProgress() {
const res = await statisticDepartment({
year: this.select.year
},true)
const data = res.departmentList
data.sort((a,b)=>{
return a.plan_department.sortnumber - b.plan_department.sortnumber
})
this.list = data
},
getCompletionRate(dept) {
return dept?Number(dept.toFixed(2)):0
},
formatToWan(num) {
if (num === null || num === undefined) return '0元';
num = Number(num);
if (isNaN(num)) return '0元';
if (Math.abs(num) < 10000) {
return num.toString() + '元'; // 1
} else {
return (num / 10000).toFixed(2) + '万元'; // 1
}
},
getDoingMoneyTotal(contractPlanSum, useMoneyTotal) {
//
const plan = Number(contractPlanSum) || 0;
const used = Number(useMoneyTotal) || 0;
// 00
const doing = Math.max(plan - used, 0);
return this.formatToWan(doing)
},
goToDetail(row) {
this.$refs.departmentProgressDetail.row = row
this.$refs.departmentProgressDetail.isShowModal = true
},
}
}
</script>
<style scoped>
.department-card {
border-radius: 8px;
margin-bottom: 15px;
transition: all 0.3s;
}
.department-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.card-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
.execution-rate {
font-size: 20px;
font-weight: bold;
}
.text-muted {
color: #909399;
text-align: center;
margin-bottom:10px;
font-size: 16px;
}
.fw-bold {
font-weight: bold;
color:#000;
font-size: 16px;
}
.text-success {
color: #67C23A;
}
.text-warning {
color: #E6A23C;
}
.text-primary{
color: #409EFF;
}
.d-flex {
display: flex;
}
.justify-between {
justify-content: space-between;
}
.align-center {
align-items: center;
}
.detail{
border:1px solid #409EFF;
text-align: center;
cursor:pointer
}
.mb-1 {
margin-bottom: 4px;
}
.mb-2 {
margin-bottom: 12px;
}
.mb-4 {
margin-bottom: 24px;
}
.mt-3 {
margin-top: 16px;
}
</style>
Loading…
Cancel
Save