Browse Source

优化

main
wang 9 hours ago
parent
commit
99c3b8abab
  1. 14
      economic-crime-api/economic-crime-admin/pom.xml
  2. 4
      economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/constant/AdminSwaggerTagConst.java
  3. 7
      economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/interceptor/AdminInterceptor.java
  4. 33
      economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/alarm/constant/AlarmCaseSourceConst.java
  5. 51
      economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/alarm/service/AlarmReportService.java
  6. 5
      economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/caserisk/controller/CaseRiskWarningController.java
  7. 5
      economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/caserisk/domain/form/CaseRiskWarningProcessForm.java
  8. 2
      economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/caserisk/engine/CaseRiskWarningEngine.java
  9. 3
      economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/caserisk/service/CaseRiskWarningService.java
  10. 6
      economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/casesupervision/controller/CaseNegativeListController.java
  11. 6
      economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/casesupervision/controller/CaseWeeklyScheduleController.java
  12. 2
      economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/casesupervision/domain/form/CaseWeeklyScheduleCreateForm.java
  13. 3
      economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/casesupervision/service/CaseNegativeListService.java
  14. 3
      economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/casesupervision/service/CaseWeeklyScheduleService.java
  15. 4
      economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/casesupervision/support/CaseSupervisionNotifySupport.java
  16. 4
      economic-crime-api/economic-crime-admin/src/main/resources/dev/application.yaml
  17. 4
      economic-crime-api/economic-crime-admin/src/main/resources/pre/application.yaml
  18. 4
      economic-crime-api/economic-crime-admin/src/main/resources/prod/application.yaml
  19. 4
      economic-crime-api/economic-crime-admin/src/main/resources/test/application.yaml
  20. 12
      economic-crime-api/economic-crime-base/src/main/java/net/lab1024/sa/base/config/FileConfig.java
  21. 2
      economic-crime-api/economic-crime-base/src/main/java/net/lab1024/sa/base/module/support/dict/manager/DictManager.java
  22. 13
      economic-crime-api/economic-crime-base/src/main/java/net/lab1024/sa/base/module/support/dict/service/DictService.java
  23. 2
      economic-crime-api/economic-crime-base/src/main/java/net/lab1024/sa/base/module/support/file/constant/FileFolderTypeEnum.java
  24. 6
      economic-crime-api/economic-crime-base/src/main/java/net/lab1024/sa/base/module/support/file/controller/FileController.java
  25. 5
      economic-crime-api/economic-crime-base/src/main/resources/code-generator-template/java/controller/Controller.java.vm
  26. 2
      economic-crime-api/economic-crime-base/src/main/resources/code-generator-template/java/service/Service.java.vm
  27. 9
      economic-crime-api/economic-crime-base/src/main/resources/mapper/support/DictDataMapper.xml
  28. 3
      economic-crime-web/package.json
  29. 30
      economic-crime-web/src/api/support/dict-api.js
  30. 9
      economic-crime-web/src/components/support/dict-cascader/index.vue
  31. 2
      economic-crime-web/src/components/support/dict-label/index.vue
  32. 14
      economic-crime-web/src/components/support/dict-select/index.vue
  33. 53
      economic-crime-web/src/constants/business/alarm/alarm-const.js
  34. 3
      economic-crime-web/src/constants/business/alarm/alarm-source-const.js
  35. 2
      economic-crime-web/src/constants/support/dict-const.js
  36. 10
      economic-crime-web/src/constants/support/table-id-const.js
  37. 43
      economic-crime-web/src/layout/components/side-menu/recursion-menu.vue
  38. 5
      economic-crime-web/src/lib/smart-watermark.js
  39. 9
      economic-crime-web/src/router/system/home.js
  40. 13
      economic-crime-web/src/store/modules/system/dict.js
  41. 18
      economic-crime-web/src/store/modules/system/user.js
  42. 139
      economic-crime-web/src/views/business/alarm/alarm-reception-list.vue
  43. 13
      economic-crime-web/src/views/business/alarm/audit-list.vue
  44. 392
      economic-crime-web/src/views/business/case-management/case-supervision.vue
  45. 371
      economic-crime-web/src/views/business/case-management/enterprise-record.vue
  46. 366
      economic-crime-web/src/views/business/case-management/evaluation.vue
  47. 321
      economic-crime-web/src/views/business/case-management/leader-record.vue
  48. 612
      economic-crime-web/src/views/business/case-management/two-books-one-form.vue
  49. 251
      economic-crime-web/src/views/business/review/general-list.vue
  50. 314
      economic-crime-web/src/views/business/review/group-config.vue
  51. 207
      economic-crime-web/src/views/business/review/opinion-flow.vue
  52. 350
      economic-crime-web/src/views/business/review/process-list.vue
  53. 236
      economic-crime-web/src/views/business/review/supervision.vue
  54. 1
      economic-crime-web/src/views/portal/module-portal.vue

14
economic-crime-api/economic-crime-admin/pom.xml

@ -79,4 +79,18 @@
</plugins>
</build>
<!-- 仅隔离数据库测试使用,不进入生产 WAR。 -->
<profiles>
<profile>
<id>work-db-test</id>
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.4</version>
<scope>test</scope>
</dependency>
</dependencies>
</profile>
</profiles>
</project>

4
economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/constant/AdminSwaggerTagConst.java

@ -29,6 +29,10 @@ public class AdminSwaggerTagConst extends SwaggerTagConst {
public static final String MANAGER_DOSSIER = "卷宗管理";
public static final String MANAGER_CASE_SUPERVISION = "案件监督管理";
public static final String MANAGER_CASE_RISK = "案件风险预警";
}

7
economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/interceptor/AdminInterceptor.java

@ -52,6 +52,13 @@ public class AdminInterceptor implements HandlerInterceptor {
return true;
}
// 容器已判定的错误由 Spring 输出原始状态,避免附件 404 被改写为登录失效。
// 仅内部 ERROR 分派到默认错误控制器可通过;直接请求和业务接口仍校验登录。
if (request.getDispatcherType() == javax.servlet.DispatcherType.ERROR
&& ((HandlerMethod) handler).getBeanType() == org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.class) {
return true;
}
try {
// --------------- 第一步: 根据token 获取用户 ---------------

33
economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/alarm/constant/AlarmCaseSourceConst.java

@ -14,12 +14,21 @@ public final class AlarmCaseSourceConst {
/** 行政执法机关移交 */
public static final String ADMIN_TRANSFER = "admin_transfer";
/** 人民法院移交 */
/** 人民法院移交(历史兼容) */
public static final String COURT_TRANSFER = "court_transfer";
/** 人民法院移送普通案件 */
public static final String COURT_TRANSFER_NORMAL = "court_transfer_normal";
/** 人民法院移送虚假诉讼类案件 */
public static final String COURT_TRANSFER_FALSE = "court_transfer_false";
/** 人民检察院移交 */
public static final String PROSECUTORATE_TRANSFER = "procuratorate_transfer";
/** 人民检察院通知立案 */
public static final String PROCURATORATE_NOTIFY = "procuratorate_notify";
/** 上级交办 */
public static final String SUPERIOR_ASSIGN = "superior_assign";
@ -35,6 +44,25 @@ public final class AlarmCaseSourceConst {
/** 检察院移交 - 其他案件 */
public static final String SUB_OTHER = "other";
/**
* 判断是否为人民法院移送虚假诉讼类
*/
public static boolean isCourtFalse(String caseSource, String caseSourceSub) {
if (COURT_TRANSFER_FALSE.equals(caseSource)) {
return true;
}
return COURT_TRANSFER.equals(caseSource) && SUB_FALSE_LITIGATION.equals(caseSourceSub);
}
/**
* 判断是否属于人民法院移送类(含历史和普通案件)
*/
public static boolean isCourtSource(String caseSource) {
return COURT_TRANSFER.equals(caseSource)
|| COURT_TRANSFER_NORMAL.equals(caseSource)
|| COURT_TRANSFER_FALSE.equals(caseSource);
}
public static String resolveLabel(String caseSource) {
if (caseSource == null || caseSource.isBlank()) {
return "当事人报案";
@ -43,7 +71,10 @@ public final class AlarmCaseSourceConst {
case PARTY_REPORT -> "当事人报案";
case ADMIN_TRANSFER -> "行政执法机关移交";
case COURT_TRANSFER -> "人民法院移交";
case COURT_TRANSFER_NORMAL -> "人民法院移送普通案件";
case COURT_TRANSFER_FALSE -> "人民法院移送虚假诉讼类案件";
case PROSECUTORATE_TRANSFER -> "人民检察院移交";
case PROCURATORATE_NOTIFY -> "人民检察院通知立案";
case SUPERIOR_ASSIGN -> "上级交办";
case WORK_DISCOVERY -> "工作中发现";
case OTHER_UNIT_TRANSFER -> "其他单位移交";

51
economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/alarm/service/AlarmReportService.java

@ -466,7 +466,7 @@ public class AlarmReportService {
if (!pageResult.getEmptyFlag()) {
// 填充字典名称
list.forEach(vo -> {
vo.setCategoryName(dictService.getDictDataLabel("CASE_TYPE", vo.getCaseTypeCode()));
vo.setCategoryName(resolveCaseTypeName(vo.getCaseTypeCode()));
fillAlarmSourceInfo(vo);
fillCaseSourceInfo(vo);
fillAlarmCategoryInfo(vo);
@ -647,7 +647,7 @@ public class AlarmReportService {
AlarmDetailVO detailVO = SmartBeanUtil.copy(entity, AlarmDetailVO.class);
// 填充字典名称
detailVO.setCategoryName(dictService.getDictDataLabel("CASE_TYPE", detailVO.getCaseTypeCode()));
detailVO.setCategoryName(resolveCaseTypeName(detailVO.getCaseTypeCode()));
fillAlarmSourceInfo(detailVO);
fillCaseSourceInfo(detailVO);
fillAlarmCategoryInfo(detailVO);
@ -854,7 +854,7 @@ public class AlarmReportService {
}
AlarmReportVO vo = SmartBeanUtil.copy(entity, AlarmReportVO.class);
vo.setCategoryName(dictService.getDictDataLabel("CASE_TYPE", vo.getCaseTypeCode()));
vo.setCategoryName(resolveCaseTypeName(vo.getCaseTypeCode()));
vo.setAlarmSourceName(dictService.getDictDataLabel("ALARM_SOURCE", vo.getAlarmSource()));
vo.setAlarmLevelName(dictService.getDictDataLabel("ALARM_LEVEL", vo.getAlarmLevel()));
fillStatusInfo(vo);
@ -888,7 +888,7 @@ public class AlarmReportService {
List<AlarmReportEntity> entities = alarmReportDao.selectList(queryWrapper);
List<AlarmReportVO> list = entities.stream().map(entity -> {
AlarmReportVO vo = SmartBeanUtil.copy(entity, AlarmReportVO.class);
vo.setCategoryName(dictService.getDictDataLabel("CASE_TYPE", vo.getCaseTypeCode()));
vo.setCategoryName(resolveCaseTypeName(vo.getCaseTypeCode()));
fillAlarmSourceInfo(vo);
fillCaseSourceInfo(vo);
fillAlarmCategoryInfo(vo);
@ -1310,7 +1310,7 @@ public class AlarmReportService {
if (StringUtils.isNotBlank(form.getAlarmSource())) {
entity.setAlarmSource(form.getAlarmSource());
}
entity.setCaseSource(normalizeCaseSource(form.getCaseSource()));
entity.setCaseSource(AlarmCaseSourceConst.normalize(form.getCaseSource(), form.getCaseSourceSub()));
entity.setCaseSourceSub(normalizeCaseSourceSub(form.getCaseSource(), form.getCaseSourceSub()));
entity.setAcceptingUnit(normalizeAcceptingUnit(form.getCaseSource(), form.getAcceptingUnit()));
}
@ -1319,10 +1319,10 @@ public class AlarmReportService {
if (form == null || !ALARM_CATEGORY_PENDING_REVIEW.equals(form.getAlarmCategory())) {
return null;
}
String caseSource = normalizeCaseSource(form.getCaseSource());
if (AlarmCaseSourceConst.COURT_TRANSFER.equals(caseSource)) {
String caseSource = AlarmCaseSourceConst.normalize(form.getCaseSource(), form.getCaseSourceSub());
if (AlarmCaseSourceConst.isCourtSource(caseSource)) {
if (StringUtils.isBlank(form.getAcceptingUnit())) {
return ResponseDTO.userErrorParam("人民法院移交时请填写受案单位");
return ResponseDTO.userErrorParam("人民法院移送时请填写受案单位");
}
}
if (AlarmCaseSourceConst.PROSECUTORATE_TRANSFER.equals(caseSource)) {
@ -1330,7 +1330,7 @@ public class AlarmReportService {
if (StringUtils.isBlank(sub)
|| (!AlarmCaseSourceConst.SUB_FALSE_LITIGATION.equals(sub)
&& !AlarmCaseSourceConst.SUB_OTHER.equals(sub))) {
return ResponseDTO.userErrorParam("人民检察院移交请选择案件类型");
return ResponseDTO.userErrorParam("人民检察院移送请选择案件类型");
}
}
return null;
@ -1344,6 +1344,13 @@ public class AlarmReportService {
}
private String normalizeCaseSourceSub(String caseSource, String caseSourceSub) {
String source = AlarmCaseSourceConst.normalize(caseSource, caseSourceSub);
if (AlarmCaseSourceConst.COURT_TRANSFER_FALSE.equals(source)) {
return AlarmCaseSourceConst.SUB_FALSE_LITIGATION;
}
if (AlarmCaseSourceConst.COURT_TRANSFER_NORMAL.equals(source)) {
return "normal";
}
if (!AlarmCaseSourceConst.PROSECUTORATE_TRANSFER.equals(normalizeCaseSource(caseSource))) {
return null;
}
@ -1351,7 +1358,7 @@ public class AlarmReportService {
}
private String normalizeAcceptingUnit(String caseSource, String acceptingUnit) {
if (!AlarmCaseSourceConst.COURT_TRANSFER.equals(normalizeCaseSource(caseSource))) {
if (!AlarmCaseSourceConst.isCourtSource(normalizeCaseSource(caseSource))) {
return null;
}
return StringUtils.isBlank(acceptingUnit) ? null : acceptingUnit.trim();
@ -1361,9 +1368,7 @@ public class AlarmReportService {
if (vo == null) {
return;
}
if (StringUtils.isBlank(vo.getCaseSource())) {
vo.setCaseSource(AlarmCaseSourceConst.PARTY_REPORT);
}
vo.setCaseSource(AlarmCaseSourceConst.normalize(vo.getCaseSource(), vo.getCaseSourceSub()));
vo.setCaseSourceName(resolveCaseSourceName(vo.getCaseSource()));
vo.setCaseSourceSubName(resolveCaseSourceSubName(vo.getCaseSourceSub()));
}
@ -1372,14 +1377,16 @@ public class AlarmReportService {
if (vo == null) {
return;
}
if (StringUtils.isBlank(vo.getCaseSource())) {
vo.setCaseSource(AlarmCaseSourceConst.PARTY_REPORT);
}
vo.setCaseSource(AlarmCaseSourceConst.normalize(vo.getCaseSource(), vo.getCaseSourceSub()));
vo.setCaseSourceName(resolveCaseSourceName(vo.getCaseSource()));
vo.setCaseSourceSubName(resolveCaseSourceSubName(vo.getCaseSourceSub()));
}
private String resolveCaseSourceName(String caseSource) {
String canonicalLabel = AlarmCaseSourceConst.resolveLabel(caseSource);
if (!java.util.Objects.equals(canonicalLabel, caseSource)) {
return canonicalLabel;
}
String label = dictService.getDictDataLabel("CASE_SOURCE", caseSource);
if (StringUtils.isNotBlank(label)) {
return label;
@ -1474,6 +1481,15 @@ public class AlarmReportService {
vo.setDisposalStatus(temp.getDisposalStatus());
}
/** 当前分类使用77类,未知的历史编码才回退旧字典,避免旧库无法回显。 */
private String resolveCaseTypeName(String code) {
if (StringUtils.isBlank(code)) return "";
if ("00".equals(code)) return "待定性";
String label = dictService.getDictDataLabel("CASE_TYPE_77", code);
if (StringUtils.isBlank(label)) label = dictService.getDictDataLabel("CASE_TYPE", code);
return StringUtils.isBlank(label) ? code : label;
}
private String resolveStatusName(String status) {
if (StringUtils.isBlank(status)) {
return "-";
@ -1514,6 +1530,9 @@ public class AlarmReportService {
if ("pending_info".equals(disposalStatus)) {
disposalStatus = "pending_review";
}
if ("pending_review".equals(disposalStatus)) {
return ALARM_CATEGORY_NON_ECONOMIC.equals(alarmCategory) ? "待审核" : "待定性审核";
}
String label = dictService.getDictDataLabel("ALARM_DISPOSAL_STATUS", disposalStatus);
if (StringUtils.isNotBlank(label)) {
return label;

5
economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/caserisk/controller/CaseRiskWarningController.java

@ -3,8 +3,7 @@ package net.lab1024.sa.admin.module.business.caserisk.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import net.lab1024.sa.admin.constant.AdminSwaggerTagConst;
import net.lab1024.sa.admin.module.business.caserisk.domain.form.CaseRiskWarningProcessForm;
import net.lab1024.sa.admin.module.business.caserisk.domain.form.CaseRiskWarningQueryForm;
@ -15,6 +14,8 @@ import net.lab1024.sa.base.common.domain.PageResult;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
import java.util.Map;

5
economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/caserisk/domain/form/CaseRiskWarningProcessForm.java

@ -1,9 +1,10 @@
package net.lab1024.sa.admin.module.business.caserisk.domain.form;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class CaseRiskWarningProcessForm {

2
economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/caserisk/engine/CaseRiskWarningEngine.java

@ -1,7 +1,6 @@
package net.lab1024.sa.admin.module.business.caserisk.engine;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import net.lab1024.sa.admin.module.business.alarm.constant.AlarmCaseSourceConst;
import net.lab1024.sa.admin.module.business.caserisk.constant.CaseLegalDocConst;
import net.lab1024.sa.admin.module.business.caserisk.constant.CaseRiskWarningConst;
@ -17,6 +16,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

3
economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/caserisk/service/CaseRiskWarningService.java

@ -1,7 +1,7 @@
package net.lab1024.sa.admin.module.business.caserisk.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.Resource;
import net.lab1024.sa.admin.module.business.caserisk.constant.CaseRiskWarningConst;
import net.lab1024.sa.admin.module.business.caserisk.dao.CaseRiskWarningDao;
import net.lab1024.sa.admin.module.business.caserisk.domain.entity.CaseRiskWarningEntity;
@ -19,6 +19,7 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;

6
economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/casesupervision/controller/CaseNegativeListController.java

@ -3,8 +3,7 @@ package net.lab1024.sa.admin.module.business.casesupervision.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import net.lab1024.sa.admin.constant.AdminSwaggerTagConst;
import net.lab1024.sa.admin.module.business.casesupervision.domain.form.CaseNegativeListQueryForm;
import net.lab1024.sa.admin.module.business.casesupervision.domain.vo.CaseNegativeListVO;
@ -15,6 +14,9 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@Tag(name = AdminSwaggerTagConst.Business.MANAGER_CASE_SUPERVISION)
public class CaseNegativeListController {

6
economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/casesupervision/controller/CaseWeeklyScheduleController.java

@ -1,10 +1,12 @@
package net.lab1024.sa.admin.module.business.casesupervision.controller;
import javax.annotation.Resource;
import javax.validation.Valid;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import net.lab1024.sa.admin.constant.AdminSwaggerTagConst;
import net.lab1024.sa.admin.module.business.casesupervision.domain.form.CaseNegativeListQueryForm;
import net.lab1024.sa.admin.module.business.casesupervision.domain.form.CaseWeeklyScheduleCreateForm;

2
economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/casesupervision/domain/form/CaseWeeklyScheduleCreateForm.java

@ -1,8 +1,8 @@
package net.lab1024.sa.admin.module.business.casesupervision.domain.form;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import java.time.LocalDateTime;
import java.util.List;

3
economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/casesupervision/service/CaseNegativeListService.java

@ -2,7 +2,7 @@ package net.lab1024.sa.admin.module.business.casesupervision.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.Resource;
import net.lab1024.sa.admin.module.business.caserisk.dao.CaseFileDao;
import net.lab1024.sa.admin.module.business.caserisk.domain.entity.CaseFileEntity;
import net.lab1024.sa.admin.module.business.casesupervision.constant.CaseSupervisionConst;
@ -14,6 +14,7 @@ import net.lab1024.sa.base.common.util.SmartPageUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;

3
economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/casesupervision/service/CaseWeeklyScheduleService.java

@ -2,7 +2,7 @@ package net.lab1024.sa.admin.module.business.casesupervision.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.Resource;
import net.lab1024.sa.admin.module.business.caserisk.dao.CaseFileDao;
import net.lab1024.sa.admin.module.business.caserisk.domain.entity.CaseFileEntity;
import net.lab1024.sa.admin.module.business.casesupervision.constant.CaseSupervisionConst;
@ -30,6 +30,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

4
economic-crime-api/economic-crime-admin/src/main/java/net/lab1024/sa/admin/module/business/casesupervision/support/CaseSupervisionNotifySupport.java

@ -1,6 +1,6 @@
package net.lab1024.sa.admin.module.business.casesupervision.support;
import jakarta.annotation.Resource;
import net.lab1024.sa.base.common.enumeration.UserTypeEnum;
import net.lab1024.sa.base.module.support.message.constant.MessageTypeEnum;
import net.lab1024.sa.base.module.support.message.domain.MessageSendForm;
@ -8,6 +8,8 @@ import net.lab1024.sa.base.module.support.message.service.MessageService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class CaseSupervisionNotifySupport {

4
economic-crime-api/economic-crime-admin/src/main/resources/dev/application.yaml

@ -23,5 +23,9 @@ management:
# 环境
spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 25MB
profiles:
active: dev

4
economic-crime-api/economic-crime-admin/src/main/resources/pre/application.yaml

@ -18,5 +18,9 @@ server:
# 环境
spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 25MB
profiles:
active: pre

4
economic-crime-api/economic-crime-admin/src/main/resources/prod/application.yaml

@ -18,5 +18,9 @@ server:
# 环境
spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 25MB
profiles:
active: prod

4
economic-crime-api/economic-crime-admin/src/main/resources/test/application.yaml

@ -18,5 +18,9 @@ server:
# 环境
spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 25MB
profiles:
active: test

12
economic-crime-api/economic-crime-base/src/main/java/net/lab1024/sa/base/config/FileConfig.java

@ -103,7 +103,17 @@ public class FileConfig implements WebMvcConfigurer {
location = location + "/";
}
log.info("本地文件访问目录: {}", uploadDir.getAbsolutePath());
registry.addResourceHandler(FileStorageLocalServiceImpl.UPLOAD_MAPPING + "/**").addResourceLocations(location);
registry.addResourceHandler(FileStorageLocalServiceImpl.UPLOAD_MAPPING + "/**").addResourceLocations(location)
.resourceChain(false).addResolver(new org.springframework.web.servlet.resource.PathResourceResolver() {
@Override
protected boolean checkResource(org.springframework.core.io.Resource resource,
org.springframework.core.io.Resource resourceLocation) throws java.io.IOException {
// 私有附件只能经业务控制器授权下载,禁止静态URL及符号链接绕过。
java.nio.file.Path privateRoot = new File(uploadDir, "private").getCanonicalFile().toPath();
return super.checkResource(resource, resourceLocation)
&& !resource.getFile().getCanonicalFile().toPath().startsWith(privateRoot);
}
});
}
}

2
economic-crime-api/economic-crime-base/src/main/java/net/lab1024/sa/base/module/support/dict/manager/DictManager.java

@ -32,7 +32,7 @@ public class DictManager {
/**
* 获取字典
*/
@Cacheable(value = CacheKeyConst.Dict.DICT_DATA, key = "#dictCode + '_' + #dataValue")
@Cacheable(value = CacheKeyConst.Dict.DICT_DATA, key = "#dictCode + '_' + #dataValue", unless = "#result == null")
public DictDataVO getDictData(String dictCode, String dataValue) {
DictEntity dictEntity = dictDao.selectByCode(dictCode);
if (dictEntity == null) {

13
economic-crime-api/economic-crime-base/src/main/java/net/lab1024/sa/base/module/support/dict/service/DictService.java

@ -89,6 +89,7 @@ public class DictService {
/**
* 禁用 启用
*/
@CacheEvict(value = CacheKeyConst.Dict.DICT_DATA, allEntries = true)
public ResponseDTO<String> updateDisabled(Long dictId) {
DictEntity dictEntity = dictDao.selectById(dictId);
if (dictEntity == null) {
@ -103,7 +104,7 @@ public class DictService {
/**
* 更新
*/
@CacheEvict(CacheKeyConst.Dict.DICT_DATA)
@CacheEvict(value = CacheKeyConst.Dict.DICT_DATA, allEntries = true)
public synchronized ResponseDTO<String> update(DictUpdateForm updateForm) {
DictEntity existDictCode = dictDao.selectByCode(updateForm.getDictCode());
if (null != existDictCode && !existDictCode.getDictId().equals(updateForm.getDictId())) {
@ -118,7 +119,7 @@ public class DictService {
/**
* 批量删除
*/
@CacheEvict(CacheKeyConst.Dict.DICT_DATA)
@CacheEvict(value = CacheKeyConst.Dict.DICT_DATA, allEntries = true)
public synchronized ResponseDTO<String> batchDelete(List<Long> idList) {
if (CollectionUtils.isEmpty(idList)) {
return ResponseDTO.ok();
@ -131,7 +132,7 @@ public class DictService {
/**
* 单个删除
*/
@CacheEvict(CacheKeyConst.Dict.DICT_DATA)
@CacheEvict(value = CacheKeyConst.Dict.DICT_DATA, allEntries = true)
public synchronized ResponseDTO<String> delete(Long dictId) {
if (null == dictId) {
return ResponseDTO.ok();
@ -170,6 +171,7 @@ public class DictService {
/**
* 添加
*/
@CacheEvict(value = CacheKeyConst.Dict.DICT_DATA, allEntries = true)
public synchronized ResponseDTO<String> addDictData(DictDataAddForm addForm) {
addForm.setDataValue(SmartStringUtil.trim(addForm.getDataValue()));
@ -192,7 +194,7 @@ public class DictService {
/**
* 更新
*/
@CacheEvict(value = CacheKeyConst.Dict.DICT_DATA, key = "#updateForm.dictCode + '_' + #updateForm.dataValue")
@CacheEvict(value = CacheKeyConst.Dict.DICT_DATA, allEntries = true)
public synchronized ResponseDTO<String> updateDictData(DictDataUpdateForm updateForm) {
updateForm.setDataValue(SmartStringUtil.trim(updateForm.getDataValue()));
@ -215,6 +217,7 @@ public class DictService {
/**
* 批量删除
*/
@CacheEvict(value = CacheKeyConst.Dict.DICT_DATA, allEntries = true)
public synchronized ResponseDTO<String> batchDeleteDictData(List<Long> idList) {
if (CollectionUtils.isEmpty(idList)) {
return ResponseDTO.ok();
@ -229,6 +232,7 @@ public class DictService {
/**
* 单个删除
*/
@CacheEvict(value = CacheKeyConst.Dict.DICT_DATA, allEntries = true)
public synchronized ResponseDTO<String> deleteDictData(Long dictDataId) {
if (null == dictDataId) {
return ResponseDTO.ok();
@ -260,6 +264,7 @@ public class DictService {
/**
* 更新启用/禁用
*/
@CacheEvict(value = CacheKeyConst.Dict.DICT_DATA, allEntries = true)
public synchronized ResponseDTO<String> updateDictDataDisabled(Long dictDataId) {
DictDataEntity dictDataEntity = dictDataDao.selectById(dictDataId);
if (dictDataEntity == null) {

2
economic-crime-api/economic-crime-base/src/main/java/net/lab1024/sa/base/module/support/file/constant/FileFolderTypeEnum.java

@ -39,6 +39,8 @@ public enum FileFolderTypeEnum implements BaseEnum {
*/
PROPERTY(5, FileFolderTypeEnum.FOLDER_PUBLIC + "/property/", "涉案财物"),
TWO_BOOKS(6, FileFolderTypeEnum.FOLDER_PRIVATE + "/two-books/", "两书一单"),
;
/**

6
economic-crime-api/economic-crime-base/src/main/java/net/lab1024/sa/base/module/support/file/controller/FileController.java

@ -41,6 +41,7 @@ public class FileController extends SupportBaseController {
@Operation(summary = "文件上传 ")
@PostMapping("/file/upload")
public ResponseDTO<FileUploadVO> upload(@RequestParam MultipartFile file, @RequestParam Integer folder) {
if (Integer.valueOf(6).equals(folder)) return ResponseDTO.userErrorParam("请通过两书一单页面上传附件");
RequestUser requestUser = SmartRequestUtil.getRequestUser();
return fileService.fileUpload(file, folder, requestUser);
}
@ -48,12 +49,17 @@ public class FileController extends SupportBaseController {
@Operation(summary = "获取文件URL:根据fileKey ")
@GetMapping("/file/getFileUrl")
public ResponseDTO<String> getUrl(@RequestParam String fileKey) {
if (fileKey.startsWith("private/")) return ResponseDTO.userErrorParam("私有附件请通过对应业务页面访问");
return fileService.getFileUrl(fileKey);
}
@Operation(summary = "下载文件流(根据fileKey) ")
@GetMapping("/file/downLoad")
public void downLoad(@RequestParam String fileKey, HttpServletRequest request, HttpServletResponse response) throws IOException {
if (fileKey.startsWith("private/")) {
SmartResponseUtil.write(response, ResponseDTO.userErrorParam("私有附件请通过对应业务页面下载"));
return;
}
String userAgent = ServletUtil.getHeaderIgnoreCase(request, RequestHeaderConst.USER_AGENT);
ResponseDTO<FileDownloadVO> downloadFileResult = fileService.getDownloadFile(fileKey, userAgent);
if (!downloadFileResult.getOk()) {

5
economic-crime-api/economic-crime-base/src/main/resources/code-generator-template/java/controller/Controller.java.vm

@ -11,8 +11,9 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import cn.dev33.satoken.annotation.SaCheckPermission;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import javax.validation.Valid;
import javax.annotation.Resource;
/**
* ${basic.description} Controller

2
economic-crime-api/economic-crime-base/src/main/resources/code-generator-template/java/service/Service.java.vm

@ -10,8 +10,8 @@ import net.lab1024.sa.base.common.domain.PageResult;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import jakarta.annotation.Resource;
/**
* ${basic.description} Service

9
economic-crime-api/economic-crime-base/src/main/resources/mapper/support/DictDataMapper.xml

@ -20,10 +20,15 @@
</select>
<select id="getAll" resultType="net.lab1024.sa.base.module.support.dict.domain.vo.DictDataVO">
select t_dict_data.*,
select t_dict_data.dict_data_id, t_dict_data.dict_id,
t_dict_data.data_value, t_dict_data.data_label, t_dict_data.remark,
t_dict_data.sort_order, t_dict_data.create_time, t_dict_data.update_time,
case when cast(t_dict.disabled_flag as varchar) in ('1', 'true')
or cast(t_dict_data.disabled_flag as varchar) in ('1', 'true')
then true else false end as disabled_flag,
t_dict.dict_code
from t_dict_data
left join t_dict on t_dict_data.dict_id = t_dict.dict_id
inner join t_dict on t_dict_data.dict_id = t_dict.dict_id
order by t_dict_data.sort_order desc
</select>

3
economic-crime-web/package.json

@ -13,7 +13,8 @@
"dev": "vite",
"build:test": "vite build --base=/admin/ --mode test",
"build:pre": "vite build --mode pre",
"build:prod": "vite build --mode production"
"build:prod": "vite build --mode production",
"build:war": "vite build --mode production && cd dist && jar -cvf ../economic-crime-web.war * && cp ../economic-crime-web.war ../实施交付包/1.部署包_WAR/ && echo 'WAR打包成功!产物已就绪:economic-crime-web.war'"
},
"type": "module",
"dependencies": {

30
economic-crime-web/src/api/support/dict-api.js

@ -8,6 +8,20 @@
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
*/
import { postRequest, getRequest } from '/@/lib/axios';
import { useDictStore } from '/@/store/modules/system/dict';
import { message } from 'ant-design-vue';
// 字典写入成功后刷新当前浏览器的选项与标签;刷新失败不误报写入失败。
async function mutateAndRefresh(request) {
const response = await request;
try {
const latest = await getRequest('/support/dict/getAllDictData');
useDictStore().initData(latest.data);
} catch (error) {
message.warning('字典已保存,但缓存刷新失败,请刷新页面后重试查看');
}
return response;
}
export const dictApi = {
@ -28,22 +42,22 @@ export const dictApi = {
// 添加 @author 1024创新实验室-主任-卓大
addDict: (param) => {
return postRequest('/support/dict/add', param);
return mutateAndRefresh(postRequest('/support/dict/add', param));
},
// 更新 @author 1024创新实验室-主任-卓大
updateDict: (param) => {
return postRequest('/support/dict/update', param);
return mutateAndRefresh(postRequest('/support/dict/update', param));
},
// 字典-删除- @author 卓大
batchDeleteDict: (dictIdList) => {
return postRequest('/support/dict/batchDelete', dictIdList);
return mutateAndRefresh(postRequest('/support/dict/batchDelete', dictIdList));
},
// 字典 启用 禁用 @author 1024创新实验室-主任-卓大
updateDisabled: (dictId) => {
return getRequest(`/support/dict/updateDisabled/${dictId}`);
return mutateAndRefresh(getRequest(`/support/dict/updateDisabled/${dictId}`));
},
// ------------- 查询字典数据 -------------
@ -55,21 +69,21 @@ export const dictApi = {
// 字典数据 添加 - @author 卓大
addDictData: (param) => {
return postRequest('/support/dict/dictData/add', param);
return mutateAndRefresh(postRequest('/support/dict/dictData/add', param));
},
// 字典数据 更新- @author 卓大
updateDictData: (param) => {
return postRequest('/support/dict/dictData/update', param);
return mutateAndRefresh(postRequest('/support/dict/dictData/update', param));
},
// 字典数据-删除- @author 卓大
batchDeleteDictData: (dictDataIdList) => {
return postRequest('/support/dict/dictData/batchDelete', dictDataIdList);
return mutateAndRefresh(postRequest('/support/dict/dictData/batchDelete', dictDataIdList));
},
// 字典数据 启用 禁用 @author 1024创新实验室-主任-卓大
updateDictDataDisabled: (dictDataId) => {
return getRequest(`/support/dict/dictData/updateDisabled/${dictDataId}`);
return mutateAndRefresh(getRequest(`/support/dict/dictData/updateDisabled/${dictDataId}`));
},
};

9
economic-crime-web/src/components/support/dict-cascader/index.vue

@ -167,9 +167,12 @@
function initOptions() {
const list = useDictStore().getDictData(props.dictCode) || [];
// 保留停用项的历史回显,但不允许新选择停用项及其子项。
pathIndex.value = buildDictPathIndex(list);
flatOptions.value = buildDictFlatOptions(list, { leafOnly: props.leafOnly });
treeData.value = buildDictTree(list).map((node) => mapTreeNode(node));
const disabledCodes = new Set(list.filter((item) => item.disabledFlag).map((item) => item.dataValue));
const activeList = list.filter((item) => !pathIndex.value.get(item.dataValue)?.codes.some((code) => disabledCodes.has(code)));
flatOptions.value = buildDictFlatOptions(activeList, { leafOnly: props.leafOnly });
treeData.value = buildDictTree(activeList).map((node) => mapTreeNode(node));
expandedKeys.value = treeData.value.slice(0, 8).map((node) => node.key);
}
@ -265,7 +268,7 @@
onMounted(initOptions);
watch(
() => [props.dictCode, props.leafOnly],
() => [props.dictCode, props.leafOnly, useDictStore().getDictData(props.dictCode)],
() => initOptions(),
);
</script>

2
economic-crime-web/src/components/support/dict-label/index.vue

@ -8,7 +8,7 @@
const props = defineProps({
dictCode: String,
dataValue: String,
dataValue: [String, Number],
});
const dataLabels = computed(() => {
return useDictStore().getDataLabels(props.dictCode, props.dataValue);

14
economic-crime-web/src/components/support/dict-select/index.vue

@ -20,7 +20,7 @@
@change="onChange"
:disabled="disabled"
>
<a-select-option v-for="item in dictDataList" :key="item.dataValue" :value="item.dataValue" :disabled="disabledOption.includes(item.valueCode)">
<a-select-option v-for="item in dictDataList" :key="item.dataValue" :value="item.dataValue" :disabled="disabledOption.includes(item.dataValue)">
{{ item.dataLabel }}
</a-select-option>
</a-select>
@ -28,7 +28,7 @@
</template>
<script setup>
import { onMounted, ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { useDictStore } from '/@/store/modules/system/dict.js';
const props = defineProps({
@ -69,13 +69,9 @@
// -------------------------- 查询 字典数据 --------------------------
const dictDataList = ref([]);
function initDictData() {
let list = useDictStore().getDictData(props.dictCode);
dictDataList.value = list.filter((item) => !props.hiddenOption.includes(item.dataValue) && !item.disabledFlag);
}
onMounted(initDictData);
const dictStore = useDictStore();
const dictDataList = computed(() => dictStore.getDictData(props.dictCode)
.filter((item) => !props.hiddenOption.includes(item.dataValue) && !item.disabledFlag));
// -------------------------- 选中 相关、事件 --------------------------

53
economic-crime-web/src/constants/business/alarm/alarm-const.js

@ -11,11 +11,13 @@ export const ALARM_DICT_CODE = {
// 警情状态枚举
export const ALARM_STATUS_ENUM = {
NO_AUDIT: { value: 'no_audit', desc: '无需审核' },
VOIDED: { value: 'voided', desc: '已作废' },
PENDING: { value: 'pending', desc: '待审核' },
REVIEWING: { value: 'reviewing', desc: '审核中' },
APPROVED: { value: 'approved', desc: '已通过' },
REJECTED: { value: 'rejected', desc: '已退回' },
TRANSFERRED: { value: 'transferred', desc: '已流转' },
APPROVED: { value: 'approved', desc: '已受理' },
REJECTED: { value: 'rejected', desc: '已驳回' },
TRANSFERRED: { value: 'transferred', desc: '已移送' },
};
// 警情级别枚举(一级~四级,数字越小等级越高)
@ -68,20 +70,55 @@ export const DEFAULT_ALARM_SOURCE = ALARM_SOURCE_ENUM.CITY_REGISTER.value;
/** 报案类警情 - 案件来源 */
export const CASE_SOURCE_ENUM = {
PARTY_REPORT: { value: 'party_report', desc: '当事人报案' },
ADMIN_TRANSFER: { value: 'admin_transfer', desc: '行政执法机关移交' },
COURT_TRANSFER: { value: 'court_transfer', desc: '人民法院移交' },
PROSECUTORATE_TRANSFER: { value: 'procuratorate_transfer', desc: '人民检察院移交' },
ADMIN_TRANSFER: { value: 'admin_transfer', desc: '行政机关移送' },
COURT_TRANSFER_NORMAL: { value: 'court_transfer_normal', desc: '人民法院移送普通案件' },
COURT_TRANSFER_FALSE: { value: 'court_transfer_false', desc: '人民法院移送虚假诉讼类案件' },
PROSECUTORATE_TRANSFER: { value: 'procuratorate_transfer', desc: '人民检察院移送' },
PROCURATORATE_NOTIFY: { value: 'procuratorate_notify', desc: '人民检察院通知立案' },
SUPERIOR_ASSIGN: { value: 'superior_assign', desc: '上级交办' },
WORK_DISCOVERY: { value: 'work_discovery', desc: '工作中发现' },
OTHER_UNIT_TRANSFER: { value: 'other_unit_transfer', desc: '其他单位移交' },
OTHER_UNIT_TRANSFER: { value: 'other_unit_transfer', desc: '其他单位移送' },
};
/** 检察院移交子类型 */
/** 案件来源子类型(法院移送普通/虚假诉讼,检察院虚假诉讼/其他) */
export const CASE_SOURCE_SUB_ENUM = {
NORMAL: { value: 'normal', desc: '普通经侦案件' },
FALSE_LITIGATION: { value: 'false_litigation', desc: '虚假诉讼案件' },
OTHER: { value: 'other', desc: '其他案件' },
};
/** 判断是否为人民法院移送类案件来源(支持历史 court_transfer 及平铺的 court_transfer_normal/court_transfer_false) */
export function isCourtCaseSource(caseSource) {
return (
caseSource === 'court_transfer' ||
caseSource === 'court_transfer_normal' ||
caseSource === 'court_transfer_false'
);
}
/** 规范化案件来源,兼容历史子项 */
export function normalizeCaseSourceValue(caseSource, caseSourceSub) {
if (!caseSource) return caseSource;
if (caseSource === 'court_transfer') {
if (caseSourceSub === CASE_SOURCE_SUB_ENUM.FALSE_LITIGATION.value) {
return 'court_transfer_false';
}
return 'court_transfer_normal';
}
return caseSource;
}
/** 根据案件来源推导子项 */
export function deriveCaseSourceSub(caseSource) {
if (caseSource === 'court_transfer_false') {
return CASE_SOURCE_SUB_ENUM.FALSE_LITIGATION.value;
}
if (caseSource === 'court_transfer_normal') {
return 'normal';
}
return null;
}
/** 报案类登记默认案件来源 */
export const DEFAULT_CASE_SOURCE = CASE_SOURCE_ENUM.PARTY_REPORT.value;

3
economic-crime-web/src/constants/business/alarm/alarm-source-const.js

@ -21,7 +21,8 @@ export function getAlarmSourceDisplayInfo(source) {
system_register: { text: '市局登记', color: 'blue' },
'110_transfer': { text: '市局登记', color: 'blue' },
};
return map[source] || { text: source || '-', color: 'default' };
const option = Object.values(ALARM_SOURCE_ENUM).find((item) => item.value === source);
return map[source] || { text: option?.desc || source || '-', color: 'default' };
}
export default {

2
economic-crime-web/src/constants/support/dict-const.js

@ -9,6 +9,8 @@ export const DICT_CODE_ENUM = {
/** 非经侦类警情案由(层级字典,remark 存 parentCode) */
ALARM_CASE_REASON: 'ALARM_CASE_REASON',
CASE_TYPE_77: 'CASE_TYPE_77',
/** 重点人员库人员类型 */
KEY_PERSON_TYPE: 'KEY_PERSON_TYPE',
};
export default {

10
economic-crime-web/src/constants/support/table-id-const.js

@ -55,6 +55,16 @@ export const TABLE_ID_CONST = {
BORROW_RETURN: businessERPInitTableId + 305,
COPY_REQUEST: businessERPInitTableId + 306,
},
CASE: {
SUPERVISION: businessERPInitTableId + 401,
EVALUATION: businessERPInitTableId + 402,
RISK_WARNING: businessERPInitTableId + 403,
NEGATIVE_LIST: businessERPInitTableId + 404,
ENTERPRISE_RECORD: businessERPInitTableId + 405,
LEADER_RECORD: businessERPInitTableId + 406,
WEEKLY_SCHEDULE: businessERPInitTableId + 407,
FILE_MANAGEMENT: businessERPInitTableId + 408,
},
},
/**

43
economic-crime-web/src/layout/components/side-menu/recursion-menu.vue

@ -29,11 +29,10 @@
<script setup>
import _ from 'lodash';
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import SubMenu from './sub-menu.vue';
import { resolveMenuIcon } from '/@/utils/menu-icon-util';
import { getSidebarMenuTree } from '/@/utils/portal-menu-util';
import { router } from '/@/router/index';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { usePortalModuleStore } from '/@/store/modules/system/portal-module';
import { useUserStore } from '/@/store/modules/system/user';
@ -62,6 +61,9 @@
const selectedKeys = ref([]);
const openKeys = ref([]);
// 与 useRoute 使用同一个已安装的路由实例,避免热更新后菜单与页面不同步。
const router = useRouter();
// 页面跳转
function turnToPage(menu) {
useUserStore().deleteKeepAliveIncludes(menu.menuId.toString());
@ -76,23 +78,33 @@
// 更新选中
selectedKeys.value = [_.toNumber(currentRoute.name)];
/**
* 更新展开(1、获取新展开的menu key集合;2、保留原有的openkeys,然后把新展开的与之合并)
*/
// 只展开当前侧栏中的祖先目录,门户一级模块不在侧栏中。
//获取需要展开的menu key集合
let menuParentIdListMap = useUserStore().getMenuParentIdListMap;
let parentList = menuParentIdListMap.get(currentRoute.name) || [];
// 如果是折叠菜单的话,则不需要设置openkey
if (!props.collapsed) {
// 使用lodash的union函数,进行 去重合并两个数组
let needOpenKeys = _.map(parentList, 'name').map(Number);
openKeys.value = _.union(openKeys.value, needOpenKeys);
const submenuKeys = new Set();
function collectSubmenus(items) {
for (const item of items) {
if (item.children?.length && item.visibleFlag && !item.disabledFlag) {
submenuKeys.add(Number(item.menuId));
collectSubmenus(item.children);
}
}
}
collectSubmenus(menuTree.value);
const needOpenKeys = parentList.map((item) => Number(item.name)).filter((key) => submenuKeys.has(key));
// 单目录模式切换页面时关闭无关目录,不能把上个页面的风险预警一直保留。
openKeys.value = flatPattern.value
? _.union(openKeys.value.filter((key) => submenuKeys.has(key)), needOpenKeys)
: needOpenKeys;
}
}
watch(
currentRoute,
[() => currentRoute.name, menuTree, flatPattern],
() => {
updateOpenKeysAndSelectKeys();
},
@ -100,17 +112,18 @@
immediate: true,
}
);
function onOpenChange(openKeysParams){
if(flatPattern.value){
function onOpenChange(openKeysParams) {
if (flatPattern.value) {
openKeys.value = openKeysParams;
return;
}
const latestOpenKey = openKeysParams.find(key => openKeys.value.indexOf(key) === -1);
if (rootSubmenuKeys.value.indexOf(latestOpenKey) === -1) {
const latestOpenKey = openKeysParams.find((key) => !openKeys.value.includes(key));
if (!rootSubmenuKeys.value.includes(latestOpenKey)) {
openKeys.value = openKeysParams;
} else {
openKeys.value = latestOpenKey ? [latestOpenKey] : [];
openKeys.value = latestOpenKey !== undefined ? [latestOpenKey] : [];
}
}
};
defineExpose({
updateOpenKeysAndSelectKeys,
});

5
economic-crime-web/src/lib/smart-watermark.js

@ -61,7 +61,10 @@ function setWatermark(id, str) {
div.style.width = '100%';
div.style.height = '100%';
div.style.background = 'url(' + can.toDataURL('image/png') + ') left top repeat';
document.getElementById(id).appendChild(div);
let parentDom = document.getElementById(id);
if (parentDom) {
parentDom.appendChild(div);
}
}
const watermark = {

9
economic-crime-web/src/router/system/home.js

@ -27,13 +27,8 @@ export const homeRouters = [
{
path: '/home',
name: HOME_PAGE_NAME,
meta: {
title: '首页',
menuType: MENU_TYPE_ENUM.MENU.value,
icon: 'HomeOutlined',
parentMenuList: [{ name: '_home', title: '首页' }],
},
component: () => import('/@/views/system/home/index.vue'),
// 暂停旧首页,保留路由名称兼容旧书签和关闭标签后的跳转。
redirect: { path: PORTAL_PAGE_PATH },
},
{
path: '/account',

13
economic-crime-web/src/store/modules/system/dict.js

@ -1,9 +1,6 @@
import { defineStore } from 'pinia';
import { dictApi } from '/@/api/support/dict-api';
import { smartSentry } from '/@/lib/smart-sentry';
import { DICT_SPLIT } from '/@/constants/support/dict-const.js';
import { getDictPathLabel as resolveDictPathLabel } from '/@/utils/dict-tree-util.js';
import _ from 'lodash';
export const useDictStore = defineStore({
id: 'dict',
@ -24,7 +21,7 @@ export const useDictStore = defineStore({
// 获取字典的值名称
getDataLabels(dictCode, dataValue) {
if (!dataValue) {
if (dataValue === null || dataValue === undefined || dataValue === '') {
return '';
}
@ -33,16 +30,14 @@ export const useDictStore = defineStore({
let dict = this.getDictData(dictCode);
if (dict.length === 0) {
return '';
return stringValue;
}
let valueArray = stringValue.split(DICT_SPLIT);
let result = [];
for (let item of valueArray) {
let target = _.find(dict, { dataValue: item });
if (target) {
result.push(target.dataLabel);
}
let target = dict.find((entry) => String(entry.dataValue) === item);
result.push(target ? target.dataLabel : item);
}
return result.join(DICT_SPLIT);
},

18
economic-crime-web/src/store/modules/system/user.js

@ -111,12 +111,8 @@ export const useUserStore = defineStore({
let localTagNav = localRead(localKey.USER_TAG_NAV) || '';
state.tagNav = localTagNav ? JSON.parse(localTagNav) : [];
}
let tagNavList = _.cloneDeep(state.tagNav) || [];
tagNavList.unshift({
menuName: HOME_PAGE_NAME,
menuTitle: '首页',
});
return tagNavList;
// 旧首页暂停使用,也过滤浏览器此前保存的首页标签。
return (_.cloneDeep(state.tagNav) || []).filter((item) => item.menuName !== HOME_PAGE_NAME);
},
},
@ -197,6 +193,12 @@ export const useUserStore = defineStore({
this.token = token;
},
// 返回门户后重新开始业务导航,同时清理持久化标签,避免刷新后旧标签恢复。
resetBusinessNavigation() {
this.tagNav = [];
this.clearKeepAliveIncludes();
localSave(localKey.USER_TAG_NAV, JSON.stringify([]));
},
//设置标签页
setTagNav(route, from) {
if (_.isNull(this.tagNav)) {
@ -271,7 +273,9 @@ export const useUserStore = defineStore({
} else {
// 查询左侧tag
let leftTagNav = this.getTagNav[index - 1];
router.push({ name: leftTagNav.menuName, query: leftTagNav.menuQuery });
router.push(leftTagNav
? { name: leftTagNav.menuName, query: leftTagNav.menuQuery }
: { path: PORTAL_PAGE_PATH });
}
}
}

139
economic-crime-web/src/views/business/alarm/alarm-reception-list.vue

@ -112,9 +112,6 @@
{{ getDisposalStatusDisplay(record).text }}
</a-tag>
<span v-else>-</span>
<a-tag v-if="getVoidStatusDisplay(record).text" :color="getVoidStatusDisplay(record).color" class="void-status-tag">
{{ getVoidStatusDisplay(record).text }}
</a-tag>
</div>
</template>
<template v-else-if="column.dataIndex === 'reporterName'">
@ -342,6 +339,13 @@
<a-col :span="12">
<a-form-item label="身份证号码" name="idCard">
<a-input v-model:value="nonEconomicForm.idCard" placeholder="选填,18位居民身份证号" allowClear :maxlength="18" />
<a-alert
v-if="nonEconomicKeyPersonMatch"
class="key-person-warning"
type="warning"
show-icon
:message="keyPersonWarningText(nonEconomicKeyPersonMatch)"
/>
</a-form-item>
</a-col>
<a-col :span="12">
@ -657,7 +661,14 @@
<a-row :gutter="18">
<a-col :span="12">
<a-form-item label="证件号码" required>
<a-input v-model:value="reporterInfo.idCard" placeholder="请输入证件号码" allowClear />
<a-input v-model:value="reporterInfo.idCard" placeholder="请输入18位居民身份证号" allowClear :maxlength="18" />
<a-alert
v-if="pendingKeyPersonMatch"
class="key-person-warning"
type="warning"
show-icon
:message="keyPersonWarningText(pendingKeyPersonMatch)"
/>
</a-form-item>
</a-col>
<a-col :span="12">
@ -825,7 +836,7 @@
</a-row>
<a-row
:gutter="18"
v-if="pendingForm.caseSource === CASE_SOURCE_ENUM.COURT_TRANSFER.value"
v-if="isCourtCaseSource(pendingForm.caseSource)"
>
<a-col :span="12">
<a-form-item label="受案单位" required>
@ -1585,6 +1596,7 @@ import { smartSentry } from '/@/lib/smart-sentry';
import { LAYER_Z_INDEX, LAYER_WRAP_CLASS } from '/@/constants/support/layer-z-index';
import { alarmReportApi } from '/@/api/business/alarm/alarm-report-api';
import { keyPersonApi } from '/@/api/business/alarm/key-person-api';
import { useUserStore } from '/@/store/modules/system/user';
import {
ALARM_LEVEL_ENUM,
@ -1604,6 +1616,9 @@ import {
CASE_SOURCE_ENUM,
CASE_SOURCE_SUB_ENUM,
DEFAULT_CASE_SOURCE,
isCourtCaseSource,
normalizeCaseSourceValue,
deriveCaseSourceSub,
} from '/@/constants/business/alarm/alarm-const';
import {
resolveRegisterAlarmSource,
@ -1681,7 +1696,7 @@ function navigateToAuditSupervision(reportNo) {
const evidenceTypeOptions = Object.values(EVIDENCE_TYPE_ENUM);
const preliminaryHandleTypeOptions = Object.values(PRELIMINARY_HANDLE_TYPE_ENUM);
const caseSourceOptions = Object.values(CASE_SOURCE_ENUM);
const caseSourceSubOptions = Object.values(CASE_SOURCE_SUB_ENUM);
const caseSourceSubOptions = Object.values(CASE_SOURCE_SUB_ENUM).filter((item) => item.value !== 'normal');
// ---------------------------- 表格列定义 ----------------------------
// 与登记表单字段保持一致:警情编号、警情来源、报案人、报警时间、案由、管辖单位
@ -1913,6 +1928,54 @@ const showDetailDrawer = ref(false);
const currentRecord = ref(null);
const detailLoading = ref(false);
const saveLoading = ref(false);
const nonEconomicKeyPersonMatch = ref(null);
const pendingKeyPersonMatch = ref(null);
const lastWarnedKeyPersonIdCard = reactive({ nonEconomic: '', pending: '' });
function isValidIdCardForMatch(raw) {
const idCard = String(raw || '').trim().toUpperCase();
if (!/^[1-9]\d{16}[0-9X]$/.test(idCard)) return false;
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const checks = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
const sum = weights.reduce((value, weight, index) => value + Number(idCard[index]) * weight, 0);
return checks[sum % 11] === idCard[17];
}
function keyPersonWarningText(record) {
return `系统提醒:该身份证号命中重点人员库,人员类型为“${record.personTypeName}”(姓名:${record.name})。请核对后继续登记。`;
}
async function matchKeyPerson(scope, rawIdCard) {
const idCard = String(rawIdCard || '').trim().toUpperCase();
const target = scope === 'nonEconomic' ? nonEconomicKeyPersonMatch : pendingKeyPersonMatch;
if (!isValidIdCardForMatch(idCard)) {
target.value = null;
lastWarnedKeyPersonIdCard[scope] = '';
return;
}
try {
const response = await keyPersonApi.match(idCard);
const currentIdCard = String(scope === 'nonEconomic' ? nonEconomicForm.idCard : reporterInfo.idCard || '').trim().toUpperCase();
if (currentIdCard !== idCard) return;
target.value = response.data || null;
if (target.value && lastWarnedKeyPersonIdCard[scope] !== idCard) {
lastWarnedKeyPersonIdCard[scope] = idCard;
Modal.warning({
title: '重点人员提醒',
content: keyPersonWarningText(target.value),
okText: '我知道了',
centered: true,
zIndex: LAYER_Z_INDEX.MESSAGE,
});
}
} catch (error) {
target.value = null;
smartSentry.captureError(error);
}
}
const debouncedMatchNonEconomicKeyPerson = _.debounce((idCard) => matchKeyPerson('nonEconomic', idCard), 450);
const debouncedMatchPendingKeyPerson = _.debounce((idCard) => matchKeyPerson('pending', idCard), 450);
// ---------------------------- 操作方法 ----------------------------
function handleAdd() {
@ -2258,8 +2321,8 @@ async function loadPendingEditData(record) {
pendingForm.bottomRemark = d.bottomRemark || '';
pendingForm.preliminaryOpinion = d.preliminaryOpinion || '';
fillPendingPreliminaryHandleFields(d);
pendingForm.caseSource = d.caseSource || DEFAULT_CASE_SOURCE;
pendingForm.caseSourceSub = d.caseSourceSub || undefined;
pendingForm.caseSource = normalizeCaseSourceValue(d.caseSource, d.caseSourceSub) || DEFAULT_CASE_SOURCE;
pendingForm.caseSourceSub = deriveCaseSourceSub(pendingForm.caseSource) || d.caseSourceSub || undefined;
pendingForm.acceptingUnit = d.acceptingUnit || '';
pendingForm.noAcceptReviewSourceAlarmId = d.noAcceptReviewSourceAlarmId || null;
reporterInfo.name = d.reporterName || '';
@ -2496,6 +2559,8 @@ watch(
function resetNonEconomicForm() {
nonEconomicMainContentAutoPrefix = '';
nonEconomicKeyPersonMatch.value = null;
lastWarnedKeyPersonIdCard.nonEconomic = '';
Object.assign(nonEconomicForm, {
reportType: 'consult',
reporterName: '',
@ -2644,6 +2709,16 @@ const reporterInfo = reactive({
businessAddress: '',
});
watch(
() => nonEconomicForm.idCard,
(idCard) => debouncedMatchNonEconomicKeyPerson(idCard),
);
watch(
() => reporterInfo.idCard,
(idCard) => debouncedMatchPendingKeyPerson(idCard),
);
const enterpriseValidateError = ref(false);
watch(
@ -2846,6 +2921,8 @@ function resolveFirstAlarmFlags() {
function resetPendingForm() {
pendingMainContentAutoPrefix = '';
pendingKeyPersonMatch.value = null;
lastWarnedKeyPersonIdCard.pending = '';
const now = new Date();
const fmt = (n) => String(n).padStart(2, '0');
// 生成警情编号
@ -2949,10 +3026,12 @@ function buildPendingFormData() {
preliminaryHandleType: pendingForm.preliminaryHandleType || null,
bottomRemark: pendingForm.bottomRemark,
caseSource: pendingForm.caseSource || DEFAULT_CASE_SOURCE,
caseSourceSub: pendingForm.caseSource === CASE_SOURCE_ENUM.PROSECUTORATE_TRANSFER.value
caseSourceSub: deriveCaseSourceSub(pendingForm.caseSource) || (
pendingForm.caseSource === CASE_SOURCE_ENUM.PROSECUTORATE_TRANSFER.value
? (pendingForm.caseSourceSub || null)
: null,
acceptingUnit: pendingForm.caseSource === CASE_SOURCE_ENUM.COURT_TRANSFER.value
: null
),
acceptingUnit: isCourtCaseSource(pendingForm.caseSource)
? (pendingForm.acceptingUnit?.trim() || null)
: null,
suspectList: [],
@ -3003,20 +3082,28 @@ function getPreliminaryHandleTypeText(value) {
}
function onCaseSourceChange(value) {
if (value !== CASE_SOURCE_ENUM.COURT_TRANSFER.value) {
if (!isCourtCaseSource(value)) {
pendingForm.acceptingUnit = '';
}
if (value !== CASE_SOURCE_ENUM.PROSECUTORATE_TRANSFER.value) {
if (
value !== CASE_SOURCE_ENUM.PROSECUTORATE_TRANSFER.value
&& !isCourtCaseSource(value)
) {
pendingForm.caseSourceSub = undefined;
}
const derived = deriveCaseSourceSub(value);
if (derived) {
pendingForm.caseSourceSub = derived;
}
}
function getCaseSourceDisplayText(record) {
if (!record) {
return '-';
}
const main = record.caseSourceName
|| caseSourceOptions.find((item) => item.value === record.caseSource)?.desc
const source = normalizeCaseSourceValue(record.caseSource, record.caseSourceSub);
const main = caseSourceOptions.find((item) => item.value === source)?.desc
|| record.caseSourceName
|| '-';
if (
record.caseSource === CASE_SOURCE_ENUM.PROSECUTORATE_TRANSFER.value
@ -3032,18 +3119,15 @@ function validateCaseSource() {
message.warning('请选择案件来源');
return false;
}
if (
pendingForm.caseSource === CASE_SOURCE_ENUM.COURT_TRANSFER.value
&& !String(pendingForm.acceptingUnit || '').trim()
) {
message.warning('人民法院移交时请填写受案单位');
if (isCourtCaseSource(pendingForm.caseSource) && !String(pendingForm.acceptingUnit || '').trim()) {
message.warning('人民法院移送时请填写受案单位');
return false;
}
if (
pendingForm.caseSource === CASE_SOURCE_ENUM.PROSECUTORATE_TRANSFER.value
&& !pendingForm.caseSourceSub
) {
message.warning('人民检察院移交请选择案件类型');
message.warning('人民检察院移送请选择案件类型');
return false;
}
return true;
@ -3149,6 +3233,14 @@ function getDisposalStatusText(status, record) {
}
function getDisposalStatusDisplay(record) {
// 作废审批中及已作废优先显示,驳回后恢复原处置状态;作废记录在详情单独展示。
if (record.voidStatus === ALARM_VOID_STATUS_ENUM.VOIDED.value
|| record.status === ALARM_VOID_STATUS_ENUM.VOIDED.value) {
return { text: ALARM_VOID_STATUS_ENUM.VOIDED.desc, color: 'red' };
}
if (record.voidStatus === ALARM_VOID_STATUS_ENUM.PENDING_VOID.value) {
return { text: ALARM_VOID_STATUS_ENUM.PENDING_VOID.desc, color: 'orange' };
}
const status = record.disposalStatus;
const text = record.disposalStatusName || getDisposalStatusText(status, record);
if (!text || text === '-') {
@ -6239,6 +6331,11 @@ const TABLE_ID_CONST = {
color: #64748b;
}
.key-person-warning {
margin-top: 8px;
line-height: 1.5;
}
.dialog-footer {
display: flex;
justify-content: flex-end;

13
economic-crime-web/src/views/business/alarm/audit-list.vue

@ -222,7 +222,7 @@
<div class="detail-evidence-list">
<div v-for="item in evidenceList" :key="item.id" class="detail-evidence-item">
<div class="detail-evidence-meta">
<a-tag color="blue" size="small">{{ item.evidenceType || '证据' }}</a-tag>
<a-tag color="blue" size="small">{{ getEvidenceTypeText(item.evidenceType) }}</a-tag>
<span>{{ item.evidenceName || item.serialNo || '-' }}</span>
<span class="muted">{{ formatDateTime(item.collectTime) }}</span>
</div>
@ -383,10 +383,16 @@ import { alarmReportApi } from '/@/api/business/alarm/alarm-report-api.js';
import { resolveFileUrl as resolveFileUrlUtil } from '/@/utils/file-preview-util';
import {
CASE_SOURCE_ENUM,
EVIDENCE_TYPE_ENUM,
normalizeCaseSourceValue,
} from '/@/constants/business/alarm/alarm-const';
const caseSourceOptions = Object.values(CASE_SOURCE_ENUM);
function getEvidenceTypeText(type) {
return Object.values(EVIDENCE_TYPE_ENUM).find((item) => item.value === type)?.desc || type || '证据';
}
const route = useRoute();
const router = useRouter();
@ -644,8 +650,9 @@ function getCaseSourceDisplayText(record) {
if (!record) {
return '-';
}
const main = record.caseSourceName
|| caseSourceOptions.find((item) => item.value === record.caseSource)?.desc
const source = normalizeCaseSourceValue(record.caseSource, record.caseSourceSub);
const main = caseSourceOptions.find((item) => item.value === source)?.desc
|| record.caseSourceName
|| '-';
if (
record.caseSource === CASE_SOURCE_ENUM.PROSECUTORATE_TRANSFER.value

392
economic-crime-web/src/views/business/case-management/case-supervision.vue

@ -1,392 +1,4 @@
<!--
* 案件监督
*
* @Author: system
* @Date: 2024-06-24
-->
<template>
<div class="case-supervision-container">
<!---------- 查询表单form begin ----------->
<a-form class="smart-query-form">
<a-row class="smart-query-form-row" v-privilege="'case-supervision:query'">
<a-form-item label="案件编号" class="smart-query-form-item">
<a-input style="width: 180px" v-model:value="queryForm.caseNo" placeholder="请输入案件编号" allowClear />
</a-form-item>
<a-form-item label="案件名称" class="smart-query-form-item">
<a-input style="width: 180px" v-model:value="queryForm.caseName" placeholder="请输入案件名称" allowClear />
</a-form-item>
<a-form-item label="案件类型" class="smart-query-form-item">
<a-select style="width: 140px" v-model:value="queryForm.caseType" placeholder="请选择案件类型" allowClear>
<a-select-option value="fraud">诈骗案</a-select-option>
<a-select-option value="abscond">非法吸收公众存款</a-select-option>
<a-select-option value="bill">票据诈骗</a-select-option>
<a-select-option value="contract">合同诈骗</a-select-option>
<a-select-option value="other">其他</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态" class="smart-query-form-item">
<a-select style="width: 120px" v-model:value="queryForm.status" placeholder="请选择状态" allowClear>
<a-select-option :value="0">办理中</a-select-option>
<a-select-option :value="1">已结案</a-select-option>
<a-select-option :value="2">已撤销</a-select-option>
<a-select-option :value="3">督办中</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="时间范围" class="smart-query-form-item">
<a-range-picker
style="width: 240px"
v-model:value="queryForm.timeRange"
:placeholder="['开始日期', '结束日期']"
format="YYYY-MM-DD"
/>
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button-group>
<a-button type="primary" @click="onSearch" v-privilege="'case-supervision:query'">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="resetQuery" v-privilege="'case-supervision:query'">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<!---------- 查询表单form end ----------->
<div class="section-card">
<!---------- 表格操作行 begin ----------->
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button type="primary" v-privilege="'case-supervision:add'" @click="handleAdd">
<template #icon><PlusOutlined /></template>
新增监督
</a-button>
</div>
<div class="smart-table-setting-block">
<TableOperator v-model="columns" :tableId="TABLE_ID_CONST.BUSINESS.CASE.SUPERVISION" :refresh="queryData" />
</div>
</a-row>
<!---------- 表格操作行 end ----------->
<a-table
size="small"
:dataSource="tableData"
:columns="columns"
rowKey="id"
:scroll="{ x: 1200, y: yHeight }"
bordered
:pagination="false"
:loading="tableLoading"
>
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'status'">
<a-tag :color="getStatusColor(record.status)">{{ getStatusName(record.status) }}</a-tag>
</template>
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button type="link" @click="handleView(record)" v-privilege="'case-supervision:view'">查看</a-button>
<a-button type="link" @click="handleSupervise(record)" v-privilege="'case-supervision:supervise'" :disabled="record.status === 1 || record.status === 2">督办</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="pagination.total"
@change="onPageChange"
@showSizeChange="onPageChange"
:show-total="(total) => `共${total}条`"
/>
</div>
</div>
<!-- 督办弹窗 -->
<a-modal
v-model:open="showSuperviseModal"
title="案件督办"
:width="560"
:maskClosable="false"
centered
@ok="submitSupervise"
:confirm-loading="superviseLoading"
>
<a-form layout="vertical" ref="superviseFormRef" :model="superviseForm">
<a-form-item label="案件编号">
<a-input :value="currentRecord?.caseNo" disabled />
</a-form-item>
<a-form-item label="案件名称">
<a-input :value="currentRecord?.caseName" disabled />
</a-form-item>
<a-form-item label="督办级别" required>
<a-select v-model:value="superviseForm.level" placeholder="请选择督办级别">
<a-select-option value="general">一般督办</a-select-option>
<a-select-option value="important">重点督办</a-select-option>
<a-select-option value="urgent">紧急督办</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="督办内容" required :rules="[{ required: true, message: '请输入督办内容' }]">
<a-textarea v-model:value="superviseForm.content" placeholder="请输入督办内容和要求" :rows="4" />
</a-form-item>
<a-form-item label="要求完成时间" required :rules="[{ required: true, message: '请选择完成时间' }]">
<a-date-picker
v-model:value="superviseForm.deadline"
format="YYYY-MM-DD"
placeholder="请选择要求完成时间"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="备注">
<a-textarea v-model:value="superviseForm.remark" placeholder="请输入备注信息" :rows="2" />
</a-form-item>
</a-form>
</a-modal>
<!-- 详情抽屉 -->
<a-drawer
v-model:open="showDetailDrawer"
title="案件详情"
placement="right"
:width="640"
:maskClosable="true"
>
<a-descriptions bordered :column="2" size="small" v-if="currentRecord">
<a-descriptions-item label="案件编号" :span="2">{{ currentRecord.caseNo }}</a-descriptions-item>
<a-descriptions-item label="案件名称" :span="2">{{ currentRecord.caseName }}</a-descriptions-item>
<a-descriptions-item label="案件类型">{{ getCaseTypeName(currentRecord.caseType) }}</a-descriptions-item>
<a-descriptions-item label="主办单位">{{ currentRecord.orgName }}</a-descriptions-item>
<a-descriptions-item label="立案时间">{{ currentRecord.filingDate }}</a-descriptions-item>
<a-descriptions-item label="当前阶段">{{ currentRecord.currentStage }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="getStatusColor(currentRecord.status)">{{ getStatusName(currentRecord.status) }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="更新时间">{{ currentRecord.updateTime }}</a-descriptions-item>
</a-descriptions>
</a-drawer>
</div>
</template>
<template><WorkPage kind="case-supervision" /></template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import {
SearchOutlined,
ReloadOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import _ from 'lodash';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
// import { getRequest, postRequest } from '/@/lib/axios';
// ======================== 表格列定义 ========================
const columns = ref([
{ title: '案件编号', dataIndex: 'caseNo', width: 180, resizable: true },
{ title: '案件名称', dataIndex: 'caseName', width: 200, ellipsis: true, resizable: true },
{ title: '案件类型', dataIndex: 'caseType', width: 140, resizable: true },
{ title: '主办单位', dataIndex: 'orgName', width: 150, ellipsis: true, resizable: true },
{ title: '立案时间', dataIndex: 'filingDate', width: 120, resizable: true },
{ title: '当前阶段', dataIndex: 'currentStage', width: 120, resizable: true },
{ title: '状态', dataIndex: 'status', width: 90, align: 'center', resizable: true },
{ title: '操作', dataIndex: 'action', fixed: 'right', width: 140 },
]);
// ======================== Mock 数据 ========================
const mockTableData = [
{ id: 1, caseNo: 'AJ202401001', caseName: '张某涉嫌集资诈骗案', caseType: 'abscond', orgName: '市经侦支队一大队', filingDate: '2024-01-05', currentStage: '侦查阶段', status: 0, updateTime: '2024-03-15 14:30' },
{ id: 2, caseNo: 'AJ202401002', caseName: '某公司非法吸收公众存款案', caseType: 'abscond', orgName: '区经侦大队', filingDate: '2024-01-18', currentStage: '审查起诉', status: 3, updateTime: '2024-03-20 09:00' },
{ id: 3, caseNo: 'AJ202402001', caseName: '李某网络诈骗案', caseType: 'fraud', orgName: '市经侦支队二大队', filingDate: '2024-02-03', currentStage: '侦查阶段', status: 0, updateTime: '2024-03-18 16:45' },
{ id: 4, caseNo: 'AJ202402002', caseName: '王某票据诈骗案', caseType: 'bill', orgName: '县经侦中队', filingDate: '2024-02-15', currentStage: '已结案', status: 1, updateTime: '2024-03-22 11:20' },
{ id: 5, caseNo: 'AJ202403001', caseName: '赵某合同诈骗案', caseType: 'contract', orgName: '市经侦支队一大队', filingDate: '2024-03-01', currentStage: '侦查阶段', status: 0, updateTime: '2024-03-23 08:30' },
{ id: 6, caseNo: 'AJ202403002', caseName: '某集团传销案', caseType: 'other', orgName: '区经侦大队', filingDate: '2024-03-10', currentStage: '已撤销', status: 2, updateTime: '2024-03-21 17:00' },
];
// ======================== 查询表单和方法 ========================
const queryFormState = {
caseNo: '',
caseName: '',
caseType: undefined,
status: undefined,
timeRange: null,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive(_.cloneDeep(queryFormState));
const tableLoading = ref(false);
const tableData = ref([]);
const pagination = reactive({ total: 0 });
function resetQuery() {
let pageSize = queryForm.pageSize;
Object.assign(queryForm, _.cloneDeep(queryFormState));
queryForm.pageSize = pageSize;
queryData();
}
function onSearch() {
queryForm.pageNum = 1;
queryData();
}
async function queryData() {
tableLoading.value = true;
try {
// TODO: 替换为实际的 API 调用
// const res = await getRequest('/api/case/supervision/list', queryForm);
tableData.value = mockTableData;
pagination.total = mockTableData.length;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
function onPageChange(page, pageSize) {
queryForm.pageNum = page;
queryForm.pageSize = pageSize;
queryData();
}
// 动态设置表格高度
const yHeight = ref(0);
function resetGetHeight() {
const doc = document.querySelector('.ant-form');
const btn = document.querySelector('.smart-table-btn-block');
const tableCell = document.querySelector('.ant-table-cell');
const pageEl = document.querySelector('.smart-query-table-page');
const box = document.querySelector('.admin-content');
setTimeout(() => {
if (doc && btn && tableCell && pageEl && box) {
let dueHeight = doc.offsetHeight + 10 + 24 + btn.offsetHeight + 15 + tableCell.offsetHeight + pageEl.offsetHeight + 20;
yHeight.value = box.offsetHeight - dueHeight;
}
}, 100);
}
// ======================== 弹窗/抽屉状态 ========================
const showSuperviseModal = ref(false);
const showDetailDrawer = ref(false);
const currentRecord = ref(null);
const superviseLoading = ref(false);
const superviseFormRef = ref();
const superviseForm = reactive({
level: undefined,
content: '',
deadline: null,
remark: '',
});
// ======================== 操作方法 ========================
function handleAdd() {
message.info('新增案件监督记录');
}
function handleView(record) {
currentRecord.value = record;
showDetailDrawer.value = true;
}
function handleSupervise(record) {
currentRecord.value = record;
Object.assign(superviseForm, { level: undefined, content: '', deadline: null, remark: '' });
showSuperviseModal.value = true;
}
async function submitSupervise() {
try {
await superviseFormRef.value?.validateFields?.();
} catch (e) {
return;
}
superviseLoading.value = true;
try {
// TODO: 替换为实际的 API 调用
// await postRequest('/api/case/supervise/add', { ...superviseForm, caseId: currentRecord.value.id });
message.success('督办成功');
showSuperviseModal.value = false;
queryData();
} catch (e) {
smartSentry.captureError(e);
} finally {
superviseLoading.value = false;
}
}
// ======================== 工具函数 ========================
function getStatusColor(status) {
const map = { 0: 'processing', 1: 'success', 2: 'default', 3: 'warning' };
return map[status] || 'default';
}
function getStatusName(status) {
const map = { 0: '办理中', 1: '已结案', 2: '已撤销', 3: '督办中' };
return map[status] || '-';
}
function getCaseTypeName(type) {
const map = { fraud: '诈骗案', abscond: '非法吸收公众存款', bill: '票据诈骗', contract: '合同诈骗', other: '其他' };
return map[type] || '-';
}
// TABLE_ID_CONST 占位
const TABLE_ID_CONST = {
BUSINESS: {
CASE: {
SUPERVISION: 900101,
},
},
};
// ======================== 生命周期 ========================
onMounted(() => {
queryData();
resetGetHeight();
});
window.addEventListener(
'resize',
_.throttle(() => {
resetGetHeight();
}, 1000)
);
import WorkPage from '/@/components/business/work/work-page.vue';
</script>
<style lang="less" scoped>
.case-supervision-container {
padding: 16px;
.section-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: 16px;
margin-bottom: 16px;
}
:deep(.smart-table-operate) {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
}
</style>

371
economic-crime-web/src/views/business/case-management/enterprise-record.vue

@ -1,371 +1,4 @@
<!--
* 涉企案件
*
* @Author: system
* @Date: 2024-06-24
-->
<template>
<div class="enterprise-record-container">
<!---------- 查询表单form begin ----------->
<a-form class="smart-query-form">
<a-row class="smart-query-form-row" v-privilege="'enterprise-record:query'">
<a-form-item label="企业名称" class="smart-query-form-item">
<a-input style="width: 200px" v-model:value="queryForm.enterpriseName" placeholder="请输入企业名称" allowClear />
</a-form-item>
<a-form-item label="统一社会信用代码" class="smart-query-form-item">
<a-input style="width: 220px" v-model:value="queryForm.creditCode" placeholder="请输入统一社会信用代码" allowClear />
</a-form-item>
<a-form-item label="案件类型" class="smart-query-form-item">
<a-select style="width: 150px" v-model:value="queryForm.caseType" placeholder="请选择案件类型" allowClear>
<a-select-option value="fraud">诈骗案</a-select-option>
<a-select-option value="abscond">非法吸收公众存款</a-select-option>
<a-select-option value="bill">票据诈骗</a-select-option>
<a-select-option value="contract">合同诈骗</a-select-option>
<a-select-option value="other">其他</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="立案时间" class="smart-query-form-item">
<a-range-picker
style="width: 240px"
v-model:value="queryForm.timeRange"
:placeholder="['开始日期', '结束日期']"
format="YYYY-MM-DD"
/>
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button-group>
<a-button type="primary" @click="onSearch" v-privilege="'enterprise-record:query'">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="resetQuery" v-privilege="'enterprise-record:query'">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<!---------- 查询表单form end ----------->
<div class="section-card">
<!---------- 表格操作行 begin ----------->
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button type="primary" v-privilege="'enterprise-record:add'" @click="handleAdd">
<template #icon><PlusOutlined /></template>
新增记录
</a-button>
</div>
<div class="smart-table-setting-block">
<TableOperator v-model="columns" :tableId="TABLE_ID_CONST.BUSINESS.CASE.ENTERPRISE_RECORD" :refresh="queryData" />
</div>
</a-row>
<!---------- 表格操作行 end ----------->
<a-table
size="small"
:dataSource="tableData"
:columns="columns"
rowKey="id"
:scroll="{ x: 1300, y: yHeight }"
bordered
:pagination="false"
:loading="tableLoading"
>
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'amount'">
<span style="color: #f5222d; font-weight: 600;">¥{{ formatAmount(record.amount) }}</span>
</template>
<template v-if="column.dataIndex === 'status'">
<a-tag :color="getEnterpriseStatusColor(record.status)">{{ getEnterpriseStatusName(record.status) }}</a-tag>
</template>
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button type="link" @click="handleDetail(record)" v-privilege="'enterprise-record:view'">查看详情</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="pagination.total"
@change="onPageChange"
@showSizeChange="onPageChange"
:show-total="(total) => `共${total}条`"
/>
</div>
</div>
<!-- 详情抽屉 -->
<a-drawer
v-model:open="showDetailDrawer"
title="涉企案件详情"
placement="right"
:width="720"
:maskClosable="true"
>
<div class="detail-content" v-if="currentRecord">
<!-- 企业基本信息 -->
<h4 class="detail-section-title">企业基本信息</h4>
<a-descriptions bordered :column="2" size="small">
<a-descriptions-item label="企业名称" :span="2">{{ currentRecord.enterpriseName }}</a-descriptions-item>
<a-descriptions-item label="统一社会信用代码" :span="2">{{ currentRecord.creditCode }}</a-descriptions-item>
<a-descriptions-item label="法人代表">{{ currentRecord.legalPerson }}</a-descriptions-item>
<a-descriptions-item label="联系电话">{{ currentRecord.phone || '-' }}</a-descriptions-item>
<a-descriptions-item label="注册地址" :span="2">{{ currentRecord.address || '-' }}</a-descriptions-item>
<a-descriptions-item label="涉案金额">
<span style="color: #f5222d; font-weight: 600;">¥{{ formatAmount(currentRecord.amount) }}</span>
</a-descriptions-item>
<a-descriptions-item label="当前状态">
<a-tag :color="getEnterpriseStatusColor(currentRecord.status)">{{ getEnterpriseStatusName(currentRecord.status) }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="立案日期">{{ currentRecord.filingDate }}</a-descriptions-item>
<a-descriptions-item label="案件类型">{{ getCaseTypeName(currentRecord.caseType) }}</a-descriptions-item>
</a-descriptions>
<!-- 关联案件列表 -->
<h4 class="detail-section-title">关联案件列表</h4>
<a-table
size="small"
:dataSource="relatedCases"
:columns="relatedCaseColumns"
:pagination="false"
bordered
rowKey="caseNo"
/>
<!-- 涉案金额统计 -->
<h4 class="detail-section-title">涉案金额统计</h4>
<a-row :gutter="16">
<a-col :span="8">
<a-statistic title="总涉案金额" :value="currentRecord.amount" prefix="¥" :precision="2" :value-style="{ color: '#f5222d' }" />
</a-col>
<a-col :span="8">
<a-statistic title="关联案件数" :value="relatedCases.length" suffix="件" />
</a-col>
<a-col :span="8">
<a-statistic title="涉及嫌疑人" :value="currentRecord.suspectCount || 0" suffix="人" />
</a-col>
</a-row>
</div>
</a-drawer>
</div>
</template>
<template><WorkPage kind="enterprise" /></template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { message } from 'ant-design-vue';
import {
SearchOutlined,
ReloadOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import _ from 'lodash';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
// import { getRequest, postRequest } from '/@/lib/axios';
// ======================== 表格列定义 ========================
const columns = ref([
{ title: '企业名称', dataIndex: 'enterpriseName', width: 220, ellipsis: true, resizable: true },
{ title: '统一社会信用代码', dataIndex: 'creditCode', width: 200, resizable: true },
{ title: '法人代表', dataIndex: 'legalPerson', width: 100, resizable: true },
{ title: '涉案金额(元)', dataIndex: 'amount', width: 140, align: 'right', resizable: true },
{ title: '案件类型', dataIndex: 'caseType', width: 140, resizable: true },
{ title: '立案日期', dataIndex: 'filingDate', width: 120, resizable: true },
{ title: '当前状态', dataIndex: 'status', width: 100, align: 'center', resizable: true },
{ title: '操作', dataIndex: 'action', fixed: 'right', width: 100 },
]);
const relatedCaseColumns = [
{ title: '案件编号', dataIndex: 'caseNo', width: 170 },
{ title: '案件名称', dataIndex: 'caseName', ellipsis: true },
{ title: '涉案金额', dataIndex: 'amount', width: 120, align: 'right' },
{ title: '立案时间', dataIndex: 'filingDate', width: 120 },
];
// ======================== Mock 数据 ========================
const mockTableData = [
{ id: 1, enterpriseName: '某某科技有限公司', creditCode: '91110108MA01XXXXX1', legalPerson: '张三', amount: 5200000, caseType: 'abscond', filingDate: '2024-01-05', status: 0, phone: '13800138001', address: '北京市海淀区XX路XX号', suspectCount: 3 },
{ id: 2, enterpriseName: '盛世投资集团有限公司', creditCode: '91110106MA02XXXXX2', legalPerson: '李四', amount: 12800000, caseType: 'fraud', filingDate: '2024-02-03', status: 0, phone: '13900139002', address: '北京市朝阳区XX大厦', suspectCount: 5 },
{ id: 3, enterpriseName: '华信商贸有限公司', creditCode: '91120105MA03XXXXX3', legalPerson: '王五', amount: 3600000, caseType: 'contract', filingDate: '2024-02-15', status: 1, phone: '13700137003', address: '天津市和平区XX街XX号', suspectCount: 2 },
{ id: 4, enterpriseName: '鑫源金融信息服务公司', creditCode: '91330104MA04XXXXX4', legalPerson: '赵六', amount: 8500000, caseType: 'bill', filingDate: '2024-03-01', status: 0, phone: '13600136004', address: '杭州市西湖区XX路XX号', suspectCount: 4 },
{ id: 5, enterpriseName: '宏达实业发展有限公司', creditCode: '91370102MA05XXXXX5', legalPerson: '孙七', amount: 2100000, caseType: 'other', filingDate: '2024-03-10', status: 2, phone: '13500135005', address: '济南市历下区XX大道', suspectCount: 1 },
{ id: 6, enterpriseName: '天成电子商务有限公司', creditCode: '91440106MA06XXXXX6', legalPerson: '周八', amount: 4300000, caseType: 'fraud', filingDate: '2024-03-18', status: 0, phone: '13400134006', address: '广州市天河区XX广场', suspectCount: 3 },
];
const relatedCases = computed(() => {
if (!currentRecord.value) return [];
return [
{ caseNo: currentRecord.value.caseNo || 'AJ' + String(currentRecord.value.id).padStart(9, '0'), caseName: `${currentRecord.value.enterpriseName}涉嫌犯罪案`, amount: currentRecord.value.amount, filingDate: currentRecord.value.filingDate },
];
});
// ======================== 查询表单和方法 ========================
const queryFormState = {
enterpriseName: '',
creditCode: '',
caseType: undefined,
timeRange: null,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive(_.cloneDeep(queryFormState));
const tableLoading = ref(false);
const tableData = ref([]);
const pagination = reactive({ total: 0 });
function resetQuery() {
let pageSize = queryForm.pageSize;
Object.assign(queryForm, _.cloneDeep(queryFormState));
queryForm.pageSize = pageSize;
queryData();
}
function onSearch() {
queryForm.pageNum = 1;
queryData();
}
async function queryData() {
tableLoading.value = true;
try {
// TODO: 替换为实际的 API 调用
// const res = await getRequest('/api/enterprise-record/list', queryForm);
tableData.value = mockTableData;
pagination.total = mockTableData.length;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
function onPageChange(page, pageSize) {
queryForm.pageNum = page;
queryForm.pageSize = pageSize;
queryData();
}
const yHeight = ref(0);
function resetGetHeight() {
const doc = document.querySelector('.ant-form');
const btn = document.querySelector('.smart-table-btn-block');
const tableCell = document.querySelector('.ant-table-cell');
const pageEl = document.querySelector('.smart-query-table-page');
const box = document.querySelector('.admin-content');
setTimeout(() => {
if (doc && btn && tableCell && pageEl && box) {
let dueHeight = doc.offsetHeight + 10 + 24 + btn.offsetHeight + 15 + tableCell.offsetHeight + pageEl.offsetHeight + 20;
yHeight.value = box.offsetHeight - dueHeight;
}
}, 100);
}
// ======================== 弹窗/抽屉状态 ========================
const showDetailDrawer = ref(false);
const currentRecord = ref(null);
// ======================== 操作方法 ========================
function handleAdd() {
message.info('新增涉企案件记录');
}
function handleDetail(record) {
currentRecord.value = record;
showDetailDrawer.value = true;
}
// ======================== 工具函数 ========================
function formatAmount(amount) {
if (!amount) return '0.00';
return Number(amount).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function getCaseTypeName(type) {
const map = { fraud: '诈骗案', abscond: '非法吸收公众存款', bill: '票据诈骗', contract: '合同诈骗', other: '其他' };
return map[type] || '-';
}
function getEnterpriseStatusColor(status) {
const map = { 0: 'processing', 1: 'success', 2: 'error', 3: 'warning' };
return map[status] || 'default';
}
function getEnterpriseStatusName(status) {
const map = { 0: '办理中', 1: '已结案', 2: '已撤销', 3: '督办中' };
return map[status] || '-';
}
const TABLE_ID_CONST = {
BUSINESS: {
CASE: {
ENTERPRISE_RECORD: 900105,
},
},
};
// ======================== 生命周期 ========================
onMounted(() => {
queryData();
resetGetHeight();
});
window.addEventListener(
'resize',
_.throttle(() => {
resetGetHeight();
}, 1000)
);
import WorkPage from '/@/components/business/work/work-page.vue';
</script>
<style lang="less" scoped>
.enterprise-record-container {
padding: 16px;
.section-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: 16px;
margin-bottom: 16px;
}
:deep(.smart-table-operate) {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.detail-content {
.detail-section-title {
font-size: 15px;
font-weight: 600;
color: #333;
margin: 20px 0 12px 0;
padding-left: 10px;
border-left: 3px solid #1890ff;
&:first-child {
margin-top: 0;
}
}
}
}
</style>

366
economic-crime-web/src/views/business/case-management/evaluation.vue

@ -1,366 +1,4 @@
<!--
* 执法考评
*
* @Author: system
* @Date: 2024-06-24
-->
<template>
<div class="evaluation-container">
<!---------- 查询表单form begin ----------->
<a-form class="smart-query-form">
<a-row class="smart-query-form-row" v-privilege="'evaluation:query'">
<a-form-item label="考评周期" class="smart-query-form-item">
<a-select style="width: 160px" v-model:value="queryForm.period" placeholder="请选择考评周期" allowClear>
<a-select-option value="2024-Q1">2024年第一季度</a-select-option>
<a-select-option value="2024-Q2">2024年第二季度</a-select-option>
<a-select-option value="2024-Q3">2024年第三季度</a-select-option>
<a-select-option value="2024-Q4">2024年第四季度</a-select-option>
<a-select-option value="2024-H1">2024年上半年</a-select-option>
<a-select-option value="2024-H2">2024年下半年</a-select-option>
<a-select-option value="2024-Y">2024年全年</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="考评对象" class="smart-query-form-item">
<a-input style="width: 160px" v-model:value="queryForm.target" placeholder="请输入考评对象" allowClear />
</a-form-item>
<a-form-item label="评分等级" class="smart-query-form-item">
<a-select style="width: 120px" v-model:value="queryForm.grade" placeholder="请选择评分等级" allowClear>
<a-select-option value="excellent">优秀</a-select-option>
<a-select-option value="good">良好</a-select-option>
<a-select-option value="qualified">合格</a-select-option>
<a-select-option value="unqualified">不合格</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button-group>
<a-button type="primary" @click="onSearch" v-privilege="'evaluation:query'">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="resetQuery" v-privilege="'evaluation:query'">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<!---------- 查询表单form end ----------->
<div class="section-card">
<!---------- 表格操作行 begin ----------->
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button type="primary" v-privilege="'evaluation:add'" @click="handleAdd">
<template #icon><PlusOutlined /></template>
发起考评
</a-button>
</div>
<div class="smart-table-setting-block">
<TableOperator v-model="columns" :tableId="TABLE_ID_CONST.BUSINESS.CASE.EVALUATION" :refresh="queryData" />
</div>
</a-row>
<!---------- 表格操作行 end ----------->
<a-table
size="small"
:dataSource="tableData"
:columns="columns"
rowKey="id"
:scroll="{ x: 1100, y: yHeight }"
bordered
:pagination="false"
:loading="tableLoading"
>
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'qualityScore'">
<a-progress :percent="record.qualityScore" size="small" :strokeColor="getScoreColor(record.qualityScore)" />
</template>
<template v-if="column.dataIndex === 'complianceScore'">
<a-progress :percent="record.complianceScore" size="small" :strokeColor="getScoreColor(record.complianceScore)" />
</template>
<template v-if="column.dataIndex === 'efficiencyScore'">
<a-progress :percent="record.efficiencyScore" size="small" :strokeColor="getScoreColor(record.efficiencyScore)" />
</template>
<template v-if="column.dataIndex === 'grade'">
<a-tag :color="getGradeColor(record.grade)">{{ getGradeName(record.grade) }}</a-tag>
</template>
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button type="link" @click="handleDetail(record)" v-privilege="'evaluation:view'">查看详情</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="pagination.total"
@change="onPageChange"
@showSizeChange="onPageChange"
:show-total="(total) => `共${total}条`"
/>
</div>
</div>
<!-- 详情抽屉 - 各维度打分明细 -->
<a-drawer
v-model:open="showDetailDrawer"
title="考评详情"
placement="right"
:width="720"
:maskClosable="true"
>
<div class="detail-content" v-if="currentRecord">
<a-descriptions bordered :column="2" size="small" class="detail-info">
<a-descriptions-item label="考评周期">{{ currentRecord.period }}</a-descriptions-item>
<a-descriptions-item label="考评对象">{{ currentRecord.target }}</a-descriptions-item>
<a-descriptions-item label="综合得分">
<a-statistic :value="currentRecord.totalScore" :precision="1" suffix="分" :value-style="{ color: getGradeColor(currentRecord.grade) === '#f5222d' ? '#f5222d' : '#1890ff' }" />
</a-descriptions-item>
<a-descriptions-item label="综合等级">
<a-tag :color="getGradeColor(currentRecord.grade)" style="font-size: 14px; padding: 2px 12px;">{{ getGradeName(currentRecord.grade) }}</a-tag>
</a-descriptions-item>
</a-descriptions>
<h4 class="detail-section-title">各维度打分明细</h4>
<a-table
size="small"
:dataSource="detailDimensionList"
:columns="detailColumns"
:pagination="false"
bordered
rowKey="dimension"
>
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'score'">
<a-progress :percent="record.score" size="small" :strokeColor="getScoreColor(record.score)" />
</template>
</template>
</a-table>
<h4 class="detail-section-title">考评意见</h4>
<div class="opinion-box">
<p>{{ currentRecord.opinion || '暂无考评意见' }}</p>
</div>
</div>
</a-drawer>
</div>
</template>
<template><WorkPage kind="evaluation" /></template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue';
import { message } from 'ant-design-vue';
import {
SearchOutlined,
ReloadOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import _ from 'lodash';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
// import { getRequest, postRequest } from '/@/lib/axios';
// ======================== 表格列定义 ========================
const columns = ref([
{ title: '考评周期', dataIndex: 'period', width: 150, resizable: true },
{ title: '考评对象', dataIndex: 'target', width: 160, ellipsis: true, resizable: true },
{ title: '执法质量得分', dataIndex: 'qualityScore', width: 150, align: 'center', resizable: true },
{ title: '程序合规得分', dataIndex: 'complianceScore', width: 150, align: 'center', resizable: true },
{ title: '效率指标得分', dataIndex: 'efficiencyScore', width: 140, align: 'center', resizable: true },
{ title: '综合等级', dataIndex: 'grade', width: 100, align: 'center', resizable: true },
{ title: '操作', dataIndex: 'action', fixed: 'right', width: 100 },
]);
const detailColumns = [
{ title: '考评维度', dataIndex: 'dimension', width: 180 },
{ title: '满分', dataIndex: 'maxScore', width: 80, align: 'center' },
{ title: '得分', dataIndex: 'score', width: 220, align: 'center' },
{ title: '评价说明', dataIndex: 'remark', ellipsis: true },
];
// ======================== Mock 数据 ========================
const mockTableData = [
{ id: 1, period: '2024年第一季度', target: '市经侦支队一大队', qualityScore: 92, complianceScore: 88, efficiencyScore: 85, grade: 'excellent', totalScore: 88.3, opinion: '执法质量优秀,程序规范,建议保持。' },
{ id: 2, period: '2024年第一季度', target: '区经侦大队', qualityScore: 78, complianceScore: 82, efficiencyScore: 75, grade: 'good', totalScore: 78.3, opinion: '整体表现良好,效率指标需提升。' },
{ id: 3, period: '2024年第一季度', target: '县经侦中队', qualityScore: 65, complianceScore: 70, efficiencyScore: 60, grade: 'qualified', totalScore: 65.0, opinion: '基本达标,多项指标有提升空间。' },
{ id: 4, period: '2024年第一季度', target: '市经侦支队二大队', qualityScore: 95, complianceScore: 93, efficiencyScore: 90, grade: 'excellent', totalScore: 92.7, opinion: '各项指标均表现优异,予以表彰。' },
{ id: 5, period: '2024年第二季度', target: '市经侦支队一大队', qualityScore: 88, complianceScore: 85, efficiencyScore: 82, grade: 'good', totalScore: 85.0, opinion: '较上季度略有下降,需关注程序合规性。' },
{ id: 6, period: '2024年第二季度', target: '区经侦大队', qualityScore: 55, complianceScore: 50, efficiencyScore: 48, grade: 'unqualified', totalScore: 51.0, opinion: '综合评分不合格,需限期整改。' },
];
// ======================== 查询表单和方法 ========================
const queryFormState = {
period: undefined,
target: '',
grade: undefined,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive(_.cloneDeep(queryFormState));
const tableLoading = ref(false);
const tableData = ref([]);
const pagination = reactive({ total: 0 });
function resetQuery() {
let pageSize = queryForm.pageSize;
Object.assign(queryForm, _.cloneDeep(queryFormState));
queryForm.pageSize = pageSize;
queryData();
}
function onSearch() {
queryForm.pageNum = 1;
queryData();
}
async function queryData() {
tableLoading.value = true;
try {
// TODO: 替换为实际的 API 调用
// const res = await getRequest('/api/evaluation/list', queryForm);
tableData.value = mockTableData;
pagination.total = mockTableData.length;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
function onPageChange(page, pageSize) {
queryForm.pageNum = page;
queryForm.pageSize = pageSize;
queryData();
}
const yHeight = ref(0);
function resetGetHeight() {
const doc = document.querySelector('.ant-form');
const btn = document.querySelector('.smart-table-btn-block');
const tableCell = document.querySelector('.ant-table-cell');
const pageEl = document.querySelector('.smart-query-table-page');
const box = document.querySelector('.admin-content');
setTimeout(() => {
if (doc && btn && tableCell && pageEl && box) {
let dueHeight = doc.offsetHeight + 10 + 24 + btn.offsetHeight + 15 + tableCell.offsetHeight + pageEl.offsetHeight + 20;
yHeight.value = box.offsetHeight - dueHeight;
}
}, 100);
}
// ======================== 弹窗/抽屉状态 ========================
const showDetailDrawer = ref(false);
const currentRecord = ref(null);
const detailDimensionList = computed(() => {
if (!currentRecord.value) return [];
return [
{ dimension: '执法质量', maxScore: 100, score: currentRecord.value.qualityScore, remark: '包含证据收集、事实认定、法律适用等方面' },
{ dimension: '程序合规', maxScore: 100, score: currentRecord.value.complianceScore, remark: '包含办案时限、审批流程、文书规范性等' },
{ dimension: '效率指标', maxScore: 100, score: currentRecord.value.efficiencyScore, remark: '包含办案周期、结案率、响应速度等' },
];
});
// ======================== 操作方法 ========================
function handleAdd() {
message.info('发起执法考评');
}
function handleDetail(record) {
currentRecord.value = record;
showDetailDrawer.value = true;
}
// ======================== 工具函数 ========================
function getScoreColor(score) {
if (score >= 90) return '#52c41a';
if (score >= 75) return '#1890ff';
if (score >= 60) return '#faad14';
return '#ff4d4f';
}
function getGradeColor(grade) {
const map = { excellent: 'success', good: 'processing', qualified: 'warning', unqualified: 'error' };
return map[grade] || 'default';
}
function getGradeName(grade) {
const map = { excellent: '优秀', good: '良好', qualified: '合格', unqualified: '不合格' };
return map[grade] || '-';
}
const TABLE_ID_CONST = {
BUSINESS: {
CASE: {
EVALUATION: 900102,
},
},
};
// ======================== 生命周期 ========================
onMounted(() => {
queryData();
resetGetHeight();
});
window.addEventListener(
'resize',
_.throttle(() => {
resetGetHeight();
}, 1000)
);
import WorkPage from '/@/components/business/work/work-page.vue';
</script>
<style lang="less" scoped>
.evaluation-container {
padding: 16px;
.section-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: 16px;
margin-bottom: 16px;
}
:deep(.smart-table-operate) {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.detail-content {
.detail-section-title {
font-size: 15px;
font-weight: 600;
color: #333;
margin: 20px 0 12px 0;
padding-left: 10px;
border-left: 3px solid #1890ff;
}
.opinion-box {
background: #fafafa;
border: 1px solid #f0f0f0;
border-radius: 6px;
padding: 12px 16px;
color: #555;
line-height: 1.8;
}
}
}
</style>

321
economic-crime-web/src/views/business/case-management/leader-record.vue

@ -1,321 +1,4 @@
<!--
* 领办包保
*
* @Author: system
* @Date: 2024-06-24
-->
<template>
<div class="leader-record-container">
<!---------- 查询表单form begin ----------->
<a-form class="smart-query-form">
<a-row class="smart-query-form-row" v-privilege="'leader-record:query'">
<a-form-item label="包保领导" class="smart-query-form-item">
<a-input style="width: 150px" v-model:value="queryForm.leaderName" placeholder="请输入领导姓名" allowClear />
</a-form-item>
<a-form-item label="案件编号" class="smart-query-form-item">
<a-input style="width: 180px" v-model:value="queryForm.caseNo" placeholder="请输入案件编号" allowClear />
</a-form-item>
<a-form-item label="包保状态" class="smart-query-form-item">
<a-select style="width: 130px" v-model:value="queryForm.status" placeholder="请选择包保状态" allowClear>
<a-select-option value="ongoing">进行中</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="overdue">已超期</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button-group>
<a-button type="primary" @click="onSearch" v-privilege="'leader-record:query'">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button @click="resetQuery" v-privilege="'leader-record:query'">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<!---------- 查询表单form end ----------->
<div class="section-card">
<!---------- 表格操作行 begin ----------->
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button type="primary" v-privilege="'leader-record:add'" @click="handleAdd">
<template #icon><PlusOutlined /></template>
新增包保
</a-button>
</div>
<div class="smart-table-setting-block">
<TableOperator v-model="columns" :tableId="TABLE_ID_CONST.BUSINESS.CASE.LEADER_RECORD" :refresh="queryData" />
</div>
</a-row>
<!---------- 表格操作行 end ----------->
<a-table
size="small"
:dataSource="tableData"
:columns="columns"
rowKey="id"
:scroll="{ x: 1300, y: yHeight }"
bordered
:pagination="false"
:loading="tableLoading"
>
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'progressStatus'">
<a-tag :color="getProgressStatusColor(record.progressStatus)">{{ getProgressStatusName(record.progressStatus) }}</a-tag>
</template>
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button type="link" @click="handleUpdateProgress(record)" v-privilege="'leader-record:update'">更新进展</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="pagination.total"
@change="onPageChange"
@showSizeChange="onPageChange"
:show-total="(total) => `共${total}条`"
/>
</div>
</div>
<!-- 更新进展弹窗 -->
<a-modal
v-model:open="showProgressModal"
title="更新包保进展"
:width="560"
:maskClosable="false"
centered
@ok="submitProgress"
:confirm-loading="progressLoading"
>
<a-alert
:message="`正在更新「${currentRecord?.leaderName}」对案件「${currentRecord?.caseNo}」的包保进展`"
type="info"
show-icon
style="margin-bottom: 16px;"
v-if="currentRecord"
/>
<a-form layout="vertical" ref="progressFormRef" :model="progressForm">
<a-form-item label="进展描述" required :rules="[{ required: true, message: '请填写进展描述' }]">
<a-textarea v-model:value="progressForm.description" placeholder="请详细描述当前工作进展情况" :rows="5" />
</a-form-item>
<a-form-item label="下一步计划" required :rules="[{ required: true, message: '请填写下一步计划' }]">
<a-textarea v-model:value="progressForm.nextPlan" placeholder="请填写下一步工作计划和时间安排" :rows="4" />
</a-form-item>
<a-form-item label="存在问题(如有)">
<a-textarea v-model:value="progressForm.problem" placeholder="如遇困难或需要协调的事项可在此说明" :rows="3" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<template><WorkPage kind="leader" /></template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import {
SearchOutlined,
ReloadOutlined,
PlusOutlined,
} from '@ant-design/icons-vue';
import _ from 'lodash';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
// import { getRequest, postRequest } from '/@/lib/axios';
// ======================== 表格列定义 ========================
const columns = ref([
{ title: '包保领导姓名', dataIndex: 'leaderName', width: 110, resizable: true },
{ title: '职务', dataIndex: 'position', width: 130, resizable: true },
{ title: '案件编号', dataIndex: 'caseNo', width: 160, resizable: true },
{ title: '案件名称', dataIndex: 'caseName', width: 200, ellipsis: true, resizable: true },
{ title: '包保开始时间', dataIndex: 'startDate', width: 130, resizable: true },
{ title: '包保截止时间', dataIndex: 'endDate', width: 130, resizable: true },
{ title: '进展状态', dataIndex: 'progressStatus', width: 100, align: 'center', resizable: true },
{ title: '操作', dataIndex: 'action', fixed: 'right', width: 110 },
]);
// ======================== Mock 数据 ========================
const mockTableData = [
{ id: 1, leaderName: '陈建国', position: '市局副局长', caseNo: 'AJ202401001', caseName: '张某涉嫌集资诈骗案', startDate: '2024-01-10', endDate: '2024-06-30', progressStatus: 'ongoing' },
{ id: 2, leaderName: '刘志强', position: '支队政委', caseNo: 'AJ202401002', caseName: '某公司非法吸收公众存款案', startDate: '2024-01-25', endDate: '2024-04-30', progressStatus: 'completed' },
{ id: 3, leaderName: '王明华', position: '大队长', caseNo: 'AJ202402001', caseName: '李某网络诈骗案', startDate: '2024-02-08', endDate: '2024-05-31', progressStatus: 'ongoing' },
{ id: 4, leaderName: '张伟东', position: '副支队长', caseNo: 'AJ202402002', caseName: '王某票据诈骗案', startDate: '2024-02-20', endDate: '2024-03-20', progressStatus: 'overdue' },
{ id: 5, leaderName: '李国强', position: '大队教导员', caseNo: 'AJ202403001', caseName: '赵某合同诈骗案', startDate: '2024-03-05', endDate: '2024-09-30', progressStatus: 'ongoing' },
{ id: 6, leaderName: '赵建军', position: '副大队长', caseNo: 'AJ202403002', caseName: '某集团传销案', startDate: '2024-03-15', endDate: '2024-06-15', progressStatus: 'ongoing' },
];
// ======================== 查询表单和方法 ========================
const queryFormState = {
leaderName: '',
caseNo: '',
status: undefined,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive(_.cloneDeep(queryFormState));
const tableLoading = ref(false);
const tableData = ref([]);
const pagination = reactive({ total: 0 });
function resetQuery() {
let pageSize = queryForm.pageSize;
Object.assign(queryForm, _.cloneDeep(queryFormState));
queryForm.pageSize = pageSize;
queryData();
}
function onSearch() {
queryForm.pageNum = 1;
queryData();
}
async function queryData() {
tableLoading.value = true;
try {
// TODO: 替换为实际的 API 调用
// const res = await getRequest('/api/leader-record/list', queryForm);
tableData.value = mockTableData;
pagination.total = mockTableData.length;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
function onPageChange(page, pageSize) {
queryForm.pageNum = page;
queryForm.pageSize = pageSize;
queryData();
}
const yHeight = ref(0);
function resetGetHeight() {
const doc = document.querySelector('.ant-form');
const btn = document.querySelector('.smart-table-btn-block');
const tableCell = document.querySelector('.ant-table-cell');
const pageEl = document.querySelector('.smart-query-table-page');
const box = document.querySelector('.admin-content');
setTimeout(() => {
if (doc && btn && tableCell && pageEl && box) {
let dueHeight = doc.offsetHeight + 10 + 24 + btn.offsetHeight + 15 + tableCell.offsetHeight + pageEl.offsetHeight + 20;
yHeight.value = box.offsetHeight - dueHeight;
}
}, 100);
}
// ======================== 更新进展弹窗 ========================
const showProgressModal = ref(false);
const progressLoading = ref(false);
const progressFormRef = ref();
const currentRecord = ref(null);
const progressForm = reactive({
description: '',
nextPlan: '',
problem: '',
});
function handleAdd() {
message.info('新增领办包保记录');
}
function handleUpdateProgress(record) {
currentRecord.value = record;
Object.assign(progressForm, { description: '', nextPlan: '', problem: '' });
showProgressModal.value = true;
}
async function submitProgress() {
try {
await progressFormRef.value?.validateFields?.();
} catch (e) {
return;
}
progressLoading.value = true;
try {
// TODO: 替换为实际的 API 调用
// await postRequest('/api/leader-record/update-progress', { id: currentRecord.value.id, ...progressForm });
message.success('进展更新成功');
showProgressModal.value = false;
queryData();
} catch (e) {
smartSentry.captureError(e);
} finally {
progressLoading.value = false;
}
}
// ======================== 工具函数 ========================
function getProgressStatusColor(status) {
const map = { ongoing: 'processing', completed: 'success', overdue: 'error' };
return map[status] || 'default';
}
function getProgressStatusName(status) {
const map = { ongoing: '进行中', completed: '已完成', overdue: '已超期' };
return map[status] || '-';
}
const TABLE_ID_CONST = {
BUSINESS: {
CASE: {
LEADER_RECORD: 900106,
},
},
};
// ======================== 生命周期 ========================
onMounted(() => {
queryData();
resetGetHeight();
});
window.addEventListener(
'resize',
_.throttle(() => {
resetGetHeight();
}, 1000)
);
import WorkPage from '/@/components/business/work/work-page.vue';
</script>
<style lang="less" scoped>
.leader-record-container {
padding: 16px;
.section-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: 16px;
margin-bottom: 16px;
}
:deep(.smart-table-operate) {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
}
</style>

612
economic-crime-web/src/views/business/case-management/two-books-one-form.vue

@ -10,13 +10,14 @@
<div class="page-header">
<h2>两书一单电子台账</h2>
<div class="header-buttons">
<a-button type="primary" @click="openAddDocumentDialog('risk_tip')">
<a-button v-privilege="'two-books:add'" type="primary" @click="openAddDocumentDialog('risk_tip')">
<template #icon><PlusOutlined /></template>
风险防范提示书
</a-button>
<a-button
type="primary"
style="background: #52c41a; border-color: #52c41a;"
v-privilege="'two-books:add'"
@click="openAddDocumentDialog('risk_suggestion')"
>
<template #icon><PlusOutlined /></template>
@ -25,6 +26,7 @@
<a-button
type="primary"
style="background: #faad14; border-color: #faad14;"
v-privilege="'two-books:add'"
@click="openAddDocumentDialog('risk_alert')"
>
<template #icon><PlusOutlined /></template>
@ -92,16 +94,16 @@
<div class="stat-label">备案单</div>
</div>
<div class="stat-item stat-total">
<div class="stat-num">{{ filteredList.length }}</div>
<div class="stat-num">{{ total }}</div>
<div class="stat-label">总计</div>
</div>
</div>
<!-- 卡片列表 -->
<div class="card-grid">
<a-spin :spinning="loading"><div class="card-grid">
<div
v-for="doc in pagedList"
:key="doc.docNo"
v-for="doc in documentList"
:key="doc.id"
class="doc-card"
:class="'card-' + doc.docType"
@click="viewDocument(doc)"
@ -159,31 +161,34 @@
</a-button>
</a-tooltip>
<a-tooltip title="下载文档" placement="top">
<a-button type="link" size="small" @click.stop="downloadDocument(doc)">
<a-button type="link" size="small" v-privilege="'two-books:download'" @click.stop="downloadDocument(doc)">
<template #icon><DownloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip title="编辑" placement="top">
<a-button type="link" size="small" @click.stop="editDocument(doc)">
<a-button type="link" size="small" v-privilege="'two-books:edit'" @click.stop="editDocument(doc)">
<template #icon><EditOutlined /></template>
</a-button>
</a-tooltip>
<a-button type="link" danger size="small" v-privilege="'two-books:delete'" @click.stop="removeDocument(doc)"><DeleteOutlined /></a-button>
</div>
</div>
</div>
</div>
</a-spin>
<!-- 空状态 -->
<a-empty v-if="filteredList.length === 0" description="暂无文档数据" />
<a-empty v-if="!loading && total === 0" description="暂无文档数据" />
<!-- 分页 -->
<div class="pagination-wrapper" v-if="filteredList.length > 0">
<div class="pagination-wrapper" v-if="total > 0">
<a-pagination
v-model:current="pagination.currentPage"
v-model:pageSize="pagination.pageSize"
:pageSizeOptions="['8', '12', '24', '48']"
:total="filteredList.length"
:total="total"
:showTotal="(total) => `共 ${total} 条`"
@change="onPageChange"
show-size-changer
show-quick-jumper
/>
@ -192,49 +197,42 @@
<!-- 文档对话框 -->
<a-modal
v-model:open="documentDialogVisible"
:title="isEditMode ? '编辑文档' : '新增文档'"
:title="readOnly ? '查看文书' : (isEditMode ? '编辑文书' : '新增文书')"
width="700px"
:footer="null"
:destroyOnClose="true"
:maskClosable="false"
:closable="!saving && !uploading"
:keyboard="!saving && !uploading"
>
<a-form :model="currentDocument" layout="vertical">
<a-form ref="formRef" :model="currentDocument" layout="vertical" :disabled="readOnly || saving">
<a-form-item label="文档编号" v-if="isEditMode">
<a-input v-model:value="currentDocument.docNo" disabled />
</a-form-item>
<a-form-item label="文档类型" required>
<a-form-item label="文档类型" name="docType" :rules="required('文档类型')">
<a-select v-model:value="currentDocument.docType" style="width: 100%;">
<a-select-option value="risk_tip">风险防范提示书</a-select-option>
<a-select-option value="risk_suggestion">风险防范建议书</a-select-option>
<a-select-option value="risk_alert">风险预警备案单</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="企业名称" required>
<a-select
v-model:value="currentDocument.enterpriseName"
show-search
allow-clear
style="width: 100%;"
placeholder="请选择或输入企业名称"
>
<a-select-option v-for="e in enterpriseList" :key="e.id" :value="e.name">
{{ e.name }}
</a-select-option>
</a-select>
<a-form-item label="企业名称" name="enterpriseName" :rules="required('企业名称')">
<a-input v-model:value="currentDocument.enterpriseName" :maxlength="200" placeholder="请输入企业全称" />
</a-form-item>
<a-form-item label="风险类型" required>
<a-input v-model:value="currentDocument.riskType" placeholder="请输入风险类型" />
<a-form-item label="风险类型" name="riskType" :rules="required('风险类型')">
<a-input v-model:value="currentDocument.riskType" :maxlength="100" placeholder="请输入风险类型" />
</a-form-item>
<a-form-item label="状态" required>
<a-form-item label="状态" name="status" :rules="required('状态')">
<a-select v-model:value="currentDocument.status" style="width: 100%;">
<a-select-option value="pending">待处理</a-select-option>
<a-select-option value="processing">处理中</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="生成人" required>
<a-input v-model:value="currentDocument.generator" placeholder="请输入生成人" />
<a-form-item label="生成人" name="generator" :rules="required('生成人')">
<a-input v-model:value="currentDocument.generator" :maxlength="100" placeholder="请输入生成人" />
</a-form-item>
<a-form-item label="生成时间" required>
<a-form-item label="生成时间" name="generateTime" :rules="required('生成时间')">
<a-date-picker
v-model:value="currentDocument.generateTime"
show-time
@ -243,459 +241,151 @@
style="width: 100%;"
/>
</a-form-item>
<a-form-item label="文档标题" required>
<a-input v-model:value="currentDocument.title" placeholder="请输入文档标题" />
<a-form-item label="文档标题" name="title" :rules="required('文档标题')">
<a-input v-model:value="currentDocument.title" :maxlength="200" placeholder="请输入文档标题" />
</a-form-item>
<a-form-item label="文档内容">
<a-form-item label="文档内容" name="content" :rules="required('文档内容')">
<a-textarea
v-model:value="currentDocument.content"
v-model:value="currentDocument.content" :maxlength="20000"
:rows="4"
placeholder="请输入文档内容"
/>
</a-form-item>
<a-form-item label="附件上传">
<a-upload
:fileList="currentDocument.attachments"
:beforeUpload="handleBeforeUpload"
:maxCount="3"
@remove="handleRemove"
>
<a-button type="primary">
<template #icon><UploadOutlined /></template>
选择文件
</a-button>
<a-upload v-if="!readOnly"
:fileList="currentDocument.attachments" :beforeUpload="handleBeforeUpload"
:disabled="saving || uploading > 0" :showUploadList="{showRemoveIcon: !saving}"
accept=".pdf,.doc,.docx,.xls,.xlsx" @remove="handleRemove">
<a-button :loading="uploading > 0" :disabled="currentDocument.attachments.length >= 3"><UploadOutlined />上传附件</a-button>
</a-upload>
<div class="upload-tip">支持 PDF/Word/Excel,最多3个文件</div>
<div v-if="currentDocument.id">
<div v-for="file in currentDocument.attachments" :key="file.fileId">
{{ file.name }}
<a-button v-if="file.persisted" :disabled="false" type="link" v-privilege="'two-books:download'" @click="downloadAttachment(file)">下载</a-button>
<span v-else>(保存后可下载)</span>
</div>
</div>
<div class="upload-tip">支持PDF/Word/Excel,每个不超过20MB,最多3个;移除附件在保存文书后生效。</div>
</a-form-item>
</a-form>
<div class="dialog-footer">
<a-button @click="documentDialogVisible = false">取消</a-button>
<a-button type="primary" @click="saveDocument">保存</a-button>
<a-button :disabled="saving || uploading > 0" @click="closeDialog">关闭</a-button>
<a-button v-if="!readOnly" type="primary" :loading="saving" :disabled="uploading > 0" @click="saveDocument">保存</a-button>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue';
import { message } from 'ant-design-vue';
import {
PlusOutlined,
FileTextOutlined,
BulbOutlined,
AlertOutlined,
SearchOutlined,
ReloadOutlined,
BankOutlined,
WarningOutlined,
EyeOutlined,
DownloadOutlined,
EditOutlined,
UploadOutlined,
BellOutlined,
ClockCircleOutlined,
CheckCircleOutlined,
FileProtectOutlined,
} from '@ant-design/icons-vue';
// ======================== 状态定义 ========================
const searchForm = reactive({
docNo: '',
docType: undefined,
enterpriseName: '',
status: undefined,
});
const pagination = reactive({
currentPage: 1,
pageSize: 12,
});
import { ref, reactive, onMounted } from 'vue';
import { message, Modal } from 'ant-design-vue';
import dayjs from 'dayjs';
import { PlusOutlined, FileTextOutlined, BulbOutlined, AlertOutlined, SearchOutlined, ReloadOutlined,
BankOutlined, WarningOutlined, EyeOutlined, DownloadOutlined, EditOutlined, UploadOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import { twoBooksApi } from '/@/api/business/case-management/two-books-api';
import { useUserStore } from '/@/store/modules/system/user';
import { smartSentry } from '/@/lib/smart-sentry';
const searchForm = reactive({ docNo: '', docType: undefined, enterpriseName: '', status: undefined });
const pagination = reactive({ currentPage: 1, pageSize: 12 });
const documentList = ref([]);
const total = ref(0);
const loading = ref(false);
const saving = ref(false);
const uploading = ref(0);
const stats = reactive({ tipCount: 0, suggestionCount: 0, alertCount: 0 });
const documentDialogVisible = ref(false);
const readOnly = ref(false);
const isEditMode = ref(false);
const currentDocument = reactive({
docNo: '',
docType: 'risk_tip',
enterpriseName: undefined,
riskType: '',
status: 'pending',
generator: '',
generateTime: null,
title: '',
content: '',
attachments: [],
});
// 企业列表
const enterpriseList = ref([
{ id: 1, name: 'XX科技有限公司' },
{ id: 2, name: 'YY金融服务公司' },
{ id: 3, name: 'ZZ贸易集团' },
{ id: 4, name: 'AA智能制造有限公司' },
{ id: 5, name: 'BB电子商务有限公司' },
{ id: 6, name: 'CC物流运输有限公司' },
{ id: 7, name: 'DD房地产开发有限公司' },
{ id: 8, name: 'EE文化传媒有限公司' },
]);
// ======================== Mock 数据(16条) ========================
const documentList = ref([
{
docNo: 'DT20240612001',
docType: 'risk_tip',
title: '关于XX科技有限公司网络安全风险防范提示书',
enterpriseName: 'XX科技有限公司',
riskType: '网络安全',
status: 'completed',
generator: '张伟',
generateTime: '2024-06-12 09:15:00',
},
{
docNo: 'DT20240612002',
docType: 'risk_suggestion',
title: 'YY金融服务公司资金安全风险防范建议书',
enterpriseName: 'YY金融服务公司',
riskType: '资金安全',
status: 'processing',
generator: '李明',
generateTime: '2024-06-11 14:30:00',
},
{
docNo: 'DT20240611001',
docType: 'risk_alert',
title: 'ZZ贸易集团合同欺诈风险预警备案单',
enterpriseName: 'ZZ贸易集团',
riskType: '合同风险',
status: 'pending',
generator: '王芳',
generateTime: '2024-06-11 10:20:00',
},
{
docNo: 'DT20240611002',
docType: 'risk_tip',
title: 'AA智能制造有限公司数据泄露风险提示书',
enterpriseName: 'AA智能制造有限公司',
riskType: '数据安全',
status: 'completed',
generator: '赵强',
generateTime: '2024-06-10 16:45:00',
},
{
docNo: 'DT20240610001',
docType: 'risk_suggestion',
title: 'BB电商公司知识产权保护建议书',
enterpriseName: 'BB电子商务有限公司',
riskType: '知识产权',
status: 'completed',
generator: '陈刚',
generateTime: '2024-06-10 11:08:00',
},
{
docNo: 'DT20240609001',
docType: 'risk_alert',
title: 'CC物流公司非法经营风险预警备案',
enterpriseName: 'CC物流运输有限公司',
riskType: '经营合规',
status: 'processing',
generator: '刘洋',
generateTime: '2024-06-09 13:55:00',
},
{
docNo: 'DT20240609002',
docType: 'risk_tip',
title: 'DD房地产公司资金链断裂风险提示',
enterpriseName: 'DD房地产开发有限公司',
riskType: '财务风险',
status: 'pending',
generator: '孙丽',
generateTime: '2024-06-09 09:30:00',
},
{
docNo: 'DT20240608001',
docType: 'risk_suggestion',
title: 'EE传媒公司舆情风险防控建议书',
enterpriseName: 'EE文化传媒有限公司',
riskType: '舆情风险',
status: 'completed',
generator: '周杰',
generateTime: '2024-06-08 15:22:00',
},
{
docNo: 'DT20240608002',
docType: 'risk_tip',
title: 'XX科技员工信息安全管理提示书',
enterpriseName: 'XX科技有限公司',
riskType: '人员管理',
status: 'completed',
generator: '吴敏',
generateTime: '2024-06-07 10:18:00',
},
{
docNo: 'DT20240607001',
docType: 'risk_alert',
title: 'YY金融公司洗钱嫌疑交易预警备案',
enterpriseName: 'YY金融服务公司',
riskType: '反洗钱',
status: 'pending',
generator: '郑凯',
generateTime: '2024-06-07 14:50:00',
},
{
docNo: 'DT20240606001',
docType: 'risk_suggestion',
title: 'ZZ贸易供应链中断风险建议',
enterpriseName: 'ZZ贸易集团',
riskType: '供应链',
status: 'processing',
generator: '钱进',
generateTime: '2024-06-06 16:33:00',
},
{
docNo: 'DT20240606002',
docType: 'risk_tip',
title: 'AA制造环保合规风险提示书',
enterpriseName: 'AA智能制造有限公司',
riskType: '环保合规',
status: 'completed',
generator: '冯雪',
generateTime: '2024-06-05 08:45:00',
},
{
docNo: 'DT20240605001',
docType: 'risk_alert',
title: 'BB电商虚假宣传风险预警备案',
enterpriseName: 'BB电子商务有限公司',
riskType: '广告合规',
status: 'handled',
generator: '蒋涛',
generateTime: '2024-06-05 11:28:00',
},
{
docNo: 'DT20240604001',
docType: 'risk_tip',
title: 'CC物流车辆安全事故防范提示',
enterpriseName: 'CC物流运输有限公司',
riskType: '安全生产',
status: 'completed',
generator: '沈琳',
generateTime: '2024-06-04 17:10:00',
},
{
docNo: 'DT20240603001',
docType: 'risk_suggestion',
title: 'DD房产预售资金监管建议书',
enterpriseName: 'DD房地产开发有限公司',
riskType: '资金监管',
status: 'completed',
generator: '韩冰',
generateTime: '2024-06-03 09:55:00',
},
{
docNo: 'DT20240602001',
docType: 'risk_alert',
title: 'EE传媒版权侵权风险预警备案',
enterpriseName: 'EE文化传媒有限公司',
riskType: '版权风险',
status: 'pending',
generator: '杨帆',
generateTime: '2024-06-02 13:42:00',
},
]);
// ======================== 计算属性 ========================
// 统计数据
const stats = computed(() => ({
tipCount: documentList.value.filter((d) => d.docType === 'risk_tip').length,
suggestionCount: documentList.value.filter((d) => d.docType === 'risk_suggestion').length,
alertCount: documentList.value.filter((d) => d.docType === 'risk_alert').length,
}));
// 过滤后的列表
const filteredList = computed(() => {
let list = documentList.value;
if (searchForm.docNo) {
list = list.filter((d) => d.docNo.toLowerCase().includes(searchForm.docNo.toLowerCase()));
}
if (searchForm.docType) {
list = list.filter((d) => d.docType === searchForm.docType);
}
if (searchForm.enterpriseName) {
list = list.filter((d) => d.enterpriseName.includes(searchForm.enterpriseName));
}
if (searchForm.status) {
list = list.filter((d) => d.status === searchForm.status);
}
return list;
});
// 分页后的列表
const pagedList = computed(() => {
const start = (pagination.currentPage - 1) * pagination.pageSize;
return filteredList.value.slice(start, start + pagination.pageSize);
});
// ======================== 工具函数 ========================
const docIconMap = {
risk_tip: FileTextOutlined,
risk_suggestion: BulbOutlined,
risk_alert: AlertOutlined,
};
const getDocIcon = (type) => {
return docIconMap[type] || FileTextOutlined;
};
const getDocTypeText = (type) => {
const map = { risk_tip: '提示书', risk_suggestion: '建议书', risk_alert: '备案单' };
return map[type] || type;
};
const getStatusText = (status) => {
const map = {
pending: '待处理',
processing: '处理中',
completed: '已完成',
handled: '已处理',
};
return map[status] || status;
};
const getStatusColor = (status) => {
const map = {
pending: 'orange',
processing: 'blue',
completed: 'green',
handled: 'default',
};
return map[status] || 'default';
};
const formatTime = (time) => {
if (!time) return '';
if (typeof time === 'string') return time.replace('T', ' ').substring(0, 16);
// dayjs 对象
if (time && typeof time.format === 'function') {
return time.format('YYYY-MM-DD HH:mm');
const formRef = ref();
const newDocument = () => ({ id: null, version: null, docNo: '', docType: 'risk_tip', enterpriseName: '',
riskType: '', status: 'pending', generator: useUserStore().actualName || '', generateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
title: '', content: '', attachments: [] });
const currentDocument = reactive(newDocument());
const required = (label) => [{ required: true, whitespace: true, message: `请填写${label}` }];
let querySerial = 0;
async function queryData() {
const serial = ++querySerial;
loading.value = true;
try {
const res = await twoBooksApi.query({ ...searchForm, pageNum: pagination.currentPage, pageSize: pagination.pageSize });
if (serial !== querySerial) return;
documentList.value = res.data.list; total.value = res.data.total; Object.assign(stats, res.data.stats);
if (!documentList.value.length && total.value > 0 && pagination.currentPage > 1) {
pagination.currentPage = Math.ceil(total.value / pagination.pageSize); return queryData();
}
return '';
};
// ======================== 搜索/重置 ========================
const handleSearch = () => {
pagination.currentPage = 1;
};
const handleReset = () => {
searchForm.docNo = '';
searchForm.docType = undefined;
searchForm.enterpriseName = '';
searchForm.status = undefined;
pagination.currentPage = 1;
};
// ======================== 卡片操作 ========================
const viewDocument = (row) => {
isEditMode.value = true;
Object.assign(currentDocument, {
docNo: row.docNo,
docType: row.docType,
enterpriseName: row.enterpriseName,
riskType: row.riskType,
status: row.status,
generator: row.generator,
generateTime: row.generateTime,
title: row.title,
content: row.content || '',
attachments: [],
});
documentDialogVisible.value = true;
};
const editDocument = (row) => {
isEditMode.value = true;
Object.assign(currentDocument, {
docNo: row.docNo,
docType: row.docType,
enterpriseName: row.enterpriseName,
riskType: row.riskType,
status: row.status,
generator: row.generator,
generateTime: row.generateTime,
title: row.title,
content: row.content || '',
attachments: [],
});
documentDialogVisible.value = true;
};
const downloadDocument = (row) => {
message.success(`正在下载: ${row.docNo}`);
};
const openAddDocumentDialog = (docType) => {
isEditMode.value = false;
const typeTextMap = {
risk_tip: '风险防范提示书',
risk_suggestion: '风险防范建议书',
risk_alert: '风险预警备案单',
};
const now = new Date();
const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
Object.assign(currentDocument, {
docNo: '',
docType,
enterpriseName: undefined,
riskType: '',
status: 'pending',
generator: '',
generateTime: dateStr,
title: `${typeTextMap[docType]} - ${now.toLocaleDateString('zh-CN')}`,
content: '',
attachments: [],
} catch (e) { smartSentry.captureError(e); }
finally { if (serial === querySerial) loading.value = false; }
}
function handleSearch() { pagination.currentPage = 1; queryData(); }
function handleReset() { Object.assign(searchForm, { docNo:'', docType:undefined, enterpriseName:'', status:undefined }); handleSearch(); }
function onPageChange(page, size) { pagination.currentPage = page; pagination.pageSize = size; queryData(); }
const getDocIcon = (type) => ({risk_tip:FileTextOutlined,risk_suggestion:BulbOutlined,risk_alert:AlertOutlined}[type] || FileTextOutlined);
const getDocTypeText = (type) => ({risk_tip:'风险防范提示书',risk_suggestion:'风险防范建议书',risk_alert:'风险预警备案单'}[type] || type);
const getStatusText = (status) => ({pending:'待处理',processing:'处理中',completed:'已完成'}[status] || status);
const getStatusColor = (status) => ({pending:'orange',processing:'blue',completed:'green'}[status] || 'default');
const formatTime = (time) => time ? String(time).replace('T',' ').slice(0,16) : '';
async function openExisting(row, view) {
try {
const res = await twoBooksApi.detail(row.id);
Object.assign(currentDocument, newDocument(), res.data.document, {
generateTime: dayjs(res.data.document.generateTime).format('YYYY-MM-DD HH:mm:ss'),
attachments: res.data.attachments.map(f => ({...f,uid:String(f.fileId),status:'done',persisted:true})),
});
documentDialogVisible.value = true;
};
// ======================== 文件上传 ========================
const handleBeforeUpload = (file) => {
// 不实际上传,只加入文件列表
currentDocument.attachments = [...currentDocument.attachments, file];
return false;
};
const handleRemove = (file) => {
currentDocument.attachments = currentDocument.attachments.filter(
(item) => item.uid !== file.uid
);
};
// ======================== 保存 ========================
const saveDocument = () => {
if (
!currentDocument.docType ||
!currentDocument.enterpriseName ||
!currentDocument.riskType ||
!currentDocument.generator
) {
message.warning('请填写所有必填项');
return;
}
if (isEditMode.value) {
const idx = documentList.value.findIndex((d) => d.docNo === currentDocument.docNo);
if (idx !== -1) {
documentList.value[idx] = { ...currentDocument };
}
message.success('编辑成功');
} else {
const now = new Date();
const no = `DT${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${String(documentList.value.length + 1).padStart(4, '0')}`;
documentList.value.unshift({ ...currentDocument, docNo: no });
message.success('新增成功');
readOnly.value = view; isEditMode.value = true; documentDialogVisible.value = true;
} catch (e) { smartSentry.captureError(e); }
}
const viewDocument = row => openExisting(row,true);
const editDocument = row => openExisting(row,false);
function openAddDocumentDialog(docType) {
Object.assign(currentDocument,newDocument(),{docType,title:getDocTypeText(docType)});
readOnly.value = false; isEditMode.value = false; documentDialogVisible.value = true;
}
function closeDialog() { if(!saving.value && !uploading.value) documentDialogVisible.value = false; }
async function handleBeforeUpload(file) {
if(currentDocument.attachments.length + uploading.value >= 3) { message.warning('最多3个附件'); return false; }
if(!/\.(pdf|docx?|xlsx?)$/i.test(file.name) || file.size > 20*1024*1024 || !file.size) {
message.warning('支持PDF/Word/Excel,单个文件须为1字节至20MB'); return false;
}
documentDialogVisible.value = false;
};
uploading.value++;
try {
const body = new FormData(); body.append('file',file);
const res = await twoBooksApi.upload(body);
currentDocument.attachments.push({fileId:res.data.fileId,uid:String(res.data.fileId),name:file.name,size:file.size,status:'done'});
} catch(e) { smartSentry.captureError(e); }
finally { uploading.value--; }
return false;
}
function handleRemove(file) {
if (readOnly.value || saving.value || uploading.value) return false;
currentDocument.attachments = currentDocument.attachments.filter(f=>f.fileId!==file.fileId);
return false;
}
function downloadAttachment(file) {
if(currentDocument.id) twoBooksApi.downloadAttachment(currentDocument.id,file.fileId);
}
const downloadDocument = row => twoBooksApi.download(row.id);
async function saveDocument() {
if(saving.value || uploading.value) return;
try { await formRef.value.validate(); } catch { return; }
saving.value = true;
try {
const {docType,enterpriseName,riskType,status,generator,generateTime,title,content,version} = currentDocument;
const body = {docType,enterpriseName,riskType,status,generator,generateTime,title,content,version,
attachmentIds:currentDocument.attachments.map(f=>f.fileId)};
if(currentDocument.id) await twoBooksApi.update(currentDocument.id,body);
else await twoBooksApi.create(body);
message.success('文书已保存'); documentDialogVisible.value = false; await queryData();
} catch(e) { smartSentry.captureError(e); }
finally { saving.value = false; }
}
function removeDocument(row) {
Modal.confirm({ title:'删除文书',content:`确认删除“${row.title}”?删除后不再显示在台账中。`,
async onOk() { await twoBooksApi.remove(row.id,row.version); message.success('已删除'); await queryData(); },
});
}
onMounted(queryData);
</script>
<style lang="less" scoped>

251
economic-crime-web/src/views/business/review/general-list.vue

@ -1,251 +1,4 @@
<template>
<div class="general-list">
<!-- 查询区 -->
<a-form class="smart-query-form" v-privilege="'review:general:query'">
<a-row class="smart-query-form-row">
<a-form-item label="议题编号" class="smart-query-form-item">
<a-input style="width: 180px" v-model:value="queryForm.topicNo" placeholder="请输入议题编号" allowClear />
</a-form-item>
<a-form-item label="议题类型" class="smart-query-form-item">
<a-select v-model:value="queryForm.topicType" style="width: 140px" placeholder="请选择" allowClear>
<a-select-option value="case_review">案件审议</a-select-option>
<a-select-option value="fund_handle">资金处置</a-select-option>
<a-select-option value="property_disposal">财物处置</a-select-option>
<a-select-option value="policy_discuss">政策研讨</a-select-option>
<a-select-option value="other">其他</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="提交人" class="smart-query-form-item">
<a-input style="width: 100px" v-model:value="queryForm.submitter" placeholder="提交人" allowClear />
</a-form-item>
<a-form-item label="审议状态" class="smart-query-form-item">
<a-select v-model:value="queryForm.reviewStatus" style="width: 110px" placeholder="请选择" allowClear>
<a-select-option value="pending">待审议</a-select-option>
<a-select-option value="in_progress">审议中</a-select-option>
<a-select-option value="passed">已通过</a-select-option>
<a-select-option value="rejected">已驳回</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="时间范围" class="smart-query-form-item">
<a-range-picker v-model:value="dateRange" style="width: 220px" @change="onDateChange" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch"><template #icon><SearchOutlined /></template>查询</a-button>
<a-button @click="onReset"><template #icon><ReloadOutlined /></template>重置</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<!-- 表格 + 分页 -->
<a-card size="small" :bordered="false">
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button type="primary" v-privilege="'review:general:add'" @click="handleAdd">
<template #icon><PlusOutlined /></template>发起议题
</a-button>
</div>
</a-row>
<a-table
rowKey="topicId"
:columns="tableColumns"
:dataSource="tableData"
:scroll="{ x: 1300 }"
:pagination="false"
:loading="tableLoading"
size="small"
bordered
>
<template #bodyCell="{ column, record, text }">
<template v-if="column.dataIndex === 'topicName'">
<a @click="handleView(record)">{{ text }}</a>
</template>
<template v-else-if="column.dataIndex === 'topicType'">
<a-tag color="blue">{{ getTopicTypeName(text) }}</a-tag>
</template>
<template v-else-if="column.dataIndex === 'status'">
<a-tag :color="getStatusColor(text)">{{ getStatusName(text) }}</a-tag>
</template>
<template v-else-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button type="link" size="small" v-if="record.status === 'pending' || record.status === 'in_progress'" v-privilege="'review:general:approve'" @click="handleApprove(record)">审议</a-button>
<a-button type="link" size="small" v-privilege="'review:general:view'" @click="handleView(record)">查看</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger showQuickJumper show-less-items
:pageSizeOptions="['10','20','50']"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryTableData"
@showSizeChange="queryTableData"
:show-total="(total) => `共${total}条`"
/>
</div>
</a-card>
<!-- 审议弹窗 -->
<a-modal v-model:open="modalVisible" :title="currentRecord?.topicName" width="800px" destroyOnClose :footer="null">
<!-- 议题详情 -->
<a-descriptions :column="2" bordered size="small" style="margin-bottom:16px;">
<a-descriptions-item label="议题编号">{{ currentRecord?.topicNo }}</a-descriptions-item>
<a-descriptions-item label="议题类型"><a-tag color="blue">{{ getTopicTypeName(currentRecord?.topicType) }}</a-tag></a-descriptions-item>
<a-descriptions-item label="提交部门">{{ currentRecord?.department }}</a-descriptions-item>
<a-descriptions-item label="提交时间">{{ currentRecord?.submitTime }}</a-descriptions-item>
<a-descriptions-item label="当前环节">{{ currentRecord?.currentStep }}</a-descriptions-item>
<a-descriptions-item label="参与人数">{{ currentRecord?.participantCount }}人</a-descriptions-item>
<a-descriptions-item label="状态" :span="2"><a-tag :color="getStatusColor(currentRecord?.status)">{{ getStatusName(currentRecord?.status) }}</a-tag></a-descriptions-item>
<a-descriptions-item label="议题内容" :span="2">
{{ currentRecord?.description || '关于该案件的详细情况说明及需要审议的具体事项...' }}
</a-descriptions-item>
</a-descriptions>
<!-- 审批意见列表 -->
<h4 style="margin-bottom:12px;">审批意见记录</h4>
<a-timeline mode="left">
<a-timeline-item color="green" v-for="(opinion, idx) in mockOpinions" :key="idx">
<p><strong>{{ opinion.approver }} - {{ opinion.role }}</strong> <a-tag :color="opinion.result === '同意' ? 'green' : opinion.result === '反对' ? 'red' : 'orange'">{{ opinion.result }}</a-tag></p>
<p>{{ opinion.comment }}</p>
<p style="color:#999;font-size:12px;">{{ opinion.time }}</p>
</a-timeline-item>
</a-timeline>
<!-- 我的意见 -->
<a-divider />
<h4 style="margin-bottom:12px;">我的意见</h4>
<a-textarea v-model:value="myComment" :rows="3" placeholder="请输入您的审议意见..." showCount :maxlength="500" />
<a-radio-group v-model:value="myVote" style="margin-top:8px;">
<a-radio value="agree">同意</a-radio>
<a-radio value="oppose">反对</a-radio>
<a-radio value="abstain">弃权</a-radio>
</a-radio-group>
<div style="margin-top:16px;text-align:right;">
<a-button @click="modalVisible = false">取消</a-button>
<a-button type="primary" style="margin-left:8px;" @click="handleSubmitOpinion" :loading="submitting">提交意见</a-button>
</div>
</a-modal>
</div>
</template>
<template><WorkPage kind="topic" /></template>
<script setup>
import { reactive, ref, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { SearchOutlined, ReloadOutlined, PlusOutlined } from '@ant-design/icons-vue';
const queryFormState = {
topicNo: '',
topicType: undefined,
submitter: '',
reviewStatus: undefined,
timeBegin: null,
timeEnd: null,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive({ ...queryFormState });
const dateRange = ref([]);
const tableColumns = ref([
{ title: '议题编号', dataIndex: 'topicNo', width: 150 },
{ title: '议题名称', dataIndex: 'topicName', width: 200, ellipsis: true },
{ title: '议题类型', dataIndex: 'topicType', width: 100, align: 'center' },
{ title: '提交部门', dataIndex: 'department', width: 120 },
{ title: '提交时间', dataIndex: 'submitTime', width: 140 },
{ title: '当前环节', dataIndex: 'currentStep', width: 100 },
{ title: '参与人数', dataIndex: 'participantCount', width: 80, align: 'center' },
{ title: '状态', dataIndex: 'status', width: 90, align: 'center' },
{ title: '操作', dataIndex: 'action', fixed: 'right', width: 130, align: 'center' },
]);
const tableData = ref([
{ topicId: 1, topicNo: 'YI20240101001', topicName: '张某非法集资案涉案资金处置方案审议', topicType: 'fund_handle', department: '经侦一大队', submitTime: '2024-01-15 09:30:00', currentStep: '大队审议', participantCount: 7, status: 'in_progress', description: '本案涉及冻结资金2500万元,需审议处置方案' },
{ topicId: 2, topicNo: 'YI20240203002', topicName: '李某合同诈骗案房产处置申请审议', topicType: 'property_disposal', department: '经侦二大队', submitTime: '2024-02-08 14:20:00', currentStep: '局领导审批', participantCount: 5, status: 'pending', description: '涉案房产位于朝阳区望京SOHO,估值850万元' },
{ topicId: 3, topicNo: 'YI20240310003', topicName: '王某洗钱案跨省协作机制讨论', topicType: 'case_review', department: '经侦一大队', submitTime: '2024-03-12 10:15:00', currentStep: '已完成', participantCount: 9, status: 'passed', description: '涉及广东、浙江两省,需建立协作机制' },
{ topicId: 4, topicNo: 'YI20240420004', topicName: '网络诈骗案件侦办流程优化建议', topicType: 'policy_discuss', department: '法制科', submitTime: '2024-04-25 16:00:00', currentStep: '科室研讨', participantCount: 6, status: 'in_progress', description: '针对新型网络诈骗案件提出流程优化建议' },
{ topicId: 5, topicNo: 'YI20240505005', topicName: '孙某传销案返还财物分配方案', topicType: 'fund_handle', department: '经侦三大队', submitTime: '2024-05-10 11:45:00', currentStep: '财务审核', participantCount: 4, status: 'rejected', description: '涉及受害人156人,需制定公平分配方案' },
{ topicId: 6, topicNo: 'YI20240612006', topicName: '虚开增值税发票案件证据链完善', topicType: 'case_review', department: '经侦二大队', submitTime: '2024-06-15 08:30:00', currentStep: '法制审核', participantCount: 5, status: 'pending', description: '补充关键证据材料,完善证据链条' },
{ topicId: 7, topicNo: 'YI20240718007', topicName: '非法吸收公众存款案受害人登记工作安排', topicType: 'other', department: '综合办公室', submitTime: '2024-07-20 13:20:00', currentStep: '筹备中', participantCount: 8, status: 'in_progress', description: '受害人预计超过300人,需做好登记准备工作' },
{ topicId: 8, topicNo: 'YI20240825008', topicName: '集资诈骗案涉案文物鉴定委托事项', topicType: 'property_disposal', department: '经侦一大队', submitTime: '2024-08-28 09:00:00', currentStep: '待发起', participantCount: 3, status: 'pending', description: '齐白石《虾图》真迹,需委托专业机构鉴定' },
]);
const mockOpinions = [
{ approver: '张大队长', role: '大队负责人', result: '同意', comment: '处置方案合理,符合相关规定,同意执行。', time: '2024-01-16 14:30:00' },
{ approver: '李教导员', role: '教导员', result: '同意', comment: '资金流向清晰,处置程序合规。', time: '2024-01-17 10:15:00' },
{ approver: '王法制员', role: '法制员', result: '反对', comment: '部分证据材料仍需补充,建议暂缓处置。', time: '2024-01-18 09:45:00' },
];
const total = ref(8);
const tableLoading = ref(false);
const modalVisible = ref(false);
const currentRecord = ref(null);
const myComment = ref('');
const myVote = ref('agree');
const submitting = ref(false);
function getTopicTypeName(type) {
const map = { case_review: '案件审议', fund_handle: '资金处置', property_disposal: '财物处置', policy_discuss: '政策研讨', other: '其他' };
return map[type] || type;
}
function getStatusColor(status) {
const map = { pending: 'default', in_progress: 'processing', passed: 'success', rejected: 'error' };
return map[status] || 'default';
}
function getStatusName(status) {
const map = { pending: '待审议', in_progress: '审议中', passed: '已通过', rejected: '已驳回' };
return map[status] || status;
}
async function queryTableData() {
try {
tableLoading.value = true;
await new Promise(resolve => setTimeout(resolve, 400));
let filtered = [...tableData.value];
if (queryForm.topicNo) filtered = filtered.filter(item => item.topicNo.includes(queryForm.topicNo));
if (queryForm.topicType) filtered = filtered.filter(item => item.topicType === queryForm.topicType);
if (queryForm.submitter) filtered = filtered.filter(item => item.department.includes(queryForm.submitter));
if (queryForm.reviewStatus) filtered = filtered.filter(item => item.status === queryForm.reviewStatus);
total.value = filtered.length;
const start = (queryForm.pageNum - 1) * queryForm.pageSize;
tableData.value = [...filtered.slice(start, start + queryForm.pageSize)];
} catch (e) {
message.error('查询失败,请稍后重试');
} finally {
tableLoading.value = false;
}
}
function onSearch() { queryForm.pageNum = 1; queryTableData(); }
function onReset() { Object.assign(queryForm, queryFormState); dateRange.value = []; queryTableData(); }
function onDateChange(dates, dateStrings) { queryForm.timeBegin = dateStrings[0]; queryForm.timeEnd = dateStrings[1]; }
function handleAdd() { message.info('发起议题功能开发中'); }
function handleView(record) { currentRecord.value = record; modalVisible.value = true; }
function handleApprove(record) { currentRecord.value = record; myComment.value = ''; myVote.value = 'agree'; modalVisible.value = true; }
async function handleSubmitOpinion() {
if (!myComment.value.trim()) { message.warning('请输入审议意见'); return; }
submitting.value = true;
try {
await new Promise(resolve => setTimeout(resolve, 600));
message.success('意见提交成功');
modalVisible.value = false;
} catch (e) {
message.error('提交失败,请稍后重试');
} finally {
submitting.value = false;
}
}
onMounted(() => { queryTableData(); });
import WorkPage from '/@/components/business/work/work-page.vue';
</script>
<style lang="less" scoped>
</style>

314
economic-crime-web/src/views/business/review/group-config.vue

@ -1,314 +1,4 @@
<template>
<div class="group-config">
<!-- 查询区 -->
<a-form class="smart-query-form" v-privilege="'review:group:query'">
<a-row class="smart-query-form-row">
<a-form-item label="组名" class="smart-query-form-item">
<a-input style="width: 180px" v-model:value="queryForm.groupName" placeholder="请输入组名" allowClear />
</a-form-item>
<a-form-item label="组长" class="smart-query-form-item">
<a-input style="width: 100px" v-model:value="queryForm.leader" placeholder="组长姓名" allowClear />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch"><template #icon><SearchOutlined /></template>查询</a-button>
<a-button @click="onReset"><template #icon><ReloadOutlined /></template>重置</a-button>
</a-button-group>
<a-button type="primary" v-privilege="'review:group:add'" style="margin-left:12px;" @click="handleAdd">
<template #icon><PlusOutlined /></template>新建审核组
</a-button>
</a-form-item>
</a-row>
</a-form>
<!-- 表格 + 分页 -->
<a-card size="small" :bordered="false">
<a-table
rowKey="groupId"
:columns="tableColumns"
:dataSource="tableData"
:scroll="{ x: 1200 }"
:pagination="false"
:loading="tableLoading"
size="small"
bordered
>
<template #bodyCell="{ column, record, text }">
<template v-if="column.dataIndex === 'memberCount'">
<a-badge :count="text" :numberStyle="{ backgroundColor: '#1890ff' }" />
<span style="margin-left:4px;">人</span>
</template>
<template v-else-if="column.dataIndex === 'status'">
<a-switch
:checked="text === 'active'"
checkedChildren="启用"
unCheckedChildren="停用"
@change="(checked) => handleStatusChange(record, checked)"
v-privilege="'review:group:status'"
/>
</template>
<template v-else-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button type="link" size="small" v-privilege="'review:group:edit'" @click="handleEditMember(record)">编辑成员</a-button>
<a-popconfirm
:title="record.status === 'active' ? '确认停用该审核组?' : '确认启用该审核组?'"
@confirm="handleToggleStatus(record)"
v-privilege="'review:group:status'"
>
<a-button type="link" size="small" :danger="record.status === 'active'">{{ record.status === 'active' ? '停用' : '启用' }}</a-button>
</a-popconfirm>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger showQuickJumper show-less-items
:pageSizeOptions="['10','20','50']"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryTableData"
@showSizeChange="queryTableData"
:show-total="(total) => `共${total}条`"
/>
</div>
</a-card>
<!-- 编辑成员弹窗(模拟Transfer) -->
<a-modal v-model:open="memberModalVisible" title="编辑审核组成员" width="800px" @ok="submitMembers" confirmLoading="savingMembers" destroyOnClose>
<div class="transfer-container">
<div class="transfer-panel transfer-left">
<h4>可选人员 ({{ availableEmployees.length }})</h4>
<a-input-search placeholder="搜索人员..." style="margin-bottom:8px;" allowClear @search="onSearchEmployee" />
<div class="transfer-list">
<div
v-for="emp in filteredAvailable"
:key="emp.employeeId"
class="transfer-item"
:class="{ selected: selectedAvailable.includes(emp.employeeId) }"
@click="toggleSelectAvailable(emp.employeeId)"
>
<a-checkbox :checked="selectedAvailable.includes(emp.employeeId)" @click.stop>{{ emp.name }} - {{ emp.department }}</a-checkbox>
</div>
<a-empty v-if="filteredAvailable.length === 0" description="暂无数据" :image-style="{ height: '60px' }" />
</div>
</div>
<div class-transfer-buttons>
<a-button type="primary" :disabled="selectedAvailable.length === 0" @click="moveToRight" block style="margin-bottom:8px;">
<RightOutlined /> &gt;
</a-button>
<a-button :disabled="selectedSelected.length === 0" @click="moveToLeft" block>
&lt; <LeftOutlined />
</a-button>
</div>
<div class="transfer-panel transfer-right">
<h4>已选成员 ({{ currentGroupMembers.length }})</h4>
<a-input-search placeholder="搜索成员..." style="margin-bottom:8px;" allowClear />
<div class="transfer-list">
<div
v-for="emp in currentGroupMembers"
:key="emp.employeeId"
class="transfer-item"
:class="{ selected: selectedSelected.includes(emp.employeeId) }"
@click="toggleSelectSelected(emp.employeeId)"
>
<a-checkbox :checked="selectedSelected.includes(emp.employeeId)" @click.stop>{{ emp.name }} - {{ emp.department }}</a-checkbox>
</div>
<a-empty v-if="currentGroupMembers.length === 0" description="暂无已选成员" :image-style="{ height: '60px' }" />
</div>
</div>
</div>
</a-modal>
</div>
</template>
<template><WorkPage kind="group" /></template>
<script setup>
import { reactive, ref, onMounted, computed } from 'vue';
import { message } from 'ant-design-vue';
import { SearchOutlined, ReloadOutlined, PlusOutlined, RightOutlined, LeftOutlined } from '@ant-design/icons-vue';
const queryFormState = {
groupName: '',
leader: '',
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive({ ...queryFormState });
const tableColumns = ref([
{ title: '组ID', dataIndex: 'groupId', width: 70 },
{ title: '组名', dataIndex: 'groupName', width: 160 },
{ title: '组长', dataIndex: 'leader', width: 90 },
{ title: '成员数', dataIndex: 'memberCount', width: 90, align: 'center' },
{ title: '负责业务范围', dataIndex: 'businessScope', ellipsis: true },
{ title: '创建时间', dataIndex: 'createTime', width: 150 },
{ title: '状态', dataIndex: 'status', width: 90, align: 'center' },
{ title: '操作', dataIndex: 'action', fixed: 'right', width: 150, align: 'center' },
]);
const tableData = ref([
{ groupId: 1, groupName: '资金处置审核组', leader: '张大队长', memberCount: 7, businessScope: '资金冻结/扣押/返还审批', createTime: '2023-06-15 10:00:00', status: 'active' },
{ groupId: 2, groupName: '财物管理审核组', leader: '李教导员', memberCount: 5, businessScope: '涉案财物入库/出库/调用审批', createTime: '2023-07-20 14:30:00', status: 'active' },
{ groupId: 3, groupName: '案件审议专家组', leader: '王政委', memberCount: 9, businessScope: '重大案件审议/政策研讨', createTime: '2023-08-01 09:15:00', status: 'active' },
{ groupId: 4, groupName: '法制审核组', leader: '刘法制员', memberCount: 4, businessScope: '法律文书审核/证据链审查', createTime: '2023-09-10 16:00:00', status: 'active' },
{ groupId: 5, groupName: '财务复核组', leader: '赵财务', memberCount: 3, businessScope: '涉案资金核算/分配方案复核', createTime: '2023-10-05 11:20:00', status: 'inactive' },
{ groupId: 6, groupName: '文物鉴定审核组', leader: '孙专家', memberCount: 6, businessScope: '文物字画鉴定委托/估值审核', createTime: '2024-01-08 08:45:00', status: 'active' },
{ groupId: 7, groupName: '网络案件专项组', leader: '周技术员', memberCount: 5, businessScope: '网络诈骗/电子取证审核', createTime: '2024-03-15 13:30:00', status: 'active' },
{ groupId: 8, groupName: '受害人登记审核组', leader: '吴主任', memberCount: 8, businessScope: '受害人信息核实/登记审核', createTime: '2024-05-20 09:00:00', status: 'inactive' },
]);
// Transfer相关
const allEmployees = ref([
{ employeeId: 101, name: '张三', department: '经侦一大队' },
{ employeeId: 102, name: '李四', department: '经侦二大队' },
{ employeeId: 103, name: '王五', department: '经侦三大队' },
{ employeeId: 104, name: '赵六', department: '法制科' },
{ employeeId: 105, name: '孙七', department: '综合办公室' },
{ employeeId: 106, name: '周八', department: '经侦一大队' },
{ employeeId: 107, name: '吴九', department: '经侦二大队' },
{ employeeId: 108, name: '郑十', department: '财务科' },
{ employeeId: 109, name: '陈十一', department: '经侦三大队' },
{ employeeId: 110, name: '刘十二', department: '法制科' },
{ employeeId: 111, name: '杨十三', department: '综合办公室' },
{ employeeId: 112, name: '黄十四', department: '技术科' },
]);
const total = ref(8);
const tableLoading = ref(false);
const memberModalVisible = ref(false);
const savingMembers = ref(false);
const editingGroup = ref(null);
const selectedAvailable = ref([]);
const selectedSelected = ref([]);
const employeeSearchText = ref('');
const currentGroupMembers = computed(() => {
if (!editingGroup.value) return [];
const groupMemberIds = [101, 104, 106, 110]; // mock当前组成员ID
return allEmployees.value.filter(e => groupMemberIds.includes(e.employeeId));
});
const availableEmployees = computed(() => {
const memberIds = currentGroupMembers.value.map(e => e.employeeId);
return allEmployees.value.filter(e => !memberIds.includes(e.employeeId));
});
const filteredAvailable = computed(() => {
if (!employeeSearchText.value) return availableEmployees.value;
return availableEmployees.value.filter(e =>
e.name.includes(employeeSearchText.value) || e.department.includes(employeeSearchText.value)
);
});
function toggleSelectAvailable(id) {
const idx = selectedAvailable.value.indexOf(id);
if (idx >= 0) selectedAvailable.value.splice(idx, 1);
else selectedAvailable.value.push(id);
}
function toggleSelectSelected(id) {
const idx = selectedSelected.value.indexOf(id);
if (idx >= 0) selectedSelected.value.splice(idx, 1);
else selectedSelected.value.push(id);
}
function moveToRight() {
message.info(`已将 ${selectedAvailable.value.length} 人移入已选列表`);
selectedAvailable.value = [];
}
function moveToLeft() {
message.info(`已将 ${selectedSelected.value.length} 人移出已选列表`);
selectedSelected.value = [];
}
function onSearchEmployee(val) { employeeSearchText.value = val; }
async function queryTableData() {
try {
tableLoading.value = true;
await new Promise(resolve => setTimeout(resolve, 400));
let filtered = [...tableData.value];
if (queryForm.groupName) filtered = filtered.filter(item => item.groupName.includes(queryForm.groupName));
if (queryForm.leader) filtered = filtered.filter(item => item.leader.includes(queryForm.leader));
total.value = filtered.length;
const start = (queryForm.pageNum - 1) * queryForm.pageSize;
tableData.value = [...filtered.slice(start, start + queryForm.pageSize)];
} catch (e) {
message.error('查询失败,请稍后重试');
} finally {
tableLoading.value = false;
}
}
function onSearch() { queryForm.pageNum = 1; queryTableData(); }
function onReset() { Object.assign(queryForm, queryFormState); queryTableData(); }
function handleAdd() { message.info('新建审核组功能开发中'); }
function handleEditMember(record) {
editingGroup.value = record;
selectedAvailable.value = [];
selectedSelected.value = [];
employeeSearchText.value = '';
memberModalVisible.value = true;
}
async function submitMembers() {
savingMembers.value = true;
try {
await new Promise(resolve => setTimeout(resolve, 500));
message.success(`${editingGroup.value?.groupName} 成员更新成功`);
memberModalVisible.value = false;
} catch (e) {
message.error('保存失败,请稍后重试');
} finally {
savingMembers.value = false;
}
}
function handleStatusChange(record, checked) {
const action = checked ? '启用' : '停用';
message.info(`${action} ${record.groupName}`);
}
function handleToggleStatus(record) {
const newStatus = record.status === 'active' ? 'inactive' : 'active';
record.status = newStatus;
message.success(`已${newStatus === 'active' ? '启用' : '停用'} ${record.groupName}`);
}
onMounted(() => { queryTableData(); });
import WorkPage from '/@/components/business/work/work-page.vue';
</script>
<style lang="less" scoped>
.transfer-container {
display: flex;
gap: 12px;
min-height: 360px;
}
.transfer-panel {
flex: 1;
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 12px;
h4 { margin-bottom: 8px; font-size: 14px; color: #333; }
}
.transfer-list {
height: 300px;
overflow-y: auto;
border: 1px solid #f0f0f0;
border-radius: 4px;
padding: 4px;
}
.transfer-item {
padding: 6px 8px;
cursor: pointer;
border-radius: 4px;
transition: background 0.2s;
&:hover { background: #f5f5f5; }
&.selected { background: #e6f7ff; }
}
.transfer-buttons {
display: flex;
flex-direction: column;
justify-content: center;
width: 50px;
}
</style>

207
economic-crime-web/src/views/business/review/opinion-flow.vue

@ -1,207 +1,4 @@
<template>
<div class="opinion-flow">
<!-- 查询区 -->
<a-form class="smart-query-form" v-privilege="'review:opinion:query'">
<a-row class="smart-query-form-row">
<a-form-item label="流程编号" class="smart-query-form-item">
<a-input style="width: 180px" v-model:value="queryForm.flowNo" placeholder="请输入流程编号" allowClear />
</a-form-item>
<a-form-item label="关联业务" class="smart-query-form-item">
<a-select v-model:value="queryForm.relatedBusiness" style="width: 160px" placeholder="请选择" allowClear>
<a-select-option value="fund">资金管理</a-select-option>
<a-select-option value="property">财物管理</a-select-option>
<a-select-option value="case">案件审议</a-select-option>
<a-select-option value="supervision">监督督办</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="审批人" class="smart-query-form-item">
<a-input style="width: 100px" v-model:value="queryForm.approver" placeholder="审批人" allowClear />
</a-form-item>
<a-form-item label="审批状态" class="smart-query-form-item">
<a-select v-model:value="queryForm.approveStatus" style="width: 110px" placeholder="请选择" allowClear>
<a-select-option value="passed">已通过</a-select-option>
<a-select-option value="returned">已退回</a-select-option>
<a-select-option value="rejected">已驳回</a-select-option>
<a-select-option value="pending">待审批</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch"><template #icon><SearchOutlined /></template>查询</a-button>
<a-button @click="onReset"><template #icon><ReloadOutlined /></template>重置</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<!-- 表格 + 分页 -->
<a-card size="small" :bordered="false">
<a-table
rowKey="flowId"
:columns="tableColumns"
:dataSource="tableData"
:scroll="{ x: 1400 }"
:pagination="false"
:loading="tableLoading"
size="small"
bordered
>
<template #bodyCell="{ column, record, text }">
<template v-if="column.dataIndex === 'approveResult'">
<a-tag v-if="text === '通过'" color="success">{{ text }}</a-tag>
<a-tag v-else-if="text === '退回'" color="warning">{{ text }}</a-tag>
<a-tag v-else-if="text === '驳回'" color="error">{{ text }}</a-tag>
<span v-else style="color:#999;">{{ text }}</span>
</template>
<template v-else-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button type="link" size="small" v-privilege="'review:opinion:detail'" @click="handleFlowDetail(record)">查看流转</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger showQuickJumper show-less-items
:pageSizeOptions="['10','20','50']"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryTableData"
@showSizeChange="queryTableData"
:show-total="(total) => `共${total}条`"
/>
</div>
</a-card>
<!-- 流转详情弹窗 -->
<a-modal v-model:open="flowDetailVisible" title="流转详情" width="800px" :footer="null" destroyOnClose>
<!-- 步骤条 -->
<a-steps :current="currentStepIndex" status="process" style="margin-bottom:24px;">
<a-step v-for="(step, idx) in flowSteps" :key="idx" :title="step.title" :description="step.time" />
</a-steps>
<!-- 每步意见详情 -->
<a-timeline mode="left">
<a-timeline-item
v-for="(item, idx) in mockFlowDetails"
:key="idx"
:color="item.result === '通过' ? 'green' : item.result === '退回' ? 'orange' : item.result === '驳回' ? 'red' : 'blue'"
>
<a-card size="small" :bordered="true" style="margin-bottom:8px;">
<a-row>
<a-col :span="12"><strong>{{ item.stepName }}</strong></a-col>
<a-col :span="12" style="text-align:right;">
<a-tag :color="item.result === '通过' ? 'success' : item.result === '退回' ? 'warning' : item.result === '驳回' ? 'error' : 'processing'">{{ item.result }}</a-tag>
</a-col>
</a-row>
<p><strong>审批人:</strong>{{ item.approver }}({{ item.role }})</p>
<p><strong>审批时间:</strong>{{ item.approveTime }}</p>
<p><strong>审批意见:</strong>{{ item.comment }}</p>
</a-card>
</a-timeline-item>
</a-timeline>
</a-modal>
</div>
</template>
<template><WorkPage kind="opinion" /></template>
<script setup>
import { reactive, ref, onMounted, computed } from 'vue';
import { message } from 'ant-design-vue';
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue';
const queryFormState = {
flowNo: '',
relatedBusiness: undefined,
approver: '',
approveStatus: undefined,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive({ ...queryFormState });
const tableColumns = ref([
{ title: '流程编号', dataIndex: 'flowNo', width: 160 },
{ title: '关联业务', dataIndex: 'relatedBusiness', width: 100 },
{ title: '当前环节', dataIndex: 'currentStep', width: 120 },
{ title: '审批人', dataIndex: 'approver', width: 90 },
{ title: '提交时间', dataIndex: 'submitTime', width: 150 },
{ title: '审批时间', dataIndex: 'approveTime', width: 150 },
{ title: '审批结果', dataIndex: 'approveResult', width: 90, align: 'center' },
{ title: '审批意见', dataIndex: 'comment', ellipsis: true },
{ title: '操作', dataIndex: 'action', fixed: 'right', width: 110, align: 'center' },
]);
const tableData = ref([
{ flowId: 1, flowNo: 'LC20240101001', relatedBusiness: '资金管理', currentStep: '大队审核', approver: '张大队长', submitTime: '2024-01-15 09:30:00', approveTime: '2024-01-16 14:20:00', approveResult: '通过', comment: '资料齐全,同意处置方案。' },
{ flowId: 2, flowNo: 'LC20240203002', relatedBusiness: '财物管理', currentStep: '局领导审批', approver: '刘局长', submitTime: '2024-02-08 10:00:00', approveTime: '-', approveResult: '待审批', comment: '' },
{ flowId: 3, flowNo: 'LC20240310003', relatedBusiness: '案件审议', currentStep: '已完成', approver: '王政委', submitTime: '2024-03-12 11:15:00', approveTime: '2024-03-14 16:30:00', approveResult: '通过', comment: '审议程序合规,结论有效。' },
{ flowId: 4, flowNo: 'LC20240420004', relatedBusiness: '资金管理', currentStep: '法制审核', approver: '李法制员', submitTime: '2024-04-25 14:30:00', approveTime: '2024-04-27 09:45:00', approveResult: '退回', comment: '部分法律依据需补充,请修改后重新提交。' },
{ flowId: 5, flowNo: 'LC20240505005', relatedBusiness: '监督督办', currentStep: '财务复核', approver: '赵财务', submitTime: '2024-05-10 08:45:00', approveTime: '2024-05-13 11:20:00', approveResult: '驳回', comment: '金额计算有误,分配方案不符合规定,予以驳回。' },
{ flowId: 6, flowNo: 'LC20240612006', relatedBusiness: '案件审议', currentStep: '法制审核', approver: '李法制员', submitTime: '2024-06-15 09:00:00', approveTime: '-', approveResult: '待审批', comment: '' },
{ flowId: 7, flowNo: 'LC20240718007', relatedBusiness: '财物管理', currentStep: '评估鉴定', approver: '鉴定中心', submitTime: '2024-07-20 13:30:00', approveTime: '2024-07-25 10:15:00', approveResult: '通过', comment: '鉴定报告符合规范要求。' },
{ flowId: 8, flowNo: 'LC20240825008', relatedBusiness: '资金管理', currentStep: '提交申请', approver: '经办民警', submitTime: '2024-08-28 09:00:00', approveTime: '-', approveResult: '待审批', comment: '' },
]);
const flowSteps = [
{ title: '提交申请', time: '2024-01-15 09:30' },
{ title: '部门初审', time: '2024-01-15 16:00' },
{ title: '法制审核', time: '2024-01-16 10:30' },
{ title: '大队审批', time: '2024-01-16 14:20' },
{ title: '局领导审批', time: '2024-01-17 09:00' },
];
const mockFlowDetails = [
{ stepName: '提交申请', approver: '王警官', role: '经办民警', result: '通过', approveTime: '2024-01-15 09:30:00', comment: '提交张某非法集资案资金处置申请,附件齐全。' },
{ stepName: '部门初审', approver: '刘副大队长', role: '副大队长', result: '通过', approveTime: '2024-01-15 16:00:00', comment: '情况属实,材料完整,同意上报。' },
{ stepName: '法制审核', approver: '李法制员', role: '法制员', result: '通过', approveTime: '2024-01-16 10:30:00', comment: '法律依据充分,程序合规。' },
{ stepName: '大队审批', approver: '张大队长', role: '大队负责人', result: '通过', approveTime: '2024-01-16 14:20:00', comment: '同意处置方案,报局领导审批。' },
{ stepName: '局领导审批', approver: '刘局长', role: '局长', result: '审批中', approveTime: '-', comment: '等待审批中...' },
];
const total = ref(8);
const tableLoading = ref(false);
const flowDetailVisible = ref(false);
const currentRecord = ref(null);
const currentStepIndex = computed(() => {
const statusMap = { '待审批': 0, '通过': 4, '退回': 2, '驳回': 1 };
return statusMap[currentRecord.value?.approveResult] ?? 2;
});
async function queryTableData() {
try {
tableLoading.value = true;
await new Promise(resolve => setTimeout(resolve, 400));
let filtered = [...tableData.value];
if (queryForm.flowNo) filtered = filtered.filter(item => item.flowNo.includes(queryForm.flowNo));
if (queryForm.relatedBusiness) filtered = filtered.filter(item => item.relatedBusiness === queryForm.relatedBusiness);
if (queryForm.approver) filtered = filtered.filter(item => item.approver.includes(queryForm.approver));
if (queryForm.approveStatus) {
const nameMap = { passed: '通过', returned: '退回', rejected: '驳回', pending: '待审批' };
filtered = filtered.filter(item => item.approveResult === nameMap[queryForm.approveStatus]);
}
total.value = filtered.length;
const start = (queryForm.pageNum - 1) * queryForm.pageSize;
tableData.value = [...filtered.slice(start, start + queryForm.pageSize)];
} catch (e) {
message.error('查询失败,请稍后重试');
} finally {
tableLoading.value = false;
}
}
function onSearch() { queryForm.pageNum = 1; queryTableData(); }
function onReset() { Object.assign(queryForm, queryFormState); queryTableData(); }
function handleFlowDetail(record) {
currentRecord.value = record;
flowDetailVisible.value = true;
}
onMounted(() => { queryTableData(); });
import WorkPage from '/@/components/business/work/work-page.vue';
</script>
<style lang="less" scoped>
</style>

350
economic-crime-web/src/views/business/review/process-list.vue

@ -1,350 +1,4 @@
<template>
<div class="process-list">
<!-- 查询区 -->
<a-form class="smart-query-form" v-privilege="'review:process:query'">
<a-row class="smart-query-form-row">
<a-form-item label="流程名称" class="smart-query-form-item">
<a-input style="width: 200px" v-model:value="queryForm.processName" placeholder="请输入流程名称" allowClear />
</a-form-item>
<a-form-item label="流程类型" class="smart-query-form-item">
<a-select v-model:value="queryForm.processType" style="width: 140px" placeholder="请选择" allowClear>
<a-select-option value="fund">资金处置流程</a-select-option>
<a-select-option value="property">财物管理流程</a-select-option>
<a-select-option value="review">案件审议流程</a-select-option>
<a-select-option value="supervision">督办管理流程</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态" class="smart-query-form-item">
<a-select v-model:value="queryForm.status" style="width: 100px" placeholder="请选择" allowClear>
<a-select-option value="active">已启用</a-select-option>
<a-select-option value="inactive">已停用</a-select-option>
<a-select-option value="draft">草稿</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch"><template #icon><SearchOutlined /></template>查询</a-button>
<a-button @click="onReset"><template #icon><ReloadOutlined /></template>重置</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<!-- 表格 + 分页 -->
<a-card size="small" :bordered="false">
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button type="primary" v-privilege="'review:process:add'" @click="handleAdd">
<template #icon><PlusOutlined /></template>新建流程
</a-button>
</div>
</a-row>
<a-table
rowKey="processId"
:columns="tableColumns"
:dataSource="tableData"
:scroll="{ x: 1300 }"
:pagination="false"
:loading="tableLoading"
size="small"
bordered
>
<template #bodyCell="{ column, record, text }">
<template v-if="column.dataIndex === 'status'">
<a-tag :color="text === '已启用' ? 'success' : text === '已停用' ? 'default' : 'warning'">{{ text }}</a-tag>
</template>
<template v-else-if="column.dataIndex === 'activeInstances'">
<a-badge :count="text" :numberStyle="{ backgroundColor: '#1890ff' }" />
</template>
<template v-else-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button type="link" size="small" v-privilege="'review:process:view'" @click="handleViewDesign(record)">查看设计</a-button>
<a-button type="link" size="small" v-privilege="'review:process:toggle'" @click="handleToggle(record)">{{ record.status === '已启用' ? '停用' : '启用' }}</a-button>
<a-button type="link" size="small" v-privilege="'review:process:version'" @click="handleVersion(record)">版本管理</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger showQuickJumper show-less-items
:pageSizeOptions="['10','20','50']"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryTableData"
@showSizeChange="queryTableData"
:show-total="(total) => `共${total}条`"
/>
</div>
</a-card>
<!-- 设计查看弹窗 -->
<a-modal v-model:open="designVisible" title="流程设计查看" width="900px" :footer="null" destroyOnClose>
<!-- 流程图(CSS模拟节点和连线) -->
<h4 style="margin-bottom:12px;">流程图</h4>
<div class="flow-diagram">
<div class="flow-node node-start">
<div class="node-circle start">开始</div>
</div>
<div class="flow-line"></div>
<div class="flow-node node-task">
<div class="node-rect"><UserOutlined /> 提交申请</div>
</div>
<div class="flow-line"></div>
<div class="flow-node node-gateway">
<div class="node-diamond">&lt;&gt;</div>
</div>
<div class="flow-line"></div>
<div class="flow-node node-task">
<div class="node-rect"><AuditOutlined /> 部门审核</div>
</div>
<div class="flow-line"></div>
<div class="flow-node node-gateway">
<div class="node-diamond">&lt;&gt;</div>
</div>
<div class="flow-line"></div>
<div class="flow-node node-task">
<div class="node-rect"><SafetyCertificateOutlined /> 法制审核</div>
</div>
<div class="flow-line"></div>
<div class="flow-node node-end">
<div class="node-circle end">结束</div>
</div>
</div>
<!-- 节点配置表 -->
<h4 style="margin:20px 0 12px;">节点配置</h4>
<a-table
:columns="nodeConfigColumns"
:dataSource="mockNodeConfigs"
:pagination="false"
size="small"
bordered
rowKey="nodeId"
>
<template #bodyCell="{ column, text }">
<template v-if="column.dataIndex === 'nodeType'">
<a-tag :color="getNodeTypeColor(text)">{{ getNodeTypeName(text) }}</a-tag>
</template>
<template v-if="column.dataIndex === 'handler'">
<span>{{ text || '-' }}</span>
</template>
</template>
</a-table>
</a-modal>
<!-- 版本管理弹窗 -->
<a-modal v-model:open="versionVisible" title="版本管理" width="700px" :footer="null" destroyOnClose>
<a-table
:columns="versionColumns"
:dataSource="mockVersions"
:pagination="false"
size="small"
bordered
rowKey="versionId"
>
<template #bodyCell="{ column, record, text }">
<template v-if="column.dataIndex === 'isCurrent'">
<a-tag v-if="text" color="success">当前版本</a-tag>
</template>
<template v-if="column.dataIndex === 'action'">
<a-button type="link" size="small" @click="handleRollback(record)" :disabled="record.isCurrent">回滚到此版本</a-button>
</template>
</template>
</a-table>
</a-modal>
</div>
</template>
<template><WorkPage kind="process" /></template>
<script setup>
import { reactive, ref, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { SearchOutlined, ReloadOutlined, PlusOutlined, UserOutlined, AuditOutlined, SafetyCertificateOutlined } from '@ant-design/icons-vue';
const queryFormState = {
processName: '',
processType: undefined,
status: undefined,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive({ ...queryFormState });
const tableColumns = ref([
{ title: '流程ID', dataIndex: 'processId', width: 70 },
{ title: '流程名称', dataIndex: 'processName', width: 180 },
{ title: '流程类型', dataIndex: 'processType', width: 130 },
{ title: '版本号', dataIndex: 'version', width: 80, align: 'center' },
{ title: '当前活跃实例数', dataIndex: 'activeInstances', width: 130, align: 'center' },
{ title: '创建人', dataIndex: 'creator', width: 90 },
{ title: '创建时间', dataIndex: 'createTime', width: 150 },
{ title: '状态', dataIndex: 'status', width: 90, align: 'center' },
{ title: '操作', dataIndex: 'action', fixed: 'right', width: 220, align: 'center' },
]);
const nodeConfigColumns = [
{ title: '节点ID', dataIndex: 'nodeId', width: 80 },
{ title: '节点名称', dataIndex: 'nodeName', width: 120 },
{ title: '节点类型', dataIndex: 'nodeType', width: 100, align: 'center' },
{ title: '处理人/角色', dataIndex: 'handler', width: 140 },
{ title: '超时设置(小时)', dataIndex: 'timeout', width: 120, align: 'center' },
{ title: '说明', dataIndex: 'description', ellipsis: true },
];
const versionColumns = [
{ title: '版本号', dataIndex: 'versionNo', width: 80, align: 'center' },
{ title: '发布人', dataIndex: 'publisher', width: 90 },
{ title: '发布时间', dataIndex: 'publishTime', width: 150 },
{ title: '变更说明', dataIndex: 'changeLog', ellipsis: true },
{ title: '状态', dataIndex: 'isCurrent', width: 90, align: 'center' },
{ title: '操作', dataIndex: 'action', width: 120, align: 'center' },
];
const tableData = ref([
{ processId: 1, processName: '资金冻结审批流程', processType: 'fund', version: 'v2.3', activeInstances: 15, creator: '张大队长', createTime: '2023-08-15 09:00:00', status: '已启用' },
{ processId: 2, processName: '涉案财物处置流程', processType: 'property', version: 'v1.8', activeInstances: 8, creator: '李教导员', createTime: '2023-09-20 14:30:00', status: '已启用' },
{ processId: 3, processName: '案件审议流程', processType: 'review', version: 'v3.1', activeInstances: 22, creator: '王政委', createTime: '2023-10-01 10:15:00', status: '已启用' },
{ processId: 4, processName: '督办事项管理流程', processType: 'supervision', version: 'v1.2', activeInstances: 5, creator: '陈副局长', createTime: '2023-11-05 16:00:00', status: '已启用' },
{ processId: 5, processName: '资金返还审批流程', processType: 'fund', version: 'v1.5', activeInstances: 3, creator: '张大队长', createTime: '2024-01-10 11:20:00', status: '已停用' },
{ processId: 6, processName: '文物鉴定委托流程', processType: 'property', version: 'v1.0', activeInstances: 0, creator: '孙专家', createTime: '2024-03-18 08:45:00', status: '草稿' },
{ processId: 7, processName: '受害人登记审核流程', processType: 'review', version: 'v1.1', activeInstances: 6, creator: '吴主任', createTime: '2024-05-22 13:30:00', status: '已启用' },
{ processId: 8, processName: '跨省协作审批流程', processType: 'review', version: 'v1.0', activeInstances: 2, creator: '刘法制员', createTime: '2024-06-28 09:00:00', status: '已启用' },
]);
const mockNodeConfigs = [
{ nodeId: 'N001', nodeName: '开始节点', nodeType: 'start', handler: '-', timeout: 0, description: '流程起始点' },
{ nodeId: 'N002', nodeName: '提交申请', nodeType: 'task', handler: '经办民警', timeout: 24, description: '提交资金处置申请及附件材料' },
{ nodeId: 'N003', nodeName: '部门初审', nodeType: 'task', handler: '副大队长', timeout: 48, description: '部门负责人初审意见' },
{ nodeId: 'N004', nodeName: '条件判断', nodeType: 'gateway', handler: '-', timeout: 0, description: '根据金额大小分流审批路径' },
{ nodeId: 'N005', nodeName: '法制审核', nodeType: 'task', handler: '法制员', timeout: 72, description: '法律合规性审查' },
{ nodeId: 'N006', nodeName: '领导审批', nodeType: 'task', handler: '局领导', timeout: 96, description: '最终审批决定' },
{ nodeId: 'N007', nodeName: '结束节点', nodeType: 'end', handler: '-', timeout: 0, description: '流程终止' },
];
const mockVersions = [
{ versionId: 1, versionNo: 'v2.3', publisher: '张大队长', publishTime: '2024-02-15 14:30:00', changeLog: '增加财务复核环节,优化超时提醒机制', isCurrent: true },
{ versionId: 2, versionNo: 'v2.2', publisher: '李教导员', publishTime: '2024-01-10 10:00:00', changeLog: '调整审批权限,增加退回重审功能', isCurrent: false },
{ versionId: 3, versionNo: 'v2.1', publisher: '王政委', publishTime: '2023-12-05 16:20:00', changeLog: '初始版本,包含基本审批链路', isCurrent: false },
];
const total = ref(8);
const tableLoading = ref(false);
const designVisible = ref(false);
const versionVisible = ref(false);
const currentRecord = ref(null);
function getNodeTypeColor(type) {
const map = { start: 'green', end: 'red', task: 'blue', gateway: 'orange' };
return map[type] || 'default';
}
function getNodeTypeName(type) {
const map = { start: '开始', end: '结束', task: '任务', gateway: '网关' };
return map[type] || type;
}
async function queryTableData() {
try {
tableLoading.value = true;
await new Promise(resolve => setTimeout(resolve, 400));
let filtered = [...tableData.value];
if (queryForm.processName) filtered = filtered.filter(item => item.processName.includes(queryForm.processName));
if (queryForm.processType) filtered = filtered.filter(item => item.processType === queryForm.processType);
if (queryForm.status) filtered = filtered.filter(item => item.status === queryForm.status);
total.value = filtered.length;
const start = (queryForm.pageNum - 1) * queryForm.pageSize;
tableData.value = [...filtered.slice(start, start + queryForm.pageSize)];
} catch (e) {
message.error('查询失败,请稍后重试');
} finally {
tableLoading.value = false;
}
}
function onSearch() { queryForm.pageNum = 1; queryTableData(); }
function onReset() { Object.assign(queryForm, queryFormState); queryTableData(); }
function handleAdd() { message.info('新建流程功能开发中'); }
function handleViewDesign(record) { currentRecord.value = record; designVisible.value = true; }
function handleToggle(record) {
const newStatus = record.status === '已启用' ? '已停用' : '已启用';
record.status = newStatus;
message.success(`${record.processName} 已${newStatus}`);
}
function handleVersion(record) { currentRecord.value = record; versionVisible.value = true; }
function handleRollback(version) { message.info(`正在回滚到 ${version.versionNo}...`); }
onMounted(() => { queryTableData(); });
import WorkPage from '/@/components/business/work/work-page.vue';
</script>
<style lang="less" scoped>
.flow-diagram {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 32px 16px;
background: #fafafa;
border-radius: 8px;
overflow-x: auto;
gap: 0;
}
.flow-node {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
}
.node-circle {
width: 56px; height: 56px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: bold;
&.start { background: #f6ffed; border: 2px solid #52c41a; color: #52c41a; }
&.end { background: #fff1f0; border: 2px solid #f5222d; color: #f5222d; }
}
.node-rect {
padding: 10px 16px;
background: #e6f7ff;
border: 2px solid #1890ff;
border-radius: 6px;
white-space: nowrap;
font-size: 13px;
color: #1890ff;
display: flex;
align-items: center;
gap: 4px;
}
.node-diamond {
width: 40px; height: 40px;
background: #fff7e6;
border: 2px solid #fa8c16;
transform: rotate(45deg);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #fa8c16;
font-weight: bold;
}
.flow-line {
width: 50px;
height: 2px;
background: linear-gradient(90deg, #1890ff, #52c41a);
position: relative;
flex-shrink: 0;
&::after {
content: '';
position: absolute;
right: -1px;
top: -4px;
border: 5px solid transparent;
border-left-color: #52c41a;
}
}
</style>

236
economic-crime-web/src/views/business/review/supervision.vue

@ -1,236 +1,4 @@
<template>
<div class="supervision">
<!-- 查询区 -->
<a-form class="smart-query-form" v-privilege="'review:supervision:query'">
<a-row class="smart-query-form-row">
<a-form-item label="督办编号" class="smart-query-form-item">
<a-input style="width: 180px" v-model:value="queryForm.superviseNo" placeholder="请输入督办编号" allowClear />
</a-form-item>
<a-form-item label="督办事项" class="smart-query-form-item">
<a-input style="width: 200px" v-model:value="queryForm.matter" placeholder="请输入督办事项" allowClear />
</a-form-item>
<a-form-item label="责任单位" class="smart-query-form-item">
<a-select v-model:value="queryForm.responsibleUnit" style="width: 140px" placeholder="请选择" allowClear>
<a-select-option value="team1">经侦一大队</a-select-option>
<a-select-option value="team2">经侦二大队</a-select-option>
<a-select-option value="team3">经侦三大队</a-select-option>
<a-select-option value="legal">法制科</a-select-option>
<a-select-option value="office">综合办公室</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="紧急程度" class="smart-query-form-item">
<a-select v-model:value="queryForm.urgencyLevel" style="width: 100px" placeholder="请选择" allowClear>
<a-select-option value="high">特急</a-select-option>
<a-select-option value="urgent">紧急</a-select-option>
<a-select-option value="normal">一般</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态" class="smart-query-form-item">
<a-select v-model:value="queryForm.status" style="width: 100px" placeholder="请选择" allowClear>
<a-select-option value="processing">进行中</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="overdue">已逾期</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch"><template #icon><SearchOutlined /></template>查询</a-button>
<a-button @click="onReset"><template #icon><ReloadOutlined /></template>重置</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<!-- 表格 + 分页 -->
<a-card size="small" :bordered="false">
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button type="primary" v-privilege="'review:supervision:add'" @click="handleAdd">
<template #icon><PlusOutlined /></template>新建督办
</a-button>
</div>
</a-row>
<a-table
rowKey="superviseId"
:columns="tableColumns"
:dataSource="tableData"
:scroll="{ x: 1500 }"
:pagination="false"
:loading="tableLoading"
size="small"
bordered
>
<template #bodyCell="{ column, record, text }">
<template v-if="column.dataIndex === 'urgencyLevel'">
<a-tag :color="getUrgencyColor(text)">{{ getUrgencyName(text) }}</a-tag>
</template>
<template v-else-if="column.dataIndex === 'progress'">
<a-progress :percent="Number(text)" size="small" :status="Number(text) >= 100 ? 'success' : Number(text) > 80 ? 'active' : 'normal'" />
</template>
<template v-else-if="column.dataIndex === 'status'">
<a-tag :color="text === '已完成' ? 'success' : text === '进行中' ? 'processing' : 'error'">{{ text }}</a-tag>
</template>
<template v-else-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button type="link" size="small" v-if="record.status !== '已完成'" v-privilege="'review:supervision:urge'" @click="handleUrge(record)">催办</a-button>
<a-button type="link" size="small" v-if="record.status !== '已完成'" v-privilege="'review:supervision:close'" @click="handleClose(record)">结案</a-button>
</div>
</template>
</template>
</a-table>
<div class="smart-query-table-page">
<a-pagination
showSizeChanger showQuickJumper show-less-items
:pageSizeOptions="['10','20','50']"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryTableData"
@showSizeChange="queryTableData"
:show-total="(total) => `共${total}条`"
/>
</div>
</a-card>
<!-- 催办弹窗 -->
<a-modal v-model:open="urgeVisible" title="催办通知" width="520px" @ok="submitUrge" confirmLoading="urging">
<a-form layout="vertical">
<a-form-item label="催办原因" required>
<a-select v-model:value="urgeForm.reason" placeholder="请选择催办原因">
<a-select-option value="timeout">即将超期</a-select-option>
<a-select-option value="overdue">已经超期</a-select-option>
<a-select-option value="important">上级要求</a-select-option>
<a-select-option value="other">其他原因</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="催办备注">
<a-textarea v-model:value="urgeForm.remark" :rows="3" placeholder="请输入催办备注信息..." showCount :maxlength="200" />
</a-form-item>
<a-form-item label="发送通知">
<a-checkbox v-model:checked="urgeForm.sendSms">短信通知责任人</a-checkbox>
<a-checkbox v-model:checked="urgeForm.sendEmail">邮件通知</a-checkbox>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<template><WorkPage kind="review-supervision" /></template>
<script setup>
import { reactive, ref, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { SearchOutlined, ReloadOutlined, PlusOutlined } from '@ant-design/icons-vue';
const queryFormState = {
superviseNo: '',
matter: '',
responsibleUnit: undefined,
urgencyLevel: undefined,
status: undefined,
pageNum: 1,
pageSize: 10,
};
const queryForm = reactive({ ...queryFormState });
const tableColumns = ref([
{ title: '督办编号', dataIndex: 'superviseNo', width: 150 },
{ title: '督办事项', dataIndex: 'matter', width: 220, ellipsis: true },
{ title: '责任单位', dataIndex: 'responsibleUnit', width: 110 },
{ title: '发起人', dataIndex: 'initiator', width: 80 },
{ title: '要求完成时限', dataIndex: 'deadline', width: 130 },
{ title: '实际完成时限', dataIndex: 'actualTime', width: 130 },
{ title: '紧急程度', dataIndex: 'urgencyLevel', width: 90, align: 'center' },
{ title: '进度', dataIndex: 'progress', width: 140, align: 'center' },
{ title: '状态', dataIndex: 'status', width: 90, align: 'center' },
{ title: '操作', dataIndex: 'action', fixed: 'right', width: 120, align: 'center' },
]);
const tableData = ref([
{ superviseId: 1, superviseNo: 'DB20240101001', matter: '张某非法集资案资金处置方案需在1月20日前完成', responsibleUnit: '经侦一大队', initiator: '刘局长', deadline: '2024-01-20', actualTime: '-', urgencyLevel: 'high', progress: 85, status: '进行中' },
{ superviseId: 2, superviseNo: 'DB20240203002', matter: '李某合同诈骗案房产评估报告提交', responsibleUnit: '经侦二大队', initiator: '陈副局长', deadline: '2024-02-15', actualTime: '2024-02-14', urgencyLevel: 'urgent', progress: 100, status: '已完成' },
{ superviseId: 3, superviseNo: 'DB20240310003', matter: '王某洗钱案跨省协作函件回复', responsibleUnit: '经侦一大队', initiator: '刘局长', deadline: '2024-03-25', actualTime: '-', urgencyLevel: 'high', progress: 60, status: '进行中' },
{ superviseId: 4, superviseNo: 'DB20240420004', matter: '网络诈骗案件专项统计报表报送', responsibleUnit: '综合办公室', initiator: '陈副局长', deadline: '2024-04-10', actualTime: '2024-04-18', urgencyLevel: 'normal', progress: 100, status: '已逾期' },
{ superviseId: 5, superviseNo: 'DB20240505005', matter: '孙某传销案受害人登记工作启动', responsibleUnit: '经侦三大队', initiator: '王政委', deadline: '2024-05-25', actualTime: '-', urgencyLevel: 'urgent', progress: 35, status: '进行中' },
{ superviseId: 6, superviseNo: 'DB20240612006', matter: '虚开增值税发票案补充侦查材料整理', responsibleUnit: '经侦二大队', initiator: '刘局长', deadline: '2024-07-01', actualTime: '-', urgencyLevel: 'high', progress: 72, status: '进行中' },
{ superviseId: 7, superviseNo: 'DB20240718007', matter: '非法吸收公众存款案受害人信息核对', responsibleUnit: '经侦一大队', initiator: '陈副局长', deadline: '2024-08-15', actualTime: '-', urgencyLevel: 'urgent', progress: 45, status: '进行中' },
{ superviseId: 8, superviseNo: 'DB20240825008', matter: '涉案文物鉴定委托合同签订', responsibleUnit: '法制科', initiator: '王政委', deadline: '2024-09-10', actualTime: '-', urgencyLevel: 'normal', progress: 15, status: '进行中' },
]);
const total = ref(8);
const tableLoading = ref(false);
const urgeVisible = ref(false);
const urging = ref(false);
const urgeRecord = ref(null);
const urgeForm = reactive({
reason: undefined,
remark: '',
sendSms: true,
sendEmail: false,
});
function getUrgencyColor(level) {
const map = { high: 'red', urgent: 'orange', normal: 'gold' };
return map[level] || 'default';
}
function getUrgencyName(level) {
const map = { high: '特急', urgent: '紧急', normal: '一般' };
return map[level] || level;
}
async function queryTableData() {
try {
tableLoading.value = true;
await new Promise(resolve => setTimeout(resolve, 400));
let filtered = [...tableData.value];
if (queryForm.superviseNo) filtered = filtered.filter(item => item.superviseNo.includes(queryForm.superviseNo));
if (queryForm.matter) filtered = filtered.filter(item => item.matter.includes(queryForm.matter));
if (queryForm.responsibleUnit) filtered = filtered.filter(item => item.responsibleUnit === queryForm.responsibleUnit);
if (queryForm.urgencyLevel) filtered = filtered.filter(item => item.urgencyLevel === queryForm.urgencyLevel);
if (queryForm.status) filtered = filtered.filter(item => item.status === queryForm.status);
total.value = filtered.length;
const start = (queryForm.pageNum - 1) * queryForm.pageSize;
tableData.value = [...filtered.slice(start, start + queryForm.pageSize)];
} catch (e) {
message.error('查询失败,请稍后重试');
} finally {
tableLoading.value = false;
}
}
function onSearch() { queryForm.pageNum = 1; queryTableData(); }
function onReset() { Object.assign(queryForm, queryFormState); queryTableData(); }
function handleAdd() { message.info('新建督办功能开发中'); }
function handleUrge(record) {
urgeRecord.value = record;
urgeForm.reason = undefined;
urgeForm.remark = '';
urgeForm.sendSms = true;
urgeForm.sendEmail = false;
urgeVisible.value = true;
}
async function submitUrge() {
if (!urgeForm.reason) { message.warning('请选择催办原因'); return; }
urging.value = true;
try {
await new Promise(resolve => setTimeout(resolve, 500));
message.success(`催办通知已发送至 ${urgeRecord.value.responsibleUnit}`);
urgeVisible.value = false;
} catch (e) {
message.error('发送失败,请稍后重试');
} finally {
urging.value = false;
}
}
function handleClose(record) {
message.warning(`正在办理 ${record.superviseNo} 的结案手续...`);
}
onMounted(() => { queryTableData(); });
import WorkPage from '/@/components/business/work/work-page.vue';
</script>
<style lang="less" scoped>
</style>

1
economic-crime-web/src/views/portal/module-portal.vue

@ -152,6 +152,7 @@ function handleLogout() {
onMounted(() => {
portalStore.clearActiveModule();
userStore.resetBusinessNavigation();
updateTime();
timer = setInterval(updateTime, 1000);
});

Loading…
Cancel
Save