Browse Source

feat:优化

master
wang 2 weeks ago
parent
commit
da2fe9ec41
  1. 7
      src/api/business/cost/firm-reports-api.js
  2. 12
      src/api/business/penalty-apply/penalty-apply-api.js
  3. 7
      src/api/business/service-applications/service-applications-api.js
  4. 7
      src/api/system/login-api.js
  5. BIN
      src/assets/images/login/login-min.gif
  6. BIN
      src/assets/images/login/login.gif
  7. BIN
      src/assets/images/login/loginnew.jpeg
  8. BIN
      src/assets/images/login/loginold.gif
  9. BIN
      src/assets/images/login/mi1n11.gif
  10. 28
      src/components/support/file-upload/index.vue
  11. 30
      src/components/system/service-count/excel-statistics-detail.vue
  12. 2
      src/layout/components/side-expand-menu/sub-menu.vue
  13. 12
      src/layout/components/side-menu/recursion-menu.vue
  14. 2
      src/layout/components/side-menu/sub-menu.vue
  15. 2
      src/layout/components/smart-footer/index.vue
  16. 2
      src/layout/components/top-expand-menu/sub-menu.vue
  17. 2
      src/layout/components/top-menu/sub-menu.vue
  18. 13
      src/views/business/erp/cost/firm-reports-form.vue
  19. 5
      src/views/business/erp/goods/components/goods-form-modal.vue
  20. 18
      src/views/business/erp/goods/goods-list.vue
  21. 8
      src/views/business/erp/penalty-apply/penalty-apply-form.vue
  22. 155
      src/views/business/erp/penalty-apply/penalty-apply-list.vue
  23. 41
      src/views/business/erp/service/firm-statistics-detail.vue
  24. 404
      src/views/business/erp/service/service-applications-form.vue
  25. 113
      src/views/business/erp/service/service-applications-list.vue
  26. 27
      src/views/business/erp/service/service-applications-report-list.vue
  27. 1063
      src/views/mobile/service/create.vue
  28. 34
      src/views/mobile/service/detail.vue
  29. 8
      src/views/system/account/components/message/index.vue
  30. 9
      src/views/system/employee/components/employee-form-modal/index.vue
  31. 21
      src/views/system/login/login.vue
  32. 4
      src/views/system/login3/login.vue

7
src/api/business/cost/firm-reports-api.js

@ -59,6 +59,13 @@ export const firmReportsApi = {
return getRequest('/firmReports/query'); return getRequest('/firmReports/query');
}, },
queryByQuarter: (year, quarter) => {
return getRequest(`/firmReports/queryByQuarter?year=${year}&quarter=${quarter}`);
},
firmHasCostPermission: () => {
return getRequest('/firmReports/firmHasCostPermission');
},
/** /**
* 查询本年度已经提交的收入 @author wzh * 查询本年度已经提交的收入 @author wzh
*/ */

12
src/api/business/penalty-apply/penalty-apply-api.js

@ -1,5 +1,5 @@
/** /**
* 无处罚证明申请主表 api 封装 * 无处分证明申请主表 api 封装
* *
* @Author: wzh * @Author: wzh
* @Date: 2026-01-07 21:36:44 * @Date: 2026-01-07 21:36:44
@ -9,11 +9,10 @@ import axios from 'axios';
import { postRequest, getRequest, getDownload } from '/@/lib/axios'; import { postRequest, getRequest, getDownload } from '/@/lib/axios';
export const penaltyApplyApi = { export const penaltyApplyApi = {
/** /**
* 分页查询 @author wzh * 分页查询 @author wzh
*/ */
queryPage : (param) => { queryPage: (param) => {
return postRequest('/penaltyApply/queryPage', param); return postRequest('/penaltyApply/queryPage', param);
}, },
@ -31,7 +30,6 @@ export const penaltyApplyApi = {
return postRequest('/penaltyApply/update', param); return postRequest('/penaltyApply/update', param);
}, },
/** /**
* 删除 @author wzh * 删除 @author wzh
*/ */
@ -47,22 +45,20 @@ export const penaltyApplyApi = {
}, },
/** /**
* 无处罚证明下载 @author wzh * 无处分证明下载 @author wzh
*/ */
exportWordCertificate: (id) => { exportWordCertificate: (id) => {
return getDownload(`/wordCertificate/export/${id}`); return getDownload(`/wordCertificate/export/${id}`);
}, },
/** /**
* 预览无处罚证明 @author wzh * 预览无处分证明 @author wzh
*/ */
/** /**
* 审核 @author wzh * 审核 @author wzh
*/ */
review: (param) => { review: (param) => {
return postRequest('/penaltyApply/review', param); return postRequest('/penaltyApply/review', param);
}, },
}; };

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

@ -205,4 +205,11 @@ export const serviceApplicationsApi = {
exportFirmActivityStatistics: (params) => { exportFirmActivityStatistics: (params) => {
return postDownload('/serviceApplications/export/firmActivityCount', params); return postDownload('/serviceApplications/export/firmActivityCount', params);
}, },
/**
* 获取同律所已签署承诺书的合格同行律师列表
*/
querySameFirmLawyers: () => {
return getRequest('/serviceApplications/querySameFirmLawyers');
},
}; };

7
src/api/system/login-api.js

@ -51,4 +51,11 @@ export const loginApi = {
getTwoFactorLoginFlag: () => { getTwoFactorLoginFlag: () => {
return getRequest('/login/getTwoFactorLoginFlag'); return getRequest('/login/getTwoFactorLoginFlag');
}, },
/**
* 获取初始密码提示
*/
getInitialPassword: () => {
return getRequest('/login/getInitialPassword');
},
}; };

BIN
src/assets/images/login/login-min.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
src/assets/images/login/login.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
src/assets/images/login/loginnew.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

BIN
src/assets/images/login/loginold.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

BIN
src/assets/images/login/mi1n11.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

28
src/components/support/file-upload/index.vue

@ -240,19 +240,6 @@
// 图片类型的后缀名 // 图片类型的后缀名
const imgFileType = ['jpg', 'jpeg', 'png', 'gif','.PNG','.GIF','.JPG']; const imgFileType = ['jpg', 'jpeg', 'png', 'gif','.PNG','.GIF','.JPG'];
// 重新修改图片展示字段
const files = computed(() => {
let res = [];
if (props.defaultFileList && props.defaultFileList.length > 0) {
props.defaultFileList.forEach((element) => {
element.url = element.fileUrl;
element.name = element.fileName;
res.push(element);
});
return res;
}
return res;
});
// -------------------- 逻辑 -------------------- // -------------------- 逻辑 --------------------
const previewVisible = ref(false); const previewVisible = ref(false);
@ -292,14 +279,19 @@
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(previewFileType.value.toLowerCase()); return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(previewFileType.value.toLowerCase());
}); });
// 弹窗复用、异步查询附件时都重新构造列表;不能直接修改 props 中的文件对象。
watch( watch(
files, () => props.defaultFileList,
(value) => { (value) => {
fileList.value = value; fileList.value = (value || []).map((element) => ({
...element,
uid: element.uid || element.fileId,
url: element.url || element.fileUrl,
name: element.name || element.fileName,
status: element.status || 'done',
}));
}, },
{ { immediate: true, deep: true }
immediate: true,
}
); );
const emit = defineEmits(['update:value', 'change']); const emit = defineEmits(['update:value', 'change']);

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

