Browse Source

4-19日

master
wang 2 months ago
parent
commit
21f42fe09e
  1. 8
      src/components/system/service-count/excel-statistics-detail.vue
  2. 4
      src/constants/business/message/message-const.js
  3. 30
      src/layout/components/header-user-space/header-message-detail-modal.vue
  4. 3
      src/store/modules/system/user.js
  5. 64
      src/utils/role-util.js
  6. 188
      src/views/business/erp/cost/firm-reports-form.vue
  7. 42
      src/views/business/erp/cost/firm-reports-list.vue
  8. 15
      src/views/business/erp/letter/letter-list.vue
  9. 4
      src/views/business/erp/penalty-apply/penalty-apply-list.vue
  10. 8
      src/views/business/erp/service/firm-statistics-detail.vue
  11. 178
      src/views/business/erp/service/law-firm-service-report-statistics.vue
  12. 175
      src/views/business/erp/service/lawyer-service-report-statistics.vue
  13. 3
      src/views/business/erp/service/lawyer-statistics-detail.vue
  14. 176
      src/views/business/erp/service/service-applications-list.vue
  15. 43
      src/views/business/erp/service/service-applications-report-list.vue
  16. 67
      src/views/system/home/home-notice.vue

8
src/components/system/service-count/excel-statistics-detail.vue

@ -63,10 +63,10 @@
<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 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> </div>
<!-- 数据行 --> <!-- 数据行 -->

4
src/constants/business/message/message-const.js

@ -14,6 +14,10 @@ export const MESSAGE_TYPE_ENUM = {
value: 2, value: 2,
desc: '订单' desc: '订单'
}, },
AUDIT: {
value: 3,
desc: '审核通知'
},
}; };

30
src/layout/components/header-user-space/header-message-detail-modal.vue

@ -11,14 +11,20 @@
</a-descriptions-item> </a-descriptions-item>
</a-descriptions> </a-descriptions>
<template #footer> <template #footer>
<a-button v-if="messageDetail.dataId && messageDetail.messageType === 3" type="primary" @click="gotoDetail">查看详情</a-button>
<a-button type="primary" @click="showFlag = false">关闭</a-button> <a-button type="primary" @click="showFlag = false">关闭</a-button>
</template> </template>
</a-modal> </a-modal>
</template> </template>
<script setup> <script setup>
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
import { messageApi } from '/@/api/support/message-api.js'; import { messageApi } from '/@/api/support/message-api.js';
import { useUserStore } from '/@/store/modules/system/user';
const router = useRouter();
const userStore = useUserStore();
const emit = defineEmits(['refresh']); const emit = defineEmits(['refresh']);
const messageDetail = reactive({ const messageDetail = reactive({
@ -26,6 +32,7 @@
title: '', title: '',
content: '', content: '',
createTime: '', createTime: '',
dataId: null,
}); });
const showFlag = ref(false); const showFlag = ref(false);
@ -43,5 +50,28 @@
} }
} }
async function gotoDetail() {
if (messageDetail.dataId) {
showFlag.value = false;
//
const roleCode = userStore.roleCode?.toLowerCase() || '';
// roleCode "user"
const isLawyer = roleCode === 'user';
// /
// /erp/service
// /erp/service/list
const targetPath = isLawyer ? '/erp/service' : '/erp/service/list';
//
await router.push({
path: targetPath,
query: { applicationId: messageDetail.dataId }
});
}
}
defineExpose({ show }); defineExpose({ show });
</script> </script>

3
src/store/modules/system/user.js

