Browse Source

feat:报表统计

master
wang 2 months ago
parent
commit
9183b0941c
  1. BIN
      dist.zip
  2. 4
      src/api/business/category/category-api.js
  3. 42
      src/api/business/service-applications/service-applications-api.js
  4. 1071
      src/lvshi.html
  5. 2
      src/views/business/erp/service/ceo-service-detail.vue
  6. 195
      src/views/business/erp/service/firm-statistics-detail.vue
  7. 656
      src/views/business/erp/service/law-firm-service-report-statistics.vue
  8. 2
      src/views/business/erp/service/law-firm.vue
  9. 549
      src/views/business/erp/service/lawyer-service-report-statistics.vue
  10. 378
      src/views/business/erp/service/lawyer-statistics-detail.vue
  11. 2
      src/views/business/erp/service/service-applications-count.vue
  12. 6
      src/views/business/erp/service/service-applications-form.vue
  13. 54
      src/views/business/erp/service/service-applications-list.vue
  14. 23
      src/views/business/erp/service/service-applications-report-list.vue
  15. 6
      src/views/mobile/service/create.vue
  16. 8
      src/views/mobile/service/detail.vue
  17. 12
      vite.config.js

BIN
dist.zip

Binary file not shown.

4
src/api/business/category/category-api.js

