diff --git a/backend/src/main/java/com/writeoff/module/audit/service/AuditService.java b/backend/src/main/java/com/writeoff/module/audit/service/AuditService.java index 59d5b39..f96fb8a 100644 --- a/backend/src/main/java/com/writeoff/module/audit/service/AuditService.java +++ b/backend/src/main/java/com/writeoff/module/audit/service/AuditService.java @@ -524,7 +524,7 @@ public class AuditService { if (task.getNode() == AuditNode.INIT_REVIEW) { // 初审拒绝:整体审核结束,会议审核状态置为已拒绝 - meetingService.updateAuditStatus(meetingId, MeetingAuditStatus.REJECTED); + meetingService.updateAuditStatus(meetingId, MeetingAuditStatus.REJECTED, request.getOpinion()); triggerAuditNotification(task, request.getOpinion(), "AUDIT_REJECTED"); } else { // 复审 / 终审拒绝:整体回到初审阶段,重新创建初审任务 @@ -539,7 +539,7 @@ public class AuditService { assigneeUserId = auditFlowConfigService.resolveAssigneeUserId(tenantId, firstNode); } - meetingService.updateAuditStatus(meetingId, MeetingAuditStatus.IN_REVIEW); + meetingService.updateAuditStatus(meetingId, MeetingAuditStatus.IN_REVIEW, request.getOpinion()); meetingService.updateCurrentAuditNode(meetingId, firstNode.name(), assigneeUserId); AuditTask resetTask = new AuditTask( null, @@ -566,7 +566,7 @@ public class AuditService { task.setStatus(AuditTaskStatus.REJECTED); task.setOpinion("退回修改:" + request.getOpinion()); auditTaskRepository.save(task); - meetingService.updateAuditStatus(task.getMeetingId(), MeetingAuditStatus.PENDING); + meetingService.updateAuditStatus(task.getMeetingId(), MeetingAuditStatus.PENDING, "退回修改:" + request.getOpinion()); logAuditTaskAction(task, "AUDIT_TASK_RETURNED", request.getOpinion()); triggerAuditNotification(task, request.getOpinion(), "AUDIT_RETURNED"); Map result = new LinkedHashMap<>(); diff --git a/backend/src/main/java/com/writeoff/module/auth/controller/AuthController.java b/backend/src/main/java/com/writeoff/module/auth/controller/AuthController.java index 6365432..31be073 100644 --- a/backend/src/main/java/com/writeoff/module/auth/controller/AuthController.java +++ b/backend/src/main/java/com/writeoff/module/auth/controller/AuthController.java @@ -120,7 +120,7 @@ public class AuthController { .body(ApiErrorResponse.of(11005, "账号已被锁定,请" + (remaining / 60 + 1) + "分钟后重试", errors)); } List> rows = jdbcTemplate.queryForList( - "SELECT u.id, u.tenant_id, u.user_name, u.phone, u.status, u.password_hash, u.valid_from, u.valid_to, " + + "SELECT u.id, u.tenant_id, u.user_name, u.phone, u.email, u.status, u.password_hash, u.valid_from, u.valid_to, " + "t.tenant_code, t.tenant_name, t.status AS tenant_status " + "FROM sys_user u JOIN tenant t ON u.tenant_id=t.id " + "WHERE u.phone=? AND t.tenant_code=? AND u.is_deleted=0 AND t.is_deleted=0 LIMIT 1", @@ -169,7 +169,7 @@ public class AuthController { String refreshToken = String.valueOf(issueResult.get("refreshToken")); String token = jwtTokenService.createTenantToken(userId, tenantId, request.getPhone(), sessionId); setRefreshCookie(httpResponse, refreshToken, false); - Map data = buildTenantAuthData(userId, tenantId, String.valueOf(row.get("user_name")), request.getPhone(), token); + Map data = buildTenantAuthData(userId, tenantId, String.valueOf(row.get("user_name")), request.getPhone(), String.valueOf(row.get("email")), token); return ResponseEntity.ok(ApiResponse.success(data)); } @@ -194,7 +194,7 @@ public class AuthController { .body(ApiErrorResponse.of(11005, "账号已被锁定,请" + (remaining / 60 + 1) + "分钟后重试", errors)); } List> rows = jdbcTemplate.queryForList( - "SELECT id, user_name, phone, status, password_hash, valid_from, valid_to " + + "SELECT id, user_name, phone, email, status, password_hash, valid_from, valid_to " + "FROM platform_user WHERE phone=? AND is_deleted=0 LIMIT 1", request.getPhone() ); @@ -235,7 +235,7 @@ public class AuthController { String refreshToken = String.valueOf(issueResult.get("refreshToken")); String token = jwtTokenService.createPlatformToken(userId, request.getPhone(), sessionId); setRefreshCookie(httpResponse, refreshToken, false); - Map data = buildPlatformAuthData(userId, String.valueOf(row.get("user_name")), request.getPhone(), token); + Map data = buildPlatformAuthData(userId, String.valueOf(row.get("user_name")), request.getPhone(), String.valueOf(row.get("email")), token); return ResponseEntity.ok(ApiResponse.success(data)); } @@ -292,36 +292,29 @@ public class AuthController { Map data; if (scope == AuthScope.TENANT) { validateTenantSession(userId, tenantId); - String phone = jdbcTemplate.queryForObject( - "SELECT phone FROM sys_user WHERE id=? AND tenant_id=? AND is_deleted=0 LIMIT 1", - String.class, - userId, - tenantId - ); - String userName = jdbcTemplate.queryForObject( - "SELECT user_name FROM sys_user WHERE id=? AND tenant_id=? AND is_deleted=0 LIMIT 1", - String.class, + Map uRow = jdbcTemplate.queryForMap( + "SELECT phone, user_name, email FROM sys_user WHERE id=? AND tenant_id=? AND is_deleted=0 LIMIT 1", userId, tenantId ); + String phone = String.valueOf(uRow.get("phone")); + String userName = String.valueOf(uRow.get("user_name")); + String email = String.valueOf(uRow.get("email")); Long sessionId = rotateResult.get("sessionId") == null ? null : ((Number) rotateResult.get("sessionId")).longValue(); String token = jwtTokenService.createTenantToken(userId, tenantId, phone, sessionId); - data = buildTenantAuthData(userId, tenantId, userName, phone, token); + data = buildTenantAuthData(userId, tenantId, userName, phone, email, token); } else { validatePlatformSession(userId); - String phone = jdbcTemplate.queryForObject( - "SELECT phone FROM platform_user WHERE id=? AND is_deleted=0 LIMIT 1", - String.class, - userId - ); - String userName = jdbcTemplate.queryForObject( - "SELECT user_name FROM platform_user WHERE id=? AND is_deleted=0 LIMIT 1", - String.class, + Map puRow = jdbcTemplate.queryForMap( + "SELECT phone, user_name, email FROM platform_user WHERE id=? AND is_deleted=0 LIMIT 1", userId ); + String phone = String.valueOf(puRow.get("phone")); + String userName = String.valueOf(puRow.get("user_name")); + String email = String.valueOf(puRow.get("email")); Long sessionId = rotateResult.get("sessionId") == null ? null : ((Number) rotateResult.get("sessionId")).longValue(); String token = jwtTokenService.createPlatformToken(userId, phone, sessionId); - data = buildPlatformAuthData(userId, userName, phone, token); + data = buildPlatformAuthData(userId, userName, phone, email, token); } setRefreshCookie(response, nextRefreshToken, false); return ApiResponse.success(data); @@ -377,7 +370,7 @@ public class AuthController { String phone = String.valueOf(targetIdentity.get("phone")); String token = jwtTokenService.createTenantToken(targetUserId, targetTenantId, phone, sessionId); setRefreshCookie(httpResponse, refreshToken, false); - return ApiResponse.success(buildTenantAuthData(targetUserId, targetTenantId, String.valueOf(targetIdentity.get("user_name")), phone, token)); + return ApiResponse.success(buildTenantAuthData(targetUserId, targetTenantId, String.valueOf(targetIdentity.get("user_name")), phone, String.valueOf(targetIdentity.get("email")), token)); } @PostMapping("/logout") @@ -462,7 +455,7 @@ public class AuthController { return dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); } - private Map buildTenantAuthData(Long userId, Long tenantId, String userName, String phone, String token) { + private Map buildTenantAuthData(Long userId, Long tenantId, String userName, String phone, String email, String token) { Map tenant = loadTenantInfo(tenantId); Map data = new LinkedHashMap(); data.put("token", token); @@ -476,11 +469,12 @@ public class AuthController { data.put("roles", systemUserService.getUserRoles(userId, tenantId)); data.put("permissions", permissionService.getPermissions(userId, tenantId)); data.put("phone", phone); + data.put("email", email); data.put("appearance", loadTenantPreferences(userId, tenantId)); return data; } - private Map buildPlatformAuthData(Long userId, String userName, String phone, String token) { + private Map buildPlatformAuthData(Long userId, String userName, String phone, String email, String token) { Map data = new LinkedHashMap(); data.put("token", token); data.put("scope", AuthScope.PLATFORM.name()); @@ -490,6 +484,7 @@ public class AuthController { data.put("roles", permissionService.getPlatformRoles(userId)); data.put("permissions", permissionService.getPlatformPermissions(userId)); data.put("phone", phone); + data.put("email", email); data.put("appearance", loadPlatformPreferences(userId)); return data; } @@ -678,7 +673,7 @@ public class AuthController { String normalizedSwitchAccountKey = normalizeTenantSwitchAccountKey(switchAccountKey); if (!normalizedSwitchAccountKey.isEmpty()) { List> accountKeyRows = jdbcTemplate.queryForList( - "SELECT u.id AS user_id, u.tenant_id, u.user_name, u.phone, u.password_hash, u.tenant_switch_account_key " + + "SELECT u.id AS user_id, u.tenant_id, u.user_name, u.phone, u.email, u.password_hash, u.tenant_switch_account_key " + "FROM sys_user u " + "JOIN tenant t ON u.tenant_id=t.id " + "WHERE u.tenant_id=? AND u.tenant_switch_account_key=? " + @@ -692,7 +687,7 @@ public class AuthController { } } List> rows = jdbcTemplate.queryForList( - "SELECT u.id AS user_id, u.tenant_id, u.user_name, u.phone, u.password_hash, u.tenant_switch_account_key " + + "SELECT u.id AS user_id, u.tenant_id, u.user_name, u.phone, u.email, u.password_hash, u.tenant_switch_account_key " + "FROM sys_user u " + "JOIN tenant t ON u.tenant_id=t.id " + "WHERE u.tenant_id=? AND u.phone=? AND u.password_hash=? " + diff --git a/backend/src/main/java/com/writeoff/module/export/controller/ExportTaskController.java b/backend/src/main/java/com/writeoff/module/export/controller/ExportTaskController.java index e03ad7e..d2397de 100644 --- a/backend/src/main/java/com/writeoff/module/export/controller/ExportTaskController.java +++ b/backend/src/main/java/com/writeoff/module/export/controller/ExportTaskController.java @@ -23,8 +23,9 @@ public class ExportTaskController { @GetMapping @RequirePermission(value = "export.task.read", dataScope = DataScopeType.TENANT, auditAction = "EXPORT_TASK_LIST") - public ApiResponse> list() { - return ApiResponse.success(exportTaskService.list()); + public ApiResponse> list(@RequestParam(value = "pageNo", defaultValue = "1") int pageNo, + @RequestParam(value = "pageSize", defaultValue = "10") int pageSize) { + return ApiResponse.success(exportTaskService.list(pageNo, pageSize)); } @PostMapping diff --git a/backend/src/main/java/com/writeoff/module/export/service/ExportTaskService.java b/backend/src/main/java/com/writeoff/module/export/service/ExportTaskService.java index 2b6af64..9cc4d95 100644 --- a/backend/src/main/java/com/writeoff/module/export/service/ExportTaskService.java +++ b/backend/src/main/java/com/writeoff/module/export/service/ExportTaskService.java @@ -64,16 +64,32 @@ public class ExportTaskService { this.meetingSummaryExportService = meetingSummaryExportService; } - public PageResult list() { - List list = jdbcTemplate.query( - "SELECT id, task_code, biz_type, biz_id, file_name, file_oss_key, status, retry_count, IFNULL(download_count,0) AS download_count, " + - "DATE_FORMAT(download_token_expire_at, '%Y-%m-%d %H:%i:%s') AS token_expire_at, error_message, " + - "DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS created_at, DATE_FORMAT(finished_at, '%Y-%m-%d %H:%i:%s') AS finished_at " + - "FROM export_task WHERE tenant_id=? AND is_deleted=0 ORDER BY id DESC LIMIT 300", - ROW_MAPPER, + public PageResult list(int pageNo, int pageSize) { + int page = Math.max(1, pageNo); + int size = Math.max(1, Math.min(1000, pageSize)); + int offset = (page - 1) * size; + + Integer totalObj = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM export_task WHERE tenant_id=? AND is_deleted=0", + Integer.class, tenantId() ); - return new PageResult(list, list.size(), 1, 300); + int totalElements = totalObj == null ? 0 : totalObj; + + List list = new ArrayList<>(); + if (totalElements > 0) { + list = jdbcTemplate.query( + "SELECT id, task_code, biz_type, biz_id, file_name, file_oss_key, status, retry_count, IFNULL(download_count,0) AS download_count, " + + "DATE_FORMAT(download_token_expire_at, '%Y-%m-%d %H:%i:%s') AS token_expire_at, error_message, " + + "DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS created_at, DATE_FORMAT(finished_at, '%Y-%m-%d %H:%i:%s') AS finished_at " + + "FROM export_task WHERE tenant_id=? AND is_deleted=0 ORDER BY id DESC LIMIT ? OFFSET ?", + ROW_MAPPER, + tenantId(), + size, + offset + ); + } + return new PageResult(list, totalElements, page, size); } @Transactional(rollbackFor = Exception.class) diff --git a/backend/src/main/java/com/writeoff/module/meeting/controller/MeetingController.java b/backend/src/main/java/com/writeoff/module/meeting/controller/MeetingController.java index ca2bb1a..e22b411 100644 --- a/backend/src/main/java/com/writeoff/module/meeting/controller/MeetingController.java +++ b/backend/src/main/java/com/writeoff/module/meeting/controller/MeetingController.java @@ -77,6 +77,15 @@ public class MeetingController { return ApiResponse.success(meetingService.list(query)); } + @GetMapping("/default-budget") + @RequirePermission(value = "meeting.create", dataScope = DataScopeType.TENANT, auditAction = "MEETING_DEFAULT_BUDGET") + public ApiResponse> getDefaultBudget(@RequestParam("projectId") Long projectId) { + long defaultBudgetCent = meetingService.getDefaultMeetingBudgetCent(projectId); + java.util.Map result = new java.util.LinkedHashMap<>(); + result.put("defaultMeetingBudgetCent", defaultBudgetCent); + return ApiResponse.success(result); + } + @GetMapping("/tenant-experts") @RequirePermission(value = "meeting.material.read", dataScope = DataScopeType.TENANT, auditAction = "MEETING_TENANT_EXPERT_LIST") public ApiResponse> tenantExperts(@RequestParam(value = "keyword", required = false) String keyword) { diff --git a/backend/src/main/java/com/writeoff/module/meeting/service/MeetingMaterialService.java b/backend/src/main/java/com/writeoff/module/meeting/service/MeetingMaterialService.java index acb48e7..1f7571a 100644 --- a/backend/src/main/java/com/writeoff/module/meeting/service/MeetingMaterialService.java +++ b/backend/src/main/java/com/writeoff/module/meeting/service/MeetingMaterialService.java @@ -915,7 +915,7 @@ public class MeetingMaterialService { return true; } String status = stringValue(issue.get("status")).toUpperCase(Locale.ROOT); - return "CHANGED".equals(status) || "RESOLVED".equals(status); + return "CHANGED".equals(status) || "RESOLVED".equals(status) || "PENDING_CONFIRM".equals(status); } private String normalizeSupportedModuleCode(String moduleCode) { @@ -2106,7 +2106,7 @@ public class MeetingMaterialService { Map profileFile = asObjectMapOrEmpty(root.get("profileFile")); String ossKey = stringValue(firstNonNull(profileFile.get("ossKey"), root.get("ossKey"))); if (ossKey != null && !ossKey.isEmpty()) { - addReviewItem(items, "expert_profile_file", "涓撳绠€浠?涓插満鏂囦欢"); + addReviewItem(items, "expert_profile_file", "专家简介/串场文件"); } } else if ("EXPERT_LIST".equals(moduleCode)) { Map onsiteRoot = asObjectMapOrEmpty(root.get("onsitePhoto")); @@ -2380,6 +2380,19 @@ public class MeetingMaterialService { } addAttachmentNodeFromSingleFile(index, "signInSheet", "签到表", root.get("signInSheet")); addAttachmentNodeFromSingleFile(index, "themePhoto", "主题照片", root.get("themePhoto")); + Object videoObj = root.get("meetingVideo"); + if (videoObj instanceof Map) { + Map videoMap = asObjectMapOrEmpty(videoObj); + String type = stringValue(videoMap.get("type")); + if ("link".equals(type)) { + String link = stringValue(videoMap.get("link")); + if (!link.isEmpty()) { + addFieldNode(index, "meetingVideo", "会议视频", "网盘链接:" + link); + } + } else { + addAttachmentNodeFromSingleFile(index, "meetingVideo", "会议视频", videoObj); + } + } Object invitationObj = root.get("invitation"); if (invitationObj instanceof Collection) { int idx = 1; diff --git a/backend/src/main/java/com/writeoff/module/meeting/service/MeetingService.java b/backend/src/main/java/com/writeoff/module/meeting/service/MeetingService.java index 62dd0bb..ebe6091 100644 --- a/backend/src/main/java/com/writeoff/module/meeting/service/MeetingService.java +++ b/backend/src/main/java/com/writeoff/module/meeting/service/MeetingService.java @@ -315,7 +315,15 @@ public class MeetingService { } } - private long calculateDefaultMeetingBudgetCent(Project project) { + public long getDefaultMeetingBudgetCent(Long projectId) { + Project project = projectService.getById(projectId); + if (project == null) { + throw new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "项目不存在"); + } + return calculateDefaultMeetingBudgetCent(project); + } + + public long calculateDefaultMeetingBudgetCent(Project project) { long projectBudgetCent = Math.max(0L, project.getBudgetCent()); ProjectFeeSummary feeSummary = parseProjectFeeSummary(project.getProjectFeeJson()); long distributableBudgetCent = projectBudgetCent - feeSummary.managementFeeCent - feeSummary.taxFeeCent - feeSummary.customFeeTotalCent; @@ -737,8 +745,18 @@ public class MeetingService { } public void updateAuditStatus(Long meetingId, MeetingAuditStatus status) { + updateAuditStatus(meetingId, status, null); + } + + public void updateAuditStatus(Long meetingId, MeetingAuditStatus status, String rejectReason) { Meeting meeting = getById(meetingId); meeting.setAuditStatus(status); + if (rejectReason != null && !rejectReason.isEmpty()) { + meeting.setLastRejectReason(rejectReason); + meeting.setRejectCount(meeting.getRejectCount() + 1); + } else if (status == MeetingAuditStatus.APPROVED) { + meeting.setLastRejectReason(""); + } meetingRepository.save(meeting); } diff --git a/backend/src/main/java/com/writeoff/module/project/controller/ProjectController.java b/backend/src/main/java/com/writeoff/module/project/controller/ProjectController.java index 919ef9e..933d2c7 100644 --- a/backend/src/main/java/com/writeoff/module/project/controller/ProjectController.java +++ b/backend/src/main/java/com/writeoff/module/project/controller/ProjectController.java @@ -30,8 +30,11 @@ public class ProjectController { @GetMapping public ApiResponse> list(@RequestParam(value = "parentOnly", required = false) Boolean parentOnly, - @RequestParam(value = "includeDeleted", required = false) Boolean includeDeleted) { - return ApiResponse.success(projectService.list(parentOnly, includeDeleted)); + @RequestParam(value = "includeDeleted", required = false) Boolean includeDeleted, + @RequestParam(value = "keyword", required = false) String keyword, + @RequestParam(value = "pageNo", required = false) Integer pageNo, + @RequestParam(value = "pageSize", required = false) Integer pageSize) { + return ApiResponse.success(projectService.list(parentOnly, includeDeleted, keyword, pageNo, pageSize)); } @GetMapping("/{id}/children") diff --git a/backend/src/main/java/com/writeoff/module/project/model/Project.java b/backend/src/main/java/com/writeoff/module/project/model/Project.java index b3a825e..7784071 100644 --- a/backend/src/main/java/com/writeoff/module/project/model/Project.java +++ b/backend/src/main/java/com/writeoff/module/project/model/Project.java @@ -264,6 +264,10 @@ public class Project { return budgetCent; } + public void setBudgetCent(long budgetCent) { + this.budgetCent = budgetCent; + } + public int getMeetingTotal() { return meetingTotal; } diff --git a/backend/src/main/java/com/writeoff/module/project/service/ProjectService.java b/backend/src/main/java/com/writeoff/module/project/service/ProjectService.java index 9596f3a..7580c40 100644 --- a/backend/src/main/java/com/writeoff/module/project/service/ProjectService.java +++ b/backend/src/main/java/com/writeoff/module/project/service/ProjectService.java @@ -64,13 +64,18 @@ public class ProjectService { this(projectRepository, null, null, null, null, null); } - public PageResult list(Boolean parentOnly, Boolean includeDeleted) { + public PageResult list(Boolean parentOnly, Boolean includeDeleted, String keyword, Integer pageNo, Integer pageSize) { List list = projectRepository.findAll(Boolean.TRUE.equals(includeDeleted)); if (Boolean.TRUE.equals(parentOnly)) { list = list.stream() .filter(project -> project.getParentProjectId() == null) .collect(Collectors.toList()); } + if (keyword != null && !keyword.trim().isEmpty()) { + list = list.stream() + .filter(project -> project.getName() != null && project.getName().contains(keyword.trim())) + .collect(Collectors.toList()); + } if (dataPermissionService != null) { DataPermissionService.DataScope scope = dataPermissionService.resolveCurrentUserScope(); final Map creatorMap = scope.isProjectOwnerOnly() @@ -80,7 +85,14 @@ public class ProjectService { .filter(project -> dataPermissionService.canAccessProject(project.getId(), creatorMap.get(project.getId()), scope)) .collect(Collectors.toList()); } - return new PageResult<>(list, list.size(), 1, 20); + int page = pageNo == null || pageNo < 1 ? 1 : pageNo; + int size = pageSize == null || pageSize < 1 ? 20 : pageSize; + int total = list.size(); + int fromIndex = (page - 1) * size; + int toIndex = Math.min(fromIndex + size, total); + List pagedList = fromIndex < total ? list.subList(fromIndex, toIndex) : new ArrayList<>(); + redactSensitiveData(pagedList); + return new PageResult<>(pagedList, total, page, size); } public List listChildren(Long parentProjectId) { @@ -95,6 +107,7 @@ public class ProjectService { .filter(project -> dataPermissionService.canAccessProject(project.getId(), creatorMap.get(project.getId()), scope)) .collect(Collectors.toList()); } + redactSensitiveData(children); return children; } @@ -110,6 +123,7 @@ public class ProjectService { .filter(project -> dataPermissionService.canAccessProject(project.getId(), creatorMap.get(project.getId()), scope)) .collect(Collectors.toList()); } + redactSensitiveData(children); return children; } @@ -134,7 +148,7 @@ public class ProjectService { double budgetExecutionRatio = calculateBudgetExecutionRatio(request.getBudgetCent(), projectFee.totalCent); assertProjectBudgetConstraint( request.getAllowProjectOverBudget() != null && request.getAllowProjectOverBudget(), - request.getOverBudgetThresholdRatio() == null ? 0.1d : request.getOverBudgetThresholdRatio(), + request.getOverBudgetThresholdRatio() == null ? 0.0d : request.getOverBudgetThresholdRatio(), budgetExecutionRatio, request.getBudgetCent(), projectFee.totalCent @@ -157,7 +171,7 @@ public class ProjectService { request.getMeetingTotal(), 0, request.getAllowMeetingOverBudget() != null && request.getAllowMeetingOverBudget(), - request.getOverBudgetThresholdRatio() == null ? 0.1d : request.getOverBudgetThresholdRatio(), + request.getOverBudgetThresholdRatio() == null ? 0.0d : request.getOverBudgetThresholdRatio(), request.getOverBudgetApprovalChainJson(), budgetExecutionRatio, null, @@ -215,7 +229,7 @@ public class ProjectService { assertCurrentBudgetCanCoverChildren(projectId, request.getBudgetCent()); assertProjectBudgetConstraint( request.getAllowProjectOverBudget() != null && request.getAllowProjectOverBudget(), - request.getOverBudgetThresholdRatio() == null ? 0.1d : request.getOverBudgetThresholdRatio(), + request.getOverBudgetThresholdRatio() == null ? 0.0d : request.getOverBudgetThresholdRatio(), budgetExecutionRatio, request.getBudgetCent(), projectFee.totalCent @@ -238,7 +252,7 @@ public class ProjectService { request.getMeetingTotal(), existing.getMeetingCompletedCount(), request.getAllowMeetingOverBudget() != null && request.getAllowMeetingOverBudget(), - request.getOverBudgetThresholdRatio() == null ? 0.1d : request.getOverBudgetThresholdRatio(), + request.getOverBudgetThresholdRatio() == null ? 0.0d : request.getOverBudgetThresholdRatio(), request.getOverBudgetApprovalChainJson(), budgetExecutionRatio, existing.getRiskFlagsJson(), @@ -607,7 +621,7 @@ public class ProjectService { try { return new ProjectFeeSummary( objectMapper.writeValueAsString(normalizedRoot), - managementFeeCent + taxFeeCent + paidAmountCent + customTotalCent + managementFeeCent + taxFeeCent + customTotalCent ); } catch (Exception e) { throw new BusinessException(10001, "项目费用配置序列化失败"); @@ -704,10 +718,10 @@ public class ProjectService { double budgetExecutionRatio, Long budgetCent, long feeTotalCent) { - if (allowProjectOverBudget) { - return; - } - double allowedRatio = 1d + Math.max(0d, thresholdRatio); + // 如果关闭了“允许超支”开关,强制阈值为 0 + double actualThreshold = allowProjectOverBudget ? Math.max(0d, thresholdRatio) : 0d; + double allowedRatio = 1d + actualThreshold; + if (budgetExecutionRatio > allowedRatio) { long budget = budgetCent == null ? 0L : budgetCent; long allowedTotalCent = Math.round(budget * allowedRatio); @@ -893,6 +907,19 @@ public class ProjectService { } return "项目变更"; } + private void redactSensitiveData(List list) { + if (list == null || list.isEmpty()) { + return; + } + Long userId = AuthContext.userId(); + boolean canReadSensitive = permissionService != null && userId != null && permissionService.hasPermission(userId, "project.sensitive_data.read"); + if (!canReadSensitive) { + for (Project project : list) { + project.setBudgetCent(-1L); + project.setProjectFeeJson("{}"); + } + } + } private static class ProjectFeeSummary { private final String normalizedJson; diff --git a/backend/src/main/resources/db/migration/V112__project_sensitive_data_read_permission.sql b/backend/src/main/resources/db/migration/V112__project_sensitive_data_read_permission.sql new file mode 100644 index 0000000..2eceabc --- /dev/null +++ b/backend/src/main/resources/db/migration/V112__project_sensitive_data_read_permission.sql @@ -0,0 +1,28 @@ +SET @next_permission_id := (SELECT IFNULL(MAX(id), 0) + 1 FROM permission); +INSERT INTO permission (id, permission_code, permission_name, module) +SELECT @next_permission_id, 'project.sensitive_data.read', '查看项目敏感数据', 'project' +FROM dual +WHERE NOT EXISTS ( + SELECT 1 + FROM permission + WHERE permission_code = 'project.sensitive_data.read' +); + +SET @next_role_permission_id := (SELECT IFNULL(MAX(id), 0) FROM role_permission); +INSERT INTO role_permission (id, tenant_id, role_id, permission_id) +SELECT + (@next_role_permission_id := @next_role_permission_id + 1) AS id, + r.tenant_id, + r.id AS role_id, + p.id AS permission_id +FROM role r +JOIN permission p ON p.permission_code = 'project.sensitive_data.read' +WHERE r.role_code IN ('TENANT_ADMIN', 'PROJECT_OWNER', 'FINANCE') + AND r.is_deleted = 0 + AND NOT EXISTS ( + SELECT 1 + FROM role_permission rp + WHERE rp.tenant_id = r.tenant_id + AND rp.role_id = r.id + AND rp.permission_id = p.id + ); diff --git a/find_mojibake.js b/find_mojibake.js new file mode 100644 index 0000000..0785e14 --- /dev/null +++ b/find_mojibake.js @@ -0,0 +1 @@ +const fs = require('fs'); ['backend/src/main/java/com/writeoff/module/meeting/service/MeetingMaterialService.java', 'frontend/src/views/modules/MeetingPage.vue'].forEach(f => { const lines = fs.readFileSync(f, 'utf8').split('\n'); lines.forEach((l, i) => { if (l.includes('\uFFFD')) console.log(f + ':' + (i+1) + ': ' + l.trim()); }); }); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bbdc2d1..496ff31 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,7 +13,8 @@ "element-plus": "^2.8.4", "pinia": "^3.0.4", "vue": "^3.5.10", - "vue-router": "^4.4.5" + "vue-router": "^4.4.5", + "xlsx": "^0.18.5" }, "devDependencies": { "@vitejs/plugin-vue": "^5.1.4", @@ -1076,6 +1077,15 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/async-validator": { "version": "4.2.5", "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", @@ -1127,6 +1137,28 @@ "node": ">= 0.4" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1164,6 +1196,18 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", @@ -1362,6 +1406,15 @@ "node": ">= 6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -1747,6 +1800,18 @@ "node": ">=0.10.0" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/superjson": { "version": "2.2.6", "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz", @@ -1868,6 +1933,45 @@ "peerDependencies": { "vue": "^3.5.0" } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 5c19abb..cf0c4af 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,7 +14,8 @@ "element-plus": "^2.8.4", "pinia": "^3.0.4", "vue": "^3.5.10", - "vue-router": "^4.4.5" + "vue-router": "^4.4.5", + "xlsx": "^0.18.5" }, "devDependencies": { "@vitejs/plugin-vue": "^5.1.4", diff --git a/frontend/src/api/modules.ts b/frontend/src/api/modules.ts index afb5d07..d9734d9 100644 --- a/frontend/src/api/modules.ts +++ b/frontend/src/api/modules.ts @@ -23,7 +23,7 @@ export const logoutAuth = () => http.post("/auth/logout"); export const logoutAllAuth = () => http.post("/auth/logout-all"); export const fetchGlobalSearch = (params: { q: string; limitPerType?: number }) => http.get("/search/global", { params }); -export const fetchProjects = (params?: { parentOnly?: boolean; includeDeleted?: boolean }) => http.get("/projects", { params }); +export const fetchProjects = (params?: { parentOnly?: boolean; includeDeleted?: boolean; keyword?: string; pageNo?: number; pageSize?: number }) => http.get("/projects", { params }); export const fetchProjectChildren = (id: number, params?: { includeDeleted?: boolean }) => http.get(`/projects/${id}/children`, { params }); export const createProject = (payload: { name: string; @@ -156,6 +156,8 @@ export const bindMeetingExperts = (meetingId: number, payload: { expertIds: numb http.post(`/meetings/${meetingId}/experts/bind`, payload); export const unbindMeetingExpert = (meetingId: number, expertId: number) => http.delete(`/meetings/${meetingId}/experts/${expertId}`); +export const fetchDefaultMeetingBudgetCent = (projectId: number) => + http.get(`/meetings/default-budget?projectId=${projectId}`); export const createMeeting = (payload: { projectId: number; topic: string; @@ -1046,7 +1048,7 @@ export const fetchInAppNotificationSummary = (params?: { ts?: number }) => http.get("/in-app-notifications/summary", { params }); export const markInAppNotificationRead = (id: number) => http.post(`/in-app-notifications/${id}/read`); export const markAllInAppNotificationsRead = () => http.post("/in-app-notifications/read-all"); -export const fetchExportTasks = () => http.get("/export-tasks"); +export const fetchExportTasks = (params?: { pageNo?: number; pageSize?: number }) => http.get("/export-tasks", { params }); export const createExportTask = (payload: { idempotencyKey: string; taskCode: string; diff --git a/frontend/src/constants/permissions.ts b/frontend/src/constants/permissions.ts index 0983ba5..c61c5f4 100644 --- a/frontend/src/constants/permissions.ts +++ b/frontend/src/constants/permissions.ts @@ -7,6 +7,7 @@ export const PERMS = { archive: "project.archive", bindUser: "project.bind.user", bindExecutorUser: "project.bind.executor_user", + sensitiveDataRead: "project.sensitive_data.read", }, meeting: { read: "meeting.read", diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts index feb8a9c..a5edcba 100644 --- a/frontend/src/stores/auth.ts +++ b/frontend/src/stores/auth.ts @@ -14,6 +14,7 @@ export type AuthPayload = { roles?: string[]; userId?: number | string | null; phone?: string; + email?: string; tenantId?: number | string | null; userName?: string; tenantName?: string; @@ -37,6 +38,7 @@ type AuthState = { roles: string[]; userId: number | null; phone: string; + email: string; tenantId: number | null; userName: string; tenantName: string; @@ -151,6 +153,7 @@ const readAuthStateFromStorage = (): AuthState => { roles: parseStringArray(localStorage.getItem("roles")), userId: parseNumber(localStorage.getItem("userId")), phone: String(localStorage.getItem("phone") || "").trim(), + email: String(localStorage.getItem("email") || "").trim(), tenantId: parseNumber(localStorage.getItem("tenantId")), userName: String(localStorage.getItem("userName") || "").trim(), tenantName: String(localStorage.getItem("tenantName") || "").trim(), @@ -170,6 +173,7 @@ const applyAuthState = (target: AuthState, source: AuthState) => { target.roles = source.roles; target.userId = source.userId; target.phone = source.phone; + target.email = source.email; target.tenantId = source.tenantId; target.userName = source.userName; target.tenantName = source.tenantName; @@ -212,6 +216,7 @@ export const useAuthStore = defineStore("auth", { localStorage.setItem("token", String(data.token || "")); localStorage.setItem("userId", String(data.userId || "")); localStorage.setItem("phone", String(data.phone || "")); + localStorage.setItem("email", String(data.email || "")); localStorage.setItem("tenantId", String(data.tenantId || "")); localStorage.setItem("userName", String(data.userName || "")); localStorage.setItem("tenantName", String(data.tenantName || "")); @@ -253,6 +258,7 @@ export const useAuthStore = defineStore("auth", { localStorage.removeItem("roles"); localStorage.removeItem("userId"); localStorage.removeItem("phone"); + localStorage.removeItem("email"); localStorage.removeItem("tenantId"); localStorage.removeItem("userName"); localStorage.removeItem("tenantName"); diff --git a/frontend/src/views/layout/AppLayout.vue b/frontend/src/views/layout/AppLayout.vue index 3a8cbfd..bd11f10 100644 --- a/frontend/src/views/layout/AppLayout.vue +++ b/frontend/src/views/layout/AppLayout.vue @@ -310,6 +310,18 @@ const notifPageSize = ref(10); const notifDetailVisible = ref(false); const currentNotifDetail = ref | null>(null); +const prefersDark = ref(false); +let mediaQueryList: MediaQueryList | null = null; +const updatePrefersDark = (e: MediaQueryListEvent | MediaQueryList) => { + prefersDark.value = e.matches; +}; + +const isDarkMode = computed(() => { + if (appearanceStore.themeMode === 'DARK') return true; + if (appearanceStore.themeMode === 'LIGHT') return false; + return prefersDark.value; +}); + let suppressNextAuthRefresh = false; const tenantSwitchSharedRoutePaths = new Set(["/dashboard", "/profile"]); @@ -337,15 +349,21 @@ const escapeSvgText = (value: string) => .replace(/"/g, """) .replace(/'/g, "'"); -const createWatermarkImage = (lines: string[]) => { +const createWatermarkImage = (lines: string[], isDark: boolean) => { const [line1 = "", line2 = ""] = lines.map((item) => escapeSvgText(item)); + + const textColor1 = isDark ? "#ffffff" : "#0f172a"; + const opacity1 = isDark ? "0.15" : "0.1"; + const textColor2 = isDark ? "#e2e8f0" : "#334155"; + const opacity2 = isDark ? "0.2" : "0.14"; + const svg = ` - + ${line1} - + ${line2} @@ -355,7 +373,7 @@ const createWatermarkImage = (lines: string[]) => { }; const watermarkStyle = computed(() => ({ - backgroundImage: createWatermarkImage([userDisplay.value, `${watermarkScopeLabel.value} ${watermarkDate}`.trim()]), + backgroundImage: createWatermarkImage([userDisplay.value, `${watermarkScopeLabel.value} ${watermarkDate}`.trim()], isDarkMode.value), })); const notifUnreadCount = computed(() => Number(unreadInAppCount.value || 0)); @@ -606,6 +624,16 @@ onMounted(async () => { window.addEventListener("auth:token-updated", handleAuthTokenUpdated as EventListener); window.addEventListener("storage", handleAuthStorageChanged); document.addEventListener("visibilitychange", handleVisibilityChange); + + if (typeof window !== 'undefined' && window.matchMedia) { + mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)'); + prefersDark.value = mediaQueryList.matches; + if (mediaQueryList.addEventListener) { + mediaQueryList.addEventListener('change', updatePrefersDark); + } else { + mediaQueryList.addListener(updatePrefersDark); + } + } try { await refreshLayoutState(); @@ -615,6 +643,13 @@ onMounted(async () => { }); onUnmounted(() => { + if (mediaQueryList) { + if (mediaQueryList.removeEventListener) { + mediaQueryList.removeEventListener('change', updatePrefersDark); + } else { + mediaQueryList.removeListener(updatePrefersDark); + } + } document.removeEventListener("visibilitychange", handleVisibilityChange); window.removeEventListener("auth:token-updated", handleAuthTokenUpdated as EventListener); window.removeEventListener("storage", handleAuthStorageChanged); diff --git a/frontend/src/views/modules/AuditPage.vue b/frontend/src/views/modules/AuditPage.vue index f2275c6..5160435 100644 --- a/frontend/src/views/modules/AuditPage.vue +++ b/frontend/src/views/modules/AuditPage.vue @@ -67,6 +67,7 @@ :get-file-url="getFileUrl" :is-image-attachment="isImageAttachment" :is-pdf-attachment="isPdfAttachment" + :is-excel-attachment="isExcelAttachment" :open-file-url="openFileUrl" :preview-oss-document="openAuditMaterialDocPreview" :build-photo-item-key="buildPhotoItemKey" @@ -85,6 +86,7 @@ { const auditMaterialPreviewVisible = ref(false); const auditMaterialPreviewUrl = ref(""); -const auditMaterialPreviewKind = ref<"image" | "pdf">("image"); +const isExcelAttachment = (name: unknown, ossKey: unknown) => { + const ext1 = extractExt(String(name || "")); + const ext2 = extractExt(String(ossKey || "")); + return [".xls", ".xlsx"].includes(ext1) || [".xls", ".xlsx"].includes(ext2); +}; -/** 与会议资料抽屉一致:弹窗内 iframe 预览 PDF 或大图预览图片 */ +const auditMaterialPreviewKind = ref<"image" | "pdf" | "excel">("image"); +const auditMaterialPreviewExcelHtml = ref(""); + +const loadExcelHtmlFromUrl = async (url: string) => { + try { + const response = await fetch(url); + const arrayBuffer = await response.arrayBuffer(); + const workbook = XLSX.read(arrayBuffer, { type: "array" }); + const firstSheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[firstSheetName]; + return XLSX.utils.sheet_to_html(worksheet); + } catch (e) { + console.error("Failed to parse excel", e); + return "无法解析该 Excel 文件"; + } +}; + +/** 与会议资料抽屉一致:弹窗内 iframe 预览 PDF、大图预览图片或表格预览 Excel */ const openAuditMaterialDocPreview = async (ossKey: unknown, fileNameHint?: string) => { const key = String(ossKey || "").trim(); if (!key) { @@ -1811,8 +1836,12 @@ const openAuditMaterialDocPreview = async (ossKey: unknown, fileNameHint?: strin ElMessage.warning("暂未获取到文件查看链接"); return; } - auditMaterialPreviewKind.value = isPdfAttachment(fileNameHint, key) ? "pdf" : "image"; + const isExcel = isExcelAttachment(fileNameHint, key); + auditMaterialPreviewKind.value = isExcel ? "excel" : isPdfAttachment(fileNameHint, key) ? "pdf" : "image"; auditMaterialPreviewUrl.value = url; + if (isExcel) { + auditMaterialPreviewExcelHtml.value = await loadExcelHtmlFromUrl(url); + } auditMaterialPreviewVisible.value = true; }; @@ -1837,9 +1866,9 @@ const handleApproveCurrentModule = async () => { } else if (savedCount <= 0) { ElMessage.warning("未写入审核结果,请刷新后重试"); } else if (skippedRejected > 0) { - ElMessage.success(`本模块审核通过(${savedCount}/${itemCount}),${skippedRejected}项不通过已保留`); + ElMessage.success(`本模块审核通过,${skippedRejected}项不通过已保留`); } else { - ElMessage.success(`本模块审核通过(${savedCount}/${itemCount})`); + ElMessage.success(`本模块审核通过`); } await refreshAuditTaskDetailCache(); await loadMaterial(); @@ -1996,6 +2025,10 @@ const loadMaterial = async () => { itemKey: buildInvitationItemKey(x, i), })) .filter((x: { ossKey: string }) => !!x.ossKey), + videoType: parsed.meetingVideo?.type || "", + videoName: parsed.meetingVideo?.name || parsed.meetingVideo?.fileName || "", + videoOssKey: parsed.meetingVideo?.ossKey || "", + videoLink: parsed.meetingVideo?.link || "", }; expertProfileView.value = { fileName: String(parsed?.profileFile?.name || parsed?.fileName || "").trim(), @@ -2006,6 +2039,7 @@ const loadMaterial = async () => { { ossKey: docView.value.signInOssKey }, { ossKey: docView.value.themePhotoOssKey }, ...docView.value.invitations.map((x: any) => ({ ossKey: x?.ossKey || "" })), + { ossKey: docView.value.videoOssKey }, ].filter((x) => !!x.ossKey); await loadFileUrls(items); } else if (materialModule.value === "EXPERT_PROFILE") { diff --git a/frontend/src/views/modules/ExportTaskPage.vue b/frontend/src/views/modules/ExportTaskPage.vue index 8d1ed39..bbf1ed8 100644 --- a/frontend/src/views/modules/ExportTaskPage.vue +++ b/frontend/src/views/modules/ExportTaskPage.vue @@ -26,6 +26,18 @@ +
+ +
+ @@ -79,6 +91,9 @@ const canRead = computed(() => authStore.hasPermission(PERMS.exportTask.read)); const canManage = computed(() => authStore.hasPermission(PERMS.exportTask.manage)); const rows = ref([]); const tokens = ref>({}); +const pageNo = ref(1); +const pageSize = ref(10); +const total = ref(0); const createVisible = ref(false); const form = ref({ taskCode: "EXPORT_LEDGER", @@ -111,8 +126,9 @@ const openCreateDrawer = () => { }; const load = async () => { - const resp = await fetchExportTasks(); + const resp = await fetchExportTasks({ pageNo: pageNo.value, pageSize: pageSize.value }); rows.value = resp?.data?.list || []; + total.value = Number(resp?.data?.total || 0); }; const handleCreate = async () => { @@ -130,6 +146,7 @@ const handleCreate = async () => { }); ElMessage.success("导出任务创建成功"); createVisible.value = false; + pageNo.value = 1; await load(); }; diff --git a/frontend/src/views/modules/MeetingPage.vue b/frontend/src/views/modules/MeetingPage.vue index 4f45db0..eb08cd9 100644 --- a/frontend/src/views/modules/MeetingPage.vue +++ b/frontend/src/views/modules/MeetingPage.vue @@ -1,4 +1,4 @@ -