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.
614 lines
17 KiB
614 lines
17 KiB
<!--
|
|
* 律师活动统计报表
|
|
*
|
|
* @Author: wzh
|
|
* @Date: 2026-02-10
|
|
* @Copyright 1.0
|
|
-->
|
|
<template>
|
|
<div class="lawyer-activity-statistics">
|
|
<!---------- 查询表单form begin ----------->
|
|
<a-form class="smart-query-form">
|
|
<a-row class="smart-query-form-row">
|
|
<a-form-item label="统计年份" class="smart-query-form-item">
|
|
<a-select v-model:value="queryForm.year" placeholder="请选择年份" style="width: 120px">
|
|
<a-select-option v-for="year in yearOptions" :key="year" :value="year">
|
|
{{ year }}年
|
|
</a-select-option>
|
|
</a-select>
|
|
</a-form-item>
|
|
<a-form-item label="统计季度" class="smart-query-form-item">
|
|
<a-select v-model:value="queryForm.quarter" placeholder="请选择季度" style="width: 120px">
|
|
<a-select-option v-for="quarter in quarterOptions" :key="quarter.value" :value="quarter.value">
|
|
{{ quarter.label }}
|
|
</a-select-option>
|
|
</a-select>
|
|
</a-form-item>
|
|
<a-form-item class="smart-query-form-item">
|
|
<a-button type="primary" @click="onSearch">
|
|
<template #icon>
|
|
<SearchOutlined />
|
|
</template>
|
|
查询
|
|
</a-button>
|
|
<a-button @click="resetQuery" class="smart-margin-left10">
|
|
<template #icon>
|
|
<ReloadOutlined />
|
|
</template>
|
|
重置
|
|
</a-button>
|
|
</a-form-item>
|
|
<div style="margin-left: auto;">
|
|
<a-button @click="handleExport" type="primary">
|
|
<template #icon>
|
|
<ExportOutlined />
|
|
</template>
|
|
导出
|
|
</a-button>
|
|
</div>
|
|
</a-row>
|
|
</a-form>
|
|
<!---------- 查询表单form end ----------->
|
|
|
|
<!---------- 统计表格 begin ----------->
|
|
<a-card size="small" :bordered="false" :hoverable="true">
|
|
<div class="statistics-title">律师公益活动统计报表</div>
|
|
|
|
<!-- 自定义表头 -->
|
|
<div class="custom-table-header">
|
|
<table class="header-table" border="1" cellspacing="0" cellpadding="0">
|
|
<thead>
|
|
<!-- 第一行:序号 + 律师名称 + 活动分类(含合计) + 服务总次数 -->
|
|
<tr>
|
|
<th rowspan="2" class="fixed-col index-col">序号</th>
|
|
<th rowspan="2" class="fixed-col fixed-left">律师名称</th>
|
|
<template v-for="category in categoryStructure" :key="category.categoryId">
|
|
<!-- 如果有多个活动,先添加合计列(跨两行) -->
|
|
<th v-if="category.activityList.length > 1" rowspan="2" class="subtotal-header-col-fixed" :title="category.categoryName + '合计'">合计</th>
|
|
<!-- 然后是分类名称(只跨越活动列) -->
|
|
<th :colspan="category.activityList.length" class="category-col" :title="category.categoryName">
|
|
{{ category.categoryName }}
|
|
</th>
|
|
</template>
|
|
<th rowspan="2" class="fixed-col fixed-right">服务总次数</th>
|
|
</tr>
|
|
<!-- 第二行:仅活动名称(合计已在上行) -->
|
|
<tr>
|
|
<template v-for="category in categoryStructure" :key="'header_' + category.categoryId">
|
|
<th v-for="activity in category.activityList" :key="activity.activityId" class="activity-col" :title="activity.activityName">
|
|
{{ activity.activityName }}
|
|
</th>
|
|
</template>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(record, index) in tableData" :key="record.lawyerId">
|
|
<td class="fixed-col index-col">{{ index + 1 }}</td>
|
|
<td class="fixed-col fixed-left lawyer-name">{{ record.lawyerName }}</td>
|
|
<template v-for="category in categoryStructure" :key="'data_' + category.categoryId">
|
|
<td v-if="category.activityList.length > 1" class="data-col subtotal-data-col">
|
|
{{ getCategoryTotalForRecord(record, category) }}
|
|
</td>
|
|
<td v-for="activity in category.activityList" :key="activity.activityId" class="data-col">
|
|
{{ record[`activity_${activity.activityId}`] || 0 }}
|
|
</td>
|
|
</template>
|
|
<td class="fixed-col fixed-right total-count">{{ record.totalCount || 0 }}</td>
|
|
</tr>
|
|
<!-- 合计行 -->
|
|
<tr class="total-row" v-if="tableData.length > 0">
|
|
<td class="fixed-col index-col">-</td>
|
|
<td class="fixed-col fixed-left">合计</td>
|
|
<template v-for="category in categoryStructure" :key="'total_' + category.categoryId">
|
|
<td v-if="category.activityList.length > 1" class="data-col">
|
|
{{ categorySubtotals[`category_${category.categoryId}`] || 0 }}
|
|
</td>
|
|
<td v-for="activity in category.activityList" :key="activity.activityId" class="data-col">
|
|
{{ totalRow[`activity_${activity.activityId}`] || 0 }}
|
|
</td>
|
|
</template>
|
|
<td class="fixed-col fixed-right">
|
|
{{ totalRow.totalCount || 0 }}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- 分页 -->
|
|
<div class="pagination-wrapper">
|
|
<a-pagination
|
|
v-model:current="pagination.current"
|
|
v-model:pageSize="pagination.pageSize"
|
|
:total="pagination.total"
|
|
:pageSizeOptions="pagination.pageSizeOptions"
|
|
showSizeChanger
|
|
showQuickJumper
|
|
showTotal
|
|
@change="handleTableChange"
|
|
/>
|
|
</div>
|
|
</a-card>
|
|
<!---------- 统计表格 end ----------->
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { reactive, ref, onMounted, computed } from 'vue';
|
|
import { message } from 'ant-design-vue';
|
|
import { SearchOutlined, ReloadOutlined, ExportOutlined } from '@ant-design/icons-vue';
|
|
import { smartSentry } from '/@/lib/smart-sentry';
|
|
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
|
|
import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api';
|
|
import { categoryApi } from '/@/api/business/category/category-api';
|
|
|
|
// ---------------------------- 查询表单 ----------------------------
|
|
|
|
const yearOptions = ref([]);
|
|
const quarterOptions = ref([
|
|
{ label: '第一季度', value: 1 },
|
|
{ label: '第二季度', value: 2 },
|
|
{ label: '第三季度', value: 3 },
|
|
{ label: '第四季度', value: 4 }
|
|
]);
|
|
|
|
const queryFormState = {
|
|
firmId: undefined,
|
|
year: new Date().getFullYear(),
|
|
quarter: null,
|
|
pageNum: 1,
|
|
pageSize: 10
|
|
};
|
|
|
|
const queryForm = reactive({ ...queryFormState });
|
|
const tableLoading = ref(false);
|
|
const tableData = ref([]);
|
|
const total = ref(0);
|
|
|
|
// 活动分类和活动名称数据结构
|
|
const categoryStructure = ref([]);
|
|
|
|
// 所有活动列表(扁平化)
|
|
const allActivities = computed(() => {
|
|
const activities = [];
|
|
categoryStructure.value.forEach(category => {
|
|
if (category.activityList) {
|
|
activities.push(...category.activityList);
|
|
}
|
|
});
|
|
return activities;
|
|
});
|
|
|
|
// 计算合计行
|
|
const totalRow = computed(() => {
|
|
const row = { totalCount: 0 };
|
|
|
|
allActivities.value.forEach(activity => {
|
|
row[`activity_${activity.activityId}`] = 0;
|
|
});
|
|
|
|
tableData.value.forEach(record => {
|
|
row.totalCount += record.totalCount || 0;
|
|
allActivities.value.forEach(activity => {
|
|
const key = `activity_${activity.activityId}`;
|
|
row[key] += record[key] || 0;
|
|
});
|
|
});
|
|
|
|
return row;
|
|
});
|
|
|
|
// 需要小类合计的分类(仅多于1个小类的分类才需要合计)
|
|
const categoryStructureWithSubtotal = computed(() => {
|
|
return categoryStructure.value.map(category => ({
|
|
...category,
|
|
activityIds: (category.activityList || []).map(a => a.activityId),
|
|
needSubtotal: (category.activityList || []).length > 1
|
|
}));
|
|
});
|
|
|
|
// 计算每个大类的合计
|
|
const categorySubtotals = computed(() => {
|
|
const subtotals = {};
|
|
categoryStructureWithSubtotal.value.forEach(category => {
|
|
let total = 0;
|
|
(category.activityList || []).forEach(activity => {
|
|
tableData.value.forEach(record => {
|
|
total += record[`activity_${activity.activityId}`] || 0;
|
|
});
|
|
});
|
|
subtotals[`category_${category.categoryId}`] = total;
|
|
});
|
|
return subtotals;
|
|
});
|
|
|
|
// 计算指定记录在指定大类的总和
|
|
function getCategoryTotalForRecord(record, category) {
|
|
let total = 0;
|
|
if (category.activityList && category.activityList.length > 1) {
|
|
category.activityList.forEach(activity => {
|
|
total += record[`activity_${activity.activityId}`] || 0;
|
|
});
|
|
}
|
|
return total;
|
|
}
|
|
|
|
// 分页配置
|
|
const pagination = reactive({
|
|
current: 1,
|
|
pageSize: 10,
|
|
total: 0,
|
|
showSizeChanger: true,
|
|
showQuickJumper: true,
|
|
showTotal: (total) => `共${total}条`,
|
|
pageSizeOptions: PAGE_SIZE_OPTIONS
|
|
});
|
|
|
|
// 初始化年份选项
|
|
function initYearOptions() {
|
|
const currentYear = new Date().getFullYear();
|
|
for (let i = currentYear - 5; i <= currentYear; i++) {
|
|
yearOptions.value.push(i);
|
|
}
|
|
}
|
|
|
|
// ---------------------------- 查询方法 ----------------------------
|
|
|
|
function resetQuery() {
|
|
Object.assign(queryForm, queryFormState);
|
|
pagination.current = 1;
|
|
pagination.pageSize = 10;
|
|
onSearch();
|
|
}
|
|
|
|
function onSearch() {
|
|
pagination.current = 1;
|
|
queryData();
|
|
}
|
|
|
|
function handleTableChange(page, pageSize) {
|
|
pagination.current = page;
|
|
pagination.pageSize = pageSize;
|
|
queryForm.pageNum = page;
|
|
queryForm.pageSize = pageSize;
|
|
queryData();
|
|
}
|
|
|
|
async function queryData() {
|
|
console.log('queryData 被调用');
|
|
tableLoading.value = true;
|
|
try {
|
|
// 先获取活动分类结构
|
|
console.log('准备调用 loadCategoryStructure');
|
|
await loadCategoryStructure();
|
|
console.log('loadCategoryStructure 完成');
|
|
|
|
// 查询统计数据
|
|
const params = {
|
|
year: queryForm.year,
|
|
quarter: queryForm.quarter,
|
|
statisticsType: 'lawyer',
|
|
pageNum: queryForm.pageNum,
|
|
pageSize: queryForm.pageSize
|
|
};
|
|
|
|
const result = await serviceApplicationsApi.statisticsByActivity(params);
|
|
|
|
// 处理数据,适配表格结构
|
|
// 新接口返回分页格式 {pageNum, pageSize, total, pages, list}
|
|
const pageData = result.data || {};
|
|
tableData.value = processStatisticsData(pageData.list || []);
|
|
total.value = pageData.total || 0;
|
|
pagination.total = total.value;
|
|
|
|
} catch (error) {
|
|
smartSentry.captureError(error);
|
|
message.error('查询统计数据失败');
|
|
} finally {
|
|
tableLoading.value = false;
|
|
}
|
|
}
|
|
|
|
// 加载活动分类结构
|
|
async function loadCategoryStructure() {
|
|
console.log('开始加载活动分类结构...');
|
|
try {
|
|
// 使用新接口 /category/tree/child 获取活动分类和活动名称(无参数)
|
|
console.log('调用 categoryApi.queryCategoryTreeChild()');
|
|
const result = await categoryApi.queryCategoryTreeChild();
|
|
console.log('活动分类接口返回:', result);
|
|
|
|
// 接口返回的数据格式:
|
|
// [
|
|
// {
|
|
// categoryId: 1,
|
|
// categoryName: '活动分类A',
|
|
// childrenGood: [
|
|
// { goodsId: 1, goodsName: '活动a' },
|
|
// { goodsId: 2, goodsName: '活动b' }
|
|
// ]
|
|
// }
|
|
// ]
|
|
const categories = result.data || [];
|
|
|
|
// 转换为报表需要的格式
|
|
categoryStructure.value = categories.map(category => ({
|
|
categoryId: category.categoryId,
|
|
categoryName: category.categoryName,
|
|
activityList: (category.childrenGood || []).map(goods => ({
|
|
activityId: goods.goodsId,
|
|
activityName: goods.goodsName
|
|
}))
|
|
})).filter(cat => cat.activityList.length > 0);
|
|
|
|
} catch (error) {
|
|
console.error('加载活动分类结构失败:', error);
|
|
categoryStructure.value = [];
|
|
}
|
|
}
|
|
|
|
// 处理统计数据,适配表格显示
|
|
function processStatisticsData(data) {
|
|
if (!data || !Array.isArray(data)) return [];
|
|
|
|
return data.map(item => {
|
|
const row = {
|
|
lawyerId: item.userId,
|
|
lawyerName: item.lawyerName,
|
|
totalCount: item.totalCount || 0
|
|
};
|
|
|
|
// 从 activityList 填充每个活动的统计数据
|
|
if (item.activityList && Array.isArray(item.activityList)) {
|
|
item.activityList.forEach(activity => {
|
|
row[`activity_${activity.activityId}`] = activity.count || 0;
|
|
});
|
|
}
|
|
|
|
return row;
|
|
});
|
|
}
|
|
|
|
// 导出功能
|
|
async function handleExport() {
|
|
try {
|
|
const params = {
|
|
year: queryForm.year,
|
|
quarter: queryForm.quarter,
|
|
statisticsType: 'lawyer'
|
|
};
|
|
|
|
await serviceApplicationsApi.exportLawyerActivityStatistics(params);
|
|
message.success('导出成功');
|
|
} catch (error) {
|
|
smartSentry.captureError(error);
|
|
message.error('导出失败');
|
|
}
|
|
}
|
|
|
|
// ---------------------------- 生命周期 ----------------------------
|
|
|
|
onMounted(() => {
|
|
initYearOptions();
|
|
queryData();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.lawyer-activity-statistics {
|
|
padding: 16px;
|
|
}
|
|
|
|
.statistics-title {
|
|
text-align: center;
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
margin-bottom: 16px;
|
|
color: #333;
|
|
}
|
|
|
|
.smart-query-form {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.smart-query-form-row {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.smart-margin-left10 {
|
|
margin-left: 10px;
|
|
}
|
|
|
|
/* 自定义表格样式 */
|
|
.custom-table-header {
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
margin-bottom: 16px;
|
|
border: 1px solid #d9d9d9;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.header-table {
|
|
width: auto;
|
|
min-width:100%;
|
|
border-collapse: collapse;
|
|
table-layout: fixed;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.header-table th,
|
|
.header-table td {
|
|
border: 1px solid #e8e8e8;
|
|
padding: 10px 6px;
|
|
text-align: center;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.header-table thead th {
|
|
font-weight: 600;
|
|
color: #333;
|
|
background-color: #fafafa;
|
|
}
|
|
|
|
/* 分类列(第一行)- 简洁风格 */
|
|
.header-table thead tr:first-child th.category-col {
|
|
background-color: #f0f0f0 !important;
|
|
color: #333 !important;
|
|
}
|
|
|
|
/* 固定列样式 */
|
|
.fixed-col {
|
|
background-color: #fafafa;
|
|
font-weight: 500;
|
|
width: 90px;
|
|
min-width: 90px;
|
|
position: sticky;
|
|
z-index: 2;
|
|
}
|
|
|
|
/* 序号列 - 第一列固定 */
|
|
.index-col {
|
|
left: 0;
|
|
width: 40px !important;
|
|
min-width: 40px !important;
|
|
max-width: 40px !important;
|
|
text-align: center;
|
|
z-index: 3;
|
|
padding: 8px 2px !important;
|
|
}
|
|
|
|
/* 左侧固定列 - 律师名称 */
|
|
.fixed-left {
|
|
left: 40px;
|
|
}
|
|
|
|
/* 右侧固定列 */
|
|
.fixed-right {
|
|
right: 0;
|
|
}
|
|
|
|
/* 表头固定列 */
|
|
.header-table thead th.fixed-col {
|
|
z-index: 3;
|
|
}
|
|
|
|
.lawyer-name {
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.total-count {
|
|
font-weight: 700;
|
|
color: #333;
|
|
}
|
|
|
|
.total-duration {
|
|
font-weight: 600;
|
|
color: #666;
|
|
}
|
|
|
|
/* 分类列样式 - 第一行(简洁灰调) */
|
|
.category-col {
|
|
color: #333;
|
|
background-color: #f5f5f5;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
padding: 10px 8px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
max-width: 200px;
|
|
}
|
|
|
|
/* 合计列样式 - 跨两行(简洁风格) */
|
|
.subtotal-header-col-fixed {
|
|
background-color: #f9f9f9;
|
|
color: #555;
|
|
font-weight: 600;
|
|
font-size: 11px;
|
|
padding: 10px 4px;
|
|
width: 60px;
|
|
min-width: 60px;
|
|
max-width: 60px;
|
|
border-left: 1px solid #d9d9d9;
|
|
border-right: 1px solid #e8e8e8;
|
|
}
|
|
|
|
/* 合计列样式 - 第二行备用(简洁风格) */
|
|
.subtotal-header-col {
|
|
background-color: #f9f9f9;
|
|
color: #555;
|
|
font-weight: 600;
|
|
font-size: 11px;
|
|
padding: 6px 4px;
|
|
width: 70px;
|
|
min-width: 70px;
|
|
max-width: 70px;
|
|
border-left: 1px solid #d9d9d9;
|
|
}
|
|
|
|
/* 活动列样式 - 第二行(简洁风格) */
|
|
.activity-col {
|
|
background-color: #fafafa;
|
|
color: #666;
|
|
font-weight: 500;
|
|
font-size: 10px;
|
|
padding: 6px 2px;
|
|
width: 60px;
|
|
min-width: 60px;
|
|
max-width: 60px;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* 数据列样式 */
|
|
.data-col {
|
|
color: #333;
|
|
font-size: 13px;
|
|
}
|
|
|
|
/* 合计行样式(简洁风格) */
|
|
.total-row {
|
|
background-color: #f5f5f5;
|
|
}
|
|
|
|
.total-row td {
|
|
border-top: 2px solid #bbb;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* 数据行hover效果(简洁) */
|
|
.header-table tbody tr:hover {
|
|
background-color: #f9f9f9;
|
|
}
|
|
|
|
/* 合计数据列样式(简洁) */
|
|
.subtotal-data-col {
|
|
background-color: #f5f5f5;
|
|
color: #333;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* 分页样式 */
|
|
.pagination-wrapper {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 16px;
|
|
padding-top: 16px;
|
|
border-top: 1px solid #f0f0f0;
|
|
}
|
|
|
|
/* 响应式处理 */
|
|
@media (max-width: 1200px) {
|
|
.header-table {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.header-table th,
|
|
.header-table td {
|
|
padding: 6px 8px;
|
|
}
|
|
}
|
|
</style>
|