@ -35,6 +35,8 @@ export const useUserStore = defineStore({
departmentId: '', departmentId: '',
//部门名词 //部门名词
departmentName: '', departmentName: '',
//角色代码
roleCode: '',
//是否需要修改密码 //是否需要修改密码
needUpdatePwdFlag: false, needUpdatePwdFlag: false,
//是否为超级管理员 //是否为超级管理员
@ -161,6 +163,7 @@ export const useUserStore = defineStore({
this.phone = data.phone; this.phone = data.phone;
this.departmentId = data.departmentId; this.departmentId = data.departmentId;
this.departmentName = data.departmentName; this.departmentName = data.departmentName;
this.roleCode = data.roleCode || '';
this.needUpdatePwdFlag = data.needUpdatePwdFlag; this.needUpdatePwdFlag = data.needUpdatePwdFlag;
this.administratorFlag = data.administratorFlag; this.administratorFlag = data.administratorFlag;
this.agreementSignFlag = data.agreementSignFlag || false; this.agreementSignFlag = data.agreementSignFlag || false;

64
src/utils/role-util.js

@ -0,0 +1,64 @@
/**
* 角色判断工具
*
* @Author: wzh
* @Date: 2025-03-21
*/
/**
* 获取角色判断结果
* @param {string} roleCode - 角色代码
* @returns {Object} 角色判断结果
*/
export function getRoleInfo(roleCode) {
if (!roleCode) {
return {
isUser: false,
isNotUser: true,
isCto: false,
isCeo: false,
isAssociationRole: false,
isFirmRole: false,
isFirmAdmin: false,
canCreateApplication: true,
};
}
const roleLower = (roleCode || '').toLowerCase();
const isUser = roleLower === 'user';
const isCto = roleLower === 'cto';
const isCeo = roleLower === 'ceo';
const isFirmAdmin = roleLower === 'staff';
const isAssociationRole = roleLower === 'ceo' ||
roleLower.includes('协会') ||
roleLower.includes('association') ||
roleLower.includes('律协') ||
roleLower.includes('律师协会');
const isFirmRole = roleLower.includes('律所') ||
roleLower.includes('firm') ||
roleLower.includes('lawyer') ||
roleLower.includes('律师') ||
isCto ||
isFirmAdmin;
const isNotUser = !isUser;
const canCreateApplication = !isAssociationRole && !isCeo && !isFirmAdmin;
return {
isUser,
isNotUser,
isCto,
isCeo,
isAssociationRole,
isFirmRole,
isFirmAdmin,
canCreateApplication,
};
}

188
src/views/business/erp/cost/firm-reports-form.vue

@ -66,7 +66,7 @@
</div> </div>
</a-form-item> </a-form-item>
<a-form-item label="公益活动成本(万元)" name="publicWelfareCost"> <a-form-item label="预上报公益成本(万元)" name="publicWelfareCost">
<a-input-number <a-input-number
style="width: 100%" style="width: 100%"
v-model:value="form.publicWelfareCost" v-model:value="form.publicWelfareCost"
@ -80,13 +80,27 @@
</div> </div>
</a-form-item> </a-form-item>
<a-form-item label="审核后实际公益成本(万元)" name="actualPublicWelfareCost">
<a-input-number
style="width: 100%"
v-model:value="form.actualPublicWelfareCost"
:min="0"
:precision="2"
disabled
addon-after="万元"
/>
<div style="font-size: 12px; color: #666; margin-top: 4px;">
律协审核后生成不可修改
</div>
</a-form-item>
<!-- 成本比例警告信息 --> <!-- 成本比例警告信息 -->
<a-form-item v-if="showCostWarning" :wrapper-col="{ offset: 8 }"> <a-form-item v-if="showCostWarning" :wrapper-col="{ offset: 8 }">
<div style="color: #ff4d4f; font-size: 12px;"> <div style="color: #ff4d4f; font-size: 12px;">
<span v-if="costRatio <= 25"> 系统计算的公益成本已经达到收入20%</span> <span v-if="costRatio <= 25"> 全年累计公益成本{{ annualActualCost }} + 本季度{{ publicWelfareCost }}已达到全年收入的20%</span>
<span v-else> 系统计算的公益成本超过收入25%上限上限为25%将按上限值</span> <span v-else> 全年累计公益成本超过全年收入的25%上限当前季度最多可填报</span>
<span v-if="costRatio > 25" style="font-weight: bold;">{{ calculatedPublicWelfareCost }}万元</span> <span v-if="costRatio > 25" style="font-weight: bold;">{{ calculatedPublicWelfareCost }}万元</span>
<span v-if="costRatio > 25">进行保存按25%比例计算</span> <span v-if="costRatio > 25">全年25%比例计算</span>
</div> </div>
</a-form-item> </a-form-item>
@ -103,7 +117,7 @@
</a-form-item> </a-form-item>
</a-col>--> </a-col>-->
<a-col :span="12"> <a-col :span="12">
<a-form-item label="成本/收入比" :label-col="{ span: 12 }" :wrapper-col="{ span: 12 }"> <a-form-item label="预上报成本收入比" :label-col="{ span: 12 }" :wrapper-col="{ span: 12 }">
<a-input <a-input
style="width: 100%" style="width: 100%"
v-model:value="form.costIncomeRatio" v-model:value="form.costIncomeRatio"
@ -112,6 +126,16 @@
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12">
<a-form-item label="实际成本收入比" :label-col="{ span: 12 }" :wrapper-col="{ span: 12 }">
<a-input
style="width: 100%"
v-model:value="form.actualCostIncomeRatio"
disabled
addon-after="%"
/>
</a-form-item>
</a-col>
</a-row> </a-row>
</a-form> </a-form>
@ -126,7 +150,7 @@
<script setup> <script setup>
import { reactive, ref, nextTick, computed, onMounted } from 'vue'; import { reactive, ref, nextTick, computed, onMounted } from 'vue';
import _ from 'lodash'; import _ from 'lodash';
import { message } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading'; import { SmartLoading } from '/@/components/framework/smart-loading';
import { firmReportsApi } from '/@/api/business/cost/firm-reports-api'; import { firmReportsApi } from '/@/api/business/cost/firm-reports-api';
import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api'; import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api';
@ -156,6 +180,9 @@
totalCost: 0 // totalCost: 0 //
}); });
//
const annualActualCost = ref(0);
// //
const quarterOptions = ref([]); const quarterOptions = ref([]);
@ -199,7 +226,8 @@
// //
async function fetchAnnualIncome() { async function fetchAnnualIncome() {
try { try {
const result = await firmReportsApi.income(); const targetYear = form.declareYear || new Date().getFullYear();
const result = await firmReportsApi.income({ declareYear: targetYear });
if (result.data) { if (result.data) {
annualIncomeInfo.value = { annualIncomeInfo.value = {
revenue: parseFloat(result.data.revenue) || 0, revenue: parseFloat(result.data.revenue) || 0,
@ -212,105 +240,67 @@
} }
} }
//
async function getPublicWelfareCost() { async function getPublicWelfareCost() {
console.log('开始获取公益成本,部门ID:', departmentId.value, '年份:', form.declareYear, '季度:', form.declareQuarter);
if (!departmentId.value) { if (!departmentId.value) {
message.warning('无法获取机构信息,请重新登录'); message.warning('无法获取机构信息,请重新登录');
console.error('部门ID为空,无法获取公益成本');
return; return;
} }
if (!form.declareYear) { if (!form.declareYear) {
console.log('年份未设置,跳过API调用');
return; return;
} }
if (!form.declareQuarter) { if (!form.declareQuarter) {
console.log('季度未选择,跳过API调用');
return; return;
} }
// 1-4" (Q1)"
let quarterNum = form.declareQuarter; let quarterNum = form.declareQuarter;
if (typeof quarterNum === 'string') { if (typeof quarterNum === 'string') {
// " (Q1)"1
const match = quarterNum.match(/(\d+)/); const match = quarterNum.match(/(\d+)/);
if (match) { if (match) {
quarterNum = parseInt(match[1], 10); quarterNum = parseInt(match[1], 10);
} else { } else {
console.error('无法解析季度字符串:', quarterNum);
return; return;
} }
} }
try { try {
// //
const startMonth = (quarterNum - 1) * 3 + 1; const response = await serviceApplicationsApi.getServiceApplicationsCost({
const endMonth = startMonth + 2;
// 3
let totalCost = 0;
for (let month = startMonth; month <= endMonth; month++) {
const queryForm = {
firmId: departmentId.value, firmId: departmentId.value,
year: form.declareYear, year: form.declareYear,
month: month quarter: quarterNum
}; });
console.log('调用接口: /serviceApplications/statistics/cost, 参数:', queryForm);
const response = await serviceApplicationsApi.getServiceApplicationsCost(queryForm);
console.log(`${month}月接口返回数据:`, response);
totalCost += parseFloat(response.data) || 0;
}
form.publicWelfareCost = totalCost;
console.log('季度累计公益成本:', form.publicWelfareCost, '万元'); form.publicWelfareCost = parseFloat(response.data) || 0;
calculateCosts(); calculateCosts();
} catch (error) { } catch (error) {
message.error('获取公益成本失败'); message.error('获取公益成本失败');
console.error('获取公益成本失败:', error);
form.publicWelfareCost = 0; form.publicWelfareCost = 0;
calculateCosts(); calculateCosts();
} }
} }
async function show(rowData) { async function show(rowData) {
console.log('表单show函数被调用,rowData:', rowData);
Object.assign(form, formDefault); Object.assign(form, formDefault);
// IDID
form.firmId = departmentId.value; form.firmId = departmentId.value;
// rowDataid
if (rowData && typeof rowData === 'object' && rowData.id) { if (rowData && typeof rowData === 'object' && rowData.id) {
console.log('编辑模式,使用现有数据:', rowData);
//
Object.assign(form, rowData); Object.assign(form, rowData);
//
//
await nextTick(); await nextTick();
console.log('编辑模式表单数据:', form.declareYear, form.declareQuarter); await fetchAnnualActualCost();
await getPublicWelfareCost(); await getPublicWelfareCost();
} else { } else {
console.log('新建模式,等待用户选择季度后获取公益成本');
//
await initQuarterOptions(); await initQuarterOptions();
//
await fetchAnnualIncome(); await fetchAnnualIncome();
// await fetchAnnualActualCost();
await getPublicWelfareCost(); await getPublicWelfareCost();
} }
visibleFlag.value = true; visibleFlag.value = true;
nextTick(() => { nextTick(() => {
formRef.value.clearValidate(); formRef.value.clearValidate();
//
calculateCosts(); calculateCosts();
}); });
} }
@ -331,9 +321,10 @@
declareYear: new Date().getFullYear(), // declareYear: new Date().getFullYear(), //
declareQuarter: undefined, // (1,2,3,4) declareQuarter: undefined, // (1,2,3,4)
revenue: undefined, // revenue: undefined, //
//totalCost: 0, // publicWelfareCost: undefined, //
publicWelfareCost: undefined, // actualPublicWelfareCost: undefined, //
costIncomeRatio: '0.00', // % costIncomeRatio: '0.00', // %
actualCostIncomeRatio: '0.00', // %
}; };
let form = reactive({ ...formDefault }); let form = reactive({ ...formDefault });
@ -345,10 +336,30 @@
publicWelfareCost: [{ required: true, message: '系统正在计算公益成本,请稍后' }], publicWelfareCost: [{ required: true, message: '系统正在计算公益成本,请稍后' }],
}; };
// 25%
const calculatedPublicWelfareCost = ref(0); const calculatedPublicWelfareCost = ref(0);
const showCostWarning = ref(false); const showCostWarning = ref(false);
const costRatio = ref(0); // const costRatio = ref(0);
/**
* 获取全年累计实际公益成本万元
* 从后端接口获取后端会计算四个季度通过审核的服务申请成本之和
*/
async function fetchAnnualActualCost() {
try {
const currentYear = form.declareYear || new Date().getFullYear();
const response = await serviceApplicationsApi.getServiceApplicationsCost({
firmId: departmentId.value,
year: currentYear
});
annualActualCost.value = parseFloat(response.data) || 0;
console.log('全年累计实际公益成本:', annualActualCost.value, '万元');
} catch (error) {
console.error('获取全年累计实际公益成本失败:', error);
annualActualCost.value = 0;
}
}
// //
function onQuarterChange() { function onQuarterChange() {
@ -363,61 +374,49 @@
calculateCosts(); calculateCosts();
} }
// /
function calculateCosts() { function calculateCosts() {
// NaN
const currentRevenue = safeParseFloat(form.revenue); const currentRevenue = safeParseFloat(form.revenue);
const publicWelfareCost = safeParseFloat(form.publicWelfareCost); const publicWelfareCost = safeParseFloat(form.publicWelfareCost);
const annualRevenue = safeParseFloat(annualIncomeInfo.value.revenue); const annualRevenue = safeParseFloat(annualIncomeInfo.value.revenue);
const annualTotalCost = safeParseFloat(annualIncomeInfo.value.totalCost);
// + // = +
const totalRevenue = currentRevenue + annualRevenue; const totalRevenue = currentRevenue + annualRevenue;
// + // = +
const totalCost = publicWelfareCost + annualTotalCost; const totalCostForRatio = annualActualCost.value + publicWelfareCost;
//
if (totalRevenue > 0) { if (totalRevenue > 0) {
costRatio.value = (totalCost / totalRevenue) * 100; costRatio.value = (totalCostForRatio / totalRevenue) * 100;
// 20%
if (costRatio.value >= 20) { if (costRatio.value >= 20) {
showCostWarning.value = true; showCostWarning.value = true;
// 25%25%
if (costRatio.value > 25) { if (costRatio.value > 25) {
const maxAllowedAnnualCost = totalRevenue * 0.25; // // * 25%
const remainingAllowedCost = Math.max(0, maxAllowedAnnualCost - annualTotalCost); const maxAllowedCost = totalRevenue * 0.25;
// -
const remainingAllowedCost = Math.max(0, maxAllowedCost - annualActualCost.value);
//
calculatedPublicWelfareCost.value = Math.min(publicWelfareCost, remainingAllowedCost).toFixed(2); calculatedPublicWelfareCost.value = Math.min(publicWelfareCost, remainingAllowedCost).toFixed(2);
// /使 const actualTotalCost = annualActualCost.value + parseFloat(calculatedPublicWelfareCost.value);
const actualAnnualCost = annualTotalCost + parseFloat(calculatedPublicWelfareCost.value); const ratio = (actualTotalCost / totalRevenue) * 100;
const ratio = (actualAnnualCost / totalRevenue) * 100;
form.costIncomeRatio = ratio.toFixed(2); form.costIncomeRatio = ratio.toFixed(2);
} else { } else {
// 20%25%使
calculatedPublicWelfareCost.value = publicWelfareCost.toFixed(2); calculatedPublicWelfareCost.value = publicWelfareCost.toFixed(2);
form.costIncomeRatio = costRatio.value.toFixed(2); form.costIncomeRatio = costRatio.value.toFixed(2);
} }
} else { } else {
// 20%
showCostWarning.value = false; showCostWarning.value = false;
calculatedPublicWelfareCost.value = publicWelfareCost.toFixed(2); calculatedPublicWelfareCost.value = publicWelfareCost.toFixed(2);
form.costIncomeRatio = costRatio.value.toFixed(2); form.costIncomeRatio = costRatio.value.toFixed(2);
} }
} else { } else {
// 0
showCostWarning.value = false; showCostWarning.value = false;
costRatio.value = 0; costRatio.value = 0;
form.costIncomeRatio = '0.00'; form.costIncomeRatio = '0.00';
calculatedPublicWelfareCost.value = publicWelfareCost.toFixed(2); calculatedPublicWelfareCost.value = publicWelfareCost.toFixed(2);
} }
//
form.totalCost = showCostWarning.value ? parseFloat(calculatedPublicWelfareCost.value) : publicWelfareCost;
} }
// //
@ -443,8 +442,18 @@
try { try {
await formRef.value.validateFields(); await formRef.value.validateFields();
form.approvalStatus = '1'; // form.approvalStatus = '1'; //
//
Modal.confirm({
title: '成本上报提醒',
content: '当前核算的公益成本支出仅为律所预上报成本,最终以律协服务审核后实际公益成本为准。',
okText: '确认提交',
cancelText: '取消',
onOk: async () => {
await save(); await save();
message.success('提交审核成功'); message.success('提交审核成功');
}
});
} catch (err) { } catch (err) {
message.error('参数验证错误,请仔细填写表单数据!'); message.error('参数验证错误,请仔细填写表单数据!');
} }
@ -457,7 +466,7 @@
// //
calculateCosts(); calculateCosts();
// 使 //
const saveData = { ...form }; const saveData = { ...form };
// //
@ -470,16 +479,21 @@
saveData.declareYear = parseInt(saveData.declareYear); saveData.declareYear = parseInt(saveData.declareYear);
} }
if (showCostWarning.value) { // 25%使
// 使 if (showCostWarning.value && costRatio.value > 25) {
//
saveData.publicWelfareCost = parseFloat(calculatedPublicWelfareCost.value); saveData.publicWelfareCost = parseFloat(calculatedPublicWelfareCost.value);
saveData.totalCost = saveData.publicWelfareCost;
// //
const revenue = parseFloat(saveData.revenue) || 0; const totalRevenue = safeParseFloat(saveData.revenue) + safeParseFloat(annualIncomeInfo.value.revenue);
if (revenue > 0) { const annualTotalCost = annualActualCost.value + safeParseFloat(saveData.publicWelfareCost);
saveData.costIncomeRatio = ((saveData.publicWelfareCost / revenue) * 100).toFixed(2); let ratio = totalRevenue > 0 ? ((annualTotalCost / totalRevenue) * 100) : 0;
// 25%
if (ratio > 25) {
ratio = 25;
} }
saveData.costIncomeRatio = ratio.toFixed(2);
} }
if (saveData.id) { if (saveData.id) {

42
src/views/business/erp/cost/firm-reports-list.vue

@ -192,18 +192,31 @@
// ellipsis: true, // ellipsis: true,
//}, //},
{ {
title: '公益成本支出(单位:万元)', title: '预上报公益成本(单位:万元)',
dataIndex: 'publicWelfareCost', dataIndex: 'publicWelfareCost',
ellipsis: true, ellipsis: true,
}, },
{ {
title: '成本收入比', title: '审核后实际公益成本(单位:万元)',
dataIndex: 'actualPublicWelfareCost',
ellipsis: true,
},
{
title: '预上报成本收入比',
dataIndex: 'costIncomeRatio', dataIndex: 'costIncomeRatio',
ellipsis: true, ellipsis: true,
customRender: ({ text }) => { customRender: ({ text }) => {
return text ? `${text}%` : ''; return text ? `${text}%` : '';
}, },
}, },
{
title: '实际成本收入比',
dataIndex: 'actualCostIncomeRatio',
ellipsis: true,
customRender: ({ text }) => {
return text ? `${text}%` : '';
},
},
/**{ /**{
title: '审批状态', title: '审批状态',
dataIndex: 'approvalStatus', dataIndex: 'approvalStatus',
@ -305,8 +318,8 @@
// //
function onSubmit(data){ function onSubmit(data){
Modal.confirm({ Modal.confirm({
title: '提示', title: '成本上报提醒',
content: '确定要提交该成本报表吗?', content: '当前核算的公益成本支出仅为律所预上报成本,最终以律协服务审核后实际公益成本为准。确认后将提交该成本报表。',
okText: '提交', okText: '提交',
okType: 'primary', okType: 'primary',
onOk() { onOk() {
@ -319,10 +332,10 @@
// //
async function requestSubmit(data){ async function requestSubmit(data){
// //
// 使==ID // staffcto
if (data.approvalStatus !== 0 || (data.userId && data.userId != currentUserId.value)) { if (data.approvalStatus !== 0) {
message.warning('没有权限提交该数据'); message.warning('只能提交未提交状态的数据');
return; return;
} }
@ -399,9 +412,9 @@
// //
async function requestDelete(data){ async function requestDelete(data){
// //
// 使==ID // staffcto
if (!isCto.value || data.approvalStatus !== 0 || (data.userId && data.userId != currentUserId.value)) { if (!isCto.value || data.approvalStatus !== 0) {
message.warning('没有权限删除该数据'); message.warning('没有权限删除该数据');
return; return;
} }
@ -440,12 +453,13 @@
return; return;
} }
// //
// staffcto
const selectedRecords = tableData.value.filter(item => selectedRowKeyList.value.includes(item.id)); const selectedRecords = tableData.value.filter(item => selectedRowKeyList.value.includes(item.id));
const invalidRecords = selectedRecords.filter(item => item.approvalStatus !== 0 || (item.userId && item.userId != currentUserId.value)); const invalidRecords = selectedRecords.filter(item => item.approvalStatus !== 0);
if (invalidRecords.length > 0) { if (invalidRecords.length > 0) {
message.warning('只能批量删除自己未提交的数据'); message.warning('只能批量删除未提交的数据');
return; return;
} }

15
src/views/business/erp/letter/letter-list.vue

@ -57,12 +57,13 @@
</template> </template>
<div style="display: flex; flex-wrap: wrap; gap: 16px;"> <div style="display: flex; flex-wrap: wrap; gap: 16px;">
<div <div
v-for="item in noticeList" v-for="item in filteredNoticeList"
:key="item.bookId || item.noticeId || item.id" :key="item.bookId || item.noticeId || item.id"
style="display: flex; align-items: center; padding: 8px 12px; background: #f5f5f5; border-radius: 4px; cursor: pointer;" style="display: flex; align-items: center; padding: 8px 12px; background: #f5f5f5; border-radius: 4px; cursor: pointer;"
@click="downloadNoticeAttachment(item)" @click="downloadNoticeAttachment(item)"
:title="hasAttachment(item) ? '点击下载' : '暂无附件'" :title="hasAttachment(item) ? '点击下载' : '暂无附件'"
> >
<FileTextOutlined style="font-size: 16px; color: #1890ff; margin-right: 8px;" /> <FileTextOutlined style="font-size: 16px; color: #1890ff; margin-right: 8px;" />
<span style="color: #1890ff; font-size: 14px;">{{ item.bookName || item.title }}</span> <span style="color: #1890ff; font-size: 14px;">{{ item.bookName || item.title }}</span>
<DownloadOutlined v-if="hasAttachment(item)" style="font-size: 12px; color: #1890ff; margin-left: 4px;" /> <DownloadOutlined v-if="hasAttachment(item)" style="font-size: 12px; color: #1890ff; margin-left: 4px;" />
@ -183,7 +184,7 @@
</a-card> </a-card>
</template> </template>
<script setup> <script setup>
import { reactive, ref, onMounted } from 'vue'; import { reactive, ref, onMounted, computed } from 'vue';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading'; import { SmartLoading } from '/@/components/framework/smart-loading';
import { letterApi } from '/@/api/business/letter/letter-api'; import { letterApi } from '/@/api/business/letter/letter-api';
@ -597,6 +598,16 @@ import AgreementModal from '/@/views/system/home/components/agreement.vue';
// //
const noticeList = ref([]); const noticeList = ref([]);
//
const filteredNoticeList = computed(() => {
if (!isFirmAdmin.value) {
return noticeList.value;
}
return noticeList.value.filter(item => {
return item.documentNumber !== '001';
});
});
// //
async function queryNoticeData() { async function queryNoticeData() {
try { try {

4
src/views/business/erp/penalty-apply/penalty-apply-list.vue

@ -53,13 +53,13 @@
申请无处罚证明 申请无处罚证明
</a-button> </a-button>
<!-- 律所主任申请个人无处罚证明 --> <!-- 律所主任申请个人无处罚证明 -->
<a-button @click="() => showForm('personal')" type="primary" v-if="isCto || isFirmAdmin"> <a-button @click="() => showForm('personal')" type="primary" v-if="isCto">
<template #icon> <template #icon>
<PlusOutlined /> <PlusOutlined />
</template> </template>
申请个人无处罚证明 申请个人无处罚证明
</a-button> </a-button>
<!-- 律所主任申请律所无处罚证明 --> <!-- 律所主任/内勤申请律所无处罚证明 -->
<a-button @click="() => showForm('firm')" type="primary" v-if="isCto || isFirmAdmin"> <a-button @click="() => showForm('firm')" type="primary" v-if="isCto || isFirmAdmin">
<template #icon> <template #icon>
<PlusOutlined /> <PlusOutlined />

8
src/views/business/erp/service/firm-statistics-detail.vue

@ -58,10 +58,10 @@
<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 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> </div>
<!-- 数据行 --> <!-- 数据行 -->

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

@ -55,66 +55,88 @@
<!---------- 统计表格 begin -----------> <!---------- 统计表格 begin ----------->
<a-card size="small" :bordered="false" :hoverable="true"> <a-card size="small" :bordered="false" :hoverable="true">
<div class="statistics-title">律所活动统计报表</div> <div class="statistics-title">律所公益活动统计报表</div>
<!-- 自定义表头 --> <!-- 自定义表头 -->
<div class="custom-table-container"> <div class="custom-table-container">
<table class="custom-table" border="1" cellspacing="0" cellpadding="0"> <table class="custom-table" border="1" cellspacing="0" cellpadding="0">
<thead> <thead>
<!-- 第一行序号 + 律所名称 + 活动分类 --> <!-- 第一行序号 + 律所名称 + 律师名称 + 活动分类含合计 + 服务总次数 -->
<tr> <tr>
<th rowspan="2" class="fixed-col index-col">序号</th> <th rowspan="2" class="fixed-col index-col">序号</th>
<th rowspan="2" class="fixed-col firm-col">律所名称</th> <th rowspan="2" class="fixed-col firm-col">律所名称</th>
<th rowspan="2" class="fixed-col lawyer-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"> <template v-for="category in categoryStructure" :key="category.categoryId">
<!-- 如果有多个活动先添加合计列跨两行 -->
<th v-if="category.activityList.length > 1" rowspan="2" class="subtotal-header-col-fixed" :title="category.categoryName + '合计'">合计</th>
<!-- 然后是分类名称只跨越活动列 -->
<th :colspan="category.activityList.length" class="category-col">
{{ category.categoryName }} {{ category.categoryName }}
</th> </th>
</template>
<th rowspan="2" class="fixed-col fixed-right total-count-col">服务总次数</th> <th rowspan="2" class="fixed-col fixed-right total-count-col">服务总次数</th>
</tr> </tr>
<!-- 第二行活动名称 --> <!-- 第二行活动名称合计已在上行 -->
<tr> <tr>
<th v-for="activity in allActivities" :key="activity.activityId" class="activity-col"> <template v-for="category in categoryStructure" :key="'header_' + category.categoryId">
<th v-for="activity in category.activityList" :key="activity.activityId" class="activity-col">
{{ activity.activityName }} {{ activity.activityName }}
</th> </th>
</template>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template v-for="(firm, firmIndex) in tableData" :key="firm.firmId"> <template v-for="(firm, firmIndex) in tableData" :key="firm.firmId">
<!-- 律所汇总行 --> <!-- 律所汇总行 -->
<tr class="firm-row" @click="toggleExpand(firm.firmId)" :class="{ 'expanded': expandedFirms.includes(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 index-col">{{ firmIndex + 1 }}</td>
<td class="fixed-col firm-name"> <td class="fixed-col firm-name">
<span class="expand-icon">{{ expandedFirms.includes(firm.firmId) ? '▼' : '▶' }}</span> <span class="expand-icon">{{ expandedFirms.includes(firm.firmId) ? '▼' : '▶' }}</span>
{{ firm.firmName }} {{ firm.firmName }}
</td> </td>
<td class="fixed-col lawyer-col">-</td> <td class="fixed-col lawyer-col">-</td>
<td v-for="activity in allActivities" :key="activity.activityId" class="data-col"> <template v-for="category in categoryStructure" :key="'firm_' + category.categoryId">
<td v-if="category.activityList.length > 1" class="data-col subtotal-data-col">
{{ getCategoryTotalForRecord(firm, category) }}
</td>
<td v-for="activity in category.activityList" :key="activity.activityId" class="data-col">
{{ firm[`activity_${activity.activityId}`] || 0 }} {{ firm[`activity_${activity.activityId}`] || 0 }}
</td> </td>
</template>
<td class="fixed-col fixed-right total-count">{{ firm.totalCount || 0 }}</td> <td class="fixed-col fixed-right total-count">{{ firm.totalCount || 0 }}</td>
</tr> </tr>
<!-- 律师明细行 --> <!-- 律师明细行 -->
<tr v-for="lawyer in firm.lawyerList" :key="lawyer.lawyerId" <tr v-for="lawyer in firm.lawyerList" :key="lawyer.lawyerId"
v-show="expandedFirms.includes(firm.firmId)" v-show="expandedFirms.includes(firm.firmId)"
class="lawyer-row"> class="lawyer-row">
<td class="fixed-col index-col" style="width: 40px; min-width: 40px; max-width: 40px;"></td> <td class="fixed-col index-col"></td>
<td class="fixed-col firm-name"></td> <td class="fixed-col firm-name"></td>
<td class="fixed-col lawyer-name">{{ lawyer.lawyerName }}</td> <td class="fixed-col lawyer-name">{{ lawyer.lawyerName }}</td>
<td v-for="activity in allActivities" :key="activity.activityId" class="data-col"> <template v-for="category in categoryStructure" :key="'lawyer_' + lawyer.lawyerId + '_' + category.categoryId">
<td v-if="category.activityList.length > 1" class="data-col subtotal-data-col">
{{ getCategoryTotalForRecord(lawyer, category) }}
</td>
<td v-for="activity in category.activityList" :key="activity.activityId" class="data-col">
{{ lawyer[`activity_${activity.activityId}`] || 0 }} {{ lawyer[`activity_${activity.activityId}`] || 0 }}
</td> </td>
</template>
<td class="fixed-col fixed-right total-count">{{ lawyer.totalCount || 0 }}</td> <td class="fixed-col fixed-right total-count">{{ lawyer.totalCount || 0 }}</td>
</tr> </tr>
</template> </template>
<!-- 合计行 --> <!-- 合计行 -->
<tr class="total-row" v-if="tableData.length > 0"> <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 index-col">-</td>
<td class="fixed-col firm-name" style="font-weight: bold; color: #ff4d4f;">合计</td> <td class="fixed-col firm-name">合计</td>
<td class="fixed-col lawyer-col" style="font-weight: bold; color: #ff4d4f;">-</td> <td class="fixed-col lawyer-col">-</td>
<td v-for="activity in allActivities" :key="activity.activityId" class="data-col" style="font-weight: bold; color: #ff4d4f;"> <template v-for="category in categoryStructure" :key="'total_' + category.categoryId">
<td v-if="category.activityList.length > 1" class="data-col">
{{ categorySubtotals[`category_${category.categoryId}`] || 0 }}
</td>
<td v-for="activity in category.activityList" :key="activity.activityId" class="data-col">
{{ totalRow[`activity_${activity.activityId}`] || 0 }} {{ totalRow[`activity_${activity.activityId}`] || 0 }}
</td> </td>
<td class="fixed-col fixed-right" style="font-weight: bold; color: #ff4d4f;"> </template>
<td class="fixed-col fixed-right">
{{ totalRow.totalCount || 0 }} {{ totalRow.totalCount || 0 }}
</td> </td>
</tr> </tr>
@ -201,17 +223,12 @@
const totalRow = computed(() => { const totalRow = computed(() => {
const row = { totalCount: 0 }; const row = { totalCount: 0 };
// 0
allActivities.value.forEach(activity => { allActivities.value.forEach(activity => {
row[`activity_${activity.activityId}`] = 0; row[`activity_${activity.activityId}`] = 0;
}); });
//
tableData.value.forEach(firm => { tableData.value.forEach(firm => {
//
row.totalCount += firm.totalCount || 0; row.totalCount += firm.totalCount || 0;
//
allActivities.value.forEach(activity => { allActivities.value.forEach(activity => {
const key = `activity_${activity.activityId}`; const key = `activity_${activity.activityId}`;
row[key] += firm[key] || 0; row[key] += firm[key] || 0;
@ -221,6 +238,41 @@
return row; return row;
}); });
// 1
const categoryStructureWithSubtotal = computed(() => {
return categoryStructure.value.map(category => ({
...category,
activityIds: (category.activityList || []).map(a => a.activityId),
needSubtotal: (category.activityList || []).length > 1
}));
});
//
const categorySubtotals = computed(() => {
const subtotals = {};
categoryStructureWithSubtotal.value.forEach(category => {
let total = 0;
(category.activityList || []).forEach(activity => {
tableData.value.forEach(firm => {
total += firm[`activity_${activity.activityId}`] || 0;
});
});
subtotals[`category_${category.categoryId}`] = total;
});
return subtotals;
});
//
function getCategoryTotalForRecord(record, category) {
let total = 0;
if (category.activityList && category.activityList.length > 1) {
category.activityList.forEach(activity => {
total += record[`activity_${activity.activityId}`] || 0;
});
}
return total;
}
// //
const pagination = reactive({ const pagination = reactive({
current: 1, current: 1,
@ -463,9 +515,15 @@
} }
.custom-table th { .custom-table th {
background-color: #f5f5f5;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
background-color: #fafafa;
}
/* 分类列(第一行)- 简洁风格 */
.custom-table thead tr:first-child th.category-col {
background-color: #f0f0f0 !important;
color: #333 !important;
} }
/* 固定列基础样式 */ /* 固定列基础样式 */
@ -523,21 +581,52 @@
min-width: 100px; min-width: 100px;
} }
/* 分类列样式 - 第一行 */ /* 分类列样式 - 第一行(简洁灰调) */
.category-col { .category-col {
color: #fff; color: #333;
background-color: #f5f5f5;
font-weight: 600; font-weight: 600;
font-size: 11px; font-size: 11px;
padding: 8px 6px; padding: 10px 6px;
line-height: 1.4; line-height: 1.4;
min-width: 180px; min-width: 180px;
max-width: 250px; max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
/* 活动列样式 - 第二行 */ /* 合计列样式 - 跨两行(简洁风格) */
.subtotal-header-col-fixed {
background-color: #f9f9f9;
color: #555;
font-weight: 600;
font-size: 11px;
padding: 10px 4px;
width: 60px;
min-width: 60px;
max-width: 60px;
border-left: 1px solid #d9d9d9;
border-right: 1px solid #e8e8e8;
}
/* 合计列样式 - 第二行备用(简洁风格) */
.subtotal-header-col {
background-color: #f9f9f9;
color: #555;
font-weight: 600;
font-size: 11px;
padding: 6px 4px;
width: 70px;
min-width: 70px;
max-width: 70px;
border-left: 1px solid #d9d9d9;
}
/* 活动列样式 - 第二行(简洁风格) */
.activity-col { .activity-col {
background-color: #f0f5ff; background-color: #fafafa;
color: #1890ff; color: #666;
font-weight: 500; font-weight: 500;
font-size: 10px; font-size: 10px;
padding: 6px 2px; padding: 6px 2px;
@ -554,36 +643,45 @@
min-width: 80px; min-width: 80px;
} }
/* 合计行样式 */ /* 合计数据列样式(简洁) */
.subtotal-data-col {
background-color: #f5f5f5;
color: #333;
font-weight: 600;
}
/* 合计行样式(简洁风格) */
.total-row { .total-row {
background-color: #fff2f0; background-color: #f5f5f5;
} }
.total-row td { .total-row td {
border-top: 2px solid #ff4d4f; border-top: 2px solid #bbb;
font-weight: 600;
} }
.firm-row { .firm-row {
cursor: pointer; cursor: pointer;
background: linear-gradient(135deg, #f6ffed 0%, #e6f7ff 100%); background-color: #fafafa;
} }
.firm-row:hover { .firm-row:hover {
background: linear-gradient(135deg, #d9f7be 0%, #bae7ff 100%); background-color: #f0f0f0;
} }
.firm-row.expanded { .firm-row.expanded {
background: linear-gradient(135deg, #b7eb8f 0%, #91d5ff 100%); background-color: #ebebeb;
} }
.firm-name { .firm-name {
text-align: left; text-align: left;
font-weight: bold; font-weight: 600;
color: #333;
} }
.expand-icon { .expand-icon {
margin-right: 8px; margin-right: 8px;
color: #52c41a; color: #666;
font-size: 12px; font-size: 12px;
} }
@ -592,23 +690,23 @@
} }
.lawyer-row:hover { .lawyer-row:hover {
background-color: #e6f7ff; background-color: #f9f9f9;
} }
.lawyer-name { .lawyer-name {
text-align: center; text-align: center;
color: #1890ff; color: #333;
font-weight: 500; font-weight: 500;
} }
.total-count { .total-count {
color: #52c41a; font-weight: 700;
font-weight: bold; color: #333;
} }
.total-duration { .total-duration {
color: #faad14; font-weight: 600;
font-weight: bold; color: #666;
} }
/* 分页样式 */ /* 分页样式 */

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

@ -52,45 +52,62 @@
<!---------- 统计表格 begin -----------> <!---------- 统计表格 begin ----------->
<a-card size="small" :bordered="false" :hoverable="true"> <a-card size="small" :bordered="false" :hoverable="true">
<div class="statistics-title">律师活动统计报表</div> <div class="statistics-title">律师公益活动统计报表</div>
<!-- 自定义表头 --> <!-- 自定义表头 -->
<div class="custom-table-header"> <div class="custom-table-header">
<table class="header-table" border="1" cellspacing="0" cellpadding="0"> <table class="header-table" border="1" cellspacing="0" cellpadding="0">
<thead> <thead>
<!-- 第一行序号 + 活动分类 --> <!-- 第一行序号 + 律师名称 + 活动分类含合计 + 服务总次数 -->
<tr> <tr>
<th rowspan="2" class="fixed-col index-col">序号</th> <th rowspan="2" class="fixed-col index-col">序号</th>
<th rowspan="2" class="fixed-col fixed-left">律师名称</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"> <template v-for="category in categoryStructure" :key="category.categoryId">
<!-- 如果有多个活动先添加合计列跨两行 -->
<th v-if="category.activityList.length > 1" rowspan="2" class="subtotal-header-col-fixed" :title="category.categoryName + '合计'">合计</th>
<!-- 然后是分类名称只跨越活动列 -->
<th :colspan="category.activityList.length" class="category-col" :title="category.categoryName">
{{ category.categoryName }} {{ category.categoryName }}
</th> </th>
</template>
<th rowspan="2" class="fixed-col fixed-right">服务总次数</th> <th rowspan="2" class="fixed-col fixed-right">服务总次数</th>
</tr> </tr>
<!-- 第二行活动名称 --> <!-- 第二行活动名称合计已在上行 -->
<tr> <tr>
<th v-for="activity in allActivities" :key="activity.activityId" class="activity-col" :title="activity.activityName"> <template v-for="category in categoryStructure" :key="'header_' + category.categoryId">
<th v-for="activity in category.activityList" :key="activity.activityId" class="activity-col" :title="activity.activityName">
{{ activity.activityName }} {{ activity.activityName }}
</th> </th>
</template>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(record, index) in tableData" :key="record.lawyerId"> <tr v-for="(record, index) in tableData" :key="record.lawyerId">
<td class="fixed-col index-col">{{ index + 1 }}</td> <td class="fixed-col index-col">{{ index + 1 }}</td>
<td class="fixed-col fixed-left lawyer-name">{{ record.lawyerName }}</td> <td class="fixed-col fixed-left lawyer-name">{{ record.lawyerName }}</td>
<td v-for="activity in allActivities" :key="activity.activityId" class="data-col"> <template v-for="category in categoryStructure" :key="'data_' + category.categoryId">
<td v-if="category.activityList.length > 1" class="data-col subtotal-data-col">
{{ getCategoryTotalForRecord(record, category) }}
</td>
<td v-for="activity in category.activityList" :key="activity.activityId" class="data-col">
{{ record[`activity_${activity.activityId}`] || 0 }} {{ record[`activity_${activity.activityId}`] || 0 }}
</td> </td>
</template>
<td class="fixed-col fixed-right total-count">{{ record.totalCount || 0 }}</td> <td class="fixed-col fixed-right total-count">{{ record.totalCount || 0 }}</td>
</tr> </tr>
<!-- 合计行 --> <!-- 合计行 -->
<tr class="total-row" v-if="tableData.length > 0"> <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 index-col">-</td>
<td class="fixed-col fixed-left" style="font-weight: bold; color: #ff4d4f;">合计</td> <td class="fixed-col fixed-left">合计</td>
<td v-for="activity in allActivities" :key="activity.activityId" class="data-col" style="font-weight: bold; color: #ff4d4f;"> <template v-for="category in categoryStructure" :key="'total_' + category.categoryId">
<td v-if="category.activityList.length > 1" class="data-col">
{{ categorySubtotals[`category_${category.categoryId}`] || 0 }}
</td>
<td v-for="activity in category.activityList" :key="activity.activityId" class="data-col">
{{ totalRow[`activity_${activity.activityId}`] || 0 }} {{ totalRow[`activity_${activity.activityId}`] || 0 }}
</td> </td>
<td class="fixed-col fixed-right" style="font-weight: bold; color: #ff4d4f;"> </template>
<td class="fixed-col fixed-right">
{{ totalRow.totalCount || 0 }} {{ totalRow.totalCount || 0 }}
</td> </td>
</tr> </tr>
@ -166,17 +183,12 @@
const totalRow = computed(() => { const totalRow = computed(() => {
const row = { totalCount: 0 }; const row = { totalCount: 0 };
// 0
allActivities.value.forEach(activity => { allActivities.value.forEach(activity => {
row[`activity_${activity.activityId}`] = 0; row[`activity_${activity.activityId}`] = 0;
}); });
//
tableData.value.forEach(record => { tableData.value.forEach(record => {
//
row.totalCount += record.totalCount || 0; row.totalCount += record.totalCount || 0;
//
allActivities.value.forEach(activity => { allActivities.value.forEach(activity => {
const key = `activity_${activity.activityId}`; const key = `activity_${activity.activityId}`;
row[key] += record[key] || 0; row[key] += record[key] || 0;
@ -186,6 +198,41 @@
return row; return row;
}); });
// 1
const categoryStructureWithSubtotal = computed(() => {
return categoryStructure.value.map(category => ({
...category,
activityIds: (category.activityList || []).map(a => a.activityId),
needSubtotal: (category.activityList || []).length > 1
}));
});
//
const categorySubtotals = computed(() => {
const subtotals = {};
categoryStructureWithSubtotal.value.forEach(category => {
let total = 0;
(category.activityList || []).forEach(activity => {
tableData.value.forEach(record => {
total += record[`activity_${activity.activityId}`] || 0;
});
});
subtotals[`category_${category.categoryId}`] = total;
});
return subtotals;
});
//
function getCategoryTotalForRecord(record, category) {
let total = 0;
if (category.activityList && category.activityList.length > 1) {
category.activityList.forEach(activity => {
total += record[`activity_${activity.activityId}`] || 0;
});
}
return total;
}
// //
const pagination = reactive({ const pagination = reactive({
current: 1, current: 1,
@ -387,7 +434,7 @@
width: auto; width: auto;
min-width:100%; min-width:100%;
border-collapse: collapse; border-collapse: collapse;
table-layout: auto; table-layout: fixed;
font-size: 13px; font-size: 13px;
} }
@ -400,9 +447,15 @@
} }
.header-table thead th { .header-table thead th {
background-color: #f5f5f5;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
background-color: #fafafa;
}
/* 分类列(第一行)- 简洁风格 */
.header-table thead tr:first-child th.category-col {
background-color: #f0f0f0 !important;
color: #333 !important;
} }
/* 固定列样式 */ /* 固定列样式 */
@ -442,35 +495,64 @@
} }
.lawyer-name { .lawyer-name {
font-weight: bold; font-weight: 600;
color: #1890ff; color: #333;
} }
.total-count { .total-count {
color: #52c41a; font-weight: 700;
font-weight: bold; color: #333;
} }
.total-duration { .total-duration {
color: #faad14; font-weight: 600;
font-weight: bold; color: #666;
} }
/* 分类列样式 - 第一行 */ /* 分类列样式 - 第一行(简洁灰调) */
.category-col { .category-col {
color: #fff; color: #333;
background-color: #f5f5f5;
font-weight: 600; font-weight: 600;
font-size: 12px; font-size: 12px;
padding: 10px 4px; padding: 10px 8px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
max-width: 200px;
}
/* 合计列样式 - 跨两行(简洁风格) */
.subtotal-header-col-fixed {
background-color: #f9f9f9;
color: #555;
font-weight: 600;
font-size: 11px;
padding: 10px 4px;
width: 60px;
min-width: 60px;
max-width: 60px;
border-left: 1px solid #d9d9d9;
border-right: 1px solid #e8e8e8;
} }
/* 活动列样式 - 第二行 */ /* 合计列样式 - 第二行备用(简洁风格) */
.subtotal-header-col {
background-color: #f9f9f9;
color: #555;
font-weight: 600;
font-size: 11px;
padding: 6px 4px;
width: 70px;
min-width: 70px;
max-width: 70px;
border-left: 1px solid #d9d9d9;
}
/* 活动列样式 - 第二行(简洁风格) */
.activity-col { .activity-col {
background-color: #f0f5ff; background-color: #fafafa;
color: #1890ff; color: #666;
font-weight: 500; font-weight: 500;
font-size: 10px; font-size: 10px;
padding: 6px 2px; padding: 6px 2px;
@ -487,41 +569,26 @@
font-size: 13px; font-size: 13px;
} }
/* 合计行样式 */ /* 合计行样式(简洁风格) */
.total-row { .total-row {
background-color: #fff2f0; background-color: #f5f5f5;
} }
.total-row td { .total-row td {
border-top: 2px solid #ff4d4f; border-top: 2px solid #bbb;
font-weight: 600;
} }
/* 数据行hover效果 */ /* 数据行hover效果(简洁) */
.header-table tbody tr:hover { .header-table tbody tr:hover {
background-color: #e6f7ff; background-color: #f9f9f9;
} }
/* 分页样式 */ /* 合计数据列样式(简洁) */
.pagination-wrapper { .subtotal-data-col {
display: flex; background-color: #f5f5f5;
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; color: #333;
font-weight: 600;
} }
/* 分页样式 */ /* 分页样式 */

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

@ -224,8 +224,7 @@ async function handleExport() {
const exportParams = { const exportParams = {
quarter: localQueryForm.quarter, quarter: localQueryForm.quarter,
year: localQueryForm.year, year: localQueryForm.year,
firmId: props.params?.firmId firmId: params.value?.firmId
//
// //
}; };

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

@ -12,20 +12,23 @@
<a-form class="smart-query-form"> <a-form class="smart-query-form">
<a-row class="smart-query-form-row"> <a-row class="smart-query-form-row">
<a-form-item label="执业机构" v-if="isAssociationRole || isCeo" class="smart-query-form-item"> <a-form-item label="执业机构" v-if="isAssociationRole || isCeo" class="smart-query-form-item">
<DepartmentTreeSelect style="width: 250px" v-model:value="queryForm.firmId" placeholder="请选择执业机构" /> <DepartmentTreeSelect style="width: 250px" v-model:value="queryForm.firmId" placeholder="请选择执业机构" @change="handleFirmChange" />
</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="isAssociationRole || isCtoRole || isCeo" class="smart-query-form-item">
<a-select <a-select
v-model:value="queryForm.userId" v-model:value="queryForm.userId"
style="width: 200px" style="width: 200px"
placeholder="请选择律师" placeholder="请选择律师"
:showSearch="true" :showSearch="true"
:allowClear="true" :allowClear="true"
:filterOption="filterLawyerOption" :filterOption="false"
optionFilterProp="children" optionFilterProp="children"
@focus="loadAllEmployees" @focus="loadEmployeesByFirm"
@search="handleLawyerSearch"
@change="handleLawyerChange"
@clear="handleLawyerClear"
> >
<a-select-option v-for="item in employeeList" :key="item.employeeId" :value="item.employeeId"> <a-select-option v-for="item in filteredEmployeeList" :key="item.employeeId" :value="item.employeeId">
{{ item.actualName }} {{ item.actualName }}
<template v-if="item.departmentName">{{ item.departmentName }}</template> <template v-if="item.departmentName">{{ item.departmentName }}</template>
<template v-if="item.positionName"> - {{ item.positionName }}</template> <template v-if="item.positionName"> - {{ item.positionName }}</template>
@ -189,19 +192,25 @@
:confirm-loading="auditLoading" :confirm-loading="auditLoading"
@ok="handleAudit" @ok="handleAudit"
@cancel="handleAuditCancel" @cancel="handleAuditCancel"
width="400px" width="560px"
> >
<div style="text-align: center; padding: 20px 0;"> <a-form :model="auditForm" layout="horizontal" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<p style="margin-bottom: 20px; font-size: 16px;">请选择审核结果</p> <a-form-item label="审批结果" required>
<a-radio-group v-model:value="auditForm.auditResult" size="large"> <a-radio-group v-model:value="auditForm.auditResult" size="large">
<a-radio :value="3" style="margin-right: 30px;"> <a-radio :value="3">同意</a-radio>
<span style="font-size: 16px;">同意</span> <a-radio :value="4">不同意</a-radio>
</a-radio>
<a-radio :value="4">
<span style="font-size: 16px;">拒绝</span>
</a-radio>
</a-radio-group> </a-radio-group>
</div> </a-form-item>
<a-form-item label="审批意见">
<a-textarea
v-model:value="auditForm.auditRemark"
placeholder="请输入审批意见(选填)"
rows="5"
:maxlength="200"
show-count
/>
</a-form-item>
</a-form>
</a-modal> </a-modal>
<!---------- 审核弹框 end -----------> <!---------- 审核弹框 end ----------->
@ -221,6 +230,16 @@
<a-radio :value="4">拒绝</a-radio> <a-radio :value="4">拒绝</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item label="审核意见(非必填)">
<a-textarea
v-model:value="batchAuditForm.auditRemark"
placeholder="请输入审核意见"
rows="3"
:maxlength="200"
show-count
style="resize: vertical;"
/>
</a-form-item>
<a-form-item label="选中记录"> <a-form-item label="选中记录">
<span style="color: #1890ff;">{{ selectedRowKeyList.length }} 条记录</span> <span style="color: #1890ff;">{{ selectedRowKeyList.length }} 条记录</span>
</a-form-item> </a-form-item>
@ -238,10 +257,10 @@
width="500px" width="500px"
> >
<a-form :model="rejectForm" layout="vertical"> <a-form :model="rejectForm" layout="vertical">
<a-form-item label="驳回原因" required> <a-form-item label="驳回原因(选填)">
<a-textarea <a-textarea
v-model:value="rejectForm.rejectReason" v-model:value="rejectForm.rejectReason"
placeholder="请输入驳回原因" placeholder="请输入驳回原因(选填)"
rows="4" rows="4"
:maxlength="200" :maxlength="200"
show-count show-count
@ -308,8 +327,8 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { reactive, ref, onMounted, computed } from 'vue'; import { reactive, ref, onMounted, computed, watch, nextTick } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading'; import { SmartLoading } from '/@/components/framework/smart-loading';
import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api'; import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api';
@ -463,6 +482,28 @@ import { getRoleInfo } from '/@/utils/role-util';
// //
const employeeList = ref([]); const employeeList = ref([]);
//
const searchKeyword = ref('');
//
const filteredEmployeeList = computed(() => {
let result = employeeList.value;
//
if (queryForm.firmId) {
result = result.filter(item => item.departmentId == queryForm.firmId);
}
//
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase();
result = result.filter(item =>
(item.actualName && item.actualName.toLowerCase().includes(keyword)) ||
(item.departmentName && item.departmentName.toLowerCase().includes(keyword))
);
}
return result;
});
// //
const auditModalVisible = ref(false); const auditModalVisible = ref(false);
@ -577,6 +618,11 @@ import { getRoleInfo } from '/@/utils/role-util';
return optionText.includes(input.toLowerCase()); return optionText.includes(input.toLowerCase());
} }
//
function handleLawyerSearch(keyword) {
searchKeyword.value = keyword;
}
// //
@ -607,6 +653,7 @@ import { getRoleInfo } from '/@/utils/role-util';
await serviceApplicationsApi.batchReview(auditData); await serviceApplicationsApi.batchReview(auditData);
message.success('批量审核成功'); message.success('批量审核成功');
batchAuditModalVisible.value = false; batchAuditModalVisible.value = false;
queryData(); // queryData(); //
selectedRowKeyList.value = []; // selectedRowKeyList.value = []; //
@ -797,6 +844,35 @@ import { getRoleInfo } from '/@/utils/role-util';
console.error('加载员工数据失败:', e); console.error('加载员工数据失败:', e);
} }
} }
//
async function loadEmployeesByFirm() {
try {
if (employeeList.value.length === 0) {
await loadAllEmployees();
}
} catch (e) {
console.error('加载员工数据失败:', e);
}
}
//
function handleFirmChange(firmId) {
//
queryForm.userId = undefined;
searchKeyword.value = '';
}
//
function handleLawyerChange() {
searchKeyword.value = '';
}
//
function handleLawyerClear() {
searchKeyword.value = '';
}
// //
const total = ref(0); const total = ref(0);
@ -805,6 +881,7 @@ import { getRoleInfo } from '/@/utils/role-util';
let pageSize = queryForm.pageSize; let pageSize = queryForm.pageSize;
Object.assign(queryForm, queryFormState); Object.assign(queryForm, queryFormState);
queryForm.pageSize = pageSize; queryForm.pageSize = pageSize;
searchKeyword.value = ''; //
queryData(); queryData();
} }
@ -876,26 +953,37 @@ import { getRoleInfo } from '/@/utils/role-util';
} }
// ---------------------------- / ----------------------------
const formRef = ref();
const costReportFormRef = ref();
const agreementModalRef = ref();
const pendingFormData = ref(null); //
// //
const route = useRoute(); const route = useRoute();
const router = useRouter();
onMounted(async () => { onMounted(async () => {
await getLoginInfo(); await getLoginInfo();
// URLfirmId
const firmId = route.query.firmId; const firmId = route.query.firmId;
if (firmId) { if (firmId) {
// firmId
queryForm.firmId = firmId; queryForm.firmId = firmId;
} }
//
queryData(); queryData();
loadAllEmployees();
handleRouteApplicationDetail(route.query.applicationId);
}); });
watch(
() => route.query.applicationId,
(applicationId) => {
handleRouteApplicationDetail(applicationId);
}
);
// ---------------------------- ---------------------------- // ---------------------------- ----------------------------
const agreementModalRef = ref();
const pendingFormData = ref(null); //
// //
function showAgreementModal(data) { function showAgreementModal(data) {
@ -914,9 +1002,33 @@ onMounted(async () => {
pendingFormData.value = null; pendingFormData.value = null;
} }
// ---------------------------- / ---------------------------- async function handleRouteApplicationDetail(applicationId) {
const formRef = ref(); if (!applicationId) {
const costReportFormRef = ref(); return;
}
try {
const detailResult = await serviceApplicationsApi.queryDetail(applicationId);
if (detailResult.data) {
// DOM 使 setTimeout Modal
await new Promise(resolve => setTimeout(resolve, 500));
if (formRef.value && typeof formRef.value.show === 'function') {
formRef.value.show(detailResult.data, true);
} else {
console.error('formRef 未初始化或 show 方法不存在');
}
}
} catch (error) {
console.error('消息跳转加载服务申报详情失败:', error);
} finally {
if (route.query.applicationId) {
const nextQuery = { ...route.query };
delete nextQuery.applicationId;
router.replace({ path: route.path, query: nextQuery });
}
}
}
function showForm(data) { function showForm(data) {
console.log('showForm传入的data:', data); console.log('showForm传入的data:', data);
@ -1049,14 +1161,17 @@ function showAuditModal(record) {
auditLoading.value = true; auditLoading.value = true;
// 使
const auditData = { const auditData = {
applicationId: currentAuditRecord.value.applicationId, applicationId: currentAuditRecord.value.applicationId,
firmAuditStatus: auditForm.auditResult [isAssociationRole.value ? 'associationAuditStatus' : 'firmAuditStatus']: auditForm.auditResult,
[isAssociationRole.value ? 'associationAuditOpinion' : 'firmAuditOpinion']: auditForm.auditRemark
}; };
try { try {
await serviceApplicationsApi.review(auditData); await serviceApplicationsApi.review(auditData);
message.success('审核成功'); message.success('审核成功');
auditModalVisible.value = false; auditModalVisible.value = false;
queryData(); queryData();
} catch (error) { } catch (error) {
@ -1403,11 +1518,6 @@ function showAuditModal(record) {
return; return;
} }
if (!rejectForm.rejectReason.trim()) {
message.warning('请输入驳回原因');
return;
}
rejectLoading.value = true; rejectLoading.value = true;
// //

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

@ -248,8 +248,8 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { reactive, ref, onMounted, computed } from 'vue'; import { reactive, ref, onMounted, computed, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading'; import { SmartLoading } from '/@/components/framework/smart-loading';
import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api'; import { serviceApplicationsApi } from '/@/api/business/service-applications/service-applications-api';
@ -731,6 +731,7 @@ import { getRoleInfo } from '/@/utils/role-util';
// //
const route = useRoute(); const route = useRoute();
const router = useRouter();
onMounted(async () => { onMounted(async () => {
await getLoginInfo(); await getLoginInfo();
@ -743,9 +744,18 @@ onMounted(async () => {
} }
// //
queryData(); await queryData();
loadAllEmployees();
handleRouteApplicationDetail(route.query.applicationId);
}); });
watch(
() => route.query.applicationId,
(applicationId) => {
handleRouteApplicationDetail(applicationId);
}
);
// ---------------------------- ---------------------------- // ---------------------------- ----------------------------
const agreementModalRef = ref(); const agreementModalRef = ref();
const pendingFormData = ref(null); // const pendingFormData = ref(null); //
@ -802,6 +812,33 @@ onMounted(async () => {
} }
} }
async function handleRouteApplicationDetail(applicationId) {
if (!applicationId) {
return;
}
try {
const detailResult = await serviceApplicationsApi.queryDetail(applicationId);
if (detailResult.data) {
await new Promise(resolve => setTimeout(resolve, 500));
if (formRef.value && typeof formRef.value.show === 'function') {
formRef.value.show(detailResult.data, true);
} else {
console.error('formRef 未初始化或 show 方法不存在');
}
}
} catch (error) {
console.error('消息跳转加载服务申报详情失败:', error);
} finally {
if (route.query.applicationId) {
const nextQuery = { ...route.query };
delete nextQuery.applicationId;
router.replace({ path: route.path, query: nextQuery });
}
}
}
// ---------------------------- ---------------------------- // ---------------------------- ----------------------------
// //
function onSubmit(data){ function onSubmit(data){

67
src/views/system/home/home-notice.vue

@ -1,60 +1,57 @@
<!-- <!--
* 首页的 通知公告 * 首页的 消息
* *
--> -->
<template> <template>
<default-home-card extra="更多" icon="SoundOutlined" title="通知公告" @extraClick="onMore"> <default-home-card extra="更多" icon="BellOutlined" title="消息通知" @extraClick="onMore">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div class="content-wrapper" style="height: 280px"> <div class="content-wrapper" style="height: 280px">
<a-empty v-if="$lodash.isEmpty(data)" /> <a-empty v-if="$lodash.isEmpty(data)" />
<ul v-else> <ul v-else>
<li v-for="(item, index) in data" :key="index" :class="[item.viewFlag ? 'read' : 'un-read']"> <li v-for="(item, index) in data" :key="index" :class="[item.readFlag ? 'read' : 'un-read']">
<a-tooltip placement="top"> <a-tooltip placement="top">
<template #title> <template #title>
<span>{{ item.title }}</span> <span>{{ item.title }}</span>
</template> </template>
<a class="content" @click="toDetail(item.noticeId)"> <a class="content" @click="toDetail(item)">
<a-badge :status="item.viewFlag ? 'default' : 'error'" /> <a-badge :status="item.readFlag ? 'default' : 'error'" />
{{ item.title }} {{ item.title }}
</a> </a>
</a-tooltip> </a-tooltip>
<span class="time"> {{ item.publishDate }}</span> <span class="time"> {{ item.createTime }}</span>
</li> </li>
</ul> </ul>
</div> </div>
</a-spin> </a-spin>
</default-home-card> </default-home-card>
<!-- 消息详情弹框 -->
<header-message-detail-modal ref="messageDetailModalRef" @refresh="queryMessageList" />
</template> </template>
<script setup> <script setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { noticeApi } from '/@/api/business/oa/notice-api'; import { messageApi } from '/@/api/support/message-api';
import { smartSentry } from '/@/lib/smart-sentry'; import { smartSentry } from '/@/lib/smart-sentry';
import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue'; import DefaultHomeCard from '/@/views/system/home/components/default-home-card.vue';
import HeaderMessageDetailModal from '/@/layout/components/header-user-space/header-message-detail-modal.vue';
const props = defineProps({ const router = useRouter();
noticeTypeId: { const messageDetailModalRef = ref();
type: Number,
default: 1,
},
});
const queryForm = {
noticeTypeId: props.noticeTypeId,
pageNum: 1,
pageSize: 6,
searchCount: false,
};
let data = ref([]); let data = ref([]);
const loading = ref(false); const loading = ref(false);
//
async function queryNoticeList() { async function queryMessageList() {
try { try {
loading.value = true; loading.value = true;
const result = await noticeApi.queryEmployeeNotice(queryForm); const result = await messageApi.queryMessage({
data.value = result.data.list; pageNum: 1,
pageSize: 6,
readFlag: null
});
data.value = result.data.list || [];
} catch (err) { } catch (err) {
smartSentry.captureError(err); smartSentry.captureError(err);
} finally { } finally {
@ -63,23 +60,25 @@
} }
onMounted(() => { onMounted(() => {
queryNoticeList(); queryMessageList();
}); });
//
function onMore() { function onMore() {
router.push({ router.push({
path: '/oa/notice/notice-employee-list', path: '/account',
query: { menuId: 'message' }
}); });
} }
// async function toDetail(item) {
const router = useRouter(); if (item?.messageId) {
function toDetail(noticeId) { await messageApi.updateReadFlag(item.messageId);
router.push({ //
path: '/oa/notice/notice-employee-detail', queryMessageList();
query: { noticeId }, }
});
//
messageDetailModalRef.value.show(item);
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

Loading…
Cancel
Save