文件
PayrollManagementSystem/frontend/src/views/reports/department-summary/index.vue
T
liuwenxin和Claude 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

297 行
9.6 KiB
Vue
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { listPersonnel } from '@/api/personnel'
import * as reportApi from '@/api/reports'
import type { DepartmentSummary } from '@/api/reports'
import { useDropdownOptions } from '@/composables/useDropdownOptions'
import { handledError } from '@/utils/error'
/**
* 部门绩效工资汇总表(FR-030):按经理团队/月份汇总。
*
* 部门口径 = personnel.manager_id 实时上溯的经理团队(无独立部门表);
* 团队基数与月结聚合行同源(manager_share 共享模块),因此可与月结
* 冻结额(frozen_amount)对账。全公司 5% 部分单列——各团队行展示的
* 5% 金额是同一基数的名义值,不能相加。
*/
const router = useRouter()
const filters = reactive({
settlement_month: currentMonth(),
manager_id: undefined as number | undefined,
})
const { options: managerOptions, load: loadManagers } = useDropdownOptions(() =>
listPersonnel({ pageNum: 1, pageSize: 200, status: 1 }).then((data) =>
data.list.map((item) => ({ label: item.personnel_name, value: item.id })),
),
)
void loadManagers()
const data = ref<DepartmentSummary | null>(null)
const loading = ref(false)
const exporting = ref(false)
function currentMonth(): string {
const now = new Date()
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
}
async function load(): Promise<void> {
if (!filters.settlement_month) {
ElMessage.warning('请选择统计月份(部门汇总按单月聚合)')
return
}
loading.value = true
try {
data.value = await reportApi.getDepartmentSummary({
settlement_month: filters.settlement_month,
manager_id: filters.manager_id,
})
} catch (error) {
handledError(error)
} finally {
loading.value = false
}
}
void load()
async function doExport(): Promise<void> {
if (!data.value) {
ElMessage.warning('请先查询')
return
}
exporting.value = true
try {
const blob = await reportApi.exportReport('department-summary', {
settlement_month: filters.settlement_month,
manager_id: filters.manager_id,
})
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> = { settlement_month: filters.settlement_month }
if (filters.manager_id) query.manager_id = String(filters.manager_id)
const route = router.resolve({
name: 'report-print',
params: { code: 'department-summary' },
query,
})
window.open(route.href, '_blank')
}
function openPersonalDetail(member: { personnel_id: number }): void {
void router.push({
name: 'reports-personal-detail',
query: {
personnel_id: String(member.personnel_id),
settlement_month: filters.settlement_month,
},
})
}
function today(): string {
const now = new Date()
return `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(
now.getDate(),
).padStart(2, '0')}`
}
function pct(value: string): string {
return `${(Number(value) * 100).toFixed(2)}%`
}
</script>
<template>
<div class="page">
<el-form inline class="filters" @submit.prevent>
<el-form-item label="统计月份" required>
<el-date-picker
v-model="filters.settlement_month"
type="month"
value-format="YYYY-MM"
placeholder="选择月份"
:clearable="false"
style="width: 140px"
@change="load"
/>
</el-form-item>
<el-form-item label="经理">
<el-select
v-model="filters.manager_id"
placeholder="全部经理"
clearable
filterable
style="width: 160px"
@change="load"
>
<el-option
v-for="option in managerOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</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 v-if="data?.params" class="params-hint">
分成参数:团队 {{ data.params.team_text }}% | 全公司 {{ data.params.company_text }}% |
经理个人户{{ data.params.manager_personal_included === 1 ? '计入' : '不计入' }}基数 |
取值时点 {{ data.params.params_as_of }}
</div>
<div v-loading="loading" class="company-card">
<span class="company-title">全公司 5% 部分(单列)</span>
<span>全公司基数:{{ data?.company?.company_base ?? '—' }}</span>
<span>5% 分成:{{ data?.company?.company_share_amount ?? '—' }}</span>
<span class="company-note">
各团队行的 5% 金额基于同一全公司基数,不可跨团队相加
</span>
</div>
<el-table v-loading="loading" :data="data?.teams ?? []" row-key="manager_id" stripe>
<el-table-column type="expand">
<template #default="{ row }">
<div class="member-wrap">
<el-table :data="row.members" size="small" stripe>
<el-table-column prop="personnel_name" label="成员" width="120" />
<el-table-column prop="role_code" label="岗位" width="120" />
<el-table-column prop="performance_sum" label="绩效金额" width="120" align="right" />
<el-table-column prop="payable_sum" label="应付绩效" width="120" align="right" />
<el-table-column label="调整补发" width="110" align="right">
<template #default="{ row: member }">{{ member.adjust_extra }}</template>
</el-table-column>
<el-table-column label="调整扣减" width="110" align="right">
<template #default="{ row: member }">{{ member.adjust_deduct }}</template>
</el-table-column>
<el-table-column prop="net_total" label="净合计" width="120" align="right" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row: member }">
<el-button link type="primary" @click="openPersonalDetail(member)">
查看明细
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="row.members.length === 0" description="该经理本月无团队成员业绩" />
</div>
</template>
</el-table-column>
<el-table-column prop="manager_name" label="经理团队" width="130">
<template #default="{ row }">
{{ row.manager_name }}
<el-tag v-if="row.manager_status === 0" size="small" type="info" disable-transitions>
离职
</el-tag>
</template>
</el-table-column>
<el-table-column prop="member_count" label="团队人数" width="90" align="right" />
<el-table-column prop="team_base" label="团队基数" width="120" align="right" />
<el-table-column prop="team_share_amount" label="12.5% 分成" width="120" align="right" />
<el-table-column prop="company_share_amount" label="5% 分成" width="110" align="right" />
<el-table-column label="占全公司比" width="110" align="right">
<template #default="{ row }">{{ pct(row.company_base_ratio) }}</template>
</el-table-column>
<el-table-column prop="adjust_net" label="调整净额" width="110" align="right" />
<el-table-column prop="net_total" label="净合计" width="120" align="right" />
<el-table-column label="月结冻结额" width="130" align="right">
<template #default="{ row }">
<span v-if="row.frozen_amount !== null" class="frozen">{{ row.frozen_amount }}</span>
<span v-else class="frozen-none">未生成月结</span>
</template>
</el-table-column>
</el-table>
<div v-if="data && data.unassigned.length" class="unassigned">
<h4>未纳入团队基数(无经理 / 非营销经理上级的下属)</h4>
<el-table :data="data.unassigned" size="small" stripe>
<el-table-column prop="personnel_name" label="人员" width="130" />
<el-table-column prop="role_code" label="岗位" width="120" />
<el-table-column prop="performance_sum" label="绩效金额" width="130" align="right" />
<el-table-column prop="net_total" label="净合计" width="130" align="right" />
</el-table>
</div>
</div>
</template>
<style scoped>
.filters {
margin-bottom: 4px;
}
.params-hint {
margin-bottom: 10px;
padding: 8px 12px;
font-size: 12px;
color: var(--ys-ink-2);
background: var(--ys-mist);
border-radius: 6px;
}
.company-card {
display: flex;
align-items: center;
gap: 24px;
margin-bottom: 12px;
padding: 12px 16px;
background: var(--ys-indigo-wash);
border-radius: 8px;
font-size: 13px;
}
.company-title {
font-weight: 600;
}
.company-note {
color: var(--ys-ink-3);
font-size: 12px;
}
.member-wrap {
padding: 8px 16px;
}
.frozen {
color: var(--ys-ink-2);
}
.frozen-none {
color: var(--ys-ink-3);
font-size: 12px;
}
.unassigned {
margin-top: 20px;
}
.unassigned h4 {
margin: 0 0 8px;
font-size: 14px;
font-weight: 600;
}
</style>