Browse Source

feat:报表

master
“wangzihua” 3 months ago
parent
commit
7a10e0d6bb
  1. 14
      src/api/business/service-applications/service-applications-api.js
  2. 94
      src/components/business/category-tree-select/index.vue
  3. 103
      src/components/system/service-count/quarter-statistics.vue
  4. 401
      src/views/business/erp/service/service-applications-count.vue
  5. 33
      src/views/business/erp/service/service-applications-list.vue
  6. 4
      src/views/system/account/account-menu.js

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

@ -92,10 +92,24 @@ export const serviceApplicationsApi = {
return postRequest('/serviceApplications/statistics', params);
},
/**
* 机构数据统计管理员 @author wzh
*/
statisticsDepartment: (params) => {
return postRequest('/serviceApplications/statistics/department', params);
},
/**
* 导出律所统计信息 @author wzh
*/
exportLawyer: (params) => {
return getDownload('/serviceApplications/exportLawyer', params);
},
/**
* 按部门导出律师数据 @author wzh
*/
exportLawyerByDepartment: (params) => {
return getDownload('/serviceApplications/exportLawyerByDepartment', params);
},
};

94
src/components/business/category-tree-select/index.vue

@ -1,24 +1,21 @@
<!--
* 目录 树形选择组件
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-12 21:01:52
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
*
-->
<template>
<div class="category-tree-select-wrapper">
<a-tree-select
v-model:value="selectValue"
:style="`width:${width}`"
:dropdown-style="{ maxHeight: '400px', overflowX: 'auto' }"
:dropdown-style="{ maxHeight: '400px', overflowX: 'auto', maxWidth: '350px' }"
:tree-data="categoryTreeData"
:placeholder="placeholder"
:allowClear="true"
tree-default-expand-all
@change="onChange"
:field-names="{ label: 'label', value: 'value', children: 'children' }"
/>
</div>
</template>
<script setup>
@ -53,12 +50,54 @@
categoryType: props.categoryType,
};
let resp = await categoryApi.queryCategoryTree(param);
categoryTreeData.value = resp.data;
console.log('接口返回的原始数据:', resp.data);
//
const processedData = resp.data.map((item, index) => {
//
const originalText = item.categoryName || item.label || '';
// 1
const sequence = index + 1;
// 15
const truncatedText = originalText.length > 15 ? originalText.substring(0, 15) + '...' : originalText;
//
const displayText = `${sequence}. ${truncatedText}`;
// item
const newItem = {
...item,
//
label: displayText,
categoryName: displayText,
//
originalText: originalText,
// title
title: originalText
};
return newItem;
});
console.log('处理后的数据:', processedData);
categoryTreeData.value = processedData;
} catch (e) {
smartSentry.captureError(e);
}
}
//
function getNodeIndex(node) {
return node.nodeIndex ? `${node.nodeIndex}.` : '';
}
// tooltip
function getOriginalText(node) {
return node.originalText || node.categoryName || node.label || '';
}
// ----------------- -----------------
const selectValue = ref(props.value);
// value
@ -84,3 +123,42 @@
onMounted(queryCategoryTree);
</script>
<style scoped>
.category-tree-select-wrapper {
width: 100%;
}
/* 为树节点内容添加样式 */
:deep(.ant-select-tree-node-content-wrapper) {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
padding: 2px 4px;
}
/* 确保下拉框有足够宽度显示序号和文本 */
:deep(.ant-tree-select-dropdown) {
max-width: 350px !important;
min-width: 300px !important;
}
/* 确保节点标题正确显示 */
:deep(.ant-select-tree-title) {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
/* 确保title属性正常显示 */
display: inline-block;
}
/* 为序号添加特殊样式,使其更明显 */
:deep(.ant-select-tree-title::before) {
font-weight: bold;
margin-right: 5px;
color: #1890ff;
}
</style>

103
src/components/system/service-count/quarter-statistics.vue

