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.

1978 lines
58 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="budget-container">
<!-- 页面头部 -->
<div class="page-header">
<h1 class="page-title">
<el-icon :size="24"><DataAnalysis /></el-icon>
预算查询
</h1>
</div>
<!-- 筛选区域 -->
<el-card class="filter-section" shadow="never">
<el-form :model="filterForm" inline>
<el-form-item label="预算年度">
<el-select v-model="filterForm.year_id" style="width: 180px" placeholder="请选择预算年度" @change="handleYearChange">
<el-option
v-for="item in yearOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
</el-card>
<!-- 汇总统计 -->
<div class="summary-section">
<!-- 总预算(部门预算 + 专项资金) -->
<el-card class="summary-row-card summary-row-card-total" shadow="never">
<div class="summary-row">
<div class="summary-row-items">
<div class="summary-row-item">
<div class="summary-row-label">总预算</div>
<div class="summary-row-value">¥{{ formatNumber(summary.totalBudget) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">已使用</div>
<div class="summary-row-value">¥{{ formatNumber(summary.departmentExecuted + summary.specialFundUsed) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">待使用</div>
<div class="summary-row-value">¥{{ formatNumber(summary.totalBudget - (summary.departmentExecuted + summary.specialFundUsed)) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">执行率</div>
<div class="summary-row-value" :style="{ color: getSummaryRateColor(totalExecutionRate) }">
{{ totalExecutionRate }}%
</div>
</div>
</div>
</div>
</el-card>
<!-- 部门预算 -->
<el-card class="summary-row-card" shadow="never">
<div class="summary-row">
<div class="summary-row-items">
<div class="summary-row-item">
<div class="summary-row-label">部门预算</div>
<div class="summary-row-value">¥{{ formatNumber(summary.departmentBudget) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">已使用</div>
<div class="summary-row-value">¥{{ formatNumber(summary.departmentExecuted) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">待使用</div>
<div class="summary-row-value">¥{{ formatNumber(summary.departmentBudget - summary.departmentExecuted) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">执行率</div>
<div class="summary-row-value" :style="{ color: getSummaryRateColor(departmentExecutionRate) }">
{{ departmentExecutionRate }}%
</div>
</div>
</div>
</div>
</el-card>
<!-- 专项资金 -->
<el-card class="summary-row-card" shadow="never">
<div class="summary-row">
<div class="summary-row-items">
<div class="summary-row-item">
<div class="summary-row-label">专项资金</div>
<div class="summary-row-value">¥{{ formatNumber(summary.specialFundBudget) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">已使用</div>
<div class="summary-row-value">¥{{ formatNumber(summary.specialFundUsed) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">待使用</div>
<div class="summary-row-value">¥{{ formatNumber(summary.specialFundBudget - summary.specialFundUsed) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">执行率</div>
<div class="summary-row-value" :style="{ color: getSummaryRateColor(specialFundExecutionRate) }">
{{ specialFundExecutionRate }}%
</div>
</div>
</div>
</div>
</el-card>
</div>
<!-- 主要内容 -->
<div class="main-content">
<div class="budget-tree">
<template v-for="section in budgetSections" :key="section.key">
<!-- 上一年结转资金:统计通栏(放在自有账户之前) -->
<el-card v-if="section.key === 'last_year_carryover'" class="summary-row-card summary-row-card-carryover" shadow="never">
<div class="summary-row">
<div class="summary-row-items">
<div class="summary-row-item">
<div class="summary-row-label">上一年结转资金</div>
<div class="summary-row-value">¥{{ formatNumber(summary.lastYearCarryoverBudget) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">已使用</div>
<div class="summary-row-value">¥{{ formatNumber(summary.lastYearCarryoverUsed) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">待使用</div>
<div class="summary-row-value">¥{{ formatNumber(summary.lastYearCarryoverBudget - summary.lastYearCarryoverUsed) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">执行率</div>
<div class="summary-row-value" :style="{ color: getSummaryRateColor(lastYearCarryoverExecutionRate) }">
{{ lastYearCarryoverExecutionRate }}%
</div>
</div>
</div>
</div>
</el-card>
<!-- 自有账户统计信息 -->
<el-card v-if="section.key === 'project'" class="project-summary-card project-summary-card-green" shadow="never">
<div class="project-summary">
<div class="project-summary-item">
<div class="project-summary-label">自有账户</div>
<div class="project-summary-value">¥{{ formatNumber(summary.projectBudget) }}</div>
</div>
<div class="project-summary-item">
<div class="project-summary-label">已使用</div>
<div class="project-summary-value">¥{{ formatNumber(summary.projectUsed) }}</div>
</div>
<div class="project-summary-item">
<div class="project-summary-label">剩余</div>
<div class="project-summary-value" style="color: #10b981;">
¥{{ formatNumber(summary.projectBudget - summary.projectUsed) }}
</div>
</div>
</div>
</el-card>
<!-- 抵消预付账款:统计通栏 -->
<el-card v-if="section.key === 'offset_prepaid'" class="summary-row-card summary-row-card-offset-prepaid" shadow="never">
<div class="summary-row">
<div class="summary-row-items">
<div class="summary-row-item">
<div class="summary-row-label">抵消预付账款</div>
<div class="summary-row-value">¥{{ formatNumber(summary.offsetPrepaidBudget) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">已使用</div>
<div class="summary-row-value">¥{{ formatNumber(summary.offsetPrepaidUsed) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">待使用</div>
<div class="summary-row-value">¥{{ formatNumber(summary.offsetPrepaidBudget - summary.offsetPrepaidUsed) }}</div>
</div>
</div>
</div>
</el-card>
<!-- 工会:统计通栏(排在现有大类最后) -->
<el-card v-if="section.key === 'trade_union'" class="summary-row-card summary-row-card-trade-union" shadow="never">
<div class="summary-row">
<div class="summary-row-items">
<div class="summary-row-item">
<div class="summary-row-label">工会</div>
<div class="summary-row-value">¥{{ formatNumber(summary.tradeUnionBudget) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">已使用</div>
<div class="summary-row-value">¥{{ formatNumber(summary.tradeUnionUsed) }}</div>
</div>
<div class="summary-row-item">
<div class="summary-row-label">待使用</div>
<div class="summary-row-value">¥{{ formatNumber(summary.tradeUnionBudget - summary.tradeUnionUsed) }}</div>
</div>
</div>
</div>
</el-card>
<el-card
:class="['type-section-card', `type-section-card-${section.key}`]"
shadow="never"
>
<template #header>
<div class="type-header">
<div class="type-title">
<el-tag :type="section.tagType" size="small" effect="dark">{{ section.label }}</el-tag>
<span class="type-count">根节点:{{ section.data.length }}</span>
</div>
<div>
<el-button size="small" text @click="expandAll(section.key)">展开</el-button>
<el-button size="small" text @click="collapseAll(section.key)">收起</el-button>
</div>
</div>
</template>
<el-empty v-if="!section.data.length" description="暂无数据" />
<template v-else>
<div
v-for="level1 in section.data"
:key="level1.id"
class="budget-level-1"
>
<div
:class="['budget-level-1-header', `budget-level-1-header-${level1.budget_type || 'department'}`, { collapsed: !level1.expanded }]"
@click="toggleLevel1(level1)"
>
<div class="budget-level-1-title">
<el-icon><Folder /></el-icon>
<span>{{ level1.name }}</span>
<el-tag size="small" type="info" effect="plain">{{ budgetTypeLabel(level1.budget_type) }}</el-tag>
</div>
<div class="budget-level-1-info">
<div class="budget-info-item">
<span class="budget-info-label">总预算</span>
<span class="budget-info-value">¥{{ formatNumber(level1.budget_amount) }}</span>
</div>
<div class="budget-info-item">
<span class="budget-info-label">已使用</span>
<span class="budget-info-value">¥{{ formatNumber(level1.used_amount || 0) }}</span>
</div>
<div class="budget-info-item">
<span class="budget-info-label">使用率</span>
<span class="budget-info-value" :style="{ color: getUsageRateColor(level1) }">
{{ getUsageRate(level1) }}%
</span>
</div>
<div class="budget-info-item">
<span class="budget-info-label">剩余</span>
<span class="budget-info-value" style="color: #10b981;">
¥{{ formatNumber((level1.budget_amount || 0) - (level1.used_amount || 0)) }}
</span>
</div>
<el-button
v-if="level1.is_leaf"
size="small"
type="primary"
link
class="level-1-detail-button"
@click.stop="viewDetail(level1.id)"
>
<el-icon><View /></el-icon>
查看
</el-button>
<el-icon v-else :size="20" style="margin-left: 15px;">
<ArrowDown v-if="level1.expanded" /><ArrowRight v-else />
</el-icon>
</div>
</div>
<el-collapse-transition>
<div v-show="level1.expanded" class="budget-level-1-content">
<div
v-for="level2 in level1.children"
:key="level2.id"
class="budget-level-2"
>
<div class="budget-level-2-name">
<el-icon><Document /></el-icon>
{{ level2.name }}
</div>
<div class="budget-amount">¥{{ formatNumber(level2.budget_amount) }}</div>
<div class="budget-used">¥{{ formatNumber(level2.used_amount || 0) }}</div>
<div class="budget-remaining positive">
¥{{ formatNumber((level2.budget_amount || 0) - (level2.used_amount || 0)) }}
</div>
<div class="budget-progress">
<el-progress
:percentage="level2.execution_rate || 0"
:stroke-width="8"
:color="progressColor(level2.execution_rate || 0)"
:show-text="false"
/>
<span class="progress-text">{{ level2.execution_rate || 0 }}%</span>
</div>
<div>
<el-button size="small" type="primary" link @click="viewDetail(level2.id)">
<el-icon><View /></el-icon>
查看
</el-button>
</div>
</div>
</div>
</el-collapse-transition>
</div>
</template>
</el-card>
</template>
</div>
</div>
<!-- 执行明细抽屉 -->
<el-drawer
v-model="drawerVisible"
title="执行明细"
:size="600"
direction="rtl"
>
<template #header>
<div class="drawer-header">
<div class="drawer-title-row">
<h3>{{ currentBudgetName }}</h3>
<el-button
type="primary"
size="small"
:icon="Download"
:disabled="!executionDetails.executed.length && !executionDetails.pending.length"
@click="exportExecutionDetails"
>
导出明细
</el-button>
</div>
<div class="drawer-summary">
<div class="summary-item">
<span class="label">已执行:</span>
<span class="value executed">¥{{ formatNumber(executionDetails.executed_total || 0) }}</span>
</div>
<div class="summary-item">
<span class="label">待执行:</span>
<span class="value pending">¥{{ formatNumber(executionDetails.pending_total || 0) }}</span>
</div>
</div>
</div>
</template>
<el-tabs v-model="activeTab" class="execution-tabs">
<el-tab-pane label="已执行" name="executed">
<el-table :data="executionDetails.executed" style="width: 100%">
<el-table-column prop="payment_serial_number" label="支付单号" width="180" />
<el-table-column prop="payment_date" label="支付日期" width="120" />
<el-table-column prop="amount" label="金额" width="120" align="right">
<template #default="scope">
<span class="amount executed">¥{{ formatNumber(scope.row.amount) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="scope">
<el-button type="primary" link size="small" @click="viewPaymentDetail(scope.row)">
详情
</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="待执行" name="pending">
<el-table :data="executionDetails.pending" style="width: 100%">
<el-table-column prop="payment_serial_number" label="支付单号" width="180" />
<el-table-column prop="payment_date" label="支付日期" width="120" />
<el-table-column label="状态" width="100">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.payment_status)" size="small">
{{ scope.row.payment_status_text }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="payee" label="收款单位" min-width="150" />
<el-table-column prop="payment_item" label="支付事项" min-width="200" />
<el-table-column prop="amount" label="金额" width="120" align="right">
<template #default="scope">
<span class="amount pending">¥{{ formatNumber(scope.row.amount) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template #default="scope">
<el-button type="primary" link size="small" @click="viewPaymentDetail(scope.row)">
详情
</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</el-drawer>
<!-- 支付明细详情弹窗(A4格式) -->
<el-dialog
v-model="detailDialogVisible"
title="支付明细详情"
width="210mm"
class="payment-detail-dialog"
:close-on-click-modal="false"
>
<div v-if="paymentDetailInfo" class="a4-container">
<div class="a4-page">
<div class="detail-header-section">
<h2 class="main-title">支付明细信息链</h2>
<div class="detail-basic-info">
<div class="info-row">
<span class="label">支付明细ID:</span>
<span class="value">{{ paymentDetailInfo.id }}</span>
</div>
<div class="info-row">
<span class="label">支付金额:</span>
<span class="value amount">¥{{ formatNumber(paymentDetailInfo.amount) }}</span>
</div>
<div v-if="paymentDetailInfo.description" class="info-row">
<span class="label">说明:</span>
<span class="value">{{ paymentDetailInfo.description }}</span>
</div>
</div>
</div>
<!-- 支付流程信息 -->
<div v-if="paymentDetailInfo.payment_info" class="info-section">
<h3 class="section-title">支付流程信息</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">支付单号:</span>
<span class="value">{{ paymentDetailInfo.payment_info.serial_number }}</span>
</div>
<div class="info-item">
<span class="label">状态:</span>
<el-tag :type="paymentDetailInfo.payment_info.status_color" size="small">
{{ paymentDetailInfo.payment_info.status_text }}
</el-tag>
</div>
<div class="info-item">
<span class="label">支付日期:</span>
<span class="value">{{ paymentDetailInfo.payment_info.payment_date || '-' }}</span>
</div>
<div class="info-item">
<span class="label">总金额:</span>
<span class="value">¥{{ formatNumber(paymentDetailInfo.payment_info.total_amount) }}</span>
</div>
<div v-if="paymentDetailInfo.payment_info.payment_category" class="info-item full-width">
<span class="label">支付分类:</span>
<span class="value">{{ paymentDetailInfo.payment_info.payment_category.breadcrumb.join(' > ') }}</span>
</div>
<div v-if="paymentDetailInfo.payment_info.oa_flow" class="info-item">
<span class="label">OA流程:</span>
<span class="value">{{ paymentDetailInfo.payment_info.oa_flow.title }}({{ paymentDetailInfo.payment_info.oa_flow.no }})</span>
</div>
<div v-if="paymentDetailInfo.payment_info.description" class="info-item full-width">
<span class="label">描述:</span>
<span class="value">{{ paymentDetailInfo.payment_info.description }}</span>
</div>
</div>
<!-- 支付流程自定义字段 -->
<div v-if="paymentDetailInfo.payment_info.element_values && paymentDetailInfo.payment_info.element_values.length > 0" class="custom-fields">
<h4 class="subsection-title">自定义字段</h4>
<div class="info-grid">
<div v-for="field in paymentDetailInfo.payment_info.element_values" :key="field.element_id" class="info-item">
<span class="label">{{ field.display_name || field.field_key }}:</span>
<span class="value">{{ formatFieldValue(field) }}</span>
</div>
</div>
</div>
</div>
<!-- 事前支出信息(非直接支付) -->
<div v-if="paymentDetailInfo.expenditure_info" class="info-section">
<h3 class="section-title">事前支出信息</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">账单编号:</span>
<span class="value">{{ paymentDetailInfo.expenditure_info.serial_number }}</span>
</div>
<div class="info-item">
<span class="label">账单标题:</span>
<span class="value">{{ paymentDetailInfo.expenditure_info.title }}</span>
</div>
<div class="info-item">
<span class="label">账单金额:</span>
<span class="value">¥{{ formatNumber(paymentDetailInfo.expenditure_info.amount) }}</span>
</div>
<div v-if="paymentDetailInfo.expenditure_info.description" class="info-item full-width">
<span class="label">描述:</span>
<span class="value">{{ paymentDetailInfo.expenditure_info.description }}</span>
</div>
</div>
<!-- 事前支出自定义字段 -->
<div v-if="paymentDetailInfo.expenditure_info.element_values && paymentDetailInfo.expenditure_info.element_values.length > 0" class="custom-fields">
<h4 class="subsection-title">自定义字段</h4>
<div class="info-grid">
<div v-for="field in paymentDetailInfo.expenditure_info.element_values" :key="field.element_id" class="info-item">
<span class="label">{{ field.display_name || field.field_key }}:</span>
<span class="value">{{ formatFieldValue(field) }}</span>
</div>
</div>
</div>
</div>
<!-- 预算来源信息 -->
<div v-if="paymentDetailInfo.budget_info" class="info-section">
<h3 class="section-title">预算来源信息</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">预算年度:</span>
<span class="value">{{ paymentDetailInfo.budget_info.year }}</span>
</div>
<div class="info-item">
<span class="label">预算类型:</span>
<span class="value">{{ paymentDetailInfo.budget_info.budget_type_text }}</span>
</div>
<div class="info-item">
<span class="label">预算金额:</span>
<span class="value">¥{{ formatNumber(paymentDetailInfo.budget_info.budget_amount) }}</span>
</div>
<div class="info-item full-width">
<span class="label">预算路径:</span>
<span class="value">{{ paymentDetailInfo.budget_info.path }}</span>
</div>
</div>
</div>
<!-- 时间信息 -->
<div class="info-section">
<h3 class="section-title">时间信息</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">创建时间:</span>
<span class="value">{{ paymentDetailInfo.created_at || '-' }}</span>
</div>
<div class="info-item">
<span class="label">更新时间:</span>
<span class="value">{{ paymentDetailInfo.updated_at || '-' }}</span>
</div>
</div>
</div>
</div>
</div>
<template #footer>
<el-button @click="detailDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="printDetail">打印</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import * as XLSX from 'xlsx'
import {
DataAnalysis,
Search,
Refresh,
List,
FullScreen,
Minus,
Folder,
Document,
View,
Download,
ArrowDown,
ArrowRight
} from '@element-plus/icons-vue'
import { budgetDataAPI, paymentDetailAPI } from '@/utils/api'
const router = useRouter()
const filterForm = ref({
year_id: ''
})
const yearOptions = ref([])
const summary = ref({
totalBudget: 0, // 总预算(部门预算 + 专项资金)
departmentBudget: 0,
specialFundBudget: 0,
lastYearCarryoverBudget: 0, // 上一年结转资金
projectBudget: 0, // 自有账户
offsetPrepaidBudget: 0, // 抵消预付账款
tradeUnionBudget: 0, // 工会
departmentExecuted: 0,
specialFundUsed: 0,
lastYearCarryoverUsed: 0,
projectUsed: 0,
offsetPrepaidUsed: 0,
tradeUnionUsed: 0
})
const budgetTree = ref([])
const loading = ref(false)
// 执行明细抽屉相关
const drawerVisible = ref(false)
const activeTab = ref('executed')
const currentBudgetName = ref('')
const currentBudgetId = ref(null)
const executionDetails = ref({
executed: [],
pending: [],
executed_total: 0,
pending_total: 0
})
// 计算总预算执行率(部门预算 + 专项资金)
const totalExecutionRate = computed(() => {
if (summary.value.totalBudget === 0) return '0.00'
const totalUsed = summary.value.departmentExecuted + summary.value.specialFundUsed
return ((totalUsed / summary.value.totalBudget) * 100).toFixed(2)
})
// 计算部门预算执行率
const departmentExecutionRate = computed(() => {
if (summary.value.departmentBudget === 0) return '0.00'
return ((summary.value.departmentExecuted / summary.value.departmentBudget) * 100).toFixed(2)
})
// 计算专项资金执行率
const specialFundExecutionRate = computed(() => {
if (summary.value.specialFundBudget === 0) return '0.00'
return ((summary.value.specialFundUsed / summary.value.specialFundBudget) * 100).toFixed(2)
})
// 计算上一年结转资金执行率
const lastYearCarryoverExecutionRate = computed(() => {
if (summary.value.lastYearCarryoverBudget === 0) return '0.00'
return ((summary.value.lastYearCarryoverUsed / summary.value.lastYearCarryoverBudget) * 100).toFixed(2)
})
// 计算自有账户使用率
const projectUsageRate = computed(() => {
if (summary.value.projectBudget === 0) return '0.00'
return ((summary.value.projectUsed / summary.value.projectBudget) * 100).toFixed(2)
})
// 计算抵消预付账款执行率
const offsetPrepaidExecutionRate = computed(() => {
if (summary.value.offsetPrepaidBudget === 0) return '0.00'
return ((summary.value.offsetPrepaidUsed / summary.value.offsetPrepaidBudget) * 100).toFixed(2)
})
// 计算工会执行率
const tradeUnionExecutionRate = computed(() => {
if (summary.value.tradeUnionBudget === 0) return '0.00'
return ((summary.value.tradeUnionUsed / summary.value.tradeUnionBudget) * 100).toFixed(2)
})
// 根据执行率返回颜色
const getSummaryRateColor = (rate) => {
const numRate = Number(rate)
if (numRate >= 80) return '#67c23a'
if (numRate >= 60) return '#409eff'
if (numRate >= 40) return '#e6a23c'
return '#f56c6c'
}
const formatNumber = (num) => {
const n = Number(num || 0)
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
// 数字转中文大写金额
const numberToChinese = (num) => {
const n = Number(num || 0)
if (n === 0) return '零元整'
const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
const units = ['', '拾', '佰', '仟']
// 分离整数和小数部分
const parts = n.toFixed(2).split('.')
const integerPart = parseInt(parts[0])
const decimalPart = parts[1]
let result = ''
// 处理整数部分
if (integerPart > 0) {
// 处理亿位
if (integerPart >= 100000000) {
const yi = Math.floor(integerPart / 100000000)
result += convertGroup(yi, digits, units) + '亿'
const remaining = integerPart % 100000000
if (remaining > 0) {
if (remaining < 10000000) result += '零'
result += convertInteger(remaining, digits, units)
}
} else {
result += convertInteger(integerPart, digits, units)
}
result += '元'
}
// 处理小数部分
const jiao = parseInt(decimalPart[0])
const fen = parseInt(decimalPart[1])
if (jiao === 0 && fen === 0) {
result += '整'
} else {
if (jiao > 0) {
result += digits[jiao] + '角'
}
if (fen > 0) {
result += digits[fen] + '分'
} else if (jiao > 0) {
result += '整'
}
}
return result
}
// 转换整数部分(处理万位和个位)
const convertInteger = (num, digits, units) => {
if (num === 0) return ''
let result = ''
if (num >= 10000) {
const wan = Math.floor(num / 10000)
result += convertGroup(wan, digits, units) + '万'
const ge = num % 10000
if (ge > 0) {
if (ge < 1000) result += '零'
result += convertGroup(ge, digits, units)
}
} else {
result += convertGroup(num, digits, units)
}
return result
}
// 转换四位数为一组(个、十、百、千)
const convertGroup = (num, digits, units) => {
if (num === 0) return ''
let result = ''
const numStr = num.toString()
const len = numStr.length
for (let i = 0; i < len; i++) {
const digit = parseInt(numStr[i])
const pos = len - i - 1
if (digit !== 0) {
// 处理"壹拾"的情况,通常简化为"拾"
if (digit === 1 && pos === 1 && len === 2) {
result += units[pos]
} else {
result += digits[digit] + units[pos]
}
} else if (result && result[result.length - 1] !== '零' && i < len - 1) {
// 添加零,但要检查后面是否有非零数字
const nextNonZero = numStr.substring(i + 1).split('').some(d => parseInt(d) !== 0)
if (nextNonZero) {
result += '零'
}
}
}
return result
}
// 进度色彩映射
const progressColor = (rate) => {
if (rate >= 80) return '#67c23a'
if (rate >= 60) return '#409eff'
if (rate >= 40) return '#e6a23c'
return '#f56c6c'
}
// 计算使用率
const getUsageRate = (item) => {
const budgetAmount = Number(item.budget_amount || 0)
const usedAmount = Number(item.used_amount || 0)
if (budgetAmount === 0) return '0.00'
const rate = (usedAmount / budgetAmount) * 100
return rate.toFixed(2)
}
// 根据使用率返回颜色
const getUsageRateColor = (item) => {
const rate = Number(getUsageRate(item))
if (rate >= 80) return '#67c23a'
if (rate >= 60) return '#409eff'
if (rate >= 40) return '#e6a23c'
return '#f56c6c'
}
const budgetTypeLabel = (type) => {
const map = {
department: '部门预算',
project: '自有账户',
special_fund: '专项资金',
last_year_carryover: '上一年结转资金',
offset_prepaid: '抵消预付账款',
trade_union: '工会'
}
return map[type] || '未指定'
}
const toggleLevel1 = (item) => {
item.expanded = !item.expanded
}
const departmentBudget = computed(() =>
(budgetTree.value || []).filter(item => (item.budget_type || 'department') === 'department')
)
const projectBudget = computed(() =>
(budgetTree.value || []).filter(item => (item.budget_type || 'department') === 'project')
)
const specialFundBudget = computed(() =>
(budgetTree.value || []).filter(item => (item.budget_type || 'department') === 'special_fund')
)
const lastYearCarryoverBudget = computed(() =>
(budgetTree.value || []).filter(item => (item.budget_type || 'department') === 'last_year_carryover')
)
const offsetPrepaidBudget = computed(() =>
(budgetTree.value || []).filter(item => (item.budget_type || 'department') === 'offset_prepaid')
)
const tradeUnionBudget = computed(() =>
(budgetTree.value || []).filter(item => (item.budget_type || 'department') === 'trade_union')
)
const budgetSections = computed(() => [
{ key: 'department', label: '部门预算', data: departmentBudget.value, tagType: 'success' },
{ key: 'special_fund', label: '专项资金', data: specialFundBudget.value, tagType: 'warning' },
{ key: 'last_year_carryover', label: '上一年结转资金', data: lastYearCarryoverBudget.value, tagType: 'danger' },
{ key: 'project', label: '自有账户', data: projectBudget.value, tagType: 'info' },
{ key: 'offset_prepaid', label: '抵消预付账款', data: offsetPrepaidBudget.value, tagType: 'primary' },
{ key: 'trade_union', label: '工会', data: tradeUnionBudget.value, tagType: 'info' }
])
const expandAll = (typeKey = null) => {
let list
if (typeKey) {
if (typeKey === 'department') {
list = departmentBudget.value
} else if (typeKey === 'project') {
list = projectBudget.value
} else if (typeKey === 'special_fund') {
list = specialFundBudget.value
} else if (typeKey === 'last_year_carryover') {
list = lastYearCarryoverBudget.value
} else if (typeKey === 'offset_prepaid') {
list = offsetPrepaidBudget.value
} else if (typeKey === 'trade_union') {
list = tradeUnionBudget.value
} else {
list = budgetTree.value
}
} else {
list = budgetTree.value
}
list.forEach(item => {
item.expanded = true
})
}
const collapseAll = (typeKey = null) => {
let list
if (typeKey) {
if (typeKey === 'department') {
list = departmentBudget.value
} else if (typeKey === 'project') {
list = projectBudget.value
} else if (typeKey === 'special_fund') {
list = specialFundBudget.value
} else if (typeKey === 'last_year_carryover') {
list = lastYearCarryoverBudget.value
} else if (typeKey === 'offset_prepaid') {
list = offsetPrepaidBudget.value
} else if (typeKey === 'trade_union') {
list = tradeUnionBudget.value
} else {
list = budgetTree.value
}
} else {
list = budgetTree.value
}
list.forEach(item => {
item.expanded = false
})
}
const handleYearChange = () => {
loadTreeData()
}
// 获取状态标签类型
const getStatusTagType = (status) => {
const typeMap = {
'draft': 'info',
'submitted': '',
'approved': 'success',
'rejected': 'danger',
'completed': 'success'
}
return typeMap[status] || ''
}
// 查看执行明细
const viewDetail = async (budgetId) => {
// 找到对应的预算节点获取名称
const findBudgetName = (nodes, id) => {
for (const node of nodes) {
if (node.id === id) {
return node.name
}
if (node.children && node.children.length > 0) {
const found = findBudgetName(node.children, id)
if (found) return found
}
}
return ''
}
currentBudgetId.value = budgetId
currentBudgetName.value = findBudgetName(budgetTree.value, budgetId) || '预算明细'
drawerVisible.value = true
activeTab.value = 'executed'
// 加载执行明细
await loadExecutionDetails(budgetId)
}
// 加载执行明细
const loadExecutionDetails = async (budgetId) => {
try {
const res = await budgetDataAPI.getExecutionDetails(budgetId)
if (res.code === 0) {
executionDetails.value = {
executed: res.data.executed || [],
pending: res.data.pending || [],
executed_total: res.data.executed_total || 0,
pending_total: res.data.pending_total || 0
}
} else {
ElMessage.error(res.msg || '加载执行明细失败')
executionDetails.value = {
executed: [],
pending: [],
executed_total: 0,
pending_total: 0
}
}
} catch (e) {
ElMessage.error('加载执行明细失败:' + e.message)
executionDetails.value = {
executed: [],
pending: [],
executed_total: 0,
pending_total: 0
}
}
}
const sanitizeFileName = (name) => {
return String(name || '预算明细')
.replace(/[\\/:*?"<>|]/g, '_')
.slice(0, 80)
}
const formatExportDateTime = (value) => {
if (!value) return ''
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/.test(value)) {
return value
}
const date = new Date(value)
if (Number.isNaN(date.getTime())) return String(value)
const pad = (num) => String(num).padStart(2, '0')
const year = date.getFullYear()
const month = pad(date.getMonth() + 1)
const day = pad(date.getDate())
const hours = pad(date.getHours())
const minutes = pad(date.getMinutes())
const seconds = pad(date.getSeconds())
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
const mapExecutionRows = (rows, statusText) => {
return (rows || []).map((row, index) => ({
序号: index + 1,
状态: statusText || row.payment_status_text || '',
支付单号: row.payment_serial_number || '',
支付日期: formatExportDateTime(row.payment_date),
收款单位: row.payee || '',
支付事项: row.payment_item || row.description || '',
金额: Number(row.amount || 0),
付款ID: row.payment_id || '',
分摊ID: row.allocation_id || '',
创建时间: formatExportDateTime(row.created_at)
}))
}
const exportExecutionDetails = () => {
const executedRows = executionDetails.value.executed || []
const pendingRows = executionDetails.value.pending || []
if (!executedRows.length && !pendingRows.length) {
ElMessage.warning('暂无可导出的明细')
return
}
const workbook = XLSX.utils.book_new()
const summaryRows = [
{ 项目: '预算节点', 数值: currentBudgetName.value || '' },
{ 项目: '已执行合计', 数值: Number(executionDetails.value.executed_total || 0) },
{ 项目: '待执行合计', 数值: Number(executionDetails.value.pending_total || 0) },
{ 项目: '导出时间', 数值: new Date().toLocaleString('zh-CN') }
]
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(summaryRows), '汇总')
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(mapExecutionRows(executedRows, '已执行')), '已执行')
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(mapExecutionRows(pendingRows, '待执行')), '待执行')
const date = new Date().toISOString().slice(0, 10)
XLSX.writeFile(workbook, `${sanitizeFileName(currentBudgetName.value)}_执行明细_${date}.xlsx`)
ElMessage.success('导出成功')
}
const buildDisplayTree = (nodes) => {
return nodes.map(node => ({
...node,
expanded: true,
children: node.children ? buildDisplayTree(node.children) : []
}))
}
const updateSummary = async () => {
// 只汇总第一层节点,因为第一层的数据已经包含了子孙层的汇总
// 计算部门预算(budget_type === 'department' 的第一层节点)
const departmentTotal = departmentBudget.value.reduce((sum, n) => sum + Number(n.budget_amount || 0), 0)
// 计算专项资金(budget_type === 'special_fund' 的第一层节点)
const specialFundTotal = specialFundBudget.value.reduce((sum, n) => sum + Number(n.budget_amount || 0), 0)
// 计算上一年结转资金(budget_type === 'last_year_carryover' 的第一层节点)
const lastYearCarryoverTotal = lastYearCarryoverBudget.value.reduce((sum, n) => sum + Number(n.budget_amount || 0), 0)
// 计算总预算(部门预算 + 专项资金,不包含自有账户)
const total = departmentTotal + specialFundTotal
// 计算自有账户(budget_type === 'project' 的第一层节点)
const projectTotal = projectBudget.value.reduce((sum, n) => sum + Number(n.budget_amount || 0), 0)
// 计算抵消预付账款(budget_type === 'offset_prepaid' 的第一层节点)
const offsetPrepaidTotal = offsetPrepaidBudget.value.reduce((sum, n) => sum + Number(n.budget_amount || 0), 0)
// 计算工会(budget_type === 'trade_union' 的第一层节点)
const tradeUnionTotal = tradeUnionBudget.value.reduce((sum, n) => sum + Number(n.budget_amount || 0), 0)
summary.value.totalBudget = total
summary.value.departmentBudget = departmentTotal
summary.value.specialFundBudget = specialFundTotal
summary.value.lastYearCarryoverBudget = lastYearCarryoverTotal
summary.value.projectBudget = projectTotal
summary.value.offsetPrepaidBudget = offsetPrepaidTotal
summary.value.tradeUnionBudget = tradeUnionTotal
// 获取执行统计
await loadExecutionStatistics()
}
// 加载执行统计
const loadExecutionStatistics = async () => {
if (!filterForm.value.year_id) {
return
}
try {
const params = {
year_id: filterForm.value.year_id
}
const res = await budgetDataAPI.getExecutionStatistics(params)
if (res.code === 0) {
summary.value.departmentExecuted = res.data.department_executed || 0
summary.value.specialFundUsed = res.data.special_fund_used || 0
summary.value.lastYearCarryoverUsed = res.data.last_year_carryover_used || 0
summary.value.projectUsed = res.data.project_used || 0
summary.value.offsetPrepaidUsed = res.data.offset_prepaid_used || 0
summary.value.tradeUnionUsed = res.data.trade_union_used || 0
}
} catch (e) {
console.error('加载执行统计失败:', e)
// 失败时不显示错误,保持默认值0
}
}
const loadYearOptions = async () => {
try {
const res = await budgetDataAPI.getYearOptions()
if (res.code === 0) {
yearOptions.value = res.data || []
if (!filterForm.value.year_id && yearOptions.value.length > 0) {
filterForm.value.year_id = yearOptions.value[0].value
}
} else {
ElMessage.error(res.msg || '加载年度失败')
}
} catch (e) {
ElMessage.error('加载年度失败:' + e.message)
}
}
const loadTreeData = async () => {
if (!filterForm.value.year_id) {
return
}
loading.value = true
try {
const params = {
year_id: filterForm.value.year_id
}
const res = await budgetDataAPI.getTree(params)
if (res.code === 0) {
const data = res.data || []
budgetTree.value = buildDisplayTree(data)
updateSummary()
} else {
ElMessage.error(res.msg || '加载预算数据失败')
}
} catch (e) {
ElMessage.error('加载预算数据失败:' + e.message)
} finally {
loading.value = false
}
}
// 支付明细详情相关
const detailDialogVisible = ref(false)
const paymentDetailInfo = ref(null)
// 查看支付明细详情
const viewPaymentDetail = async (row) => {
try {
// Step2:执行明细可能来自 Payment(row.id 为空)或 legacy PaymentDetail(row.id 有值)
if (row?.id) {
const res = await paymentDetailAPI.getDetailInfo(row.id)
if (res.code === 0) {
paymentDetailInfo.value = res.data
detailDialogVisible.value = true
} else {
ElMessage.error(res.msg || '加载详情失败')
}
return
}
// Payment 级记录:直接跳转到付款打印页
if (row?.payment_id) {
router.push({ path: `/payment/payment-detail-print/${row.payment_id}` })
return
}
ElMessage.warning('无法打开详情:缺少ID')
} catch (e) {
ElMessage.error('加载详情失败:' + e.message)
}
}
// 格式化字段值
const formatFieldValue = (field) => {
if (!field.value) return '-'
if (typeof field.value === 'object') {
if (field.value.title) return field.value.title
if (field.value.name) return field.value.name
return JSON.stringify(field.value)
}
return field.value
}
// 打印详情
const printDetail = () => {
window.print()
}
onMounted(async () => {
await loadYearOptions()
await loadTreeData()
})
</script>
<style scoped>
.budget-container {
padding: 20px;
background: #f5f7fa;
min-height: 100%;
}
.page-header {
background: white;
padding: 25px 30px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
display: flex;
justify-content: space-between;
align-items: center;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #333;
margin: 0;
display: flex;
align-items: center;
gap: 10px;
}
.filter-section {
margin-bottom: 20px;
}
.summary-section {
margin-bottom: 20px;
}
.summary-row-card {
margin-bottom: 16px;
}
.summary-row-card:last-child {
margin-bottom: 0;
}
.summary-row-card-total {
background: linear-gradient(135deg, #409fff 0%, #93c5fd 100%);
border: none;
box-shadow: 0 4px 12px rgba(64, 159, 255, 0.25);
}
.summary-row-card-total :deep(.el-card__body) {
padding: 24px 20px;
}
.summary-row-card-total .summary-row-label {
color: rgba(255, 255, 255, 0.9);
font-size: 15px;
font-weight: 500;
}
.summary-row-card-total .summary-row-value {
color: #ffffff;
font-size: 24px;
font-weight: 700;
}
/* 上一年结转资金通栏 - 靛蓝渐变风格(调淡) */
.summary-row-card-carryover {
background: linear-gradient(135deg, #818cf8 0%, #c7d2fe 100%);
border: none;
box-shadow: 0 4px 12px rgba(129, 140, 248, 0.2);
}
.summary-row-card-carryover :deep(.el-card__body) {
padding: 24px 20px;
}
.summary-row-card-carryover .summary-row-label {
color: #4338ca;
font-size: 15px;
font-weight: 500;
}
.summary-row-card-carryover .summary-row-value {
color: #3730a3;
font-size: 24px;
font-weight: 700;
}
/* 抵消预付账款通栏 - 紫色渐变风格(调淡) */
.summary-row-card-offset-prepaid {
background: linear-gradient(135deg, #8b5cf6 0%, #c4b5fd 100%);
border: none;
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.2);
}
.summary-row-card-offset-prepaid :deep(.el-card__body) {
padding: 24px 20px;
}
.summary-row-card-offset-prepaid .summary-row-label {
color: #5b21b6;
font-size: 15px;
font-weight: 500;
}
.summary-row-card-offset-prepaid .summary-row-value {
color: #4c1d95;
font-size: 24px;
font-weight: 700;
}
/* 工会通栏 - 青色渐变风格 */
.summary-row-card-trade-union {
background: linear-gradient(135deg, #0d9488 0%, #99f6e4 100%);
border: none;
box-shadow: 0 4px 12px rgba(13, 148, 136, 0.2);
}
.summary-row-card-trade-union :deep(.el-card__body) {
padding: 24px 20px;
}
.summary-row-card-trade-union .summary-row-label {
color: #0f766e;
font-size: 15px;
font-weight: 500;
}
.summary-row-card-trade-union .summary-row-value {
color: #115e59;
font-size: 24px;
font-weight: 700;
}
/* 抵消预付账款明细项表头 - 紫色渐变风格(调淡) */
.budget-level-1-header-trade_union {
background: linear-gradient(135deg, #0d9488 0%, #99f6e4 100%) !important;
color: #115e59 !important;
}
.budget-level-1-header-trade_union.collapsed {
background: #99f6e4 !important;
color: #115e59 !important;
}
.summary-row {
display: flex;
align-items: center;
}
.summary-row-items {
display: flex;
gap: 40px;
width: 100%;
justify-content: space-around;
}
.summary-row-item {
flex: 1;
text-align: center;
}
.summary-row-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.summary-row-value {
font-size: 20px;
font-weight: 600;
color: #333;
}
.project-summary-card {
margin-bottom: 16px;
}
.project-summary {
display: flex;
gap: 40px;
width: 100%;
justify-content: space-around;
}
.project-summary-item {
flex: 1;
text-align: center;
}
.project-summary-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.project-summary-value {
font-size: 20px;
font-weight: 600;
color: #333;
}
/* 自有账户通栏 - 绿色渐变风格(调淡) */
.project-summary-card-green {
background: linear-gradient(135deg, #34d399 0%, #a7f3d0 100%);
border: none;
box-shadow: 0 4px 12px rgba(52, 211, 153, 0.2);
}
.project-summary-card-green :deep(.el-card__body) {
padding: 24px 20px;
}
.project-summary-card-green .project-summary-label {
color: #065f46;
font-size: 15px;
font-weight: 500;
}
.project-summary-card-green .project-summary-value {
color: #047857;
font-size: 24px;
font-weight: 700;
}
.summary-item {
text-align: center;
}
.summary-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.summary-value {
font-size: 28px;
font-weight: 700;
color: #333;
margin-bottom: 4px;
}
.summary-value.positive {
color: #10b981;
}
.summary-value-chinese {
font-size: 12px;
color: #909399;
line-height: 1.4;
word-break: break-all;
margin-top: 4px;
}
.execution-percentage {
font-size: 16px;
font-weight: 500;
color: #409eff;
margin-left: 8px;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.content-title {
font-size: 18px;
font-weight: 600;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
.budget-tree {
padding: 0;
}
.type-section-card {
margin-bottom: 20px;
}
.type-section-card:last-child {
margin-bottom: 0;
}
/* 明细表头保持白色背景,不修改 */
/* 标题标签背景色随大块主色调 */
.type-section-card-department :deep(.type-title .el-tag--dark),
.type-section-card-special_fund :deep(.type-title .el-tag--dark) {
background-color: #409fff !important;
border-color: #409fff !important;
}
.type-section-card-last_year_carryover :deep(.type-title .el-tag--dark) {
background-color: #6366f1 !important;
border-color: #6366f1 !important;
}
.type-section-card-project :deep(.type-title .el-tag--dark) {
background-color: #10b981 !important;
border-color: #10b981 !important;
}
.type-section-card-offset_prepaid :deep(.type-title .el-tag--dark) {
background-color: #8b5cf6 !important;
border-color: #8b5cf6 !important;
}
.type-section-card-trade_union :deep(.type-title .el-tag--dark) {
background-color: #0d9488 !important;
border-color: #0d9488 !important;
}
.type-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.type-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.type-count {
color: #6b7280;
font-size: 12px;
}
.budget-level-1 {
margin-bottom: 15px;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
background: #fafafa;
}
.budget-level-1-header {
padding: 15px 20px;
background: linear-gradient(135deg, #409fff 0%, #93c5fd 100%);
color: white;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.3s;
}
.budget-level-1-header:hover {
opacity: 0.9;
}
.budget-level-1-header.collapsed {
background: #f3f4f6;
color: #333;
}
/* 上一年结转资金明细项表头 - 靛蓝渐变风格(调淡) */
.budget-level-1-header-last_year_carryover {
background: linear-gradient(135deg, #818cf8 0%, #c7d2fe 100%);
color: #312e81;
}
.budget-level-1-header-last_year_carryover.collapsed {
background: #eef2ff;
color: #4338ca;
}
/* 自有账户明细项表头 - 绿色渐变风格(调淡) */
.budget-level-1-header-project {
background: linear-gradient(135deg, #34d399 0%, #a7f3d0 100%);
color: #064e3b;
}
.budget-level-1-header-project.collapsed {
background: #f0fdf4;
color: #059669;
}
/* 抵消预付账款明细项表头 - 紫色渐变风格(调淡) */
.budget-level-1-header-offset_prepaid {
background: linear-gradient(135deg, #8b5cf6 0%, #c4b5fd 100%);
color: #4c1d95;
}
.budget-level-1-header-offset_prepaid.collapsed {
background: #f3e8ff;
color: #7c3aed;
}
.budget-level-1-title {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
font-size: 16px;
}
.budget-level-1-info {
display: flex;
gap: 30px;
align-items: center;
}
.budget-info-item {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.level-1-detail-button {
margin-left: 12px;
font-weight: 600;
}
.budget-info-label {
font-size: 12px;
opacity: 0.8;
margin-bottom: 3px;
}
.budget-info-value {
font-size: 18px;
font-weight: 700;
}
.budget-level-1-content {
padding: 0;
}
.budget-level-2 {
padding: 15px 20px;
border-bottom: 1px solid #e5e7eb;
background: white;
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr 1fr 150px;
gap: 15px;
align-items: center;
transition: background-color 0.3s;
}
.budget-level-2:hover {
background-color: #f8f9fa;
}
.budget-level-2:last-child {
border-bottom: none;
}
.budget-level-2-name {
font-weight: 500;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
.budget-amount {
text-align: right;
font-weight: 600;
color: #333;
}
.budget-used {
text-align: right;
color: #666;
}
.budget-remaining {
text-align: right;
font-weight: 600;
}
.budget-remaining.positive {
color: #10b981;
}
.budget-progress {
display: flex;
align-items: center;
gap: 10px;
}
.progress-text {
font-size: 12px;
color: #666;
min-width: 45px;
text-align: right;
}
@media (max-width: 1200px) {
.budget-level-2 {
grid-template-columns: 2fr 1fr 1fr 1fr;
gap: 10px;
}
.budget-progress {
grid-column: 1 / -1;
margin-top: 10px;
}
}
/* 执行明细抽屉样式 */
.drawer-header {
display: flex;
flex-direction: column;
gap: 15px;
}
.drawer-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.drawer-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.drawer-summary {
display: flex;
gap: 30px;
padding: 15px;
background: #f5f7fa;
border-radius: 6px;
}
.drawer-summary .summary-item {
display: flex;
align-items: center;
gap: 8px;
}
.drawer-summary .label {
font-size: 14px;
color: #666;
}
.drawer-summary .value {
font-size: 18px;
font-weight: 700;
}
.drawer-summary .value.executed {
color: #10b981;
}
.drawer-summary .value.pending {
color: #409eff;
}
.execution-tabs {
margin-top: 20px;
}
.detail-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.empty-state {
padding: 40px 0;
text-align: center;
}
.detail-item {
padding: 20px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
transition: all 0.3s;
}
.detail-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 15px;
border-bottom: 1px solid #e5e7eb;
margin-bottom: 15px;
}
.detail-header .serial-number {
font-size: 16px;
font-weight: 600;
color: #333;
}
.detail-header .header-right {
display: flex;
align-items: center;
gap: 15px;
}
.detail-header .amount {
font-size: 18px;
font-weight: 700;
}
.detail-header .amount.executed {
color: #10b981;
}
.detail-header .amount.pending {
color: #409eff;
}
.detail-info {
display: flex;
flex-direction: column;
gap: 10px;
}
.info-row {
display: flex;
font-size: 14px;
line-height: 1.6;
}
.info-label {
color: #666;
min-width: 80px;
flex-shrink: 0;
}
.info-value {
color: #333;
flex: 1;
}
/* A4格式详情弹窗样式 */
:deep(.payment-detail-dialog) {
.el-dialog__body {
padding: 0;
background: #f5f5f5;
}
}
.a4-container {
display: flex;
justify-content: center;
padding: 20px;
}
.a4-page {
width: 210mm;
min-height: 297mm;
background: white;
padding: 20mm;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.detail-header-section {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #333;
}
.main-title {
font-size: 24px;
font-weight: 700;
color: #333;
margin: 0 0 20px 0;
text-align: center;
}
.detail-basic-info {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.detail-basic-info .info-row {
display: flex;
align-items: center;
font-size: 14px;
}
.detail-basic-info .label {
font-weight: 600;
color: #666;
min-width: 100px;
}
.detail-basic-info .value {
color: #333;
}
.detail-basic-info .value.amount {
font-size: 18px;
font-weight: 700;
color: #10b981;
}
.info-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e5e7eb;
border-radius: 6px;
background: #fafafa;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 20px 0;
padding-bottom: 10px;
border-bottom: 1px solid #e5e7eb;
}
.subsection-title {
font-size: 16px;
font-weight: 600;
color: #666;
margin: 20px 0 15px 0;
}
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.info-item {
display: flex;
align-items: flex-start;
font-size: 14px;
line-height: 1.6;
}
.info-item.full-width {
grid-column: 1 / -1;
}
.info-item .label {
font-weight: 600;
color: #666;
min-width: 120px;
flex-shrink: 0;
}
.info-item .value {
color: #333;
word-break: break-word;
}
.custom-fields {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
}
/* 打印样式 */
@media print {
.a4-container {
padding: 0;
}
.a4-page {
box-shadow: none;
page-break-after: always;
}
:deep(.el-dialog__header),
:deep(.el-dialog__footer) {
display: none;
}
:deep(.el-dialog__body) {
padding: 0;
}
}
</style>