Browse Source

服务修改,签约修改

master
wang 9 months ago
parent
commit
63cb3f97e5
  1. BIN
      dist.zip
  2. 34
      src/api/business/letter/letter-api.js
  3. 10
      src/api/business/service-applications/service-applications-api.js
  4. 2
      src/constants/support/file-const.js
  5. 2
      src/layout/components/side-help-doc/index.vue
  6. 431
      src/views/business/erp/letter/letter-list.vue
  7. 139
      src/views/business/erp/service/service-applications-form.vue
  8. 19
      src/views/business/erp/service/service-applications-list.vue
  9. 1276
      src/views/business/erp/service/service-applications-report-list.vue
  10. 20
      src/views/mobile/service/create.vue
  11. 30
      src/views/mobile/service/detail.vue
  12. 2
      src/views/system/home/components/agreement.vue
  13. 12
      vite.config.js

BIN
dist.zip

Binary file not shown.

34
src/api/business/letter/letter-api.js

@ -5,7 +5,7 @@
* @Date: 2025-12-22 17:43:06 * @Date: 2025-12-22 17:43:06
* @Copyright 1 * @Copyright 1
*/ */
import { postRequest, getRequest } from '/@/lib/axios'; import { postRequest, getRequest, getDownload } from '/@/lib/axios';
export const letterApi = { export const letterApi = {
@ -33,10 +33,40 @@ export const letterApi = {
/** /**
* 查看承诺书详情 @author wzh * 查看承诺书详情 @author wzh
*/ */
isLetter: (letterId) => { detail: (letterId) => {
return getRequest(`/letter/detail/${letterId}`); return getRequest(`/letter/detail/${letterId}`);
}, },
/**
* 检查用户是否已签署承诺书 @author wzh
*/
isLetter: (userId) => {
return getRequest(`/letter/isLetter/${userId}`);
},
/**
* 查询所有承诺书 @author wzh
*/
queryAllBook: () => {
return getRequest('/letter/queryAllBook');
},
/**
* 下载承诺书文件 @author wzh
*/
downloadLetter: (letterId) => {
return getDownload(`/letter/download/${letterId}`);
},
/**
* 上传承诺书证明文件 @author wzh
*/
uploadProof: (formData) => {
return postRequest('/letter/uploadProof', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
},
}; };

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

@ -9,13 +9,19 @@ import { postRequest, getRequest,getDownload, postDownload } from '/@/lib/axios'
export const serviceApplicationsApi = { export const serviceApplicationsApi = {
/** /**
* 分页查询 @author wzh * 服务申报页面报告查询 @author wzh
*/
queryPageReport : (param) => {
return postRequest('/serviceApplications/queryPageReport', param);
},
/**
* 服务审批分页查询 @author wzh
*/ */
queryPage : (param) => { queryPage : (param) => {
return postRequest('/serviceApplications/queryPage', param); return postRequest('/serviceApplications/queryPage', param);
}, },
/** /**
* 增加 @author wzh * 增加 @author wzh
*/ */

2
src/constants/support/file-const.js

@ -23,7 +23,7 @@ export const FILE_FOLDER_TYPE_ENUM = {
}, },
FEEDBACK: { FEEDBACK: {
value: 4, value: 4,
desc: '意见反馈', desc: '承诺书',
}, },
}; };
export default { export default {

2
src/layout/components/side-help-doc/index.vue

@ -20,7 +20,7 @@
<a href="http://8.148.67.92:9000/file/public/notice/c620241c460a4634afdb4b4cebf271d8_20260204121215.docx" target="_blank"> <a href="http://8.148.67.92:9000/file/public/notice/c620241c460a4634afdb4b4cebf271d8_20260204121215.docx" target="_blank">
<file-outlined style="margin-right: 5px;" /> <file-outlined style="margin-right: 5px;" />
合肥市律师公益法律服务管理系统 全角色操作手册.docx 合肥市律师公益法律服务管理系统操作手册.docx
</a> </a>
</div> </div>

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

@ -50,10 +50,46 @@
</a-form> </a-form>
<!---------- 查询表单form end -----------> <!---------- 查询表单form end ----------->
<!---------- 承诺书列表显示区域 begin ----------->
<a-card v-if="!isCeo" size="small" :bordered="false" :hoverable="true" style="margin-bottom: 16px;">
<template #title>
<span>承诺书列表</span>
</template>
<div style="display: flex; flex-wrap: wrap; gap: 16px;">
<div
v-for="item in noticeList"
:key="item.bookId || item.noticeId || item.id"
style="display: flex; align-items: center; padding: 8px 12px; background: #f5f5f5; border-radius: 4px; cursor: pointer;"
@click="downloadNoticeAttachment(item)"
:title="hasAttachment(item) ? '点击下载' : '暂无附件'"
>
<FileTextOutlined style="font-size: 16px; color: #1890ff; margin-right: 8px;" />
<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;" />
</div>
</div>
</a-card>
<!---------- 承诺书列表显示区域 end ----------->
<a-card size="small" :bordered="false" :hoverable="true"> <a-card size="small" :bordered="false" :hoverable="true">
<!---------- 表格操作行 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">
<!-- user角色只显示律师承诺书按钮 -->
<a-button type="primary" @click="handleLawyerUpload" :loading="proofUploadLoading" v-if="isLawyerRole && !isFirmRole">
<UploadOutlined />
上传律师承诺书
</a-button>
<!-- cto角色显示两个按钮 -->
<a-button type="primary" @click="handleLawyerUpload" :loading="proofUploadLoading" v-if="isFirmRole" style="margin-left: 8px;">
<UploadOutlined />
上传律师承诺书
</a-button>
<a-button type="primary" @click="handleFirmUpload" :loading="proofUploadLoading" v-if="isFirmRole" style="margin-left: 8px;">
<UploadOutlined />
上传律所承诺书
</a-button>
</div> </div>
<div class="smart-table-setting-block"> <div class="smart-table-setting-block">
<TableOperator v-model="columns" :tableId="null" :refresh="queryData" /> <TableOperator v-model="columns" :tableId="null" :refresh="queryData" />
@ -106,6 +142,43 @@
@cancel="handleAgreementCancel" @cancel="handleAgreementCancel"
/> />
<!-- 上传证明文件弹框 -->
<a-modal
v-model:visible="showUploadModal"
:title="letterType === 'user' ? '上传律师承诺书' : '上传律所承诺书'"
@cancel="showUploadModal = false"
:confirm-loading="proofUploadLoading"
@ok="handleUpload"
>
<a-form layout="vertical">
<a-form-item label="上传文件" required>
<a-upload
:file-list="proofFileList"
:customRequest="handleCustomUpload"
:max-count="1"
accept=".jpg,.jpeg,.png"
@change="handleFileChange"
>
<a-button>
<UploadOutlined />
选择文件
</a-button>
</a-upload>
<div v-if="proofFileList.length > 0" style="margin-top: 8px; color: #52c41a; font-size: 12px;">
已选择文件: {{ proofFileList[0].name }}
</div>
<div style="margin-top: 8px; color: #999; font-size: 12px;">
支持格式:图片(JPG/PNG),文件大小不超过10MB
</div>
</a-form-item>
</a-form>
</a-modal>
<!-- 图片预览弹框 -->
<a-modal :footer="null" :open="previewVisible" @cancel="handleCancelPreview">
<img :src="previewUrl" alt="预览图片" style="width: 100%; max-height: 80vh; object-fit: contain;" />
</a-modal>
</a-card> </a-card>
</template> </template>
<script setup> <script setup>
@ -114,8 +187,12 @@ 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';
import { employeeApi } from '/@/api/system/employee-api'; import { employeeApi } from '/@/api/system/employee-api';
import { noticeApi } from '/@/api/business/oa/notice-api';
import { fileApi } from '/@/api/support/file-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const'; import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { loginApi } from '/@/api/system/login-api';
import { smartSentry } from '/@/lib/smart-sentry'; import { smartSentry } from '/@/lib/smart-sentry';
import { FILE_FOLDER_TYPE_ENUM } from '/@/constants/support/file-const';
import TableOperator from '/@/components/support/table-operator/index.vue'; import TableOperator from '/@/components/support/table-operator/index.vue';
import LetterForm from './letter-form.vue'; import LetterForm from './letter-form.vue';
import AgreementModal from '/@/views/system/home/components/agreement.vue'; import AgreementModal from '/@/views/system/home/components/agreement.vue';
@ -124,13 +201,13 @@ import AgreementModal from '/@/views/system/home/components/agreement.vue';
const columns = ref([ const columns = ref([
{ {
title: '承诺书名称', title: '用户名称',
dataIndex: 'letterName', dataIndex: 'userName',
ellipsis: true, ellipsis: true,
}, },
{ {
title: '用户名称', title: '承诺书类型',
dataIndex: 'userName', dataIndex: 'letterName',
ellipsis: true, ellipsis: true,
}, },
{ {
@ -153,18 +230,246 @@ import AgreementModal from '/@/views/system/home/components/agreement.vue';
// 查看详情 // 查看详情
async function viewDetail(record) { async function viewDetail(record) {
try { try {
// 设置当前letterId并显示弹框 // 检查是否有文件地址
currentLetterId.value = record.letterId; if (!record.fileUrl) {
showAgreementModal.value = true; message.warning('该承诺书没有附件文件');
return;
}
// 直接使用查询结果中的fileUrl进行预览
const fileUrl = record.fileUrl;
// 根据文件名推断文件类型
const fileName = record.letterName || '';
const fileExtension = fileName.split('.').pop() || '';
showImagePreview(fileUrl);
} catch (error) {
console.error('预览承诺书失败:', error);
message.error('预览承诺书失败,请稍后重试');
}
}
// 图片预览相关
const previewVisible = ref(false);
const previewUrl = ref('');
function showImagePreview(url) {
previewUrl.value = url;
previewVisible.value = true;
}
function handleCancelPreview() {
previewVisible.value = false;
previewUrl.value = '';
}
// ---------------------------- 上传证明功能 ----------------------------
// 上传证明相关状态
const proofFileList = ref([]);
const proofUploadLoading = ref(false);
const letterType = ref('user'); // 默认为用户类型
const showUploadModal = ref(false);
const uploadedFileId = ref(null);
// 证明文件上传前验证
function beforeUploadProof(file) {
// 检查文件类型
const isImage = file.type === 'image/jpeg' || file.type === 'image/jpg' || file.type === 'image/png';
if (!isImage) {
message.error('只能上传PDF、Word文档或图片文件!');
return false;
}
// 检查文件大小(10MB)
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
message.error('文件大小不能超过10MB!');
return false;
}
return true;
}
// 律师承诺书上传
async function handleLawyerUpload() {
try {
// 在点击上传按钮时立即检查用户是否已签署承诺书
const hasSigned = await checkUserSigned();
if (hasSigned) {
message.warning('您已经签署过律师承诺书,无需重复上传');
return;
}
// 只有未签约的用户才能打开上传弹框
letterType.value = 'user';
showUploadModal.value = true;
proofFileList.value = [];
} catch (error) { } catch (error) {
console.error('查看承诺书详情失败:', error); console.error('检查签约状态失败:', error);
// 如果出错,显示错误信息 message.error('检查签约状态失败,请稍后重试');
Modal.error({ }
title: '错误', }
content: '获取承诺书详情失败,请稍后重试',
okText: '确定', // 律所承诺书上传
async function handleFirmUpload() {
try {
// 在点击上传按钮时立即检查用户是否已签署承诺书
const hasSigned = await checkUserSigned();
if (hasSigned) {
message.warning('您已经签署过律所承诺书,无需重复上传');
return;
}
// 只有未签约的用户才能打开上传弹框
letterType.value = 'cto';
showUploadModal.value = true;
proofFileList.value = [];
} catch (error) {
console.error('检查签约状态失败:', error);
message.error('检查签约状态失败,请稍后重试');
}
}
// 处理文件选择变化
function handleFileChange(info) {
let fileList = [...info.fileList];
// 限制只能上传一个文件
fileList = fileList.slice(-1);
// 确保文件有正确的originFileObj
fileList = fileList.map(file => {
if (file.response) {
// 组件会将文件信息转换为包含response的对象
file.url = file.response.url;
}
return file;
}); });
proofFileList.value = fileList;
}
// 自定义上传请求 - 参考公告中的正确实现
async function handleCustomUpload(options) {
try {
const formData = new FormData();
formData.append('file', options.file);
const uploadResult = await fileApi.uploadFile(formData, FILE_FOLDER_TYPE_ENUM.FEEDBACK.value);
if (uploadResult.code === 0 && uploadResult.data) {
// 上传成功,调用onSuccess回调,并将结果设置到file.response中
options.onSuccess(uploadResult.data);
// 同时更新文件列表中的response数据,以便handleUpload函数能检测到
if (proofFileList.value.length > 0) {
proofFileList.value[0].response = uploadResult;
}
} else {
// 上传失败,调用onError回调
options.onError(new Error(uploadResult.msg || '上传失败'));
}
} catch (error) {
// 异常情况,调用onError回调
options.onError(error);
}
}
// 保存承诺书
async function handleUpload() {
if (proofFileList.value.length === 0) {
message.warning('请选择要上传的证明文件');
return;
}
try {
proofUploadLoading.value = true;
// 检查文件是否已经上传成功(通过customRequest)
const file = proofFileList.value[0];
if (!file.response || !file.response.data) {
message.warning('文件尚未上传完成,请稍后重试');
return;
}
// 获取已上传文件的ID - 根据接口返回的数据结构
const fileId = file.response.data.fileId;
if (!fileId) {
message.error('获取文件ID失败,请重新上传文件');
return;
}
// 调用承诺书保存接口,传递文件ID、类型和文件地址
const saveResult = await letterApi.add({
attachmentIds: fileId,
letterType: letterType.value,
fileUrl: file.response.data.fileUrl
});
if (saveResult.code === 0) {
// 直接显示接口返回的data内容
message.success(saveResult.data || '承诺书保存成功');
// 清空文件列表
proofFileList.value = [];
// 刷新列表
queryData();
// 关闭弹框
showUploadModal.value = false;
} else {
message.error(saveResult.msg || '保存承诺书失败');
}
} catch (error) {
console.error('保存承诺书失败:', error);
message.error('保存失败,请稍后重试');
} finally {
proofUploadLoading.value = false;
}
}
// 检查公告是否有附件
function hasAttachment(notice) {
// 检查常见的附件字段
return !!(notice.attachment || notice.fileKey || notice.fileId ||
notice.attachmentId || notice.fileUrl || notice.attachmentUrl);
}
// 获取公告附件文件key
function getAttachmentFileKey(notice) {
// 根据常见的附件字段获取文件key
return notice.attachment || notice.fileKey || notice.fileId ||
notice.attachmentId || notice.fileUrl || notice.attachmentUrl;
}
// 下载公告附件
async function downloadNoticeAttachment(notice) {
try {
SmartLoading.show();
// 获取附件文件key
let fileKey = getAttachmentFileKey(notice);
if (!fileKey) {
message.warning('该公告没有附件文件');
return;
}
// 使用file-api下载文件
await fileApi.downLoadFile(fileKey);
message.success('附件下载成功');
} catch (error) {
console.error('下载公告附件失败:', error);
message.error('下载附件失败,请稍后重试');
} finally {
SmartLoading.hide();
} }
} }
@ -202,6 +507,96 @@ import AgreementModal from '/@/views/system/home/components/agreement.vue';
// 总数 // 总数
const total = ref(0); const total = ref(0);
// ---------------------------- 角色判断相关 ----------------------------
// 判断用户是否为CEO角色
const isCeo = ref(false);
// 判断用户是否为律师角色
const isLawyerRole = ref(false);
// 判断用户是否为律所角色
const isFirmRole = ref(false);
// 获取登录信息并检查角色
async function getLoginInfo() {
try {
const res = await loginApi.getLoginInfo();
const loginInfo = res.data;
// 根据用户角色判断角色类型
if (loginInfo) {
const roleCode = loginInfo.roleCode || '';
const userType = loginInfo.userType || '';
const departmentName = loginInfo.departmentName || '';
// 根据roleCode、userType和部门名称判断角色
const roleLower = roleCode.toLowerCase();
isCeo.value = roleLower === 'ceo';
// 律师角色:user
isLawyerRole.value = roleLower === 'user' || userType === 'user';
// 律所角色:cto
isFirmRole.value = roleLower === 'cto' || userType === 'cto';
}
} catch (e) {
console.error('获取登录信息失败:', e);
}
}
// 检查用户是否已签署承诺书
async function checkUserSigned() {
try {
// 先获取当前登录用户信息
const loginRes = await loginApi.getLoginInfo();
const loginInfo = loginRes.data;
if (!loginInfo || !loginInfo.employeeId) {
console.error('无法获取当前用户ID');
return false;
}
// 使用正确的后端接口检查签约状态
const res = await letterApi.isLetter(loginInfo.employeeId);
if (res.code === 0 && res.data !== undefined) {
// 接口返回true代表已签约,false代表未签约
return res.data === true;
}
return false;
} catch (error) {
console.error('检查用户签约状态失败:', error);
// 如果专用接口调用失败,回退到登录信息接口的agreementSignFlag字段
try {
const loginRes = await loginApi.getLoginInfo();
const loginInfo = loginRes.data;
if (loginInfo) {
const agreementSignFlag = loginInfo.agreementSignFlag;
return agreementSignFlag === true;
}
} catch (fallbackError) {
console.error('回退检查用户签约状态失败:', fallbackError);
}
return false;
}
}
// ---------------------------- 公告数据相关 ----------------------------
// 公告列表数据
const noticeList = ref([]);
// 查询承诺书数据
async function queryNoticeData() {
try {
// 使用新的承诺书接口查询数据
const result = await letterApi.queryAllBook();
if (result.data) {
noticeList.value = result.data;
}
} catch (error) {
console.error('查询承诺书数据失败:', error);
}
}
// 加载所有员工列表 // 加载所有员工列表
async function loadAllEmployees() { async function loadAllEmployees() {
if (employeeList.value.length > 0) { if (employeeList.value.length > 0) {
@ -287,10 +682,18 @@ import AgreementModal from '/@/views/system/home/components/agreement.vue';
} }
onMounted(() => { onMounted(async () => {
// 获取登录信息并检查角色
await getLoginInfo();
queryData(); queryData();
// 页面加载时自动加载员工列表 // 页面加载时自动加载员工列表
loadAllEmployees(); loadAllEmployees();
// 如果不是CEO角色,查询承诺书数据
if (!isCeo.value) {
queryNoticeData();
}
}); });
// 查看详情 // 查看详情

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

@ -93,12 +93,12 @@
<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:00:00" valueFormat="YYYY-MM-DD HH:00:00" v-model:value="form.serviceStart" style="width: 100%" placeholder="服务开始时间" :disabled="readonlyMode" /> <a-date-picker show-time format="YYYY-MM-DD HH:mm:ss" 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:00:00" valueFormat="YYYY-MM-DD HH:00:00" v-model:value="form.serviceEnd" style="width: 100%" placeholder="服务结束时间" :disabled="readonlyMode" /> <a-date-picker show-time format="YYYY-MM-DD HH:mm:ss" 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">
@ -107,9 +107,18 @@
<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 style="width: 100%" v-model:value="form.serviceDuration" placeholder="服务时长(小时)" :disabled="readonlyMode" /> <a-input-number
style="width: 100%"
v-model:value="form.serviceDuration"
placeholder="服务时长(小时)"
:disabled="readonlyMode"
:min="0"
:precision="1"
:parser="value => value.replace(/[^\d.]/g, '')"
@blur="handleServiceDurationBlur"
/>
</a-form-item> </a-form-item>
<div style="font-size: 12px; color: #f00d0dff; margin-top: -12px; margin-bottom: 12px;">注:不足30分钟(不含本数)的,不计入时长,超过30分钟不足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>
@ -130,7 +139,16 @@
<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 style="width: 100%" v-model:value="form.serviceDuration" placeholder="服务时长(小时)" :disabled="readonlyMode" /> <a-input-number
style="width: 100%"
v-model:value="form.serviceDuration"
placeholder="服务时长(小时)"
:disabled="readonlyMode"
:min="0"
:precision="2"
:parser="value => value.replace(/[^\d.]/g, '')"
@blur="handleServiceDurationBlur"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
@ -558,6 +576,40 @@
let form = reactive({ ...formDefault }); let form = reactive({ ...formDefault });
// 服务时长格式化函数
function formatServiceDuration(minutes) {
if (minutes <= 30) {
return 0.5; // 30分钟及以下按0.5小时
} else if (minutes <= 60) {
return 1; // 超过30分钟但不超过60分钟按1小时
} else {
// 超过1小时的,按实际小时数计算,但遵循同样的分钟规则
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
if (remainingMinutes === 0) {
return hours; // 整小时,直接返回小时数
} else if (remainingMinutes <= 30) {
return hours + 0.5; // 剩余30分钟及以下加0.5小时
} else {
return hours + 1; // 剩余超过30分钟加1小时
}
}
}
// 服务时长输入验证函数(用于手动输入)
function validateServiceDuration(hours) {
if (hours === undefined || hours === null || isNaN(hours) || hours < 0) {
return undefined;
}
// 将小时转换为分钟
const totalMinutes = Math.round(hours * 60);
// 使用格式化函数计算正确的值
return formatServiceDuration(totalMinutes);
}
// 监听服务开始时间和结束时间变化,自动计算服务时长 // 监听服务开始时间和结束时间变化,自动计算服务时长
watch( watch(
() => [form.serviceStart, form.serviceEnd], () => [form.serviceStart, form.serviceEnd],
@ -567,9 +619,11 @@
const start = dayjs(startTime); const start = dayjs(startTime);
const end = dayjs(endTime); const end = dayjs(endTime);
if (end.isAfter(start)) { if (end.isAfter(start)) {
// 计算时间差(分钟),然后转换为小时(保留2位小数) // 计算时间差(分钟)
const durationMinutes = end.diff(start, 'minute'); const durationMinutes = end.diff(start, 'minute');
form.serviceDuration = parseFloat((durationMinutes / 60).toFixed(2));
// 根据规则计算服务时长
form.serviceDuration = formatServiceDuration(durationMinutes);
} else { } else {
form.serviceDuration = undefined; form.serviceDuration = undefined;
message.warning('服务结束时间必须晚于开始时间'); message.warning('服务结束时间必须晚于开始时间');
@ -582,6 +636,20 @@
{ immediate: true } { immediate: true }
); );
// 监听手动输入的服务时长变化(仅用于清空处理)
watch(
() => form.serviceDuration,
(newValue, oldValue) => {
// 如果用户清空了服务时长(设置为undefined或null),允许清空
if (newValue === undefined || newValue === null) {
return;
}
// 其他验证逻辑通过事件处理函数处理,避免重复
},
{ immediate: true }
);
// 监听活动名称变化,更新活动类型和设置默认服务时长 // 监听活动名称变化,更新活动类型和设置默认服务时长
watch( watch(
() => form.activityNameId, () => form.activityNameId,
@ -700,6 +768,18 @@
return Promise.reject('你填写的此类公益活动时长超出该活动要求的100-200小时,请按规范要求填报具体时长'); return Promise.reject('你填写的此类公益活动时长超出该活动要求的100-200小时,请按规范要求填报具体时长');
} }
} }
// 验证服务时长是否符合规则(适用于所有类型)
if (value !== undefined && value !== null) {
// 将小时转换为分钟进行验证
const inputMinutes = Math.round(value * 60);
const formattedDuration = formatServiceDuration(inputMinutes);
// 检查是否符合规则
if (Math.abs(formattedDuration - value) > 0.01) {
}
}
return Promise.resolve(); return Promise.resolve();
} }
} }
@ -823,6 +903,11 @@
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 {
@ -871,6 +956,46 @@
show, show,
}); });
// 服务时长输入框blur事件处理
function handleServiceDurationBlur() {
const value = form.serviceDuration;
// 当用户删除输入时,value会是null或undefined,允许这种情况
if (value === null || value === undefined) {
return;
}
// 验证输入值是否为有效数字,防止异常输入
if (isNaN(value) || value < 0) {
// 无效输入,清空并提示
form.serviceDuration = undefined;
message.warning('请输入有效的服务时长');
return;
}
// 限制输入范围,防止异常值
if (value > 50) {
form.serviceDuration = 50;
message.warning('服务时长不能超过50小时');
return;
}
// 无论是否有时间选择,都对用户手动输入进行格式化验证
// 将小时转换为分钟进行验证
const inputMinutes = Math.round(value * 60);
// 使用相同的格式化规则
const formattedDuration = formatServiceDuration(inputMinutes);
// 如果格式化后的值与输入值不同,说明用户输入了不符合规则的值
if (Math.abs(formattedDuration - value) > 0.01) {
// 提示用户并自动修正为符合规则的值
message.info(`服务时长已自动调整为符合规则的值:${formattedDuration}小时`);
// 直接设置格式化后的值
form.serviceDuration = formattedDuration;
}
}
// 获取文件URL // 获取文件URL
function getFileUrl(file) { function getFileUrl(file) {

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

@ -1,5 +1,5 @@
<!-- <!--
* 服务申报表 * 服务审批表
* *
* @Author: wzh * @Author: wzh
* @Date: 2025-12-20 14:44:06 * @Date: 2025-12-20 14:44:06
@ -45,12 +45,6 @@
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="本人本所" v-if="isCtoRole" class="smart-query-form-item">
<a-select style="width: 150px" v-model:value="queryForm.selfFirmFilter" placeholder="请选择筛选条件">
<a-select-option value="self">本人</a-select-option>
<a-select-option value="firm">本所</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="协会审核状态" v-if="isCeo" class="smart-query-form-item"> <a-form-item label="协会审核状态" v-if="isCeo" class="smart-query-form-item">
<a-select style="width: 150px" v-model:value="queryForm.associationAuditStatus" placeholder="请选择审核状态"> <a-select style="width: 150px" v-model:value="queryForm.associationAuditStatus" placeholder="请选择审核状态">
<a-select-option v-for="status in Object.values(REVIEW_ENUM)" :key="status.value" :value="status.value"> <a-select-option v-for="status in Object.values(REVIEW_ENUM)" :key="status.value" :value="status.value">
@ -81,13 +75,13 @@
<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">
<!-- 新建申报按钮:非协会角色且非CEO角色显示 --> <!-- 新建申报按钮:非协会角色且非CEO角色显示
<a-button v-if="!isAssociationRole && !isCeo" @click="showForm" type="primary"> <a-button v-if="!isAssociationRole && !isCeo" @click="showForm" type="primary">
<template #icon> <template #icon>
<PlusOutlined /> <PlusOutlined />
</template> </template>
新建申报 新建申报
</a-button> </a-button>-->
<!-- 批量提交按钮:非CEO角色显示 --> <!-- 批量提交按钮:非CEO角色显示 -->
<a-button v-if="!isCeo" @click="batchSubmit" type="primary" :disabled="selectedRowKeyList.length == 0"> <a-button v-if="!isCeo" @click="batchSubmit" type="primary" :disabled="selectedRowKeyList.length == 0">
@ -123,13 +117,13 @@
批量上报 批量上报
</a-button> </a-button>
<a-button v-if="!isCeo" @click="confirmBatchDelete" type="primary" danger :disabled="!canBatchDelete()"> <!--<a-button v-if="!isCeo" @click="confirmBatchDelete" type="primary" danger :disabled="!canBatchDelete()">
<template #icon> <template #icon>
<DeleteOutlined /> <DeleteOutlined />
</template> </template>
批量删除 批量删除
</a-button> </a-button>
<!--<a-button @click="showImportModal" type="primary"> <a-button @click="showImportModal" type="primary">
<template #icon> <template #icon>
<ImportOutlined /> <ImportOutlined />
</template> </template>
@ -161,11 +155,8 @@
<a-button @click="showDetail(record)" type="link">详情</a-button> <a-button @click="showDetail(record)" type="link">详情</a-button>
<!-- 执业机构审核状态为拒绝时,只显示详情按钮 --> <!-- 执业机构审核状态为拒绝时,只显示详情按钮 -->
<template v-if="record.firmAuditStatus !== 4"> <template v-if="record.firmAuditStatus !== 4">
<a-button v-if="(record.firmAuditStatus === 0 || record.firmAuditStatus === 4) && record.userId === loginInfo?.userId" @click="showForm(record)" type="link">编辑</a-button>
<a-button v-if="(record.firmAuditStatus === 0 && record.userId === loginInfo?.userId)" @click="onSubmit(record)" type="link">提交</a-button>
<a-button v-if="canAuditRecord(record)" @click="showAuditModal(record)" type="link">审核</a-button> <a-button v-if="canAuditRecord(record)" @click="showAuditModal(record)" type="link">审核</a-button>
<a-button v-if="canRejectRecord(record)" @click="showRejectModal(record)" danger type="link">驳回</a-button> <a-button v-if="canRejectRecord(record)" @click="showRejectModal(record)" danger type="link">驳回</a-button>
<a-button v-if="record.firmAuditStatus === 0 && record.userId === loginInfo?.userId" @click="onDelete(record)" danger type="link">删除</a-button>
</template> </template>
</div> </div>
</template> </template>

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

File diff suppressed because it is too large

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

@ -150,7 +150,7 @@
class="form-input" class="form-input"
placeholder="服务时长(小时)" placeholder="服务时长(小时)"
/> />
<div style="font-size: 12px; color: #f00d0d; margin-top: 6px;">注:不足30分钟(不含本数)的,不计入时长,超过30分钟不足1个小时的(含本数),按照一个小时填报</div> <div style="font-size: 12px; color: #f00d0d; margin-top: 6px;">注:不足30分钟(不含本数)的,按照0.5小时填报,超过30分钟不足1个小时的(含本数),按照一个小时填报</div>
</div> </div>
</template> </template>
@ -195,25 +195,7 @@
/> />
</div> </div>
<!-- 服务时长 -->
<div class="form-item">
<label class="form-label required">服务时长(小时)</label>
<input
v-model="form.serviceDuration"
class="form-input"
placeholder="服务时长(小时)"
/>
<div style="font-size: 12px; color: #f00d0d; margin-top: 6px;">注:服务时长必须在100-200小时范围内</div>
</div>
</template>
<!-- 加载中状态 -->
<template v-else key="loading-template">
<div class="form-item">
<div style="text-align: center; padding: 20px; color: #999;">
加载中...
</div>
</div>
</template> </template>
</div> </div>

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

@ -159,8 +159,8 @@
<label class="form-label">服务开始时间</label> <label class="form-label">服务开始时间</label>
<a-date-picker <a-date-picker
show-time show-time
format="YYYY-MM-DD HH:00:00" format="YYYY-MM-DD HH:mm:ss"
valueFormat="YYYY-MM-DD HH:00:00" valueFormat="YYYY-MM-DD HH:mm:ss"
v-model:value="form.serviceStart" v-model:value="form.serviceStart"
class="form-input" class="form-input"
placeholder="服务开始时间" placeholder="服务开始时间"
@ -173,8 +173,8 @@
<label class="form-label">服务结束时间</label> <label class="form-label">服务结束时间</label>
<a-date-picker <a-date-picker
show-time show-time
format="YYYY-MM-DD HH:00:00" format="YYYY-MM-DD HH:mm:ss"
valueFormat="YYYY-MM-DD HH:00:00" valueFormat="YYYY-MM-DD HH:mm:ss"
v-model:value="form.serviceEnd" v-model:value="form.serviceEnd"
class="form-input" class="form-input"
placeholder="服务结束时间" placeholder="服务结束时间"
@ -194,7 +194,7 @@
placeholder="服务时长(小时)" placeholder="服务时长(小时)"
:readonly="readonlyMode" :readonly="readonlyMode"
/> />
<div style="font-size: 12px; color: #f00d0d; margin-top: 6px;">注:不足30分钟(不含本数)的,不计入时长,超过30分钟不足1个小时的(含本数),按照一个小时填报</div> <div style="font-size: 12px; color: #f00d0d; margin-top: 6px;">注:不足30分钟(不含本数)的,按照0.5小时填报,超过30分钟不足1个小时的(含本数),按照一个小时填报</div>
</div> </div>
</template> </template>
@ -241,26 +241,6 @@
/> />
</div> </div>
<!-- 服务时长 -->
<div class="form-item">
<label class="form-label">服务时长(小时)</label>
<input
v-model="form.serviceDuration"
class="form-input"
placeholder="服务时长(小时)"
:readonly="readonlyMode"
/>
<div style="font-size: 12px; color: #f00d0d; margin-top: 6px;">注:服务时长必须在100-200小时范围内</div>
</div>
</template>
<!-- 加载中状态 -->
<template v-else>
<div class="form-item">
<div style="text-align: center; padding: 20px; color: #999;">
加载中...
</div>
</div>
</template> </template>
</div> </div>

2
src/views/system/home/components/agreement.vue

@ -89,7 +89,7 @@ async function getLetterDetail(letterId) {
loading.value = true; loading.value = true;
// 使用/letter/detail/{letterId}接口获取承诺书内容 // 使用/letter/detail/{letterId}接口获取承诺书内容
const result = await letterApi.isLetter(letterId); const result = await letterApi.detail(letterId);
if (result.data) { if (result.data) {
Object.assign(noticeDetail, result.data); Object.assign(noticeDetail, result.data);

12
vite.config.js

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

Loading…
Cancel
Save