@ -26,12 +26,12 @@
</a-select>
</a-form-item>
<a-form-item label="律师姓名">
<a-form-item label="律师姓名" v-if="!isAdmin">
<a-input v-model:value="queryForm.lawyerName" placeholder="请输入律师姓名" style="width: 150px" />
</a-form-item>
<a-form-item label="律所名称">
<a-input v-model:value="queryForm.firmName" placeholder="请输入律所名称" style="width: 150px" />
<a-input v-model:value="queryForm.firmName" :placeholder="isAdmin ? '请输入机构名称' : '请输入律所名称'" style="width: 150px" />
</a-form-item>
<a-form-item>
@ -52,7 +52,7 @@
</a-card>
<!-- 统计表格 -->
<a-card title="季/年度服务统计" size="small" class="table-card">
<a-card :title="props.isAdmin ? '机构数据统计' : '季/年度服务统计'" size="small" class="table-card">
<a-table
:columns="columns"
:dataSource="tableData"
@ -81,11 +81,19 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ref, reactive, onMounted, computed, watch } from 'vue';
import { SearchOutlined, ReloadOutlined, ExportOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api';
// isAdmin
const props = defineProps({
isAdmin: {
type: Boolean,
default: false
}
});
//
const queryForm = reactive({
quarter: undefined,
@ -106,7 +114,50 @@ const quarterOptions = [
const yearOptions = ref([]);
//
const columns = [
const columns = computed(() => {
if (props.isAdmin) {
// Admin
return [
{
title: '机构名称',
dataIndex: 'firmName',
key: 'firmName',
width: 150
},
{
title: '季度累计服务时长',
dataIndex: 'quarterlyServiceDuration',
key: 'quarterlyServiceDuration',
width: 120
},
{
title: '季度累计服务成本',
dataIndex: 'quarterlyServiceCost',
key: 'quarterlyServiceCost',
width: 120
},
{
title: '年度累计服务时长',
dataIndex: 'annualServiceDuration',
key: 'annualServiceDuration',
width: 120
},
{
title: '年度累计服务成本',
dataIndex: 'annualServiceCost',
key: 'annualServiceCost',
width: 120
},
{
title: '律师总数',
dataIndex: 'lawyerCount',
key: 'lawyerCount',
width: 100
}
];
} else {
//
return [
{
title: '执业律师姓名',
dataIndex: 'lawyerName',
@ -144,6 +195,8 @@ const columns = [
width: 120
}
];
}
});
const tableData = ref([]);
const loading = ref(false);
@ -174,8 +227,14 @@ async function handleSearch() {
pageSize: pagination.pageSize
};
// API
const result = await serviceApplicationsApi.statistics(params);
let result;
if (props.isAdmin) {
// 使
result = await serviceApplicationsApi.statisticsDepartment(params);
} else {
// 使
result = await serviceApplicationsApi.statistics(params);
}
if (result.data) {
tableData.value = result.data.list || [];
@ -191,12 +250,18 @@ async function handleSearch() {
//
function handleReset() {
Object.assign(queryForm, {
const resetData = {
quarter: undefined,
year: new Date().getFullYear(),
lawyerName: '',
firmName: ''
});
};
//
if (!props.isAdmin) {
resetData.lawyerName = '';
}
Object.assign(queryForm, resetData);
handleSearch();
}
@ -211,8 +276,13 @@ async function handleExport() {
firmName: queryForm.firmName
};
//
if (props.isAdmin) {
//
await serviceApplicationsApi.exportDepartment(exportParams);
} else {
//
await serviceApplicationsApi.exportLawyer(exportParams);
}
message.success('导出成功');
} catch (error) {
@ -228,8 +298,19 @@ function handleTableChange(pag) {
handleSearch();
}
// isAdmin
watch(() => props.isAdmin, (newVal, oldVal) => {
console.log('isAdmin发生变化:', oldVal, '->', newVal);
if (newVal !== oldVal) {
//
pagination.current = 1;
handleSearch();
}
}, { immediate: true });
onMounted(() => {
initYearOptions();
console.log('组件挂载,当前isAdmin:', props.isAdmin);
handleSearch();
});
</script>

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

@ -7,24 +7,419 @@
-->
<template>
<div class="service-applications-count">
<a-tabs v-model:activeKey="activeTab" type="card">
<div v-if="!loginInfo">加载中...</div>
<div v-else-if="isAdmin" class="admin-excel-view">
<!-- 查询页面只在没有显示结果时显示 -->
<div v-if="!showResultView">
<a-card title="机构数据查询" class="admin-excel-card">
<div class="excel-description">
<p>作为协议您可以查看机构级别的统计数据数据以Excel表格样式展示</p>
<p>请选择需要查看的季度和年度然后点击查询按钮</p>
</div>
<a-form :model="queryForm" layout="horizontal" class="query-form">
<a-form-item label="季度" :label-col="{ span: 4 }" :wrapper-col="{ span: 8 }">
<a-select v-model:value="queryForm.quarter" placeholder="请选择季度" style="width: 200px">
<a-select-option :value="1">第一季度</a-select-option>
<a-select-option :value="2">第二季度</a-select-option>
<a-select-option :value="3">第三季度</a-select-option>
<a-select-option :value="4">第四季度</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="年度" :label-col="{ span: 4 }" :wrapper-col="{ span: 8 }">
<a-select v-model:value="queryForm.year" placeholder="请选择年度" style="width: 200px">
<a-select-option :value="2026">2026</a-select-option>
<a-select-option :value="2027">2027</a-select-option>
<a-select-option :value="2028">2028</a-select-option>
<a-select-option :value="2029">2029</a-select-option>
<a-select-option :value="2030">2030</a-select-option>
</a-select>
</a-form-item>
<!--<a-form-item label="机构名称" :label-col="{ span: 4 }" :wrapper-col="{ span: 8 }">
<a-input v-model:value="queryForm.firmName" placeholder="请输入机构名称(可选)" style="width: 300px" />
</a-form-item>-->
<a-form-item :wrapper-col="{ span: 8, offset: 4 }">
<a-button type="primary" @click="handleQuery" :loading="queryLoading" style="margin-right: 8px;">
<SearchOutlined />
查询数据
</a-button>
<a-button @click="handleReset" style="margin-right: 8px;">
<ReloadOutlined />
重置
</a-button>
<!--<a-button type="primary" @click="handleExport" :loading="exportLoading">
<ExportOutlined />
导出Excel
</a-button>-->
</a-form-item>
</a-form>
<!-- 空状态 -->
<div v-if="hasQueried && !showResultView" class="empty-state">
<a-empty description="暂无数据" />
</div>
</a-card>
</div>
<!-- 详情页面查询成功后完全替换查询页面 -->
<div v-else class="result-view">
<FirmStatisticsDetail
:query-params="queryForm"
:table-data="tableData"
@back="handleBackToQuery"
/>
</div>
</div>
<a-tabs v-else v-model:activeKey="activeTab" type="card">
<!-- 季度统计 -->
<a-tab-pane key="quarter" tab="季度统计">
<QuarterStatistics />
<QuarterStatistics :is-admin="isAdmin" />
</a-tab-pane>
</a-tabs>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted, reactive } from 'vue';
import { ExportOutlined, SearchOutlined, ReloadOutlined, ArrowLeftOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import QuarterStatistics from '/@/components/system/service-count/quarter-statistics.vue';
import FirmStatisticsDetail from '/@/components/system/service-count/firm-statistics-detail.vue';
import { loginApi } from '/@/api/system/login-api';
import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api';
const activeTab = ref('quarter');
const loginInfo = ref(null);
const isAdmin = ref(false);
const queryLoading = ref(false);
const exportLoading = ref(false);
const hasQueried = ref(false);
const queryForm = reactive({
quarter: null,
year: new Date().getFullYear(),
//firmName: ''
});
const tableData = ref([]);
const summaryData = ref(null);
const showResultView = ref(false);
//
async function getLoginInfo() {
try {
console.log('开始获取登录信息...');
const res = await loginApi.getLoginInfo();
console.log('登录接口返回:', res);
loginInfo.value = res.data;
console.log('登录信息:', res.data);
// admin
const loginNameCheck = res.data?.loginName === 'admin';
const userTypeCheck = res.data?.userType === 1;
isAdmin.value = loginNameCheck || userTypeCheck;
console.log('管理员状态判断:');
console.log('- loginName:', res.data?.loginName);
console.log('- loginName === "admin":', loginNameCheck);
console.log('- userType:', res.data?.userType);
console.log('- userType === 1:', userTypeCheck);
console.log('- 最终isAdmin:', isAdmin.value);
} catch (error) {
console.error('获取登录信息失败:', error);
}
}
//
getLoginInfo();
onMounted(() => {
//
});
//
async function handleQuery() {
//if (!queryForm.quarter) {
// message.warning('');
// return;
//}
if (!queryForm.year) {
message.warning('请选择年度');
return;
}
queryLoading.value = true;
try {
console.log('开始查询机构数据...');
const queryParams = {
quarter: queryForm.quarter,
year: queryForm.year,
firmName: queryForm.firmName,
//
pageNum: 1,
pageSize: 500
};
console.log('查询参数:', queryParams);
const res = await serviceApplicationsApi.statisticsDepartment(queryParams);
console.log('机构数据查询结果:', res);
//
if (res.data && res.data.list && Array.isArray(res.data.list)) {
const dataList = res.data.list;
if (dataList.length > 0) {
tableData.value = dataList.map(item => ({
...item,
//
firmName: item.firmName || '-',
lawyerCount: item.lawyerCount || 0,
applicationCount: item.applicationCount || 0,
approvedCount: item.approvedCount || 0,
rejectedCount: item.rejectedCount || 0,
approvalRate: item.approvalRate ? `${(item.approvalRate * 100).toFixed(2)}%` : '0%',
statisticTime: item.statisticTime || '-',
//
quarterlyServiceDuration: item.quarterlyServiceDuration || 0,
quarterlyServiceCost: item.quarterlyServiceCost || 0,
annualServiceDuration: item.annualServiceDuration || 0,
annualServiceCost: item.annualServiceCost || 0
}));
//
showResultView.value = true;
hasQueried.value = true;
message.success(`查询成功,共 ${res.data.total} 条数据`);
} else {
tableData.value = [];
summaryData.value = null;
showResultView.value = false;
message.warning('暂无数据');
}
} else {
tableData.value = [];
summaryData.value = null;
message.warning('暂无数据');
}
} catch (error) {
console.error('查询机构数据失败:', error);
message.error('查询失败');
tableData.value = [];
summaryData.value = null;
} finally {
queryLoading.value = false;
}
}
//
function handleReset() {
queryForm.quarter = null;
queryForm.year = new Date().getFullYear();
queryForm.firmName = '';
tableData.value = [];
summaryData.value = null;
hasQueried.value = false;
}
// Excel
async function handleExport() {
if (tableData.value.length === 0) {
message.warning('请先查询数据');
return;
}
exportLoading.value = true;
try {
console.log('开始导出机构数据...');
const exportParams = {
quarter: queryForm.quarter,
year: queryForm.year,
firmName: queryForm.firmName
};
await serviceApplicationsApi.exportFirm(exportParams);
message.success('导出成功');
console.log('机构数据导出成功');
} catch (error) {
message.error('导出失败');
console.error('机构数据导出失败:', error);
} finally {
exportLoading.value = false;
}
}
//
function calculateSummary() {
if (tableData.value.length === 0) {
summaryData.value = null;
return;
}
const summary = {
totalLawyerCount: 0,
totalApplicationCount: 0,
totalApprovedCount: 0,
totalRejectedCount: 0
};
tableData.value.forEach(item => {
summary.totalLawyerCount += item.lawyerCount || 0;
summary.totalApplicationCount += item.applicationCount || 0;
summary.totalApprovedCount += item.approvedCount || 0;
summary.totalRejectedCount += item.rejectedCount || 0;
});
//
if (summary.totalApplicationCount > 0) {
summary.totalApprovalRate = `${((summary.totalApprovedCount / summary.totalApplicationCount) * 100).toFixed(2)}%`;
} else {
summary.totalApprovalRate = '0%';
}
summaryData.value = summary;
}
//
function handleBackToQuery() {
showResultView.value = false;
}
</script>
<style scoped>
.service-applications-count {
padding: 16px;
background: #fff;
min-height: 600px;
}
.admin-excel-view {
max-width: 1200px;
margin: 0 auto;
}
.admin-excel-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.excel-description {
background: #f0f7ff;
border: 1px solid #b3d8ff;
border-radius: 4px;
padding: 16px;
margin-bottom: 24px;
}
.excel-description p {
margin: 0;
line-height: 1.6;
color: #333;
}
.excel-description p:first-child {
margin-bottom: 8px;
font-weight: 500;
}
.query-form {
margin-bottom: 24px;
}
.excel-table-container {
border: 2px solid #d9d9d9;
border-radius: 4px;
overflow: hidden;
margin-top: 24px;
}
.excel-table-header {
background: #f5f5f5;
padding: 16px;
border-bottom: 1px solid #d9d9d9;
text-align: center;
}
.excel-table-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.excel-table-subtitle {
font-size: 14px;
color: #666;
}
.excel-table {
width: 100%;
}
.excel-row {
display: flex;
border-bottom: 1px solid #d9d9d9;
}
.excel-row:last-child {
border-bottom: none;
}
.excel-header {
background: #e6f7ff;
font-weight: 600;
}
.excel-summary {
background: #f0f0f0;
font-weight: 600;
}
.excel-cell {
padding: 8px 12px;
border-right: 1px solid #d9d9d9;
display: flex;
align-items: center;
justify-content: center;
min-height: 40px;
box-sizing: border-box;
}
.excel-cell:last-child {
border-right: none;
}
.excel-header .excel-cell {
background: #1890ff;
color: white;
border-right: 1px solid #40a9ff;
}
.excel-data .excel-cell {
background: white;
color: #333;
}
.excel-summary .excel-cell {
background: #fafafa;
color: #333;
}
.empty-state {
padding: 60px 0;
text-align: center;
}
:deep(.ant-form-item-label) {
font-weight: 500;
}
:deep(.ant-card-head-title) {
font-size: 18px;
font-weight: 600;
color: #1890ff;
}
</style>

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

@ -40,7 +40,7 @@
<a-input style="width: 150px" v-model:value="queryForm.managerName" placeholder="负责人姓名" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button type="primary" @click="onSearch">
<a-button v-privilege="'serviceApplications:query'" type="primary" @click="onSearch">
<template #icon>
<SearchOutlined />
</template>
@ -61,7 +61,7 @@
<!---------- 表格操作行 begin ----------->
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button @click="showForm" type="primary">
<a-button v-privilege="'serviceApplications:add'" @click="showForm" type="primary">
<template #icon>
<PlusOutlined />
</template>
@ -110,10 +110,10 @@
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button v-if="record.firmAuditStatus === 0 || record.firmAuditStatus === 4" @click="showForm(record)" type="link">编辑</a-button>
<a-button v-if="record.firmAuditStatus === 0" @click="onSubmit(record)" type="link">提交</a-button>
<a-button v-if="record.firmAuditStatus === 1 || record.firmAuditStatus === 2" @click="showAuditModal(record)" type="link">审核</a-button>
<a-button @click="onDelete(record)" danger type="link">删除</a-button>
<a-button v-if="(record.firmAuditStatus === 0 || record.firmAuditStatus === 4) && record.userId === loginInfo?.userId" @click="showForm(record)" type="link">编辑</a-button>
<a-button v-if="(record.firmAuditStatus === 0 && record.userId === loginInfo?.userId)" @click="onSubmit(record)" type="link">提交</a-button>
<a-button v-if="(record.firmAuditStatus === 1 || record.firmAuditStatus === 2) && loginInfo?.dataScopeView === 1" @click="showAuditModal(record)" type="link">审核</a-button>
<a-button v-if="record.userId === loginInfo?.userId" @click="onDelete(record)" danger type="link">删除</a-button>
</div>
</template>
</template>
@ -208,6 +208,7 @@
import { employeeApi } from '/@/api/system/employee-api';
import { REVIEW_ENUM } from '/@/constants/system/review-const';
import { PlusOutlined, DeleteOutlined, SendOutlined, ImportOutlined, ExportOutlined, DownloadOutlined, UploadOutlined } from '@ant-design/icons-vue';
import { loginApi } from '/@/api/system/login-api';
// ---------------------------- ----------------------------
const columns = ref([
@ -272,7 +273,7 @@
},
{
title: '执业机构审核人',
dataIndex: 'firmAuditUser',
dataIndex: 'firmAuditUserName',
ellipsis: true,
},
{
@ -319,6 +320,9 @@
auditRemark: ''
});
//
const loginInfo = ref(null);
//
async function loadAllEmployees() {
try {
@ -362,7 +366,20 @@
}
onMounted(queryData);
onMounted(async () => {
queryData();
await getLoginInfo();
});
//
async function getLoginInfo() {
try {
const res = await loginApi.getLoginInfo();
loginInfo.value = res.data;
} catch (error) {
console.error('获取登录信息失败:', error);
}
}
// ---------------------------- / ----------------------------
const formRef = ref();

4
src/views/system/account/account-menu.js

@ -26,7 +26,7 @@ export const ACCOUNT_MENU = {
menuName: '通知公告',
components: markRaw(defineAsyncComponent(() => import('./components/notice/index.vue'))),
},
LOGIN_LOG: {
/**LOGIN_LOG: {
menuId: 'login-log',
menuName: '登录日志',
components: markRaw(defineAsyncComponent(() => import('./components/login-log/index.vue'))),
@ -35,5 +35,5 @@ export const ACCOUNT_MENU = {
menuId: 'operate-log',
menuName: '操作日志',
components: markRaw(defineAsyncComponent(() => import('./components/operate-log/index.vue'))),
},
},**/
};

Loading…
Cancel
Save