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