17 changed files with 2925 additions and 91 deletions
Binary file not shown.
File diff suppressed because it is too large
@ -0,0 +1,656 @@ |
|||||
|
<!-- |
||||
|
* 律所活动统计报表 |
||||
|
* |
||||
|
* @Author: wzh |
||||
|
* @Date: 2026-02-10 |
||||
|
* @Copyright 1.0 |
||||
|
--> |
||||
|
<template> |
||||
|
<div class="law-firm-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" v-if="isCeo"> |
||||
|
<DepartmentTreeSelect style="width: 250px" v-model:value="queryForm.firmId" placeholder="请选择执业机构" /> |
||||
|
</a-form-item> |
||||
|
<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-container"> |
||||
|
<table class="custom-table" border="1" cellspacing="0" cellpadding="0"> |
||||
|
<thead> |
||||
|
<!-- 第一行:序号 + 律所名称 + 活动分类 --> |
||||
|
<tr> |
||||
|
<th rowspan="2" class="fixed-col index-col">序号</th> |
||||
|
<th rowspan="2" class="fixed-col firm-col">律所名称</th> |
||||
|
<th rowspan="2" class="fixed-col lawyer-col">律师名称</th> |
||||
|
<th v-for="category in categoryStructure" :key="category.categoryId" :colspan="category.activityList.length" class="category-col"> |
||||
|
{{ category.categoryName }} |
||||
|
</th> |
||||
|
<th rowspan="2" class="fixed-col fixed-right total-count-col">服务总次数</th> |
||||
|
</tr> |
||||
|
<!-- 第二行:活动名称 --> |
||||
|
<tr> |
||||
|
<th v-for="activity in allActivities" :key="activity.activityId" class="activity-col"> |
||||
|
{{ activity.activityName }} |
||||
|
</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<template v-for="(firm, firmIndex) in tableData" :key="firm.firmId"> |
||||
|
<!-- 律所汇总行 --> |
||||
|
<tr class="firm-row" @click="toggleExpand(firm.firmId)" :class="{ 'expanded': expandedFirms.includes(firm.firmId) }"> |
||||
|
<td class="fixed-col index-col" style="width: 60px; min-width: 60px; max-width: 40px;">{{ firmIndex + 1 }}</td> |
||||
|
<td class="fixed-col firm-name"> |
||||
|
<span class="expand-icon">{{ expandedFirms.includes(firm.firmId) ? '▼' : '▶' }}</span> |
||||
|
{{ firm.firmName }} |
||||
|
</td> |
||||
|
<td class="fixed-col lawyer-col">-</td> |
||||
|
<td v-for="activity in allActivities" :key="activity.activityId" class="data-col"> |
||||
|
{{ firm[`activity_${activity.activityId}`] || 0 }} |
||||
|
</td> |
||||
|
<td class="fixed-col fixed-right total-count">{{ firm.totalCount || 0 }}</td> |
||||
|
</tr> |
||||
|
<!-- 律师明细行 --> |
||||
|
<tr v-for="lawyer in firm.lawyerList" :key="lawyer.lawyerId" |
||||
|
v-show="expandedFirms.includes(firm.firmId)" |
||||
|
class="lawyer-row"> |
||||
|
<td class="fixed-col index-col" style="width: 40px; min-width: 40px; max-width: 40px;"></td> |
||||
|
<td class="fixed-col firm-name"></td> |
||||
|
<td class="fixed-col lawyer-name">{{ lawyer.lawyerName }}</td> |
||||
|
<td v-for="activity in allActivities" :key="activity.activityId" class="data-col"> |
||||
|
{{ lawyer[`activity_${activity.activityId}`] || 0 }} |
||||
|
</td> |
||||
|
<td class="fixed-col fixed-right total-count">{{ lawyer.totalCount || 0 }}</td> |
||||
|
</tr> |
||||
|
</template> |
||||
|
<!-- 合计行 --> |
||||
|
<tr class="total-row" v-if="tableData.length > 0"> |
||||
|
<td class="fixed-col index-col" style="width: 40px; min-width: 40px; max-width: 40px;">-</td> |
||||
|
<td class="fixed-col firm-name" style="font-weight: bold; color: #ff4d4f;">合计</td> |
||||
|
<td class="fixed-col lawyer-col" 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'; |
||||
|
import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue'; |
||||
|
import { useUserStore } from '/@/store/modules/system/user'; |
||||
|
|
||||
|
// ---------------------------- 用户权限 ---------------------------- |
||||
|
|
||||
|
const userStore = useUserStore(); |
||||
|
const isCeo = computed(() => userStore.userRole === 'ceo'); |
||||
|
|
||||
|
// ---------------------------- 查询表单 ---------------------------- |
||||
|
|
||||
|
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: 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([]); |
||||
|
|
||||
|
// 展开的律所ID列表 |
||||
|
const expandedFirms = 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(firm => { |
||||
|
// 累加总次数 |
||||
|
row.totalCount += firm.totalCount || 0; |
||||
|
|
||||
|
// 累加每个活动的次数 |
||||
|
allActivities.value.forEach(activity => { |
||||
|
const key = `activity_${activity.activityId}`; |
||||
|
row[key] += firm[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 toggleExpand(firmId) { |
||||
|
const index = expandedFirms.value.indexOf(firmId); |
||||
|
if (index > -1) { |
||||
|
expandedFirms.value.splice(index, 1); |
||||
|
} else { |
||||
|
expandedFirms.value.push(firmId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ---------------------------- 查询方法 ---------------------------- |
||||
|
|
||||
|
function resetQuery() { |
||||
|
Object.assign(queryForm, queryFormState); |
||||
|
pagination.current = 1; |
||||
|
pagination.pageSize = 10; |
||||
|
onSearch(); |
||||
|
} |
||||
|
|
||||
|
function onSearch() { |
||||
|
pagination.current = 1; |
||||
|
queryData(); |
||||
|
} |
||||
|
|
||||
|
function handleTableChange(pag) { |
||||
|
pagination.current = pag.current; |
||||
|
pagination.pageSize = pag.pageSize; |
||||
|
queryForm.pageNum = pag.current; |
||||
|
queryForm.pageSize = pag.pageSize; |
||||
|
queryData(); |
||||
|
} |
||||
|
|
||||
|
async function queryData() { |
||||
|
tableLoading.value = true; |
||||
|
try { |
||||
|
// 先获取活动分类结构 |
||||
|
await loadCategoryStructure(); |
||||
|
|
||||
|
// 查询统计数据 |
||||
|
const params = { |
||||
|
firmId: queryForm.firmId, |
||||
|
year: queryForm.year, |
||||
|
quarter: queryForm.quarter, |
||||
|
statisticsType: 'lawFirm', |
||||
|
pageNum: queryForm.pageNum, |
||||
|
pageSize: queryForm.pageSize |
||||
|
}; |
||||
|
|
||||
|
const result = await serviceApplicationsApi.statisticsFirmByActivity(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() { |
||||
|
try { |
||||
|
// 使用新接口 /category/tree/child 获取活动分类和活动名称(无参数) |
||||
|
const result = await categoryApi.queryCategoryTreeChild(); |
||||
|
|
||||
|
// 接口返回的数据格式: |
||||
|
// [ |
||||
|
// { |
||||
|
// 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 = { |
||||
|
firmId: item.firmId, |
||||
|
firmName: item.firmName, |
||||
|
totalCount: item.totalCount || 0, |
||||
|
lawyerList: [] |
||||
|
}; |
||||
|
|
||||
|
// 填充律所每个活动的统计数据 |
||||
|
if (item.activityCountMap) { |
||||
|
Object.keys(item.activityCountMap).forEach(activityId => { |
||||
|
row[`activity_${activityId}`] = item.activityCountMap[activityId] || 0; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 处理律师列表数据 |
||||
|
if (item.lawyerList && Array.isArray(item.lawyerList)) { |
||||
|
row.lawyerList = item.lawyerList.map(lawyer => { |
||||
|
const lawyerRow = { |
||||
|
lawyerId: lawyer.userId, |
||||
|
lawyerName: lawyer.lawyerName, |
||||
|
totalCount: lawyer.totalCount || 0 |
||||
|
}; |
||||
|
|
||||
|
// 填充律师每个活动的统计数据 |
||||
|
if (lawyer.activityList && Array.isArray(lawyer.activityList)) { |
||||
|
lawyer.activityList.forEach(activity => { |
||||
|
lawyerRow[`activity_${activity.activityId}`] = activity.count || 0; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return lawyerRow; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return row; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 导出功能 |
||||
|
async function handleExport() { |
||||
|
try { |
||||
|
const params = { |
||||
|
firmId: queryForm.firmId, |
||||
|
year: queryForm.year, |
||||
|
quarter: queryForm.quarter, |
||||
|
statisticsType: 'lawFirm' |
||||
|
}; |
||||
|
|
||||
|
await serviceApplicationsApi.exportFirmActivityStatistics(params); |
||||
|
message.success('导出成功'); |
||||
|
} catch (error) { |
||||
|
smartSentry.captureError(error); |
||||
|
message.error('导出失败'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ---------------------------- 生命周期 ---------------------------- |
||||
|
|
||||
|
onMounted(() => { |
||||
|
initYearOptions(); |
||||
|
queryData(); |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.law-firm-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-container { |
||||
|
overflow-x: auto; |
||||
|
overflow-y: hidden; |
||||
|
margin-bottom: 16px; |
||||
|
border: 1px solid #d9d9d9; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
.custom-table { |
||||
|
width: auto; |
||||
|
min-width: 100%; |
||||
|
border-collapse: collapse; |
||||
|
table-layout: fixed; |
||||
|
font-size: 13px; |
||||
|
} |
||||
|
|
||||
|
.custom-table th, |
||||
|
.custom-table td { |
||||
|
border: 1px solid #e8e8e8; |
||||
|
padding: 10px 8px; |
||||
|
text-align: center; |
||||
|
vertical-align: middle; |
||||
|
} |
||||
|
|
||||
|
.custom-table th { |
||||
|
background-color: #f5f5f5; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
/* 固定列基础样式 */ |
||||
|
.fixed-col { |
||||
|
background-color: #fafafa; |
||||
|
font-weight: 500; |
||||
|
position: sticky; |
||||
|
z-index: 1; |
||||
|
} |
||||
|
|
||||
|
/* 序号列 - 第一列固定 */ |
||||
|
.index-col { |
||||
|
width: 40px !important; |
||||
|
min-width: 40px !important; |
||||
|
max-width: 40px !important; |
||||
|
left: 0 !important; |
||||
|
z-index: 3 !important; |
||||
|
text-align: center; |
||||
|
padding: 8px 2px !important; |
||||
|
} |
||||
|
|
||||
|
/* 律所名称列 - 第二列固定 */ |
||||
|
.firm-col, |
||||
|
.firm-name { |
||||
|
width: 150px; |
||||
|
min-width: 150px; |
||||
|
max-width: 180px; |
||||
|
left: 40px; |
||||
|
z-index: 2; |
||||
|
} |
||||
|
|
||||
|
/* 律师名称列 - 第三列固定 */ |
||||
|
.lawyer-col, |
||||
|
.lawyer-name { |
||||
|
width: 120px; |
||||
|
min-width: 120px; |
||||
|
max-width: 140px; |
||||
|
left: 190px; |
||||
|
z-index: 2; |
||||
|
} |
||||
|
|
||||
|
/* 右侧固定列 */ |
||||
|
.fixed-right { |
||||
|
right: 0; |
||||
|
z-index: 2; |
||||
|
} |
||||
|
|
||||
|
/* 表头固定列 */ |
||||
|
.custom-table thead th.fixed-col { |
||||
|
z-index: 3; |
||||
|
} |
||||
|
|
||||
|
.total-count-col { |
||||
|
width: 100px; |
||||
|
min-width: 100px; |
||||
|
} |
||||
|
|
||||
|
/* 分类列样式 - 第一行 */ |
||||
|
.category-col { |
||||
|
color: #fff; |
||||
|
font-weight: 600; |
||||
|
font-size: 11px; |
||||
|
padding: 8px 6px; |
||||
|
line-height: 1.4; |
||||
|
min-width: 180px; |
||||
|
max-width: 250px; |
||||
|
} |
||||
|
|
||||
|
/* 活动列样式 - 第二行 */ |
||||
|
.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; |
||||
|
min-width: 80px; |
||||
|
} |
||||
|
|
||||
|
/* 合计行样式 */ |
||||
|
.total-row { |
||||
|
background-color: #fff2f0; |
||||
|
} |
||||
|
|
||||
|
.total-row td { |
||||
|
border-top: 2px solid #ff4d4f; |
||||
|
} |
||||
|
|
||||
|
.firm-row { |
||||
|
cursor: pointer; |
||||
|
background: linear-gradient(135deg, #f6ffed 0%, #e6f7ff 100%); |
||||
|
} |
||||
|
|
||||
|
.firm-row:hover { |
||||
|
background: linear-gradient(135deg, #d9f7be 0%, #bae7ff 100%); |
||||
|
} |
||||
|
|
||||
|
.firm-row.expanded { |
||||
|
background: linear-gradient(135deg, #b7eb8f 0%, #91d5ff 100%); |
||||
|
} |
||||
|
|
||||
|
.firm-name { |
||||
|
text-align: left; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.expand-icon { |
||||
|
margin-right: 8px; |
||||
|
color: #52c41a; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.lawyer-row { |
||||
|
background-color: #ffffff; |
||||
|
} |
||||
|
|
||||
|
.lawyer-row:hover { |
||||
|
background-color: #e6f7ff; |
||||
|
} |
||||
|
|
||||
|
.lawyer-name { |
||||
|
text-align: center; |
||||
|
color: #1890ff; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.total-count { |
||||
|
color: #52c41a; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.total-duration { |
||||
|
color: #faad14; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
/* 分页样式 */ |
||||
|
.pagination-wrapper { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
margin-top: 16px; |
||||
|
padding-top: 16px; |
||||
|
border-top: 1px solid #f0f0f0; |
||||
|
} |
||||
|
|
||||
|
/* 响应式处理 */ |
||||
|
@media (max-width: 1200px) { |
||||
|
.custom-table { |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.custom-table th, |
||||
|
.custom-table td { |
||||
|
padding: 6px 4px; |
||||
|
} |
||||
|
|
||||
|
.category-col { |
||||
|
min-width: 120px; |
||||
|
} |
||||
|
|
||||
|
.activity-col { |
||||
|
min-width: 100px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.total-count { |
||||
|
color: #52c41a; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.total-duration { |
||||
|
color: #1890ff; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
|
||||
|
.pagination-wrapper { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
padding-top: 16px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,549 @@ |
|||||
|
<!-- |
||||
|
* 律师活动统计报表 |
||||
|
* |
||||
|
* @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> |
||||
@ -0,0 +1,378 @@ |
|||||
|
<template> |
||||
|
<div class="firm-statistics-detail"> |
||||
|
|
||||
|
<!-- 查询条件和导出按钮 --> |
||||
|
<div class="query-section"> |
||||
|
<a-form layout="inline" :model="localQueryForm"> |
||||
|
<a-form-item label="年度"> |
||||
|
<a-select v-model:value="localQueryForm.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="季度"> |
||||
|
<a-select v-model:value="localQueryForm.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> |
||||
|
<a-button type="primary" @click="handleQuery" :loading="loading"> |
||||
|
<SearchOutlined /> |
||||
|
查询 |
||||
|
</a-button> |
||||
|
<a-button @click="handleReset" style="margin-left: 8px;"> |
||||
|
<ReloadOutlined /> |
||||
|
重置 |
||||
|
</a-button> |
||||
|
</a-form-item> |
||||
|
<a-form-item style="margin-left: auto;"> |
||||
|
<a-button type="primary" @click="handleExport" :loading="exportLoading"> |
||||
|
<ExportOutlined /> |
||||
|
导出 |
||||
|
</a-button> |
||||
|
</a-form-item> |
||||
|
</a-form> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 统计报表 --> |
||||
|
<div class="statistics-report-container"> |
||||
|
<!-- 报表标题 --> |
||||
|
<div class="report-header"> |
||||
|
<h1>本所律师参与公益法律服务统计表</h1> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 加载状态 --> |
||||
|
<div v-if="loading" class="loading-section"> |
||||
|
<a-spin size="large" /> |
||||
|
<div>正在加载数据...</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 报表表格 --> |
||||
|
<div v-else class="report-table"> |
||||
|
<!-- 表头 --> |
||||
|
<div class="report-row header-row"> |
||||
|
<div class="report-cell">序号</div> |
||||
|
<div class="report-cell">律师名称</div> |
||||
|
<div class="report-cell">执业证号</div> |
||||
|
<div class="report-cell">季度累计服务时长</div> |
||||
|
<div class="report-cell">季度累计服务成本</div> |
||||
|
<div class="report-cell">年度累计服务时长</div> |
||||
|
<div class="report-cell">年度累计服务成本</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 数据行 --> |
||||
|
<template v-for="(item, index) in tableData" :key="index"> |
||||
|
<!-- 律师数据行 --> |
||||
|
<div class="report-row data-row"> |
||||
|
<div class="report-cell">{{ index + 1 }}</div> |
||||
|
<div class="report-cell">{{ item.lawyerName || '-' }}</div> |
||||
|
<div class="report-cell">{{ item.certificateNumber || '-' }}</div> |
||||
|
<div class="report-cell">{{ formatNumber(item.quarterlyServiceDuration) }}</div> |
||||
|
<div class="report-cell">{{ formatCurrency(item.quarterlyServiceCost) }}</div> |
||||
|
<div class="report-cell">{{ formatNumber(item.annualServiceDuration) }}</div> |
||||
|
<div class="report-cell">{{ formatCurrency(item.annualServiceCost) }}</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<!-- 汇总行 --> |
||||
|
<div v-if="summaryData" class="report-row summary-row"> |
||||
|
<div class="report-cell">汇总</div> |
||||
|
<div class="report-cell">-</div> |
||||
|
<div class="report-cell">-</div> |
||||
|
<div class="report-cell">{{ formatNumber(summaryData.totalQuarterlyDuration) }}</div> |
||||
|
<div class="report-cell">{{ formatCurrency(summaryData.totalQuarterlyCost) }}</div> |
||||
|
<div class="report-cell">{{ formatNumber(summaryData.totalAnnualDuration) }}</div> |
||||
|
<div class="report-cell">{{ formatCurrency(summaryData.totalAnnualCost) }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup> |
||||
|
import { ref, computed, onMounted, watch, reactive } from 'vue'; |
||||
|
import { ExportOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'; |
||||
|
import { message } from 'ant-design-vue'; |
||||
|
import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api'; |
||||
|
|
||||
|
// 接收父组件传递的参数 |
||||
|
const props = defineProps({ |
||||
|
params: { |
||||
|
type: Object, |
||||
|
required: true |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
const emit = defineEmits(['back']); |
||||
|
|
||||
|
// 使用 computed 或 ref 来访问 params |
||||
|
const params = computed(() => props.params || {}); |
||||
|
|
||||
|
const exportLoading = ref(false); |
||||
|
const tableData = ref([]); |
||||
|
const loading = ref(false); |
||||
|
|
||||
|
// 本地查询表单 |
||||
|
const localQueryForm = reactive({ |
||||
|
year: new Date().getFullYear(), |
||||
|
quarter: null |
||||
|
}); |
||||
|
|
||||
|
// 年度选项 |
||||
|
const yearOptions = ref([]); |
||||
|
// 季度选项 |
||||
|
const quarterOptions = ref([ |
||||
|
{ value: 1, label: '第一季度' }, |
||||
|
{ value: 2, label: '第二季度' }, |
||||
|
{ value: 3, label: '第三季度' }, |
||||
|
{ value: 4, label: '第四季度' } |
||||
|
]); |
||||
|
|
||||
|
// 初始化年度选项 |
||||
|
function initYearOptions() { |
||||
|
const currentYear = new Date().getFullYear(); |
||||
|
const years = []; |
||||
|
for (let i = currentYear - 5; i <= currentYear + 1; i++) { |
||||
|
years.push(i); |
||||
|
} |
||||
|
yearOptions.value = years; |
||||
|
} |
||||
|
|
||||
|
// 查询按钮点击事件 |
||||
|
async function handleQuery() { |
||||
|
loading.value = true; |
||||
|
try { |
||||
|
const queryParams = { |
||||
|
year: localQueryForm.year, |
||||
|
quarter: localQueryForm.quarter, |
||||
|
firmId: props.params?.firmId |
||||
|
}; |
||||
|
|
||||
|
const response = await serviceApplicationsApi.statistics(queryParams); |
||||
|
tableData.value = response.data || []; |
||||
|
|
||||
|
} catch (error) { |
||||
|
message.error('查询失败'); |
||||
|
console.error('查询失败:', error); |
||||
|
} finally { |
||||
|
loading.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 重置查询条件 |
||||
|
function handleReset() { |
||||
|
localQueryForm.quarter = null; |
||||
|
// 重置后自动查询 |
||||
|
handleQuery(); |
||||
|
} |
||||
|
|
||||
|
// 计算汇总数据 |
||||
|
const summaryData = computed(() => { |
||||
|
if (!tableData.value || tableData.value.length === 0) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const summary = { |
||||
|
totalQuarterlyDuration: 0, |
||||
|
totalQuarterlyCost: 0, |
||||
|
totalAnnualDuration: 0, |
||||
|
totalAnnualCost: 0 |
||||
|
}; |
||||
|
|
||||
|
tableData.value.forEach(item => { |
||||
|
summary.totalQuarterlyDuration += Number(item.quarterlyServiceDuration) || 0; |
||||
|
summary.totalQuarterlyCost += Number(item.quarterlyServiceCost) || 0; |
||||
|
summary.totalAnnualDuration += Number(item.annualServiceDuration) || 0; |
||||
|
summary.totalAnnualCost += Number(item.annualServiceCost) || 0; |
||||
|
}); |
||||
|
|
||||
|
return summary; |
||||
|
}); |
||||
|
|
||||
|
// 格式化数字 |
||||
|
function formatNumber(value) { |
||||
|
if (value === null || value === undefined) return '-'; |
||||
|
const num = Number(value); |
||||
|
return isNaN(num) ? '-' : num.toFixed(2); |
||||
|
} |
||||
|
|
||||
|
// 格式化货币 |
||||
|
function formatCurrency(value) { |
||||
|
if (value === null || value === undefined) return '-'; |
||||
|
const num = Number(value); |
||||
|
return isNaN(num) ? '-' : `¥${num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`; |
||||
|
} |
||||
|
|
||||
|
// 导出Excel |
||||
|
async function handleExport() { |
||||
|
if (!tableData.value || tableData.value.length === 0) { |
||||
|
message.warning('暂无数据可导出'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
exportLoading.value = true; |
||||
|
try { |
||||
|
console.log('开始导出律所统计详情...'); |
||||
|
const exportParams = { |
||||
|
quarter: props.params.quarter, |
||||
|
year: props.params.year, |
||||
|
firmId: props.params.firmId |
||||
|
// 去除分页参数,导出所有数据 |
||||
|
}; |
||||
|
|
||||
|
await serviceApplicationsApi.exportLawyer(exportParams); |
||||
|
message.success('导出成功'); |
||||
|
console.log('律所统计详情导出成功'); |
||||
|
} catch (error) { |
||||
|
message.error('导出失败'); |
||||
|
console.error('律所统计详情导出失败:', error); |
||||
|
} finally { |
||||
|
exportLoading.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 监听参数变化,重新获取数据 |
||||
|
watch(() => props.params, (newParams) => { |
||||
|
if (newParams) { |
||||
|
// 同步参数到本地表单 |
||||
|
if (newParams.year) { |
||||
|
localQueryForm.year = newParams.year; |
||||
|
} |
||||
|
if (newParams.quarter !== undefined) { |
||||
|
localQueryForm.quarter = newParams.quarter; |
||||
|
} |
||||
|
// 自动调用查询 |
||||
|
handleQuery(); |
||||
|
} |
||||
|
}, { immediate: true, deep: true }); |
||||
|
|
||||
|
onMounted(() => { |
||||
|
console.log('律师统计详情组件已加载,参数:', props.params); |
||||
|
initYearOptions(); |
||||
|
// 如果没有props参数,使用默认参数查询 |
||||
|
if (!props.params || !props.params.year) { |
||||
|
handleQuery(); |
||||
|
} |
||||
|
}); |
||||
|
</script> |
||||
|
<style scoped> |
||||
|
.firm-statistics-detail { |
||||
|
padding: 0 16px; |
||||
|
} |
||||
|
|
||||
|
.detail-header { |
||||
|
margin-bottom: 20px; |
||||
|
padding: 16px 0; |
||||
|
} |
||||
|
|
||||
|
.back-btn { |
||||
|
color: #1e3a8a; |
||||
|
} |
||||
|
|
||||
|
.query-section { |
||||
|
margin-bottom: 20px; |
||||
|
padding: 16px; |
||||
|
background: #f5f5f5; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
.query-section :deep(.ant-form) { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.query-section :deep(.ant-form-item:last-child) { |
||||
|
margin-left: auto; |
||||
|
} |
||||
|
|
||||
|
.statistics-report-container { |
||||
|
border: 1px solid #ddd; |
||||
|
border-radius: 4px; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.report-header { |
||||
|
background: #f5f5f5; |
||||
|
padding: 20px; |
||||
|
text-align: center; |
||||
|
border-bottom: 1px solid #ddd; |
||||
|
} |
||||
|
|
||||
|
.report-header h1 { |
||||
|
margin: 0 0 8px 0; |
||||
|
font-size: 24px; |
||||
|
color: #333; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.report-subtitle { |
||||
|
font-size: 14px; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.loading-section { |
||||
|
padding: 60px 20px; |
||||
|
text-align: center; |
||||
|
background: #fff; |
||||
|
} |
||||
|
|
||||
|
.loading-section div:last-child { |
||||
|
margin-top: 16px; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.report-table { |
||||
|
width: 100%; |
||||
|
border-collapse: collapse; |
||||
|
} |
||||
|
|
||||
|
.report-row { |
||||
|
display: flex; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.report-cell { |
||||
|
padding: 12px; |
||||
|
border-right: 1px solid #ddd; |
||||
|
border-bottom: 1px solid #ddd; |
||||
|
text-align: center; |
||||
|
min-height: 50px; |
||||
|
box-sizing: border-box; |
||||
|
font-size: 14px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
|
||||
|
/* 所有列平分宽度 */ |
||||
|
.report-cell { |
||||
|
flex: 1; |
||||
|
min-width: 0; |
||||
|
} |
||||
|
|
||||
|
.report-cell:last-child { |
||||
|
border-right: none; |
||||
|
} |
||||
|
|
||||
|
.header-row { |
||||
|
background-color: #0044cc; |
||||
|
color: white; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.data-row { |
||||
|
background-color: white; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.summary-row { |
||||
|
background-color: #f0f0f0; |
||||
|
color: #333; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
/* 最后一行单元格没有下边框 */ |
||||
|
.report-row:last-child .report-cell { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue