Files
PayrollManagementSystem/frontend/src/views/reports/payment-stats/index.vue
T
liuwenxinandClaude Code 04a1a0d307 feat(report): 报表四页与打印视图
- 个人绩效明细/部门绩效汇总/业务统计/回款统计四页,路由组「报表统计」order19-22(allowedRoles admin/finance/leader)
- 明细分页页脚全筛选合计;部门汇总成员内联与跨页下钻;业务统计展开回款/台账;回款统计 8 档区间(档位由后端下发)与桶下钻
- 打印视图独立顶层路由(不挂 BasicLayout):公司页眉走 public 端点、循环翻页取全量(2000 行截断提示)、@media print A4 横版每页表头、签字栏
- AppPagination 只派发 page-change/size-change,四页按事件绑定并补初始加载(@change/v-model 不生效的同类缺陷)
- e2e report_flow.py:四页筛选与导出下载、打印视图 window.print 桩断言

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-19 16:33:37 +08:00

311 lines
9.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { CUSTOMER_TYPE_OPTIONS, customerTypeLabel } from '@/api/customers'
import * as reportApi from '@/api/reports'
import type { BucketRow, PaymentDetailRow, PaymentStats } from '@/api/reports'
import AppPagination from '@/components/table/AppPagination.vue'
import { handledError } from '@/utils/error'
/**
* 回款情况统计表(FR-031 之二):按回款天数区间/客户类型/产品类别统计。
*
* 区间档位由后端下发(禁止前端硬编码);空桶恒在且显示 0。回款金额侧
* 按回款单去重(二次分摊拆行不会让金额翻倍)。进度登记回款无台账行,
* 不在本表口径内(页面已注明)。
*/
const router = useRouter()
const filters = reactive({
settlement_month: '',
customer_type: '',
product_category: '',
})
const dateRange = ref<[string, string] | null>(null)
const data = ref<PaymentStats | null>(null)
const loading = ref(false)
const exporting = ref(false)
function getFilters() {
return {
settlement_month: filters.settlement_month || undefined,
customer_type: filters.customer_type || undefined,
product_category: filters.product_category || undefined,
date_from: dateRange.value?.[0],
date_to: dateRange.value?.[1],
}
}
async function load(): Promise<void> {
loading.value = true
try {
data.value = await reportApi.getPaymentStats(getFilters())
} catch (error) {
handledError(error)
} finally {
loading.value = false
}
}
void load()
/** 桶展开下钻:分页明细按 (桶编码) 缓存当前页 */
const drill = reactive({
bucket: '',
rows: [] as PaymentDetailRow[],
total: 0,
pageNum: 1,
pageSize: 10,
loading: false,
})
async function loadDrill(bucket: BucketRow, expanded: boolean): Promise<void> {
if (!expanded) return
drill.bucket = bucket.code
drill.pageNum = 1
await fetchDrill()
}
async function fetchDrill(): Promise<void> {
if (!drill.bucket) return
drill.loading = true
try {
const page = await reportApi.listPaymentDetail({
...getFilters(),
bucket: drill.bucket,
pageNum: drill.pageNum,
pageSize: drill.pageSize,
})
drill.rows = page.list
drill.total = page.total
} catch (error) {
handledError(error)
} finally {
drill.loading = false
}
}
function onDrillPage(page: number): void {
drill.pageNum = page
void fetchDrill()
}
function onDrillSize(size: number): void {
drill.pageSize = size
drill.pageNum = 1 // 改每页条数回到第 1 页,否则可能停在空页
void fetchDrill()
}
async function doExport(): Promise<void> {
exporting.value = true
try {
const blob = await reportApi.exportReport('payment-stats', getFilters())
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `回款情况统计表_${today()}.xlsx`
link.click()
URL.revokeObjectURL(url)
ElMessage.success('已导出')
} catch (error) {
handledError(error)
} finally {
exporting.value = false
}
}
function doPrint(): void {
const query: Record<string, string> = {}
for (const [key, value] of Object.entries(getFilters())) {
if (value !== undefined && value !== null && value !== '') query[key] = String(value)
}
const route = router.resolve({
name: 'report-print',
params: { code: 'payment-stats' },
query,
})
window.open(route.href, '_blank')
}
function today(): string {
const now = new Date()
return `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(
now.getDate(),
).padStart(2, '0')}`
}
</script>
<template>
<div class="page">
<el-form inline class="filters" @submit.prevent>
<el-form-item label="月份">
<el-date-picker
v-model="filters.settlement_month"
type="month"
value-format="YYYY-MM"
placeholder="全部月份"
clearable
style="width: 140px"
/>
</el-form-item>
<el-form-item label="回款日期">
<el-date-picker
v-model="dateRange"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="至"
start-placeholder="开始"
end-placeholder="结束"
style="width: 240px"
/>
</el-form-item>
<el-form-item label="客户类型">
<el-select v-model="filters.customer_type" placeholder="全部" clearable style="width: 130px">
<el-option
v-for="option in CUSTOMER_TYPE_OPTIONS"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</el-form-item>
<el-form-item label="产品类别">
<el-select
v-model="filters.product_category"
placeholder="全部"
clearable
style="width: 130px"
>
<el-option label="市政水利" value="municipal_water" />
<el-option label="轨道交通" value="rail_transit" />
<el-option label="混合业务" value="mixed" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="load">查询</el-button>
<el-button :loading="exporting" @click="doExport">导出 Excel</el-button>
<el-button @click="doPrint">打印</el-button>
</el-form-item>
</el-form>
<div class="hint">
统计口径:已计提回款(有台账行)的金额与绩效;进度登记回款不计入。空区间显示 0。
</div>
<el-table
v-loading="loading"
:data="data?.buckets ?? []"
row-key="code"
stripe
@expand-change="loadDrill"
>
<el-table-column type="expand">
<template #default>
<div class="drill-wrap">
<el-table v-loading="drill.loading" :data="drill.rows" size="small" stripe>
<el-table-column prop="business_no" label="合同编号" min-width="150" />
<el-table-column prop="receipt_batch_no" label="回款批次号" width="160" />
<el-table-column prop="receipt_date" label="回款日" width="110" />
<el-table-column prop="amount" label="回款金额" width="120" align="right" />
<el-table-column prop="personnel_name" label="归属人员" width="110" />
<el-table-column prop="payment_days" label="天数" width="80" align="right" />
<el-table-column label="客户类型" width="100">
<template #default="{ row }">
{{ customerTypeLabel(row.customer_type) }}
</template>
</el-table-column>
<el-table-column label="产品类别" width="100">
<template #default="{ row }">
{{ reportApi.reportCategoryLabel(row.product_category) }}
</template>
</el-table-column>
<el-table-column prop="performance_amount" label="绩效金额" width="120" align="right" />
</el-table>
<AppPagination
:total="drill.total"
:page-num="drill.pageNum"
:page-size="drill.pageSize"
@page-change="onDrillPage"
@size-change="onDrillSize"
/>
</div>
</template>
</el-table-column>
<el-table-column prop="label" label="回款天数区间" min-width="150" />
<el-table-column prop="receipt_amount" label="回款金额" width="140" align="right" />
<el-table-column prop="receipt_count" label="回款笔数" width="100" align="right" />
<el-table-column prop="performance_amount" label="绩效金额" width="130" align="right" />
<el-table-column prop="payable_amount" label="应付绩效" width="130" align="right" />
<el-table-column prop="ledger_count" label="台账行数" width="100" align="right" />
</el-table>
<div v-if="data" class="summary-bar">
合计:回款 {{ data.totals.receipt_amount ?? '0.00' }} | 笔数
{{ data.totals.receipt_count ?? 0 }} | 绩效 {{ data.totals.performance_amount ?? '0.00' }}
| 应付 {{ data.totals.payable_amount ?? '0.00' }}
</div>
<div v-if="data" class="dimension-grid">
<div>
<h4>客户类型对比</h4>
<el-table :data="data.by_customer_type" size="small" stripe>
<el-table-column prop="label" label="客户类型" width="120" />
<el-table-column prop="receipt_amount" label="回款金额" width="130" align="right" />
<el-table-column prop="performance_amount" label="绩效金额" width="130" align="right" />
<el-table-column prop="ledger_count" label="台账行数" width="100" align="right" />
</el-table>
</div>
<div>
<h4>产品类别对比</h4>
<el-table :data="data.by_product_category" size="small" stripe>
<el-table-column prop="label" label="产品类别" width="120" />
<el-table-column prop="receipt_amount" label="回款金额" width="130" align="right" />
<el-table-column prop="performance_amount" label="绩效金额" width="130" align="right" />
<el-table-column prop="ledger_count" label="台账行数" width="100" align="right" />
</el-table>
</div>
</div>
</div>
</template>
<style scoped>
.filters {
margin-bottom: 4px;
}
.hint {
margin-bottom: 8px;
font-size: 12px;
color: var(--ys-ink-3);
}
.drill-wrap {
padding: 8px 16px;
}
.summary-bar {
margin-top: 10px;
padding: 8px 12px;
font-size: 13px;
background: var(--ys-indigo-wash);
border-radius: 6px;
}
.dimension-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
gap: 20px;
margin-top: 20px;
}
.dimension-grid h4 {
margin: 0 0 8px;
font-size: 14px;
font-weight: 600;
}
</style>