@ -23,6 +23,10 @@ export const categoryApi = {
queryCategoryTree: (param) => { queryCategoryTree: (param) => {
return postRequest('/category/tree', param); return postRequest('/category/tree', param);
}, },
// 查询类目层级树(带子节点-活动)@author wzh
queryCategoryTreeChild: () => {
return getRequest('/category/tree/child');
},
// 更新类目 @author 卓大 // 更新类目 @author 卓大
updateCategory: (param) => { updateCategory: (param) => {
return postRequest('/category/update', param); return postRequest('/category/update', param);

42
src/api/business/service-applications/service-applications-api.js

@ -143,6 +143,13 @@ export const serviceApplicationsApi = {
return getDownload('/serviceApplications/exportLawyerByDepartment', params); return getDownload('/serviceApplications/exportLawyerByDepartment', params);
}, },
/**
* 查询是否有未审核的数据 @author wzh
*/
queryNoReview: () => {
return getRequest('/serviceApplications/queryNoReview');
},
/** /**
* 服务上报统计 @author * 服务上报统计 @author
*/ */
@ -156,4 +163,39 @@ export const serviceApplicationsApi = {
getServiceApplicationsCost: (queryForm) => { getServiceApplicationsCost: (queryForm) => {
return postRequest('/serviceApplications/statistics/cost', queryForm); return postRequest('/serviceApplications/statistics/cost', queryForm);
}, },
/**
* 按活动统计律师服务数据 @author wzh
*/
statisticsByActivity: (params) => {
return postRequest('/serviceApplications/statistics/lawyerActivityCount', params);
},
/**
* 导出律师活动统计 @author wzh
*/
exportLawyerActivityStatistics: (params) => {
return postDownload('/serviceApplications/export/lawyerActivityCount', params);
},
/**
* 导出服务申报明细 @author wzh
*/
exportServiceApplications: (params) => {
return postDownload('/serviceApplications/export/activityDetail', params);
},
/**
* 按活动统计律所服务数据 @author wzh
*/
statisticsFirmByActivity: (params) => {
return postRequest('/serviceApplications/statistics/firmActivityCount', params);
},
/**
* 导出律所活动统计 @author wzh
*/
exportFirmActivityStatistics: (params) => {
return postDownload('/serviceApplications/export/firmActivityCount', params);
},
}; };

1071
src/lvshi.html

File diff suppressed because it is too large

2
src/views/business/erp/service/ceo-service-detail.vue

@ -563,7 +563,7 @@ const columns = ref([
width: 100 width: 100
}, },
{ {
title: '职务名称', title: '所属服务团',
dataIndex: 'positionName', dataIndex: 'positionName',
key: 'positionName', key: 'positionName',
ellipsis: true, ellipsis: true,

195
src/components/system/service-count/firm-statistics-detail.vue → src/views/business/erp/service/firm-statistics-detail.vue

@ -1,12 +1,39 @@
<template> <template>
<div class="firm-statistics-detail"> <div class="firm-statistics-detail">
<!-- 导出按钮 --> <!-- 查询条件 -->
<div class="export-section"> <div class="query-section">
<a-form layout="inline" :model="queryForm">
<a-form-item label="年度">
<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="季度">
<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 label="律所名称">
<DepartmentTreeSelect v-model:value="queryForm.firmId" placeholder="请选择机构" style="width: 220px" />
</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"> <a-button type="primary" @click="handleExport" :loading="exportLoading">
<ExportOutlined /> <ExportOutlined />
导出Excel 导出
</a-button> </a-button>
</a-form-item>
</a-form>
</div> </div>
<!-- 统计报表 --> <!-- 统计报表 -->
@ -14,7 +41,7 @@
<!-- 报表标题 --> <!-- 报表标题 -->
<div class="report-header"> <div class="report-header">
<h1>律师事务所律师参与公益法律服务统计表</h1> <h1>律师事务所律师参与公益法律服务统计表</h1>
<div class="report-subtitle">统计时间{{ params.year }}<span v-if="params.quarter != null">{{ params.quarter }}季度</span></div> <div class="report-subtitle">统计时间{{ queryForm.year }}<span v-if="queryForm.quarter != null">{{ queryForm.quarter }}季度</span></div>
</div> </div>
<!-- 加载状态 --> <!-- 加载状态 -->
@ -98,10 +125,11 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, watch } from 'vue'; import { ref, computed, onMounted, watch, reactive } from 'vue';
import { ExportOutlined, ArrowLeftOutlined, DownOutlined, RightOutlined } from '@ant-design/icons-vue'; import { ExportOutlined, ArrowLeftOutlined, DownOutlined, RightOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api'; import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api';
import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue';
// / // /
const expandedRows = ref(new Set()); const expandedRows = ref(new Set());
@ -126,20 +154,77 @@ function hasLawyerData(item) {
return item && item.lawyerServiceVOList && Array.isArray(item.lawyerServiceVOList) && item.lawyerServiceVOList.length > 0; return item && item.lawyerServiceVOList && Array.isArray(item.lawyerServiceVOList) && item.lawyerServiceVOList.length > 0;
} }
// //
const props = defineProps({ const props = defineProps({
params: { params: {
type: Object, type: Object,
required: true default: null
} }
}); });
const emit = defineEmits(['back']);
const exportLoading = ref(false); const exportLoading = ref(false);
const tableData = ref([]); const tableData = ref([]);
const loading = ref(false); const loading = ref(false);
//
const queryForm = reactive({
year: new Date().getFullYear(),
quarter: null,
firmId: null
});
//
const yearOptions = ref([]);
//
const quarterOptions = ref([
{ value: 1, label: '第一季度' },
{ value: 2, label: '第二季度' },
{ value: 3, label: '第三季度' },
{ value: 4, label: '第四季度' }
]);
// CEO
const isCeo = ref(false);
//
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 params = {
year: queryForm.year,
quarter: queryForm.quarter,
firmId: queryForm.firmId || props.params?.firmId
};
const response = await serviceApplicationsApi.statisticsDepartment(params);
tableData.value = response.data || [];
} catch (error) {
message.error('查询失败');
console.error('查询失败:', error);
} finally {
loading.value = false;
}
}
//
function handleReset() {
queryForm.quarter = null;
queryForm.firmId = null;
//
handleQuery();
}
// //
const summaryData = computed(() => { const summaryData = computed(() => {
if (!tableData.value || tableData.value.length === 0) { if (!tableData.value || tableData.value.length === 0) {
@ -177,11 +262,6 @@ function formatCurrency(value) {
return isNaN(num) ? '-' : `¥${num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`; return isNaN(num) ? '-' : `¥${num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`;
} }
//
function handleBack() {
emit('back');
}
// //
async function fetchStatisticsData() { async function fetchStatisticsData() {
if (!props.params || !props.params.year) { if (!props.params || !props.params.year) {
@ -249,6 +329,21 @@ watch(() => props.params, (newParams) => {
onMounted(() => { onMounted(() => {
console.log('律所统计详情组件已加载,参数:', props.params); console.log('律所统计详情组件已加载,参数:', props.params);
initYearOptions();
//
if (props.params) {
if (props.params.year) {
queryForm.year = props.params.year;
}
if (props.params.quarter !== undefined) {
queryForm.quarter = props.params.quarter;
}
if (props.params.firmId) {
queryForm.firmId = props.params.firmId;
}
}
//
handleQuery();
}); });
</script> </script>
<style scoped> <style scoped>
@ -265,9 +360,25 @@ onMounted(() => {
color: #1e3a8a; color: #1e3a8a;
} }
.export-section { .query-section {
margin-bottom: 20px; margin-bottom: 20px;
text-align: right; 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;
}
.export-section {
display: none;
} }
.statistics-report-container { .statistics-report-container {
@ -329,52 +440,10 @@ onMounted(() => {
justify-content: center; justify-content: center;
} }
/* 序号列 - 最窄 */ /* 所有列平分宽度 */
.report-cell:nth-child(1) { .report-cell {
flex: 0 0 60px; flex: 1;
min-width: 60px; min-width: 0;
}
/* 律所名称列 */
.report-cell:nth-child(2) {
flex: 0 0 150px;
min-width: 150px;
}
/* 律师名称列 */
.report-cell:nth-child(3) {
flex: 0 0 120px;
min-width: 120px;
}
/* 执业证号列 - 最宽 */
.report-cell:nth-child(4) {
flex: 0 0 200px;
min-width: 200px;
}
/* 季度累计服务时长 */
.report-cell:nth-child(5) {
flex: 0 0 140px;
min-width: 140px;
}
/* 季度累计服务成本 */
.report-cell:nth-child(6) {
flex: 0 0 140px;
min-width: 140px;
}
/* 年度累计服务时长 */
.report-cell:nth-child(7) {
flex: 0 0 140px;
min-width: 140px;
}
/* 年度累计服务成本 */
.report-cell:nth-child(8) {
flex: 0 0 140px;
min-width: 140px;
} }
.report-cell:last-child { .report-cell:last-child {

656
src/views/business/erp/service/law-firm-service-report-statistics.vue

@ -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>

2
src/components/system/service-count/law-firm.vue → src/views/business/erp/service/law-firm.vue

@ -74,7 +74,7 @@ import { SearchOutlined, ReloadOutlined, DownloadOutlined, ArrowLeftOutlined } f
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api'; import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api';
import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue'; import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue';
import FirmStatisticsDetail from './firm-statistics-detail.vue'; // detail import FirmStatisticsDetail from '/@/views/business/erp/service/firm-statistics-detail.vue'; // detail
// //
const loading = ref(false); const loading = ref(false);

549
src/views/business/erp/service/lawyer-service-report-statistics.vue

@ -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>

378
src/views/business/erp/service/lawyer-statistics-detail.vue

@ -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>

2
src/views/business/erp/service/service-applications-count.vue

@ -44,7 +44,7 @@ import { ref, onMounted, reactive, watch } from 'vue';
import { ExportOutlined, SearchOutlined, ReloadOutlined, ArrowLeftOutlined } from '@ant-design/icons-vue'; import { ExportOutlined, SearchOutlined, ReloadOutlined, ArrowLeftOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import QuarterStatistics from '/@/components/system/service-count/quarter-statistics.vue'; import QuarterStatistics from '/@/components/system/service-count/quarter-statistics.vue';
import LawFirmStatistics from '/@/components/system/service-count/law-firm.vue'; import LawFirmStatistics from '/@/views/business/erp/service/law-firm.vue';
import ExcelStatisticsDetail from '/@/components/system/service-count/excel-statistics-detail.vue'; import ExcelStatisticsDetail from '/@/components/system/service-count/excel-statistics-detail.vue';
import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue'; import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue';
import { loginApi } from '/@/api/system/login-api'; import { loginApi } from '/@/api/system/login-api';

6
src/views/business/erp/service/service-applications-form.vue

@ -43,8 +43,8 @@
</a-row> </a-row>
<a-row :gutter="24"> <a-row :gutter="24">
<a-col :span="8"> <a-col :span="8">
<a-form-item label="职务" name="positionId" required> <a-form-item label="所属服务团" name="positionId" required>
<PositionSelect v-model:value="form.positionId" placeholder="请选择职务" width="100%" :disabled="readonlyMode" /> <PositionSelect v-model:value="form.positionId" placeholder="请选择所属服务团" width="100%" :disabled="readonlyMode" />
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
@ -559,7 +559,7 @@
certificateNumber: undefined, certificateNumber: undefined,
firmId: undefined, //ID firmId: undefined, //ID
departmentName: undefined, // departmentName: undefined, //
positionId: undefined, //ID positionId: undefined, //ID
serviceStart: undefined, // serviceStart: undefined, //
serviceEnd: undefined, // serviceEnd: undefined, //
serviceDuration: undefined, // serviceDuration: undefined, //

54
src/views/business/erp/service/service-applications-list.vue

@ -32,10 +32,10 @@
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="职务" v-if="isCtoRole || isCeo" class="smart-query-form-item"> <a-form-item label="所属服务团" v-if="isCtoRole || isCeo" class="smart-query-form-item">
<PositionSelect <PositionSelect
v-model:value="queryForm.positionId" v-model:value="queryForm.positionId"
placeholder="请选择职务" placeholder="请选择所属服务团"
style="width: 250px" style="width: 250px"
/> />
</a-form-item> </a-form-item>
@ -111,6 +111,8 @@
批量审核 批量审核
</a-button> </a-button>
<!-- 批量上报按钮cto角色显示 --> <!-- 批量上报按钮cto角色显示 -->
<a-button <a-button
v-if="isCtoRole" v-if="isCtoRole"
@ -123,7 +125,13 @@
</template> </template>
批量上报 批量上报
</a-button> </a-button>
<!-- 导出按钮 -->
<a-button @click="handleExport" type="primary">
<template #icon>
<ExportOutlined />
</template>
导出
</a-button>
<!--<a-button v-if="!isCeo" @click="confirmBatchDelete" type="primary" danger :disabled="!canBatchDelete()"> <!--<a-button v-if="!isCeo" @click="confirmBatchDelete" type="primary" danger :disabled="!canBatchDelete()">
<template #icon> <template #icon>
<DeleteOutlined /> <DeleteOutlined />
@ -138,6 +146,7 @@
</a-button>--> </a-button>-->
</div> </div>
<div class="smart-table-setting-block"> <div class="smart-table-setting-block">
<TableOperator v-model="columns" :tableId="null" :refresh="queryData" /> <TableOperator v-model="columns" :tableId="null" :refresh="queryData" />
</div> </div>
@ -332,7 +341,7 @@ import AgreementModal from '/@/views/system/home/components/agreement-modal.vue'
ellipsis: true, ellipsis: true,
}, },
{ {
title: '职务名称', title: '所属服务团',
dataIndex: 'positionName', dataIndex: 'positionName',
ellipsis: true, ellipsis: true,
width: 100, width: 100,
@ -665,6 +674,13 @@ import AgreementModal from '/@/views/system/home/components/agreement-modal.vue'
// //
if (loginInfo.value.costVisibleFlag === true) { if (loginInfo.value.costVisibleFlag === true) {
try { try {
//
const hasUnreviewedData = await checkUnreviewedData();
if (hasUnreviewedData) {
message.warning('有律师提交的数据尚未审核,请先完成审核后再进行批量上报。');
return;
}
const costPermissionResult = await firmReportsApi.query(); const costPermissionResult = await firmReportsApi.query();
if (costPermissionResult.data === false) { if (costPermissionResult.data === false) {
// false // false
@ -746,6 +762,21 @@ import AgreementModal from '/@/views/system/home/components/agreement-modal.vue'
return reportYear === lastMonthYear && reportMonth === lastMonth; return reportYear === lastMonthYear && reportMonth === lastMonth;
} }
//
async function checkUnreviewedData() {
try {
//
const result = await serviceApplicationsApi.queryNoReview();
// 0
return result.data && result.data > 0;
} catch (error) {
console.error('检查未审核数据失败:', error);
return false; // false
}
}
// //
async function handleBatchReport(validRecords) { async function handleBatchReport(validRecords) {
try { try {
@ -1196,8 +1227,19 @@ function showAuditModal(record) {
} }
} }
async function onExport() { //
await serviceApplicationsApi.exportServiceApplications(); async function handleExport() {
try {
const params = processQueryParams({ ...queryForm });
//
delete params.pageNum;
delete params.pageSize;
await serviceApplicationsApi.exportServiceApplications(params);
message.success('导出成功');
} catch (error) {
smartSentry.captureError(error);
message.error('导出失败');
}
} }
// ---------------------------- ---------------------------- // ---------------------------- ----------------------------

23
src/views/business/erp/service/service-applications-report-list.vue

@ -73,6 +73,14 @@
</template> </template>
批量删除 批量删除
</a-button> </a-button>
<!-- 导出按钮只有user用户显示 -->
<a-button v-if="!isNotUser" @click="handleExport" type="primary">
<template #icon>
<ExportOutlined />
</template>
导出
</a-button>
<!--<a-button @click="showImportModal" type="primary"> <!--<a-button @click="showImportModal" type="primary">
<template #icon> <template #icon>
<ImportOutlined /> <ImportOutlined />
@ -690,6 +698,21 @@ import AgreementModal from '/@/views/system/home/components/agreement-modal.vue'
queryData(); queryData();
} }
//
async function handleExport() {
try {
const params = processQueryParams({ ...queryForm });
//
delete params.pageNum;
delete params.pageSize;
await serviceApplicationsApi.exportServiceApplications(params);
message.success('导出成功');
} catch (error) {
smartSentry.captureError(error);
message.error('导出失败');
}
}
// //
function processSelfFirmFilter(params) { function processSelfFirmFilter(params) {
const processedParams = { ...params }; const processedParams = { ...params };

6
src/views/mobile/service/create.vue

@ -56,9 +56,9 @@
<!-- 职务 --> <!-- 职务 -->
<div class="form-item"> <div class="form-item">
<label class="form-label required">职务</label> <label class="form-label required">所属服务团</label>
<select v-model="form.positionId" class="form-select"> <select v-model="form.positionId" class="form-select">
<option value="">请选择职务</option> <option value="">请选择所属服务团</option>
<option <option
v-for="position in positionList" v-for="position in positionList"
:key="position.positionId" :key="position.positionId"
@ -728,7 +728,7 @@ if (!agreementSigned.value) {
// //
if (!form.positionId) { if (!form.positionId) {
message.error('请选择职务') message.error('请选择所属服务团')
return return
} }

8
src/views/mobile/service/detail.vue

@ -56,12 +56,12 @@
<!-- 职务 --> <!-- 职务 -->
<div class="form-item"> <div class="form-item">
<label class="form-label">职务</label> <label class="form-label">所属服务团</label>
<template v-if="readonlyMode"> <template v-if="readonlyMode">
<input <input
v-model="form.positionName" v-model="form.positionName"
class="form-input" class="form-input"
placeholder="职务" placeholder="所属服务团"
readonly readonly
/> />
</template> </template>
@ -71,7 +71,7 @@
class="form-select" class="form-select"
:disabled="readonlyMode" :disabled="readonlyMode"
> >
<option value="">请选择职务</option> <option value="">请选择所属服务团</option>
<option <option
v-for="position in positionList" v-for="position in positionList"
:key="position.positionId" :key="position.positionId"
@ -837,7 +837,7 @@ async function handleSave() {
// //
if (!form.positionId) { if (!form.positionId) {
message.error('请选择职务') message.error('请选择所属服务团')
return return
} }

12
vite.config.js

@ -35,20 +35,20 @@ export default {
proxy: { proxy: {
// 代理API路径 // 代理API路径
'/api': { '/api': {
//target: 'http://8.148.67.92:8080/', // 目标服务器地址 target: 'http://8.148.67.92:8080/', // 目标服务器地址
target: 'http://127.0.0.1:8080/', //target: 'http://127.0.0.1:8080/',
changeOrigin: true, // 是否修改请求头中的 Origin 字段 changeOrigin: true, // 是否修改请求头中的 Origin 字段
rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径 rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径
}, },
'/login': { '/login': {
//target: 'http://8.148.67.92:8080/', // 目标服务器地址 target: 'http://8.148.67.92:8080/', // 目标服务器地址
target: 'http://127.0.0.1:8080/', //target: 'http://127.0.0.1:8080/',
changeOrigin: true, // 是否修改请求头中的 Origin 字段 changeOrigin: true, // 是否修改请求头中的 Origin 字段
rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径 rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径
}, },
'/mobile/login': { '/mobile/login': {
//target: 'http://8.148.67.92:8080/', // 目标服务器地址 target: 'http://8.148.67.92:8080/', // 目标服务器地址
target: 'http://127.0.0.1:8080/', //target: 'http://127.0.0.1:8080/',
changeOrigin: true, // 是否修改请求头中的 Origin 字段 changeOrigin: true, // 是否修改请求头中的 Origin 字段
rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径 rewrite: (path) => path.replace(/^\/api/, ''), // 重写路径
}, },

Loading…
Cancel
Save