@ -114,6 +114,7 @@ import { ref, reactive, computed, onMounted } from 'vue';
import { ExportOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'; import { ExportOutlined, 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 { getRoleInfo } from '/@/utils/role-util';
// 接收父组件传递的参数 // 接收父组件传递的参数
const props = defineProps({ const props = defineProps({
@ -338,6 +339,35 @@ async function handleExport() {
} }
} }
// 律所汇总导出(仅律协角色可见)
async function handleExportDepartment() {
if (tableData.value.length === 0) {
message.warning('暂无数据可导出');
return;
}
exportDepartmentLoading.value = true;
try {
console.log('开始导出律所汇总统计...');
const exportParams = {
quarter: localQueryForm.quarter,
year: localQueryForm.year,
lawyerName: localQueryForm.lawyerName,
firmName: localQueryForm.firmName
};
// 调用律所汇总导出接口
await serviceApplicationsApi.exportLawyerByDepartment(exportParams);
message.success('导出成功');
console.log('律所汇总统计导出成功');
} catch (error) {
message.error('导出失败');
console.error('导出律所汇总统计失败:', error);
} finally {
exportDepartmentLoading.value = false;
}
}
onMounted(() => { onMounted(() => {
initYearOptions(); initYearOptions();
// 如果没有传入数据,则自动查询 // 如果没有传入数据,则自动查询

2
src/layout/components/side-expand-menu/sub-menu.vue

@ -15,7 +15,7 @@
<template #title>{{ props.menuInfo.menuName }}</template> <template #title>{{ props.menuInfo.menuName }}</template>
<template v-for="item in props.menuInfo.children" :key="item.menuId"> <template v-for="item in props.menuInfo.children" :key="item.menuId">
<template v-if="item.visibleFlag"> <template v-if="item.visibleFlag">
<template v-if="!item.children"> <template v-if="!item.children || item.children.length === 0">
<a-menu-item :key="item.menuId.toString()" @click="turnToPage(item)"> <a-menu-item :key="item.menuId.toString()" @click="turnToPage(item)">
<template #icon> <template #icon>
<component :is="$antIcons[item.icon]" /> <component :is="$antIcons[item.icon]" />

12
src/layout/components/side-menu/recursion-menu.vue

@ -65,7 +65,10 @@
*/ */
function updateOpenKeysAndSelectKeys() { function updateOpenKeysAndSelectKeys() {
// 更新选中 // 更新选中
selectedKeys.value = [_.toNumber(currentRoute.name)]; const nextSelectedKeys = [_.toNumber(currentRoute.name)];
if (!_.isEqual(selectedKeys.value, nextSelectedKeys)) {
selectedKeys.value = nextSelectedKeys;
}
/** /**
* 更新展开(1、获取新展开的menu key集合;2、保留原有的openkeys,然后把新展开的与之合并) * 更新展开(1、获取新展开的menu key集合;2、保留原有的openkeys,然后把新展开的与之合并)
@ -81,10 +84,15 @@
let needOpenKeys = _.map(parentList, 'name').map(Number); let needOpenKeys = _.map(parentList, 'name').map(Number);
if (menuSingleExpandFlag.value) { if (menuSingleExpandFlag.value) {
if (!_.isEqual(openKeys.value, needOpenKeys)) {
openKeys.value = [...needOpenKeys]; openKeys.value = [...needOpenKeys];
}
} else { } else {
// 使用lodash的union函数,进行 去重合并两个数组 // 使用lodash的union函数,进行 去重合并两个数组
openKeys.value = _.union(openKeys.value, needOpenKeys); const nextOpenKeys = _.union(openKeys.value, needOpenKeys);
if (!_.isEqual(openKeys.value, nextOpenKeys)) {
openKeys.value = nextOpenKeys;
}
} }
} }

2
src/layout/components/side-menu/sub-menu.vue

@ -10,7 +10,7 @@
<template #title>{{ menuInfo.menuName }}</template> <template #title>{{ menuInfo.menuName }}</template>
<template v-for="item in menuInfo.children" :key="item.menuId"> <template v-for="item in menuInfo.children" :key="item.menuId">
<template v-if="item.visibleFlag && !item.disabledFlag"> <template v-if="item.visibleFlag && !item.disabledFlag">
<template v-if="!item.children"> <template v-if="!item.children || item.children.length === 0">
<a-menu-item :key="item.menuId" @click="turnToPage(item)"> <a-menu-item :key="item.menuId" @click="turnToPage(item)">
<template #icon> <template #icon>
<component :is="$antIcons[item.icon]" /> <component :is="$antIcons[item.icon]" />

2
src/layout/components/smart-footer/index.vue

@ -3,7 +3,7 @@
--> -->
<template> <template>
<div class="version"> <div class="version">
<a target="_blank" class="smart-copyright">{{ currentYear }}@ 安徽三朵云信息科技有限公司</a><a style="margin-left:20px">服务经理:王经理</a><a style="margin-left:10px">联系方式:18226700205</a> <a target="_blank" class="smart-copyright">{{ currentYear }}@ 安徽三朵云信息科技有限公司</a><a style="margin-left:20px">技术人员:</a><a style="margin-left:10px">联系方式:18755103557</a>
<a style="margin-left:20px">应急联系人:耿经理</a><a style="margin-left:10px">应急电话:15255503352</a> <a style="margin-left:20px">应急联系人:耿经理</a><a style="margin-left:10px">应急电话:15255503352</a>
</div> </div>
</template> </template>

2
src/layout/components/top-expand-menu/sub-menu.vue

@ -15,7 +15,7 @@
<template #title>{{ props.menuInfo.menuName }}</template> <template #title>{{ props.menuInfo.menuName }}</template>
<template v-for="item in props.menuInfo.children" :key="item.menuId"> <template v-for="item in props.menuInfo.children" :key="item.menuId">
<template v-if="item.visibleFlag"> <template v-if="item.visibleFlag">
<template v-if="!item.children"> <template v-if="!item.children || item.children.length === 0">
<a-menu-item :key="item.menuId.toString()" @click="turnToPage(item)"> <a-menu-item :key="item.menuId.toString()" @click="turnToPage(item)">
<template #icon> <template #icon>
<component :is="$antIcons[item.icon]" /> <component :is="$antIcons[item.icon]" />

2
src/layout/components/top-menu/sub-menu.vue

@ -15,7 +15,7 @@
<template #title>{{ menuInfo.menuName }}</template> <template #title>{{ menuInfo.menuName }}</template>
<template v-for="item in menuInfo.children" :key="item.menuId"> <template v-for="item in menuInfo.children" :key="item.menuId">
<template v-if="item.visibleFlag && !item.disabledFlag"> <template v-if="item.visibleFlag && !item.disabledFlag">
<template v-if="!item.children"> <template v-if="!item.children || item.children.length === 0">
<a-menu-item :key="item.menuId" @click="turnToPage(item)"> <a-menu-item :key="item.menuId" @click="turnToPage(item)">
<template #icon> <template #icon>
<component :is="$antIcons[item.icon]" /> <component :is="$antIcons[item.icon]" />

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

@ -187,7 +187,16 @@
const quarterOptions = ref([]); const quarterOptions = ref([]);
// 初始化季度选项,只能选择上一个季度 // 初始化季度选项,只能选择上一个季度
async function initQuarterOptions() { async function initQuarterOptions(presetPeriod) {
if (presetPeriod?.year && presetPeriod?.quarter) {
form.declareYear = Number(presetPeriod.year);
form.declareQuarter = Number(presetPeriod.quarter);
quarterOptions.value = [
{ label: `第${form.declareQuarter}季度`, value: form.declareQuarter }
];
return;
}
const currentDate = new Date(); const currentDate = new Date();
const currentYear = currentDate.getFullYear(); const currentYear = currentDate.getFullYear();
const currentMonth = currentDate.getMonth() + 1; const currentMonth = currentDate.getMonth() + 1;
@ -292,7 +301,7 @@
await fetchAnnualActualCost(); await fetchAnnualActualCost();
await getPublicWelfareCost(); await getPublicWelfareCost();
} else { } else {
await initQuarterOptions(); await initQuarterOptions(rowData);
await fetchAnnualIncome(); await fetchAnnualIncome();
await fetchAnnualActualCost(); await fetchAnnualActualCost();
await getPublicWelfareCost(); await getPublicWelfareCost();

5
src/views/business/erp/goods/components/goods-form-modal.vue

@ -27,6 +27,10 @@
style="width: 100%" style="width: 100%"
/> />
</a-form-item> </a-form-item>
<a-form-item label="允许多人代报" name="canBatchSelect">
<a-switch v-model:checked="form.canBatchSelect" checked-children="允许" un-checked-children="禁止" />
<div style="color: #8c8c8c; font-size: 12px; margin-top: 4px;">开启后,申报该活动时可勾选同律所同行一并申报。</div>
</a-form-item>
<a-form-item label="备注" name="remark"> <a-form-item label="备注" name="remark">
<a-input style="width: 100%" placeholder="请输入备注" v-model:value="form.remark" /> <a-input style="width: 100%" placeholder="请输入备注" v-model:value="form.remark" />
</a-form-item> </a-form-item>
@ -81,6 +85,7 @@
shelvesFlag: true, shelvesFlag: true,
//计算类型 //计算类型
timeType: undefined, timeType: undefined,
canBatchSelect: false,
//备注 //备注
remark: '', remark: '',
//活动id //活动id

18
src/views/business/erp/goods/goods-list.vue

@ -229,14 +229,10 @@
sorter: true, sorter: true,
width: 250, width: 250,
ellipsis: true, ellipsis: true,
customRender: ({ text }) => { customRender: ({ text }) => text,
return { customCell: (record) => ({
children: text, title: record.categoryName,
attrs: { }),
title: text
}
};
}
}, },
{ {
title: '活动名称', title: '活动名称',
@ -275,6 +271,12 @@
width: 100, width: 100,
customRender: ({ text }) => h(DictLabel, { dictCode: 'COMPUTATIONTYPE', dataValue: text }), customRender: ({ text }) => h(DictLabel, { dictCode: 'COMPUTATIONTYPE', dataValue: text }),
}, },
{
title: '多人代报',
dataIndex: 'canBatchSelect',
width: 100,
customRender: ({ text }) => text ? '允许' : '禁止',
},
{ {
title: '创建时间', title: '创建时间',
dataIndex: 'createTime', dataIndex: 'createTime',

8
src/views/business/erp/penalty-apply/penalty-apply-form.vue

@ -1,5 +1,5 @@
<!-- <!--
* 无处罚证明申请主表 * 无处分证明申请主表
* *
* @Author: wzh * @Author: wzh
* @Date: 2026-01-07 21:36:44 * @Date: 2026-01-07 21:36:44
@ -7,14 +7,14 @@
--> -->
<template> <template>
<a-modal <a-modal
:title="form.id ? '编辑无处罚证明申请' : '新增无处罚证明申请'" :title="form.id ? '编辑无处分证明申请' : '新增无处分证明申请'"
:width="500" :width="500"
:open="visibleFlag" :open="visibleFlag"
@cancel="onClose" @cancel="onClose"
:maskClosable="false" :maskClosable="false"
:destroyOnClose="true" :destroyOnClose="true"
> >
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 6 }" > <a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 6 }">
<!-- 只有律所主任显示申请类型选择(个人/律所),行政人员只申请律所证明 --> <!-- 只有律所主任显示申请类型选择(个人/律所),行政人员只申请律所证明 -->
<a-form-item v-if="showApplyTypeSelect" label="申请类型" name="applyType"> <a-form-item v-if="showApplyTypeSelect" label="申请类型" name="applyType">
<a-radio-group v-model:value="form.applyType" @change="onApplyTypeChange"> <a-radio-group v-model:value="form.applyType" @change="onApplyTypeChange">
@ -61,7 +61,7 @@
userId: userStore.userId, userId: userStore.userId,
userName: userStore.actualName, userName: userStore.actualName,
departmentName: userStore.departmentName, departmentName: userStore.departmentName,
roleCodeList: userStore.roleCodeList || [] roleCodeList: userStore.roleCodeList || [],
})); }));
// 判断是否为律所主任(CTO角色) // 判断是否为律所主任(CTO角色)

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

@ -1,5 +1,5 @@
<!-- <!--
* 无处罚证明申请主表 * 无处分证明申请主表
* *
* @Author: wzh * @Author: wzh
* @Date: 2026-01-07 21:36:44 * @Date: 2026-01-07 21:36:44
@ -45,26 +45,26 @@
<!---------- 表格操作行 begin -----------> <!---------- 表格操作行 begin ----------->
<a-row class="smart-table-btn-block"> <a-row class="smart-table-btn-block">
<div class="smart-table-operate-block"> <div class="smart-table-operate-block">
<!-- 律师申请无处罚证明 --> <!-- 律师申请无处分证明 -->
<a-button @click="() => showForm('user')" type="primary" v-if="isUser"> <a-button @click="() => showForm('user')" type="primary" v-if="isUser">
<template #icon> <template #icon>
<PlusOutlined /> <PlusOutlined />
</template> </template>
申请无处罚证明 申请无处分证明
</a-button> </a-button>
<!-- 律所主任申请个人无处罚证明 --> <!-- 律所主任申请个人无处分证明 -->
<a-button @click="() => showForm('personal')" type="primary" v-if="isCto"> <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 />
</template> </template>
申请律所无处罚证明 申请律所无处分证明
</a-button> </a-button>
</div> </div>
<div class="smart-table-setting-block"> <div class="smart-table-setting-block">
@ -94,10 +94,18 @@
<a-button type="link" size="small" @click="handlePreview(record)" v-if="isCeo">预览</a-button> <a-button type="link" size="small" @click="handlePreview(record)" v-if="isCeo">预览</a-button>
<!-- CTO角色和行政人员:判断status字段,状态为1(已提交)时显示审批按钮 --> <!-- CTO角色和行政人员:判断status字段,状态为1(已提交)时显示审批按钮 -->
<a-button type="link" size="small" @click="showAuditModal(record)" :disabled="record.status !== 1" v-if="isCto || isFirmAdmin">审批</a-button> <a-button type="link" size="small" @click="showAuditModal(record)" :disabled="record.status !== 1" v-if="isCto || isFirmAdmin"
>审批</a-button
>
<!-- 用户角色:当auditStatus为3(已批准)且当前用户是文件所有者时显示下载 --> <!-- 用户角色:当auditStatus为3(已批准)且当前用户是文件所有者时显示下载 -->
<a-button type="link" size="small" @click="handleDownload(record)" v-if="record.auditStatus === 3 && !isCeo && record.userId === loginInfo?.userId">下载</a-button> <a-button
type="link"
size="small"
@click="handleDownload(record)"
v-if="record.auditStatus === 3 && !isCeo && record.userId === loginInfo?.userId"
>下载</a-button
>
</a-space> </a-space>
</div> </div>
</template> </template>
@ -121,7 +129,7 @@
/> />
</div> </div>
<PenaltyApplyForm ref="formRef" @reloadList="queryData"/> <PenaltyApplyForm ref="formRef" @reloadList="queryData" />
<!-- 图片预览弹框 --> <!-- 图片预览弹框 -->
<a-modal <a-modal
@ -137,12 +145,7 @@
> >
<div class="preview-container"> <div class="preview-container">
<div class="preview-content"> <div class="preview-content">
<img <img :src="previewUrl" :alt="previewTitle" class="preview-image" @error="handleImageError" />
:src="previewUrl"
:alt="previewTitle"
class="preview-image"
@error="handleImageError"
/>
</div> </div>
</div> </div>
</a-modal> </a-modal>
@ -166,100 +169,98 @@
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
</a-card> </a-card>
</template> </template>
<style scoped> <style scoped>
.preview-container { .preview-container {
text-align: center; text-align: center;
padding: 0; padding: 0;
background: #fff; background: #fff;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
} }
.preview-header { .preview-header {
padding: 16px 20px; padding: 16px 20px;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
} }
.preview-title { .preview-title {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: #2c3e50; color: #2c3e50;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
.preview-content { .preview-content {
padding: 20px; padding: 20px;
min-height: 200px; min-height: 200px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: #f8f9fa; background: #f8f9fa;
} }
.preview-image { .preview-image {
max-width: 95vw; max-width: 95vw;
max-height: 85vh; max-height: 85vh;
object-fit: contain; object-fit: contain;
border-radius: 6px; border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
.preview-image:hover { .preview-image:hover {
transform: scale(1.01); transform: scale(1.01);
} }
.preview-footer { .preview-footer {
padding: 12px 20px; padding: 12px 20px;
border-top: 1px solid #f0f0f0; border-top: 1px solid #f0f0f0;
background: #fafafa; background: #fafafa;
} }
.preview-tip { .preview-tip {
font-size: 12px; font-size: 12px;
color: #7f8c8d; color: #7f8c8d;
font-style: italic; font-style: italic;
} }
/* 弹框样式优化 */ /* 弹框样式优化 */
:deep(.image-preview-modal .ant-modal-content) { :deep(.image-preview-modal .ant-modal-content) {
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
} }
:deep(.image-preview-modal .ant-modal-close) { :deep(.image-preview-modal .ant-modal-close) {
top: 12px; top: 12px;
right: 12px; right: 12px;
color: #666; color: #666;
} }
:deep(.image-preview-modal .ant-modal-close:hover) { :deep(.image-preview-modal .ant-modal-close:hover) {
color: #333; color: #333;
background: rgba(0, 0, 0, 0.05); background: rgba(0, 0, 0, 0.05);
} }
</style> </style>
<script setup> <script setup>
import { reactive, ref, onMounted } from 'vue'; import { reactive, ref, onMounted } 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 { penaltyApplyApi } from '/@/api/business/penalty-apply/penalty-apply-api'; import { penaltyApplyApi } from '/@/api/business/penalty-apply/penalty-apply-api';
import { loginApi } from '/@/api/system/login-api'; import { loginApi } from '/@/api/system/login-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const'; import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { REVIEW_ENUM } from '/@/constants/system/review-const'; import { REVIEW_ENUM } from '/@/constants/system/review-const';
import { smartSentry } from '/@/lib/smart-sentry'; import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue'; import TableOperator from '/@/components/support/table-operator/index.vue';
import PenaltyApplyForm from './penalty-apply-form.vue'; import PenaltyApplyForm from './penalty-apply-form.vue';
// ---------------------------- 表格列 ---------------------------- // ---------------------------- 表格列 ----------------------------
const columns = ref([ const columns = ref([
{ {
title: '申请人', title: '申请人',
dataIndex: 'userName', dataIndex: 'userName',
@ -282,15 +283,15 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
}, },
}, },
{ {
title: '无处罚证明类型', title: '无处分证明类型',
dataIndex: 'type', dataIndex: 'type',
ellipsis: true, ellipsis: true,
customRender: ({ text, record }) => { customRender: ({ text, record }) => {
// 根据 applyType 字段值显示对应的证明类型 // 根据 applyType 字段值显示对应的证明类型
if (record.applyType === 'PERSONAL') { if (record.applyType === 'PERSONAL') {
return '个人无处罚证明'; return '个人无处分证明';
} else if (record.applyType === 'FIRM') { } else if (record.applyType === 'FIRM') {
return '律所无处罚证明'; return '律所无处分证明';
} else { } else {
return '未知类型'; return '未知类型';
} }
@ -311,7 +312,7 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
dataIndex: 'status', dataIndex: 'status',
ellipsis: true, ellipsis: true,
customRender: ({ text }) => { customRender: ({ text }) => {
const status = Object.values(REVIEW_ENUM).find(item => item.value === text); const status = Object.values(REVIEW_ENUM).find((item) => item.value === text);
return status ? status.desc : text; return status ? status.desc : text;
}, },
}, },
@ -320,7 +321,7 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
dataIndex: 'auditStatus', dataIndex: 'auditStatus',
ellipsis: true, ellipsis: true,
customRender: ({ text }) => { customRender: ({ text }) => {
const auditStatus = Object.values(REVIEW_ENUM).find(item => item.value === text); const auditStatus = Object.values(REVIEW_ENUM).find((item) => item.value === text);
return auditStatus ? auditStatus.desc : text; return auditStatus ? auditStatus.desc : text;
}, },
}, },
@ -384,7 +385,7 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
} }
// 搜索 // 搜索
function onSearch(){ function onSearch() {
queryForm.pageNum = 1; queryForm.pageNum = 1;
// 处理申请日期范围 // 处理申请日期范围
@ -413,7 +414,6 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
} }
} }
onMounted(() => { onMounted(() => {
getLoginInfo(); getLoginInfo();
queryData(); queryData();
@ -439,7 +439,7 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
if (loginInfo.value.roleCodeList && loginInfo.value.roleCodeList.length > 0) { if (loginInfo.value.roleCodeList && loginInfo.value.roleCodeList.length > 0) {
// 使用 roleCodeList 数组 // 使用 roleCodeList 数组
roleCodes = loginInfo.value.roleCodeList.map(r => r.toLowerCase()); roleCodes = loginInfo.value.roleCodeList.map((r) => r.toLowerCase());
} else if (loginInfo.value.roleCode) { } else if (loginInfo.value.roleCode) {
// 使用 roleCode 字符串 // 使用 roleCode 字符串
roleCodes = [loginInfo.value.roleCode.toLowerCase()]; roleCodes = [loginInfo.value.roleCode.toLowerCase()];
@ -468,7 +468,18 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
queryForm.userType = 'user'; queryForm.userType = 'user';
} }
console.log('用户角色代码:', roleCodes, 'isCeo:', isCeo.value, 'isUser:', isUser.value, 'isCto:', isCto.value, 'isFirmAdmin:', isFirmAdmin.value); console.log(
'用户角色代码:',
roleCodes,
'isCeo:',
isCeo.value,
'isUser:',
isUser.value,
'isCto:',
isCto.value,
'isFirmAdmin:',
isFirmAdmin.value
);
} }
} }
@ -483,7 +494,7 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
// ---------------------------- 单个删除 ---------------------------- // ---------------------------- 单个删除 ----------------------------
//确认删除 //确认删除
function onDelete(data){ function onDelete(data) {
Modal.confirm({ Modal.confirm({
title: '提示', title: '提示',
content: '确定要删除选吗?', content: '确定要删除选吗?',
@ -498,7 +509,7 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
} }
//请求删除 //请求删除
async function requestDelete(data){ async function requestDelete(data) {
SmartLoading.show(); SmartLoading.show();
try { try {
let deleteForm = { let deleteForm = {
@ -521,7 +532,7 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
const currentAuditRecord = ref(null); const currentAuditRecord = ref(null);
const auditForm = reactive({ const auditForm = reactive({
auditStatus: 3, // 默认同意 auditStatus: 3, // 默认同意
auditRemark: '' auditRemark: '',
}); });
// 显示审批弹框 // 显示审批弹框
@ -544,7 +555,7 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
// 构建审核数据,使用专门的审核接口 // 构建审核数据,使用专门的审核接口
const auditData = { const auditData = {
id: currentAuditRecord.value.id, id: currentAuditRecord.value.id,
auditStatus: auditForm.auditStatus // 审核结果:3-已批准,5-已驳回 auditStatus: auditForm.auditStatus, // 审核结果:3-已批准,5-已驳回
}; };
// 根据角色使用不同的审核状态字段 // 根据角色使用不同的审核状态字段
@ -593,8 +604,8 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
const response = await fetch(`/api/wordCertificate/export/${record.id}`, { const response = await fetch(`/api/wordCertificate/export/${record.id}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Authorization': `Bearer ${token}` Authorization: `Bearer ${token}`,
} },
}); });
if (response.ok) { if (response.ok) {
@ -672,7 +683,7 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
const previewVisible = ref(false); const previewVisible = ref(false);
const previewUrl = ref(''); const previewUrl = ref('');
const previewLoading = ref(false); const previewLoading = ref(false);
const previewTitle = ref('无处罚证明预览'); const previewTitle = ref('无处分证明预览');
const currentPreviewRecord = ref(null); const currentPreviewRecord = ref(null);
// 显示图片预览 // 显示图片预览
@ -682,14 +693,14 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
// 根据申请类型设置预览标题 // 根据申请类型设置预览标题
if (record) { if (record) {
if (record.applyType === 'PERSONAL' || record.type === 'USER') { if (record.applyType === 'PERSONAL' || record.type === 'USER') {
previewTitle.value = '个人无处罚证明预览'; previewTitle.value = '个人无处分证明预览';
} else if (record.applyType === 'FIRM' || record.type === 'CTO') { } else if (record.applyType === 'FIRM' || record.type === 'CTO') {
previewTitle.value = '律所无处罚证明预览'; previewTitle.value = '律所无处分证明预览';
} else { } else {
previewTitle.value = '无处罚证明预览'; previewTitle.value = '无处分证明预览';
} }
} else { } else {
previewTitle.value = '无处罚证明预览'; previewTitle.value = '无处分证明预览';
} }
previewVisible.value = true; previewVisible.value = true;
} }
@ -698,7 +709,7 @@ import PenaltyApplyForm from './penalty-apply-form.vue';
function handleCancelPreview() { function handleCancelPreview() {
previewVisible.value = false; previewVisible.value = false;
previewUrl.value = ''; previewUrl.value = '';
previewTitle.value = '无处罚证明预览'; previewTitle.value = '无处分证明预览';
currentPreviewRecord.value = null; currentPreviewRecord.value = null;
} }

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

@ -30,7 +30,11 @@
<a-form-item style="margin-left: auto;"> <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 />
导出 律师明细导出
</a-button>
<a-button v-if="isAssociationRole" type="primary" @click="handleExportDepartment" :loading="exportDepartmentLoading" style="margin-left: 8px">
<ExportOutlined />
律所汇总导出
</a-button> </a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -144,6 +148,8 @@ import { ExportOutlined, ArrowLeftOutlined, DownOutlined, RightOutlined, SearchO
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 { getRoleInfo } from '/@/utils/role-util';
import { useUserStore } from '/@/store/modules/system/user';
// 展开/折叠状态管理 // 展开/折叠状态管理
const expandedRows = ref(new Set()); const expandedRows = ref(new Set());
@ -177,9 +183,15 @@ const props = defineProps({
}); });
const exportLoading = ref(false); const exportLoading = ref(false);
const exportDepartmentLoading = ref(false);
const tableData = ref([]); const tableData = ref([]);
const loading = ref(false); const loading = ref(false);
// 角色判断
const userStore = useUserStore();
const roleInfo = getRoleInfo(userStore.roleCode);
const isAssociationRole = roleInfo.isAssociationRole;
// 分页配置 // 分页配置
const pagination = reactive({ const pagination = reactive({
pageNum: 1, pageNum: 1,
@ -381,6 +393,33 @@ async function handleExport() {
} }
} }
// 律所汇总导出(仅律协角色可见)
async function handleExportDepartment() {
if (!tableData.value || tableData.value.length === 0) {
message.warning('暂无数据可导出');
return;
}
exportDepartmentLoading.value = true;
try {
console.log('开始导出律所汇总统计...');
const exportParams = {
quarter: queryForm.quarter,
year: queryForm.year,
firmId: queryForm.firmId
};
await serviceApplicationsApi.exportLawyerByDepartment(exportParams);
message.success('导出成功');
console.log('律所汇总统计导出成功');
} catch (error) {
message.error('导出失败');
console.error('导出律所汇总统计失败:', error);
} finally {
exportDepartmentLoading.value = false;
}
}
// 监听参数变化,重新获取数据 // 监听参数变化,重新获取数据
watch(() => props.params, (newParams) => { watch(() => props.params, (newParams) => {
if (newParams) { if (newParams) {

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

@ -11,14 +11,15 @@
:width="1000" :width="1000"
:open="visibleFlag" :open="visibleFlag"
@cancel="onClose" @cancel="onClose"
@afterClose="resetAfterClose"
:maskClosable="false" :maskClosable="false"
:destroyOnClose="true" :destroyOnClose="true"
> >
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical" > <a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<!-- 基本信息 --> <!-- 基本信息 -->
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="24"> <a-col :span="24">
<div style="font-weight: bold; font-size: 16px; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 1px solid #d9d9d9;"> <div style="font-weight: bold; font-size: 16px; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 1px solid #d9d9d9">
一、基础信息 一、基础信息
</div> </div>
</a-col> </a-col>
@ -51,13 +52,12 @@
<!-- 服务信息 --> <!-- 服务信息 -->
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="24"> <a-col :span="24">
<div style="font-weight: bold; font-size: 16px; margin: 24px 0 16px 0; padding-bottom: 8px; border-bottom: 1px solid #d9d9d9;"> <div style="font-weight: bold; font-size: 16px; margin: 24px 0 16px 0; padding-bottom: 8px; border-bottom: 1px solid #d9d9d9">
二、服务信息 二、服务信息
</div> </div>
</a-col> </a-col>
</a-row> </a-row>
<!-- 活动信息 --> <!-- 活动信息 -->
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="8"> <a-col :span="8">
@ -87,25 +87,80 @@
</a-col> </a-col>
</a-row> </a-row>
<!-- 本所同行人代报律师选择 (当所选类目允许本所同行人代报时呈现) -->
<a-row
:gutter="16"
v-if="
showBatchSelect &&
(!form.applicationId || form.firmAuditStatus == null || form.firmAuditStatus === 0)
"
>
<a-col :span="24">
<a-form-item label="本所同行人代报(选填)" name="participantLawyerIds">
<a-select
v-model:value="form.participantLawyerIds"
mode="multiple"
placeholder="不选择则仅申报本人;需要代报时可选择多名本所同行人"
style="width: 100%"
:disabled="readonlyMode"
:options="sameFirmLawyerOptions"
:filter-option="filterSameFirmLawyerOption"
allow-clear
/>
<div style="color: #ff4d4f; font-size: 12px; margin-top: 4px">
注:仅显示本所已签署有效申报承诺书的律师;不选择则仅申报本人。
</div>
</a-form-item>
</a-col>
</a-row>
<!-- 已提交主记录的多人代报信息:选择控件隐藏后仍需在详情中可见。 -->
<a-row :gutter="16" v-if="readonlyMode && Number(form.isBatch) === 1">
<a-col :span="24">
<a-form-item v-if="!form.parentApplicationId" label="同所同行人">
<a-input :value="peerParticipantNames" readonly />
</a-form-item>
<a-form-item v-else label="代报人">
<a-input :value="form.declarerName || '-'" readonly />
</a-form-item>
</a-col>
</a-row>
<!-- 根据活动类型显示不同字段 --> <!-- 根据活动类型显示不同字段 -->
<template v-if="currentActivityType === 'DICT' || !currentActivityType"> <template v-if="currentActivityType === 'DICT' || !currentActivityType">
<!-- dict类型:显示服务时长、服务开始时间和服务结束时间 --> <!-- dict类型:显示服务时长、服务开始时间和服务结束时间 -->
<a-row :gutter="24"> <a-row :gutter="24">
<a-col :span="8"> <a-col :span="8">
<a-form-item label="服务开始时间" name="serviceStart" label-align="left"> <a-form-item label="服务开始时间" name="serviceStart" label-align="left">
<a-date-picker show-time format="YYYY-MM-DD HH:mm" valueFormat="YYYY-MM-DD HH:mm:ss" v-model:value="form.serviceStart" style="width: 100%" placeholder="服务开始时间" :disabled="readonlyMode" /> <a-date-picker
show-time
format="YYYY-MM-DD HH:mm"
valueFormat="YYYY-MM-DD HH:mm:ss"
v-model:value="form.serviceStart"
style="width: 100%"
placeholder="服务开始时间"
:disabled="readonlyMode"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="8"> <a-col :span="8">
<a-form-item label="服务结束时间" name="serviceEnd" label-align="left"> <a-form-item label="服务结束时间" name="serviceEnd" label-align="left">
<a-date-picker show-time format="YYYY-MM-DD HH:mm" valueFormat="YYYY-MM-DD HH:mm:ss" v-model:value="form.serviceEnd" style="width: 100%" placeholder="服务结束时间" :disabled="readonlyMode" /> <a-date-picker
show-time
format="YYYY-MM-DD HH:mm"
valueFormat="YYYY-MM-DD HH:mm:ss"
v-model:value="form.serviceEnd"
style="width: 100%"
placeholder="服务结束时间"
:disabled="readonlyMode"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="8"> <a-col :span="8">
<a-form-item name="serviceDuration"> <a-form-item name="serviceDuration">
<template #label> <template #label>
<span>服务时长(小时)</span> <span>服务时长(小时)</span>
<span style="color: #999; font-size: 12px; margin-left: 4px;">可通过时间选择也可手动填写</span> <span style="color: #999; font-size: 12px; margin-left: 4px">可通过时间选择也可手动填写</span>
</template> </template>
<a-input-number <a-input-number
style="width: 100%" style="width: 100%"
@ -114,21 +169,35 @@
:disabled="readonlyMode" :disabled="readonlyMode"
:min="0" :min="0"
:precision="1" :precision="1"
:parser="value => value.replace(/[^\d.]/g, '')" :parser="(value) => value.replace(/[^\d.]/g, '')"
@blur="handleServiceDurationBlur" @blur="handleServiceDurationBlur"
/> />
</a-form-item> </a-form-item>
<div style="font-size: 12px; color: #f00d0dff; margin-top: -12px; margin-bottom: 12px;">注:不足30分钟(含30分钟)的,按照0.5小时填报,超过30分钟不足1小时的(含1小时),按照1小时填报。</div> <div style="font-size: 12px; color: #f00d0dff; margin-top: -12px; margin-bottom: 12px">
注:不足30分钟(含30分钟)的,按照0.5小时填报,超过30分钟不足1小时的(含1小时),按照1小时填报。
</div>
</a-col> </a-col>
</a-row> </a-row>
</template> </template>
<template v-else-if="currentActivityType === 'TIME'"> <template v-else-if="currentActivityType === 'TIME'">
<!-- time类型:显示案件编号和服务时长在一行 --> <!-- time类型:活动完成日期用于确定履职和成本季度 -->
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="8"> <a-col :span="8">
<a-form-item label="案件编号" name="recordNo" required> <a-form-item label="活动完成时间" name="serviceEnd" required>
<a-input style="width: 100%" v-model:value="form.recordNo" placeholder="请输入案件编号" :disabled="readonlyMode" /> <a-date-picker
format="YYYY-MM-DD"
valueFormat="YYYY-MM-DD HH:mm:ss"
v-model:value="form.serviceEnd"
style="width: 100%"
placeholder="请选择活动完成时间"
:disabled="readonlyMode"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="备案/案件编号" name="recordNo" required>
<a-input style="width: 100%" v-model:value="form.recordNo" placeholder="请输入备案/案件编号" :disabled="readonlyMode" />
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -137,7 +206,7 @@
<a-form-item name="serviceDuration"> <a-form-item name="serviceDuration">
<template #label> <template #label>
<span>服务时长(小时)</span> <span>服务时长(小时)</span>
<span style="color: #999; font-size: 12px; margin-left: 4px;">请在{{ currentActivity.price }}范围内填写</span> <span style="color: #999; font-size: 12px; margin-left: 4px">请在{{ currentActivity.price }}范围内填写</span>
</template> </template>
<a-input-number <a-input-number
style="width: 100%" style="width: 100%"
@ -146,7 +215,7 @@
:disabled="readonlyMode" :disabled="readonlyMode"
:min="0" :min="0"
:precision="2" :precision="2"
:parser="value => value.replace(/[^\d.]/g, '')" :parser="(value) => value.replace(/[^\d.]/g, '')"
@blur="handleServiceDurationBlur" @blur="handleServiceDurationBlur"
/> />
</a-form-item> </a-form-item>
@ -168,12 +237,32 @@
</a-row> </a-row>
</template> </template>
<template v-else-if="currentActivityType === 'AMOUT'"> <template v-else-if="['AMOUNT', 'AMOUT'].includes(currentActivityType)">
<!-- amount类型:显示金额输入框 --> <!-- amount类型:显示金额输入框 -->
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="8">
<a-form-item label="费用发生时间" name="serviceEnd" required>
<a-date-picker
show-time
format="YYYY-MM-DD HH:mm"
valueFormat="YYYY-MM-DD HH:mm:ss"
v-model:value="form.serviceEnd"
style="width: 100%"
placeholder="请选择费用发生时间"
:disabled="readonlyMode"
/>
</a-form-item>
</a-col>
<a-col :span="8"> <a-col :span="8">
<a-form-item label="金额" name="workloadScore"> <a-form-item label="金额" name="workloadScore">
<a-input-number style="width: 100%" v-model:value="form.workloadScore" placeholder="请输入金额" :disabled="readonlyMode" :min="0" :precision="2" /> <a-input-number
style="width: 100%"
v-model:value="form.workloadScore"
placeholder="请输入金额"
:disabled="readonlyMode"
:min="0"
:precision="2"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
@ -201,7 +290,12 @@
</a-col> </a-col>
<a-col :span="16"> <a-col :span="16">
<a-form-item label="联系方式" name="organizerPhone"> <a-form-item label="联系方式" name="organizerPhone">
<a-input style="width: 100%" v-model:value="form.organizerPhone" placeholder="联系电话或邮箱" :disabled="readonlyMode" /> <a-input
style="width: 100%"
v-model:value="form.organizerPhone"
placeholder="手机号、座机或邮箱(如 0571-12345678)"
:disabled="readonlyMode"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
@ -210,18 +304,15 @@
<a-form-item name="serviceContent"> <a-form-item name="serviceContent">
<template #label> <template #label>
<span>服务内容描述</span> <span>服务内容描述</span>
<span style="color: #f00d0dff; font-size: 12px; margin-left: 4px;">注:内容描述需要包括的内容主要是时间、地点、主题、参与人员、服务内容、活动效果等</span> <span style="color: #f00d0dff; font-size: 12px; margin-left: 4px"
>注:内容描述需要包括的内容主要是时间、地点、主题、参与人员、服务内容、活动效果等</span
>
</template> </template>
<SmartWangeditor ref="serviceContentRef" v-model="form.serviceContent" :height="200" :readonly="readonlyMode" /> <SmartWangeditor ref="serviceContentRef" v-model="form.serviceContent" :height="200" :readonly="readonlyMode" />
</a-form-item> </a-form-item>
<!-- 备案信息(仅详情模式且已备案时展示) --> <!-- 备案信息(仅详情模式且已备案时展示) -->
<a-row :gutter="16" v-if="readonlyMode && form.recordStatus === 1"> <a-row :gutter="16" v-if="readonlyMode && form.recordStatus === 1">
<a-col :span="12"> <a-col :span="24">
<a-form-item label="备案编号">
<a-input :value="form.filingNo" :disabled="true" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="备案时间"> <a-form-item label="备案时间">
<a-input :value="form.recordTime" :disabled="true" /> <a-input :value="form.recordTime" :disabled="true" />
</a-form-item> </a-form-item>
@ -231,7 +322,9 @@
<a-form-item name="proofMaterials" required> <a-form-item name="proofMaterials" required>
<template #label> <template #label>
<span class="ant-form-item-required">证明材料</span> <span class="ant-form-item-required">证明材料</span>
<span style="font-size: 12px; color: #f00d0dff;"> 注:(请上传活动方案、活动记录、照片、新闻报道等材料)支持图片(JPG/PNG)、文档(PDF/Word/PPT)格式,单文件最大10MB,最多上传5个文件</span> <span style="font-size: 12px; color: #f00d0dff">
注:(请上传活动方案、活动记录、照片、新闻报道等材料)支持图片(JPG/PNG)、文档(PDF/Word/PPT)格式,单文件最大10MB,最多上传5个文件</span
>
</template> </template>
<template v-if="readonlyMode"> <template v-if="readonlyMode">
<div v-if="defaultFileList.length > 0"> <div v-if="defaultFileList.length > 0">
@ -251,8 +344,8 @@
<div v-else>无证明材料</div> <div v-else>无证明材料</div>
</template> </template>
<Upload <Upload
:key="uploadRenderKey"
v-else v-else
:defaultFileList="defaultFileList" :defaultFileList="defaultFileList"
:maxUploadSize="5" :maxUploadSize="5"
@ -266,7 +359,6 @@
@change="changeAttachment" @change="changeAttachment"
/> />
</a-form-item> </a-form-item>
</a-form> </a-form>
<template #footer> <template #footer>
@ -278,7 +370,7 @@
</a-modal> </a-modal>
</template> </template>
<script setup> <script setup>
import { reactive, ref, nextTick, watch, onMounted } from 'vue'; import { computed, reactive, ref, nextTick, watch, onMounted } from 'vue';
import _ from 'lodash'; import _ from 'lodash';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@ -331,7 +423,25 @@
// 当前选中的活动和活动类型 // 当前选中的活动和活动类型
const currentActivity = ref({}); const currentActivity = ref({});
const currentActivityType = ref(''); const currentActivityType = ref('');
const initializingForm = ref(false);
const showBatchSelect = computed(() => {
// 新增只认当前活动开关;编辑草稿才允许用已保存人员兜底回显。
if (!form.applicationId) {
return canBatchSelectActivity.value;
}
return canBatchSelectActivity.value || form.participantLawyerIds?.length > 0 || !!form.draftParticipantIds;
});
// 兼容历史数据:旧记录的 participantNames 可能包含申报人本人。
const peerParticipantNames = computed(() => {
const selfName = String(form.actualName || '').trim();
const names = String(form.participantNames || '')
.split('、')
.map((name) => name.trim())
.filter((name) => name && name !== selfName);
return names.join('、') || '-';
});
// 每次打开不同记录时重新初始化上传组件,确保已有材料稳定回显。
const uploadRenderKey = ref(0);
// 活动类型变化处理 // 活动类型变化处理
async function onActivityCategoryChange(categoryId) { async function onActivityCategoryChange(categoryId) {
@ -360,10 +470,18 @@
if (!categoryId) { if (!categoryId) {
activityList.value = []; activityList.value = [];
canBatchSelectActivity.value = false;
return; return;
} }
try { try {
canBatchSelectActivity.value = false;
// 编辑时必须保留草稿中已保存的同行人;仅新增切换分类时清空。
if (isNewMode) {
form.participantLawyerIds = [];
form.draftParticipantIds = '';
}
// 根据分类ID查询活动列表 // 根据分类ID查询活动列表
const queryParams = { const queryParams = {
categoryId: categoryId, categoryId: categoryId,
@ -376,12 +494,12 @@
if (result.data && result.data.list) { if (result.data && result.data.list) {
// 将活动列表转换为下拉选项格式,保存完整的活动信息 // 将活动列表转换为下拉选项格式,保存完整的活动信息
activityList.value = result.data.list.map(item => ({ activityList.value = result.data.list.map((item) => ({
label: item.goodsName, label: item.goodsName,
value: item.goodsId, // 传递活动ID而不是名称 value: item.goodsId, // 传递活动ID而不是名称
goodsId: item.goodsId, goodsId: item.goodsId,
timeType: item.timeType, timeType: item.timeType,
...item ...item,
})); }));
} else { } else {
activityList.value = []; activityList.value = [];
@ -399,7 +517,14 @@
} }
async function show(rowData, isDetail = false) { async function show(rowData, isDetail = false) {
initializingForm.value = true;
Object.assign(form, formDefault); Object.assign(form, formDefault);
form.participantLawyerIds = [];
form.draftParticipantIds = '';
canBatchSelectActivity.value = false;
sameFirmLawyerOptions.value = [];
defaultFileList.value = [];
uploadRenderKey.value += 1;
// 设置只读模式 // 设置只读模式
readonlyMode.value = isDetail; readonlyMode.value = isDetail;
@ -414,6 +539,13 @@
if (rowData && !_.isEmpty(rowData)) { if (rowData && !_.isEmpty(rowData)) {
// 编辑模式或详情模式:使用接口返回的数据 // 编辑模式或详情模式:使用接口返回的数据
Object.assign(form, rowData); Object.assign(form, rowData);
form.participantLawyerIds = rowData.draftParticipantIds
? String(rowData.draftParticipantIds).split(',').filter(Boolean).map(Number)
: [];
if (form.participantLawyerIds.length > 0) {
// 已保存的草稿必须优先回显,不能依赖活动列表接口中的开关字段。
loadSameFirmLawyers();
}
// 只有在新增模式下才使用当前用户信息 // 只有在新增模式下才使用当前用户信息
// 详情/编辑模式使用接口返回的原始数据,不覆盖 // 详情/编辑模式使用接口返回的原始数据,不覆盖
@ -423,8 +555,12 @@
} }
// 编辑模式或详情模式下,如果有attachmentIds,则获取文件列表 // 编辑模式或详情模式下,如果有attachmentIds,则获取文件列表
if (form.attachmentIds && form.attachmentIds.trim()) { const attachmentIds = form.attachmentIds == null ? '' : String(form.attachmentIds).trim();
await getFileListByAttachmentIds(form.attachmentIds); form.attachmentIds = attachmentIds;
if (attachmentIds) {
await getFileListByAttachmentIds(attachmentIds);
// 文件接口异步返回后重建一次组件,避免组件沿用上一次弹窗的内部列表。
uploadRenderKey.value += 1;
} }
} else { } else {
// 新增模式:自动填充当前用户信息 // 新增模式:自动填充当前用户信息
@ -448,10 +584,16 @@
// 加载活动列表后,根据活动名称ID更新currentActivity和currentActivityType // 加载活动列表后,根据活动名称ID更新currentActivity和currentActivityType
if (form.activityNameId && activityList.value.length > 0) { if (form.activityNameId && activityList.value.length > 0) {
const selectedActivity = activityList.value.find(activity => activity.goodsId === form.activityNameId); const selectedActivity = activityList.value.find(
(activity) => String(activity.goodsId) === String(form.activityNameId)
);
if (selectedActivity) { if (selectedActivity) {
currentActivity.value = selectedActivity; currentActivity.value = selectedActivity;
currentActivityType.value = selectedActivity.timeType || ''; currentActivityType.value = selectedActivity.timeType || '';
canBatchSelectActivity.value = Boolean(selectedActivity.canBatchSelect);
if (canBatchSelectActivity.value) {
loadSameFirmLawyers();
}
// 将timeType放入serviceType字段中传递给后端 // 将timeType放入serviceType字段中传递给后端
form.serviceType = selectedActivity.timeType || ''; form.serviceType = selectedActivity.timeType || '';
} }
@ -465,6 +607,7 @@
visibleFlag.value = true; visibleFlag.value = true;
nextTick(() => { nextTick(() => {
formRef.value.clearValidate(); formRef.value.clearValidate();
initializingForm.value = false;
}); });
} }
@ -488,7 +631,7 @@
const result = await fileApi.getFileList(attachmentIds); const result = await fileApi.getFileList(attachmentIds);
if (result.data && result.data.length > 0) { if (result.data && result.data.length > 0) {
// 将文件信息转换为Upload组件需要的格式,并清理URL // 将文件信息转换为Upload组件需要的格式,并清理URL
defaultFileList.value = result.data.map(file => { defaultFileList.value = result.data.map((file) => {
const cleanUrl = file.fileUrl ? file.fileUrl.replace(/[`\s]/g, '') : ''; const cleanUrl = file.fileUrl ? file.fileUrl.replace(/[`\s]/g, '') : '';
return { return {
fileId: file.fileId, fileId: file.fileId,
@ -499,15 +642,15 @@
uid: file.fileId, uid: file.fileId,
name: file.fileName, name: file.fileName,
status: 'done', status: 'done',
url: cleanUrl url: cleanUrl,
}; };
}); });
// 同时更新表单中的proofMaterials字段 // 同时更新表单中的proofMaterials字段
form.proofMaterials = result.data.map(file => ({ form.proofMaterials = result.data.map((file) => ({
fileName: file.fileName, fileName: file.fileName,
fileUrl: file.fileUrl ? file.fileUrl.replace(/[`\s]/g, '') : '', fileUrl: file.fileUrl ? file.fileUrl.replace(/[`\s]/g, '') : '',
fileType: file.fileType fileType: file.fileType,
})); }));
console.log('文件列表已清理URL:', defaultFileList.value); console.log('文件列表已清理URL:', defaultFileList.value);
@ -526,19 +669,21 @@
// 处理附件上传 // 处理附件上传
function changeAttachment(fileList) { function changeAttachment(fileList) {
// 将文件信息保存到表单中 // 将文件信息保存到表单中
form.proofMaterials = fileList.map(file => { form.proofMaterials = fileList.map((file) => {
return { return {
fileName: file.name, fileName: file.name,
fileUrl: file.url || file.response?.data?.fileUrl, fileUrl: file.url || file.response?.data?.fileUrl,
fileType: file.type fileType: file.type,
}; };
}); });
// 提取fileId并用逗号隔开 // 提取fileId并用逗号隔开
const fileIds = fileList.map(file => { const fileIds = fileList
.map((file) => {
// 从文件对象中获取fileId,优先从response中获取 // 从文件对象中获取fileId,优先从response中获取
return file.response?.data?.fileId || file.fileId || ''; return file.response?.data?.fileId || file.fileId || '';
}).filter(id => id); // 过滤掉空值 })
.filter((id) => id); // 过滤掉空值
form.attachmentIds = fileIds.join(','); form.attachmentIds = fileIds.join(',');
@ -551,6 +696,12 @@
} }
function onClose() { function onClose() {
visibleFlag.value = false;
}
// 等弹窗关闭动画和内部下拉菜单卸载完成后再清理响应式数据,
// 避免 a-select 菜单丢失注入上下文或节点被重复移除。
function resetAfterClose() {
Object.assign(form, formDefault); Object.assign(form, formDefault);
// 清空附件数据 // 清空附件数据
defaultFileList.value = []; defaultFileList.value = [];
@ -558,7 +709,10 @@
form.attachmentIds = ''; form.attachmentIds = '';
// 重置只读模式 // 重置只读模式
readonlyMode.value = false; readonlyMode.value = false;
visibleFlag.value = false; currentActivity.value = {};
currentActivityType.value = '';
activityList.value = [];
sameFirmLawyers.value = [];
} }
// ------------------------ 表单 ------------------------ // ------------------------ 表单 ------------------------
@ -592,9 +746,9 @@
serviceContent: undefined, //服务内容描述 serviceContent: undefined, //服务内容描述
proofMaterials: undefined, //证明材料 proofMaterials: undefined, //证明材料
attachmentIds: undefined, //附件ID列表(逗号隔开) attachmentIds: undefined, //附件ID列表(逗号隔开)
participantLawyerIds: [], // 勾选同所同行律师ID列表
firmAuditStatus: undefined, //执业机构审核状态:0-待审核,1-通过,2-退回 firmAuditStatus: undefined, //执业机构审核状态:0-待审核,1-通过,2-退回
recordStatus: undefined, //备案状态:0-未备案,1-已备案 recordStatus: undefined, //备案状态:0-未备案,1-已备案
filingNo: undefined, //备案编号
recordTime: undefined, //备案时间 recordTime: undefined, //备案时间
updateTime: undefined, //更新时间 updateTime: undefined, //更新时间
createTime: undefined, //创建时间 createTime: undefined, //创建时间
@ -602,6 +756,31 @@
let form = reactive({ ...formDefault }); let form = reactive({ ...formDefault });
// 本所同行人代报分类与人员状态
const canBatchSelectActivity = ref(false);
const sameFirmLawyers = ref([]);
const sameFirmLawyerOptions = computed(() => {
return sameFirmLawyers.value.map((emp) => ({
label: `${emp.actualName} (${emp.phone || emp.loginName || '本所律师'})`,
value: emp.employeeId,
}));
});
function filterSameFirmLawyerOption(input, option) {
return option.label.toLowerCase().includes(input.toLowerCase());
}
async function loadSameFirmLawyers() {
try {
const res = await serviceApplicationsApi.querySameFirmLawyers();
if (res && res.data) {
sameFirmLawyers.value = res.data;
}
} catch (e) {
console.error('加载同所律师失败:', e);
}
}
// 服务时长格式化函数 // 服务时长格式化函数
function formatServiceDuration(minutes) { function formatServiceDuration(minutes) {
if (minutes <= 30) { if (minutes <= 30) {
@ -636,12 +815,21 @@
return formatServiceDuration(totalMinutes); return formatServiceDuration(totalMinutes);
} }
// 监听服务开始时间和结束时间变化,自动计算服务时长 // 监听服务开始时间和结束时间变化,自动计算服务时长(仅适用于按时间段计算时长的活动,如 DICT 类型)
watch( watch(
() => [form.serviceStart, form.serviceEnd], () => [form.serviceStart, form.serviceEnd],
([startTime, endTime]) => { ([startTime, endTime], [oldStartTime, oldEndTime] = []) => {
// 只有在非编辑模式下或当前活动类型为DICT时才自动计算服务时长 // 编辑弹窗初始化时,起止时间与手工填写的服务时长会一起回填。
if ((!form.applicationId || currentActivityType.value === 'DICT') && (startTime && endTime)) { // 此时不能用时间差覆盖数据库中已保存的服务时长。
if (initializingForm.value) {
return;
}
if (form.applicationId && oldStartTime == null && oldEndTime == null) {
return;
}
// 只有在 DICT 类型(或默认未指定类型且同时有开始和结束时间)时才根据起止时间自动计算或联动清空服务时长
if (currentActivityType.value === 'DICT' || (!currentActivityType.value && startTime && endTime)) {
if (startTime && endTime) {
const start = dayjs(startTime); const start = dayjs(startTime);
const end = dayjs(endTime); const end = dayjs(endTime);
if (end.isAfter(start)) { if (end.isAfter(start)) {
@ -654,10 +842,11 @@
form.serviceDuration = undefined; form.serviceDuration = undefined;
message.warning('服务结束时间必须晚于开始时间'); message.warning('服务结束时间必须晚于开始时间');
} }
} else if (!form.applicationId && (!startTime || !endTime)) { } else if (!form.applicationId) {
// 只有在非编辑模式下才清空服务时长 // 仅在新建且为 DICT 类型时,缺少起止时间才清空时长
form.serviceDuration = undefined; form.serviceDuration = undefined;
} }
}
}, },
{ immediate: true } { immediate: true }
); );
@ -680,12 +869,28 @@
watch( watch(
() => form.activityNameId, () => form.activityNameId,
(activityNameId, oldActivityNameId) => { (activityNameId, oldActivityNameId) => {
// 只有用户实际更换活动名称时才清空同行人;编辑页初始化回填必须保留。
if (
!initializingForm.value &&
oldActivityNameId != null &&
String(activityNameId) !== String(oldActivityNameId)
) {
form.participantLawyerIds = [];
form.draftParticipantIds = '';
}
if (activityNameId) { if (activityNameId) {
const selectedActivity = activityList.value.find(activity => activity.goodsId === activityNameId); const selectedActivity = activityList.value.find(
(activity) => String(activity.goodsId) === String(activityNameId)
);
if (selectedActivity) { if (selectedActivity) {
currentActivity.value = selectedActivity; currentActivity.value = selectedActivity;
currentActivityType.value = selectedActivity.timeType || ''; currentActivityType.value = selectedActivity.timeType || '';
canBatchSelectActivity.value = Boolean(selectedActivity.canBatchSelect);
if (canBatchSelectActivity.value) {
loadSameFirmLawyers();
} else {
form.participantLawyerIds = [];
}
// 将timeType放入serviceType字段中传递给后端 // 将timeType放入serviceType字段中传递给后端
form.serviceType = selectedActivity.timeType || ''; form.serviceType = selectedActivity.timeType || '';
@ -706,7 +911,7 @@
// 默认值 // 默认值
form.serviceDuration = 25; form.serviceDuration = 25;
} }
} else if (selectedActivity.timeType === 'AMOUT') { } else if (['AMOUNT', 'AMOUT'].includes(selectedActivity.timeType)) {
form.serviceDuration = undefined; form.serviceDuration = undefined;
} else if (selectedActivity.timeType === 'DICT') { } else if (selectedActivity.timeType === 'DICT') {
// DICT类型:清空服务时长,让用户通过选择时间或手动填写 // DICT类型:清空服务时长,让用户通过选择时间或手动填写
@ -717,12 +922,13 @@
} else { } else {
currentActivity.value = {}; currentActivity.value = {};
currentActivityType.value = ''; currentActivityType.value = '';
canBatchSelectActivity.value = false;
form.participantLawyerIds = [];
} }
}, },
{ immediate: true } { immediate: true }
); );
const rules = { const rules = {
actualName: [{ required: true, message: '姓名 必填' }], actualName: [{ required: true, message: '姓名 必填' }],
certificateNumber: [{ required: true, message: '执业证号 必填' }], certificateNumber: [{ required: true, message: '执业证号 必填' }],
@ -735,19 +941,21 @@
// return Promise.reject('服务开始时间 必填'); // return Promise.reject('服务开始时间 必填');
//} //}
return Promise.resolve(); return Promise.resolve();
} },
} },
], ],
serviceEnd: [ serviceEnd: [
{ {
validator: async (rule, value) => { validator: async (rule, value) => {
// 只有dict类型需要验证服务结束时间 if (!value) {
//if (currentActivityType.value === 'DICT' && !value) { return Promise.reject('活动结束时间 必填');
//return Promise.reject('服务结束时间 必填');
//}
return Promise.resolve();
} }
if (dayjs(value).isAfter(dayjs())) {
return Promise.reject('活动结束时间不能晚于当前时间');
} }
return Promise.resolve();
},
},
], ],
serviceDuration: [ serviceDuration: [
{ {
@ -770,7 +978,7 @@
} }
} }
// amount类型需要验证服务时长范围 // amount类型需要验证服务时长范围
if (currentActivityType.value === 'AMOUT') { if (['AMOUNT', 'AMOUT'].includes(currentActivityType.value)) {
if (!value) { if (!value) {
return Promise.reject('服务时长(小时) 必填'); return Promise.reject('服务时长(小时) 必填');
} }
@ -791,8 +999,8 @@
} }
return Promise.resolve(); return Promise.resolve();
} },
} },
], ],
recordNo: [ recordNo: [
{ {
@ -802,19 +1010,19 @@
return Promise.reject('案件编号 必填'); return Promise.reject('案件编号 必填');
} }
return Promise.resolve(); return Promise.resolve();
} },
} },
], ],
workloadScore: [ workloadScore: [
{ {
validator: async (rule, value) => { validator: async (rule, value) => {
// amount类型需要验证金额 // amount类型需要验证金额
if (currentActivityType.value === 'AMOUT' && !value) { if (['AMOUNT', 'AMOUT'].includes(currentActivityType.value) && !value) {
return Promise.reject('金额 必填'); return Promise.reject('金额 必填');
} }
return Promise.resolve(); return Promise.resolve();
} },
} },
], ],
activityCategoryId: [{ required: true, message: '活动类型 必填' }], activityCategoryId: [{ required: true, message: '活动类型 必填' }],
activityNameId: [{ required: true, message: '活动名称 必填' }], activityNameId: [{ required: true, message: '活动名称 必填' }],
@ -824,9 +1032,9 @@
organizerPhone: [ organizerPhone: [
{ required: true, message: '联系方式 必填' }, { required: true, message: '联系方式 必填' },
{ {
pattern: /^(1[3-9]\d{9}|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/, pattern: /^(1[3-9]\d{9}|0\d{2,3}-?\d{7,8}(?:-\d{1,6})?|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/,
message: '请输入正确的手机号或邮箱格式' message: '请输入正确的手机号、座机或邮箱格式',
} },
], ],
serviceContent: [{ required: true, message: '服务内容描述 必填' }], serviceContent: [{ required: true, message: '服务内容描述 必填' }],
proofMaterials: [ proofMaterials: [
@ -837,8 +1045,8 @@
return Promise.reject('请上传证明材料'); return Promise.reject('请上传证明材料');
} }
return Promise.resolve(); return Promise.resolve();
} },
} },
], ],
}; };
@ -882,19 +1090,6 @@
async function save() { async function save() {
SmartLoading.show(); SmartLoading.show();
try { try {
// 案号重复检查
if (form.recordNo) {
try {
const checkResult = await serviceApplicationsApi.checkCaseNumber(form.recordNo, form.applicationId);
if (!checkResult.ok) {
message.error(`本案仅限一人申报,已由${checkResult.data.lawyerName}律师申报`);
return;
}
} catch (error) {
console.error('案号校验失败:', error);
}
}
// 准备提交数据 // 准备提交数据
const submitData = { ...form }; const submitData = { ...form };
@ -910,14 +1105,9 @@
// 新增保存:调用add接口 // 新增保存:调用add接口
await serviceApplicationsApi.add(submitData); await serviceApplicationsApi.add(submitData);
} }
message.success('保存成功'); message.success('保存成功,请在列表中提交');
emits('reloadList'); emits('reloadList');
onClose(); onClose();
// 保存成功后刷新页面
setTimeout(() => {
window.location.reload();
}, 1000);
} catch (err) { } catch (err) {
smartSentry.captureError(err); smartSentry.captureError(err);
} finally { } finally {
@ -929,19 +1119,6 @@
async function submit() { async function submit() {
SmartLoading.show(); SmartLoading.show();
try { try {
// 案号重复检查
if (form.recordNo) {
try {
const checkResult = await serviceApplicationsApi.checkCaseNumber(form.recordNo, form.applicationId);
if (!checkResult.ok) {
message.error(`本案仅限一人申报,已由${checkResult.data.lawyerName}律师申报`);
return;
}
} catch (error) {
console.error('案号校验失败:', error);
}
}
// 准备提交数据 // 准备提交数据
const submitData = { ...form }; const submitData = { ...form };
@ -951,7 +1128,7 @@
} }
// 调用submit接口,传递完整数据 // 调用submit接口,传递完整数据
await serviceApplicationsApi.submit(submitData); await serviceApplicationsApi.addSubmit(submitData);
message.success('提交成功'); message.success('提交成功');
emits('reloadList'); emits('reloadList');
onClose(); onClose();
@ -1007,15 +1184,14 @@
} }
} }
// 获取文件URL // 获取文件URL
function getFileUrl(file) { function getFileUrl(file) {
const url = file.fileUrl; const url = file.fileUrl;
return url; return url;
} }
// 获取文件名 // 获取文件名
function getFileName(file) { function getFileName(file) {
return file.fileName; return file.fileName;
} }
</script> </script>

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

@ -63,7 +63,7 @@
<a-range-picker style="width: 240px" v-model:value="queryForm.firmAuditTimeRange" :placeholder="['开始时间', '结束时间']" /> <a-range-picker style="width: 240px" v-model:value="queryForm.firmAuditTimeRange" :placeholder="['开始时间', '结束时间']" />
</a-form-item> </a-form-item>
<a-form-item label="备案编号" class="smart-query-form-item"> <a-form-item label="备案编号" class="smart-query-form-item">
<a-input style="width: 160px" v-model:value="queryForm.filingNo" placeholder="请输入备案编号" /> <a-input style="width: 160px" v-model:value="queryForm.recordNo" placeholder="请输入备案编号" />
</a-form-item> </a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10"> <a-form-item class="smart-query-form-item smart-margin-left10">
<a-button v-privilege="'serviceApplications:query'" type="primary" @click="onSearch"> <a-button v-privilege="'serviceApplications:query'" type="primary" @click="onSearch">
@ -172,11 +172,21 @@
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }" :row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
> >
<template #bodyCell="{ record, column }"> <template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'userName'">
<span>{{ record.userName }}</span>
<a-tag v-if="record.isBatch === 1 && record.parentApplicationId" color="orange" style="margin-left: 6px;">
由 {{ record.declarerName || '同行律师' }} 代报批
</a-tag>
<a-tag v-else-if="record.isBatch === 1 && !record.parentApplicationId" color="blue" style="margin-left: 6px;">
同行统一代报批 (包含: {{ record.participantNames || '多人' }})
</a-tag>
</template>
<template v-if="column.dataIndex === 'action'"> <template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate"> <div class="smart-table-operate">
<a-button @click="showDetail(record)" type="link">详情</a-button> <a-button @click="showDetail(record)" type="link">详情</a-button>
<!-- 只有未提交状态才能编辑,且只能编辑自己的记录 --> <!-- 只有未提交状态才能编辑,且只能编辑自己的主记录 -->
<a-button v-if="record.firmAuditStatus === 0 && record.userId === loginInfo?.userId" @click="showEdit(record)" type="link">编辑</a-button> <a-button v-if="record.firmAuditStatus === 0 && record.userId === loginInfo?.userId && (!record.declarerId || record.declarerId === loginInfo?.userId)" @click="showEdit(record)" type="link">编辑</a-button>
<a-button v-if="record.firmAuditStatus === 0 && record.userId === loginInfo?.userId && (!record.declarerId || record.declarerId === loginInfo?.userId)" @click="onSubmit(record)" type="link">提交</a-button>
<!-- 执业机构审核状态为拒绝时,只显示详情按钮 --> <!-- 执业机构审核状态为拒绝时,只显示详情按钮 -->
<template v-if="record.firmAuditStatus !== 4"> <template v-if="record.firmAuditStatus !== 4">
<a-button v-if="canAuditRecord(record)" @click="showAuditModal(record)" type="link">审核</a-button> <a-button v-if="canAuditRecord(record)" @click="showAuditModal(record)" type="link">审核</a-button>
@ -457,7 +467,7 @@ import dayjs from 'dayjs';
}, },
{ {
title: '备案编号', title: '备案编号',
dataIndex: 'filingNo', dataIndex: 'recordNo',
ellipsis: true, ellipsis: true,
width: 150, width: 150,
}, },
@ -483,7 +493,7 @@ import dayjs from 'dayjs';
firmAuditStatus: undefined, // 执业机构审核状态 firmAuditStatus: undefined, // 执业机构审核状态
firmAuditTimeRange: undefined, // 执业机构审核时间范围 firmAuditTimeRange: undefined, // 执业机构审核时间范围
associationAuditStatus: undefined, // 协会审核状态 associationAuditStatus: undefined, // 协会审核状态
filingNo: undefined, // 备案编号 recordNo: undefined, // 用户填写的备案/案件编号
selfFirmFilter: undefined, // 本人本所筛选条件 selfFirmFilter: undefined, // 本人本所筛选条件
}; };
// 查询表单form // 查询表单form
@ -679,31 +689,20 @@ import dayjs from 'dayjs';
return; return;
} }
// 如果已签署律所承诺书,检查成本权限 // 成本权限按律所判断:本所任一有效人员具备权限即视为本所有权限
if (loginInfo.value && loginInfo.value.lawFirmAgreementSignFlag === true) { if (loginInfo.value && loginInfo.value.lawFirmAgreementSignFlag === true) {
// 检查是否有成本查询权限 const permissionResult = await firmReportsApi.firmHasCostPermission();
if (loginInfo.value.costVisibleFlag === true) { if (permissionResult.data === true) {
try { try {
// 先检查是否有未审核的数据
const hasUnreviewedData = await checkUnreviewedData(); const hasUnreviewedData = await checkUnreviewedData();
if (hasUnreviewedData) { if (hasUnreviewedData) {
message.warning('有律师提交的数据尚未审核,请先完成审核后再进行批量上报。'); message.warning('有律师提交的数据尚未审核,请先完成审核后再进行批量上报。');
return; return;
} }
const costPermissionResult = await firmReportsApi.query();
if (costPermissionResult.data === false) {
// 成本权限检查返回false,需要先填报成本报表
message.warning('您需要先填报和提交律所收入,才能进行服务上报。');
// 调用成本管理新增弹框
showCostReportForm();
return;
}
} catch (error) { } catch (error) {
console.error('检查成本权限失败:', error); console.error('检查成本权限失败:', error);
} }
} }
// 如果没有成本查询权限,直接跳过成本检查
} }
// 检查选中的记录是否符合上报条件 // 检查选中的记录是否符合上报条件
@ -712,7 +711,14 @@ import dayjs from 'dayjs';
); );
if (canReportRecords.length === 0) { if (canReportRecords.length === 0) {
message.warning('选中的记录不符合上报条件,只能上报上个季度的数据'); message.warning('选中的记录不符合上报条件,请选择已通过律所审核且尚未上报的记录');
return;
}
const missingPeriods = await queryMissingIncomePeriods(canReportRecords);
if (missingPeriods.length > 0) {
message.warning(`以下活动所属季度尚未提交营业收入:${missingPeriods.map(item => item.label).join('、')},请完成填报后再上报律协`);
showCostReportForm(missingPeriods[0]);
return; return;
} }
@ -733,60 +739,41 @@ import dayjs from 'dayjs';
const isAuditPassed = record.firmAuditStatus === 3 && const isAuditPassed = record.firmAuditStatus === 3 &&
(record.associationAuditStatus === 0 || record.associationAuditStatus === 4); (record.associationAuditStatus === 0 || record.associationAuditStatus === 4);
// 如果有成本查询权限,需要检查季度限制;否则不限制 // 活动季度及对应收入报表由后端按 serviceEnd 统一校验
const hasCostPermission = loginInfo.value && loginInfo.value.costVisibleFlag === true;
if (hasCostPermission) {
// 有成本权限:只能上报上个季度的数据
// 律协要求:二季度上报一季度的数据,三季度上报二季度的数据
const isLastQuarterData = isLastQuarterRecord(record);
return isAuditPassed && isLastQuarterData;
} else {
// 无成本权限:不限制季度,可以上报所有数据
return isAuditPassed; return isAuditPassed;
} }
}
// 判断记录是否为上个季度的数据 async function queryMissingIncomePeriods(records) {
function isLastQuarterRecord(record) { const periodMap = new Map();
if (!record.reportTime) return false; records.forEach(record => {
if (!record.serviceEnd) return;
const currentDate = new Date(); const activityDate = dayjs(record.serviceEnd);
const currentYear = currentDate.getFullYear(); const year = activityDate.year();
const currentMonth = currentDate.getMonth() + 1; const quarter = Math.ceil((activityDate.month() + 1) / 3);
const currentQuarter = Math.ceil(currentMonth / 3); periodMap.set(`${year}-${quarter}`, { year, quarter });
});
// 计算上个季度
let lastQuarter = currentQuarter - 1;
let lastQuarterYear = currentYear;
if (lastQuarter === 0) { const missing = [];
lastQuarter = 4; for (const period of periodMap.values()) {
lastQuarterYear = currentYear - 1; const result = await firmReportsApi.queryByQuarter(period.year, period.quarter);
if (result.data !== true) {
missing.push({
...period,
label: `${period.year}年第${period.quarter}季度`,
});
} }
}
// 解析记录的提交时间 return missing;
const reportDate = new Date(record.reportTime);
const reportYear = reportDate.getFullYear();
const reportMonth = reportDate.getMonth() + 1;
const reportQuarter = Math.ceil(reportMonth / 3);
// 检查是否为上个季度的数据
return reportYear === lastQuarterYear && reportQuarter === lastQuarter;
} }
// 检查是否有未审核的数据 // 检查当前律所是否有未审核的数据
async function checkUnreviewedData() { async function checkUnreviewedData() {
try { try {
// 调用后端提供的专门接口查询是否有未审核的数据
const result = await serviceApplicationsApi.queryNoReview(); const result = await serviceApplicationsApi.queryNoReview();
// 如果返回的数据大于0,说明有未审核的记录
return result.data && result.data > 0; return result.data && result.data > 0;
} catch (error) { } catch (error) {
console.error('检查未审核数据失败:', error); console.error('检查未审核数据失败:', error);
return false; // 出错时默认返回false,避免阻塞用户操作 return false;
} }
} }
@ -1057,9 +1044,9 @@ import dayjs from 'dayjs';
} }
// ---------------------------- 成本报表 ---------------------------- // ---------------------------- 成本报表 ----------------------------
function showCostReportForm() { function showCostReportForm(period) {
// 打开成本报表新增弹框 // 打开成本报表新增弹框
costReportFormRef.value.show(); costReportFormRef.value.show(period);
} }
// ---------------------------- 详情 ---------------------------- // ---------------------------- 详情 ----------------------------

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

@ -253,6 +253,7 @@ 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';
import { firmReportsApi } from '/@/api/business/cost/firm-reports-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const'; import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry'; import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue'; import TableOperator from '/@/components/support/table-operator/index.vue';
@ -562,7 +563,7 @@ import { getRoleInfo } from '/@/utils/role-util';
} }
// 批量上报 // 批量上报
function batchReport() { async function batchReport() {
if (selectedRowKeyList.value.length === 0) { if (selectedRowKeyList.value.length === 0) {
message.warning('请选择要上报的记录'); message.warning('请选择要上报的记录');
return; return;
@ -578,6 +579,26 @@ import { getRoleInfo } from '/@/utils/role-util';
return; return;
} }
const periods = new Map();
canReportRecords.forEach(record => {
if (!record.serviceEnd) return;
const date = dayjs(record.serviceEnd);
const year = date.year();
const quarter = Math.ceil((date.month() + 1) / 3);
periods.set(`${year}-${quarter}`, { year, quarter });
});
const missingPeriods = [];
for (const period of periods.values()) {
const result = await firmReportsApi.queryByQuarter(period.year, period.quarter);
if (result.data !== true) {
missingPeriods.push(`${period.year}年第${period.quarter}季度`);
}
}
if (missingPeriods.length > 0) {
message.warning(`以下活动所属季度尚未提交营业收入:${missingPeriods.join('、')},请完成填报后再上报律协`);
return;
}
// 确认上报 // 确认上报
Modal.confirm({ Modal.confirm({
title: '批量上报确认', title: '批量上报确认',
@ -591,9 +612,9 @@ import { getRoleInfo } from '/@/utils/role-util';
// 判断记录是否符合上报条件 // 判断记录是否符合上报条件
function canReportRecord(record) { function canReportRecord(record) {
// 只能上报已通过执业机构审核的记录(状态为3:已通过) // 只能上报已通过执业机构审核的记录(状态为3:已通过)
// 且协会审核状态必须是未提交(0) // 且协会审核状态必须是未提交(0)或被驳回(4)
return record.firmAuditStatus === 3 && return record.firmAuditStatus === 3 &&
record.associationAuditStatus === 0; (record.associationAuditStatus === 0 || record.associationAuditStatus === 4);
} }
// 执行批量上报 // 执行批量上报

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

File diff suppressed because it is too large

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

@ -152,6 +152,16 @@
</template> </template>
</div> </div>
<!-- 多人代报信息 -->
<div class="form-item" v-if="readonlyMode && Number(form.isBatch) === 1 && !form.parentApplicationId">
<label class="form-label">同所同行人</label>
<input :value="peerParticipantNames" class="form-input" readonly />
</div>
<div class="form-item" v-if="readonlyMode && Number(form.isBatch) === 1 && form.parentApplicationId">
<label class="form-label">代报人</label>
<input :value="form.declarerName || '-'" class="form-input" readonly />
</div>
<!-- dict类型:显示服务时长、服务开始时间和服务结束时间 --> <!-- dict类型:显示服务时长、服务开始时间和服务结束时间 -->
<template v-if="activityType === 'DICT'"> <template v-if="activityType === 'DICT'">
<!-- 服务开始时间 --> <!-- 服务开始时间 -->
@ -202,11 +212,11 @@
<template v-else-if="activityType === 'TIME'"> <template v-else-if="activityType === 'TIME'">
<!-- 案件编号 --> <!-- 案件编号 -->
<div class="form-item"> <div class="form-item">
<label class="form-label">案件编号</label> <label class="form-label">备案/案件编号</label>
<input <input
v-model="form.recordNo" v-model="form.recordNo"
class="form-input" class="form-input"
placeholder="请输入案件编号" placeholder="请输入备案/案件编号"
:readonly="readonlyMode" :readonly="readonlyMode"
/> />
</div> </div>
@ -227,7 +237,7 @@
</template> </template>
<!-- amount类型:显示金额输入框 --> <!-- amount类型:显示金额输入框 -->
<template v-else-if="activityType === 'AMOUT'"> <template v-else-if="['AMOUNT', 'AMOUT'].includes(activityType)">
<!-- 金额 --> <!-- 金额 -->
<div class="form-item"> <div class="form-item">
<label class="form-label">金额</label> <label class="form-label">金额</label>
@ -287,7 +297,7 @@
<input <input
v-model="form.organizerPhone" v-model="form.organizerPhone"
class="form-input" class="form-input"
placeholder="联系方式" placeholder="手机号、座机或邮箱(如 0571-12345678)"
:readonly="readonlyMode" :readonly="readonlyMode"
/> />
</div> </div>
@ -534,7 +544,7 @@ watch(
// 默认值 // 默认值
form.serviceDuration = 25; form.serviceDuration = 25;
} }
} else if (selectedActivity.timeType === 'AMOUT') { } else if (['AMOUNT', 'AMOUT'].includes(selectedActivity.timeType)) {
// 保留现有值,不设为undefined // 保留现有值,不设为undefined
} else if (selectedActivity.timeType === 'DICT') { } else if (selectedActivity.timeType === 'DICT') {
// DICT类型:清空服务时长,让用户通过选择时间或手动填写 // DICT类型:清空服务时长,让用户通过选择时间或手动填写
@ -888,7 +898,7 @@ async function handleSave() {
return return
} }
} }
} else if (currentActivityType.value === 'AMOUT') { } else if (['AMOUNT', 'AMOUT'].includes(currentActivityType.value)) {
// AMOUNT类型:验证金额和服务时长范围 // AMOUNT类型:验证金额和服务时长范围
if (!form.workloadScore) { if (!form.workloadScore) {
message.error('请填写金额') message.error('请填写金额')
@ -992,7 +1002,7 @@ async function handleSubmit() {
return return
} }
} }
} else if (currentActivityType.value === 'AMOUT') { } else if (['AMOUNT', 'AMOUT'].includes(currentActivityType.value)) {
// AMOUNT类型:验证金额和服务时长范围 // AMOUNT类型:验证金额和服务时长范围
if (!form.workloadScore) { if (!form.workloadScore) {
message.error('请填写金额') message.error('请填写金额')
@ -1117,6 +1127,16 @@ function getActivityName() {
onMounted(() => { onMounted(() => {
getDetail() getDetail()
}) })
// 兼容历史数据:旧记录的 participantNames 可能包含申报人本人。
const peerParticipantNames = computed(() => {
const selfName = String(form.actualName || '').trim()
return String(form.participantNames || '')
.split('、')
.map((name) => name.trim())
.filter((name) => name && name !== selfName)
.join('、') || '-'
})
</script> </script>
<style scoped> <style scoped>

8
src/views/system/account/components/message/index.vue

@ -74,7 +74,7 @@
v-model:current="queryForm.pageNum" v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="handlePageChange"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>
@ -143,6 +143,12 @@
ajaxQuery(); ajaxQuery();
} }
function handlePageChange(pageNum, pageSize) {
queryForm.pageNum = pageNum;
queryForm.pageSize = pageSize;
ajaxQuery();
}
// 查询 // 查询
async function ajaxQuery() { async function ajaxQuery() {
try { try {

9
src/views/system/employee/components/employee-form-modal/index.vue

@ -16,7 +16,6 @@
@close="onClose" @close="onClose"
destroyOnClose destroyOnClose
> >
<br /> <br />
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical"> <a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<a-form-item label="姓名" name="actualName"> <a-form-item label="姓名" name="actualName">
@ -50,13 +49,15 @@
<a-form-item label="角色" name="roleIdList"> <a-form-item label="角色" name="roleIdList">
<a-select v-model:value="form.roleIdList" optionFilterProp="title" placeholder="请选择角色" :mode="null"> <a-select v-model:value="form.roleIdList" optionFilterProp="title" placeholder="请选择角色" :mode="null">
<a-select-option v-for="item in roleList" :key="item.roleId" :value="item.roleId" :title="item.roleName">{{ item.roleName }}</a-select-option> <a-select-option v-for="item in roleList" :key="item.roleId" :value="item.roleId" :title="item.roleName">{{
item.roleName
}}</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="成本查看权限" name="costVisibleFlag"> <a-form-item label="成本查看权限" name="costVisibleFlag">
<a-switch v-model:checked="form.costVisibleFlag" checked-children="有" un-checked-children="无" /> <a-switch v-model:checked="form.costVisibleFlag" checked-children="有" un-checked-children="无" />
</a-form-item> </a-form-item>
<a-form-item label="无处罚证明查看权限" name="penaltyFlag"> <a-form-item label="无处分证明查看权限" name="penaltyFlag">
<a-switch v-model:checked="form.penaltyFlag" checked-children="有" un-checked-children="无" /> <a-switch v-model:checked="form.penaltyFlag" checked-children="有" un-checked-children="无" />
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -132,7 +133,7 @@
positionId: undefined, positionId: undefined,
email: undefined, email: undefined,
costVisibleFlag: false, //成本查看权限 costVisibleFlag: false, //成本查看权限
penaltyFlag: false, //无处罚证明查看权限 penaltyFlag: false, //无处分证明查看权限
}; };
let form = reactive(_.cloneDeep(formDefault)); let form = reactive(_.cloneDeep(formDefault));

21
src/views/system/login/login.vue

@ -31,7 +31,7 @@
</a-form-item> </a-form-item>
</a-form> </a-form>
</div> </div>
<div class="tech-contact">技术支持电话:18226700205</div> <div class="tech-contact">技术支持电话:18755103557</div>
</div> </div>
</template> </template>
<script setup> <script setup>
@ -79,25 +79,6 @@
onLogin(); onLogin();
} }
}; };
notification['success']({
message: '温馨提示',
description: 'SmartAdmin 提供 9种 登录背景风格哦!',
duration: 8,
onClick: () => {},
btn: () =>
h(
Button,
{
type: 'primary',
target: '_blank',
size: 'small',
href: 'https://smartadmin.vip/views/doc/front/Login.html',
onClick: () => {},
},
{ default: () => '去看看' }
),
});
}); });
onUnmounted(() => { onUnmounted(() => {

4
src/views/system/login3/login.vue

@ -39,7 +39,7 @@
<div class="btn" @click="onLogin">登录</div> <div class="btn" @click="onLogin">登录</div>
</a-form-item> </a-form-item>
</a-form> </a-form>
<div class="tech-contact">技术支持联系电话:王经理 18226700205</div> <div class="tech-contact">技术支持联系电话: 18755103557</div>
</div> </div>
</div> </div>
</div> </div>
@ -53,7 +53,7 @@
import { LOGIN_DEVICE_ENUM } from '/@/constants/system/login-device-const'; import { LOGIN_DEVICE_ENUM } from '/@/constants/system/login-device-const';
import { useUserStore } from '/@/store/modules/system/user'; import { useUserStore } from '/@/store/modules/system/user';
import loginQR from '/@/assets/images/login/login-qr.png'; import loginQR from '/@/assets/images/login/login-qr.png';
import loginGif from '/@/assets/images/login/login-min.gif'; import loginGif from '/src/assets/images/login/loginnew.jpeg';
import { buildRoutes } from '/@/router/index'; import { buildRoutes } from '/@/router/index';
import { smartSentry } from '/@/lib/smart-sentry'; import { smartSentry } from '/@/lib/smart-sentry';

Loading…
Cancel
Save