- 个人绩效明细/部门绩效汇总/业务统计/回款统计四页,路由组「报表统计」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>
297 lines
9.6 KiB
Vue
297 lines
9.6 KiB
Vue
<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>
|