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

549 lines
14 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">
<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>
<th v-for="category in categoryStructure" :key="category.categoryId" :colspan="category.activityList.length" class="category-col" :title="category.categoryName">
{{ category.categoryName }}
</th>
<th rowspan="2" class="fixed-col fixed-right">服务总次数</th>
</tr>
<!-- 第二行活动名称 -->
<tr>
<th v-for="activity in allActivities" :key="activity.activityId" class="activity-col" :title="activity.activityName">
{{ activity.activityName }}
</th>
</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>
<td v-for="activity in allActivities" :key="activity.activityId" class="data-col">
{{ record[`activity_${activity.activityId}`] || 0 }}
</td>
<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" style="font-weight: bold; color: #ff4d4f;">-</td>
<td class="fixed-col fixed-left" style="font-weight: bold; color: #ff4d4f;">合计</td>
<td v-for="activity in allActivities" :key="activity.activityId" class="data-col" style="font-weight: bold; color: #ff4d4f;">
{{ totalRow[`activity_${activity.activityId}`] || 0 }}
</td>
<td class="fixed-col fixed-right" style="font-weight: bold; color: #ff4d4f;">
{{ 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 = {
year: new Date().getFullYear(),
quarter: Math.ceil((new Date().getMonth() + 1) / 3),
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 };
// 初始化每个活动的合计为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;
});
// 分页配置
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(() => {
console.log('律师报表页面 onMounted 被调用');
initYearOptions();
console.log('准备调用 queryData');
queryData();
console.log('onMounted 完成');
});
</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: auto;
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 {
background-color: #f5f5f5;
font-weight: 600;
color: #333;
}
/* 固定列样式 */
.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: bold;
color: #1890ff;
}
.total-count {
color: #52c41a;
font-weight: bold;
}
.total-duration {
color: #faad14;
font-weight: bold;
}
/* 分类列样式 - 第一行 */
.category-col {
color: #fff;
font-weight: 600;
font-size: 12px;
padding: 10px 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 活动列样式 - 第二行 */
.activity-col {
background-color: #f0f5ff;
color: #1890ff;
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: #fff2f0;
}
.total-row td {
border-top: 2px solid #ff4d4f;
}
/* 数据行hover效果 */
.header-table tbody tr:hover {
background-color: #e6f7ff;
}
/* 分页样式 */
.pagination-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
/* 活动列样式 */
.activity-col {
background-color: #f6ffed;
color: #52c41a;
font-weight: 500;
min-width: 80px;
}
/* 数据列样式 */
.data-col {
min-width: 80px;
color: #333;
}
/* 分页样式 */
.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>