律师公益法律服务系统前端
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

3 months ago
<!--
* 律师活动统计报表
*
* @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">
2 months ago
<div class="statistics-title">律师公益活动统计报表</div>
3 months ago
<!-- 自定义表头 -->
<div class="custom-table-header">
<table class="header-table" border="1" cellspacing="0" cellpadding="0">
<thead>
2 months ago
<!-- 第一行序号 + 律师名称 + 活动分类含合计 + 服务总次数 -->
3 months ago
<tr>
<th rowspan="2" class="fixed-col index-col">序号</th>
<th rowspan="2" class="fixed-col fixed-left">律师名称</th>
2 months ago
<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>
3 months ago
<th rowspan="2" class="fixed-col fixed-right">服务总次数</th>
</tr>
2 months ago
<!-- 第二行仅活动名称合计已在上行 -->
3 months ago
<tr>
2 months ago
<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>
3 months ago
</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>
2 months ago
<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>
3 months ago
<td class="fixed-col fixed-right total-count">{{ record.totalCount || 0 }}</td>
</tr>
<!-- 合计行 -->
<tr class="total-row" v-if="tableData.length > 0">
2 months ago
<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">
3 months ago
{{ 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 }
]);
2 months ago
const queryFormState = {
firmId: undefined,
3 months ago
year: new Date().getFullYear(),
2 months ago
quarter: null,
3 months ago
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;
});
2 months ago
// 需要小类合计的分类(仅多于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;
}
3 months ago
// 分页配置
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;
2 months ago
table-layout: fixed;
3 months ago
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;
2 months ago
background-color: #fafafa;
}
/* 分类列(第一行)- 简洁风格 */
.header-table thead tr:first-child th.category-col {
background-color: #f0f0f0 !important;
color: #333 !important;
3 months ago
}
/* 固定列样式 */
.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 {
2 months ago
font-weight: 600;
color: #333;
3 months ago
}
.total-count {
2 months ago
font-weight: 700;
color: #333;
3 months ago
}
.total-duration {
2 months ago
font-weight: 600;
color: #666;
3 months ago
}
2 months ago
/* 分类列样式 - 第一行(简洁灰调) */
3 months ago
.category-col {
2 months ago
color: #333;
background-color: #f5f5f5;
3 months ago
font-weight: 600;
font-size: 12px;
2 months ago
padding: 10px 8px;
3 months ago
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
2 months ago
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;
3 months ago
}
2 months ago
/* 合计列样式 - 第二行备用(简洁风格) */
.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;
}
/* 活动列样式 - 第二行(简洁风格) */
3 months ago
.activity-col {
2 months ago
background-color: #fafafa;
color: #666;
3 months ago
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;
}
2 months ago
/* 合计行样式(简洁风格) */
3 months ago
.total-row {
2 months ago
background-color: #f5f5f5;
3 months ago
}
.total-row td {
2 months ago
border-top: 2px solid #bbb;
font-weight: 600;
3 months ago
}
2 months ago
/* 数据行hover效果(简洁) */
3 months ago
.header-table tbody tr:hover {
2 months ago
background-color: #f9f9f9;
3 months ago
}
2 months ago
/* 合计数据列样式(简洁) */
.subtotal-data-col {
background-color: #f5f5f5;
3 months ago
color: #333;
2 months ago
font-weight: 600;
3 months ago
}
/* 分页样式 */
.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>