优化
This commit is contained in:
parent
db10401b13
commit
52fd2e7560
@ -524,7 +524,7 @@ public class AuditService {
|
|||||||
|
|
||||||
if (task.getNode() == AuditNode.INIT_REVIEW) {
|
if (task.getNode() == AuditNode.INIT_REVIEW) {
|
||||||
// 初审拒绝:整体审核结束,会议审核状态置为已拒绝
|
// 初审拒绝:整体审核结束,会议审核状态置为已拒绝
|
||||||
meetingService.updateAuditStatus(meetingId, MeetingAuditStatus.REJECTED);
|
meetingService.updateAuditStatus(meetingId, MeetingAuditStatus.REJECTED, request.getOpinion());
|
||||||
triggerAuditNotification(task, request.getOpinion(), "AUDIT_REJECTED");
|
triggerAuditNotification(task, request.getOpinion(), "AUDIT_REJECTED");
|
||||||
} else {
|
} else {
|
||||||
// 复审 / 终审拒绝:整体回到初审阶段,重新创建初审任务
|
// 复审 / 终审拒绝:整体回到初审阶段,重新创建初审任务
|
||||||
@ -539,7 +539,7 @@ public class AuditService {
|
|||||||
assigneeUserId = auditFlowConfigService.resolveAssigneeUserId(tenantId, firstNode);
|
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);
|
meetingService.updateCurrentAuditNode(meetingId, firstNode.name(), assigneeUserId);
|
||||||
AuditTask resetTask = new AuditTask(
|
AuditTask resetTask = new AuditTask(
|
||||||
null,
|
null,
|
||||||
@ -566,7 +566,7 @@ public class AuditService {
|
|||||||
task.setStatus(AuditTaskStatus.REJECTED);
|
task.setStatus(AuditTaskStatus.REJECTED);
|
||||||
task.setOpinion("退回修改:" + request.getOpinion());
|
task.setOpinion("退回修改:" + request.getOpinion());
|
||||||
auditTaskRepository.save(task);
|
auditTaskRepository.save(task);
|
||||||
meetingService.updateAuditStatus(task.getMeetingId(), MeetingAuditStatus.PENDING);
|
meetingService.updateAuditStatus(task.getMeetingId(), MeetingAuditStatus.PENDING, "退回修改:" + request.getOpinion());
|
||||||
logAuditTaskAction(task, "AUDIT_TASK_RETURNED", request.getOpinion());
|
logAuditTaskAction(task, "AUDIT_TASK_RETURNED", request.getOpinion());
|
||||||
triggerAuditNotification(task, request.getOpinion(), "AUDIT_RETURNED");
|
triggerAuditNotification(task, request.getOpinion(), "AUDIT_RETURNED");
|
||||||
Map<String, Object> result = new LinkedHashMap<>();
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
|||||||
@ -120,7 +120,7 @@ public class AuthController {
|
|||||||
.body(ApiErrorResponse.of(11005, "账号已被锁定,请" + (remaining / 60 + 1) + "分钟后重试", errors));
|
.body(ApiErrorResponse.of(11005, "账号已被锁定,请" + (remaining / 60 + 1) + "分钟后重试", errors));
|
||||||
}
|
}
|
||||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
List<Map<String, Object>> 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 " +
|
"t.tenant_code, t.tenant_name, t.status AS tenant_status " +
|
||||||
"FROM sys_user u JOIN tenant t ON u.tenant_id=t.id " +
|
"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",
|
"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 refreshToken = String.valueOf(issueResult.get("refreshToken"));
|
||||||
String token = jwtTokenService.createTenantToken(userId, tenantId, request.getPhone(), sessionId);
|
String token = jwtTokenService.createTenantToken(userId, tenantId, request.getPhone(), sessionId);
|
||||||
setRefreshCookie(httpResponse, refreshToken, false);
|
setRefreshCookie(httpResponse, refreshToken, false);
|
||||||
Map<String, Object> data = buildTenantAuthData(userId, tenantId, String.valueOf(row.get("user_name")), request.getPhone(), token);
|
Map<String, Object> data = buildTenantAuthData(userId, tenantId, String.valueOf(row.get("user_name")), request.getPhone(), String.valueOf(row.get("email")), token);
|
||||||
return ResponseEntity.ok(ApiResponse.success(data));
|
return ResponseEntity.ok(ApiResponse.success(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,7 +194,7 @@ public class AuthController {
|
|||||||
.body(ApiErrorResponse.of(11005, "账号已被锁定,请" + (remaining / 60 + 1) + "分钟后重试", errors));
|
.body(ApiErrorResponse.of(11005, "账号已被锁定,请" + (remaining / 60 + 1) + "分钟后重试", errors));
|
||||||
}
|
}
|
||||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
List<Map<String, Object>> 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",
|
"FROM platform_user WHERE phone=? AND is_deleted=0 LIMIT 1",
|
||||||
request.getPhone()
|
request.getPhone()
|
||||||
);
|
);
|
||||||
@ -235,7 +235,7 @@ public class AuthController {
|
|||||||
String refreshToken = String.valueOf(issueResult.get("refreshToken"));
|
String refreshToken = String.valueOf(issueResult.get("refreshToken"));
|
||||||
String token = jwtTokenService.createPlatformToken(userId, request.getPhone(), sessionId);
|
String token = jwtTokenService.createPlatformToken(userId, request.getPhone(), sessionId);
|
||||||
setRefreshCookie(httpResponse, refreshToken, false);
|
setRefreshCookie(httpResponse, refreshToken, false);
|
||||||
Map<String, Object> data = buildPlatformAuthData(userId, String.valueOf(row.get("user_name")), request.getPhone(), token);
|
Map<String, Object> data = buildPlatformAuthData(userId, String.valueOf(row.get("user_name")), request.getPhone(), String.valueOf(row.get("email")), token);
|
||||||
return ResponseEntity.ok(ApiResponse.success(data));
|
return ResponseEntity.ok(ApiResponse.success(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,36 +292,29 @@ public class AuthController {
|
|||||||
Map<String, Object> data;
|
Map<String, Object> data;
|
||||||
if (scope == AuthScope.TENANT) {
|
if (scope == AuthScope.TENANT) {
|
||||||
validateTenantSession(userId, tenantId);
|
validateTenantSession(userId, tenantId);
|
||||||
String phone = jdbcTemplate.queryForObject(
|
Map<String, Object> uRow = jdbcTemplate.queryForMap(
|
||||||
"SELECT phone FROM sys_user WHERE id=? AND tenant_id=? AND is_deleted=0 LIMIT 1",
|
"SELECT phone, user_name, email 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,
|
|
||||||
userId,
|
userId,
|
||||||
tenantId
|
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();
|
Long sessionId = rotateResult.get("sessionId") == null ? null : ((Number) rotateResult.get("sessionId")).longValue();
|
||||||
String token = jwtTokenService.createTenantToken(userId, tenantId, phone, sessionId);
|
String token = jwtTokenService.createTenantToken(userId, tenantId, phone, sessionId);
|
||||||
data = buildTenantAuthData(userId, tenantId, userName, phone, token);
|
data = buildTenantAuthData(userId, tenantId, userName, phone, email, token);
|
||||||
} else {
|
} else {
|
||||||
validatePlatformSession(userId);
|
validatePlatformSession(userId);
|
||||||
String phone = jdbcTemplate.queryForObject(
|
Map<String, Object> puRow = jdbcTemplate.queryForMap(
|
||||||
"SELECT phone FROM platform_user WHERE id=? AND is_deleted=0 LIMIT 1",
|
"SELECT phone, user_name, email 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,
|
|
||||||
userId
|
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();
|
Long sessionId = rotateResult.get("sessionId") == null ? null : ((Number) rotateResult.get("sessionId")).longValue();
|
||||||
String token = jwtTokenService.createPlatformToken(userId, phone, sessionId);
|
String token = jwtTokenService.createPlatformToken(userId, phone, sessionId);
|
||||||
data = buildPlatformAuthData(userId, userName, phone, token);
|
data = buildPlatformAuthData(userId, userName, phone, email, token);
|
||||||
}
|
}
|
||||||
setRefreshCookie(response, nextRefreshToken, false);
|
setRefreshCookie(response, nextRefreshToken, false);
|
||||||
return ApiResponse.success(data);
|
return ApiResponse.success(data);
|
||||||
@ -377,7 +370,7 @@ public class AuthController {
|
|||||||
String phone = String.valueOf(targetIdentity.get("phone"));
|
String phone = String.valueOf(targetIdentity.get("phone"));
|
||||||
String token = jwtTokenService.createTenantToken(targetUserId, targetTenantId, phone, sessionId);
|
String token = jwtTokenService.createTenantToken(targetUserId, targetTenantId, phone, sessionId);
|
||||||
setRefreshCookie(httpResponse, refreshToken, false);
|
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")
|
@PostMapping("/logout")
|
||||||
@ -462,7 +455,7 @@ public class AuthController {
|
|||||||
return dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
return dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> buildTenantAuthData(Long userId, Long tenantId, String userName, String phone, String token) {
|
private Map<String, Object> buildTenantAuthData(Long userId, Long tenantId, String userName, String phone, String email, String token) {
|
||||||
Map<String, Object> tenant = loadTenantInfo(tenantId);
|
Map<String, Object> tenant = loadTenantInfo(tenantId);
|
||||||
Map<String, Object> data = new LinkedHashMap<String, Object>();
|
Map<String, Object> data = new LinkedHashMap<String, Object>();
|
||||||
data.put("token", token);
|
data.put("token", token);
|
||||||
@ -476,11 +469,12 @@ public class AuthController {
|
|||||||
data.put("roles", systemUserService.getUserRoles(userId, tenantId));
|
data.put("roles", systemUserService.getUserRoles(userId, tenantId));
|
||||||
data.put("permissions", permissionService.getPermissions(userId, tenantId));
|
data.put("permissions", permissionService.getPermissions(userId, tenantId));
|
||||||
data.put("phone", phone);
|
data.put("phone", phone);
|
||||||
|
data.put("email", email);
|
||||||
data.put("appearance", loadTenantPreferences(userId, tenantId));
|
data.put("appearance", loadTenantPreferences(userId, tenantId));
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> buildPlatformAuthData(Long userId, String userName, String phone, String token) {
|
private Map<String, Object> buildPlatformAuthData(Long userId, String userName, String phone, String email, String token) {
|
||||||
Map<String, Object> data = new LinkedHashMap<String, Object>();
|
Map<String, Object> data = new LinkedHashMap<String, Object>();
|
||||||
data.put("token", token);
|
data.put("token", token);
|
||||||
data.put("scope", AuthScope.PLATFORM.name());
|
data.put("scope", AuthScope.PLATFORM.name());
|
||||||
@ -490,6 +484,7 @@ public class AuthController {
|
|||||||
data.put("roles", permissionService.getPlatformRoles(userId));
|
data.put("roles", permissionService.getPlatformRoles(userId));
|
||||||
data.put("permissions", permissionService.getPlatformPermissions(userId));
|
data.put("permissions", permissionService.getPlatformPermissions(userId));
|
||||||
data.put("phone", phone);
|
data.put("phone", phone);
|
||||||
|
data.put("email", email);
|
||||||
data.put("appearance", loadPlatformPreferences(userId));
|
data.put("appearance", loadPlatformPreferences(userId));
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@ -678,7 +673,7 @@ public class AuthController {
|
|||||||
String normalizedSwitchAccountKey = normalizeTenantSwitchAccountKey(switchAccountKey);
|
String normalizedSwitchAccountKey = normalizeTenantSwitchAccountKey(switchAccountKey);
|
||||||
if (!normalizedSwitchAccountKey.isEmpty()) {
|
if (!normalizedSwitchAccountKey.isEmpty()) {
|
||||||
List<Map<String, Object>> accountKeyRows = jdbcTemplate.queryForList(
|
List<Map<String, Object>> 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 " +
|
"FROM sys_user u " +
|
||||||
"JOIN tenant t ON u.tenant_id=t.id " +
|
"JOIN tenant t ON u.tenant_id=t.id " +
|
||||||
"WHERE u.tenant_id=? AND u.tenant_switch_account_key=? " +
|
"WHERE u.tenant_id=? AND u.tenant_switch_account_key=? " +
|
||||||
@ -692,7 +687,7 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
List<Map<String, Object>> 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 " +
|
"FROM sys_user u " +
|
||||||
"JOIN tenant t ON u.tenant_id=t.id " +
|
"JOIN tenant t ON u.tenant_id=t.id " +
|
||||||
"WHERE u.tenant_id=? AND u.phone=? AND u.password_hash=? " +
|
"WHERE u.tenant_id=? AND u.phone=? AND u.password_hash=? " +
|
||||||
|
|||||||
@ -23,8 +23,9 @@ public class ExportTaskController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@RequirePermission(value = "export.task.read", dataScope = DataScopeType.TENANT, auditAction = "EXPORT_TASK_LIST")
|
@RequirePermission(value = "export.task.read", dataScope = DataScopeType.TENANT, auditAction = "EXPORT_TASK_LIST")
|
||||||
public ApiResponse<PageResult<ExportTaskInfo>> list() {
|
public ApiResponse<PageResult<ExportTaskInfo>> list(@RequestParam(value = "pageNo", defaultValue = "1") int pageNo,
|
||||||
return ApiResponse.success(exportTaskService.list());
|
@RequestParam(value = "pageSize", defaultValue = "10") int pageSize) {
|
||||||
|
return ApiResponse.success(exportTaskService.list(pageNo, pageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
|
|||||||
@ -64,16 +64,32 @@ public class ExportTaskService {
|
|||||||
this.meetingSummaryExportService = meetingSummaryExportService;
|
this.meetingSummaryExportService = meetingSummaryExportService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResult<ExportTaskInfo> list() {
|
public PageResult<ExportTaskInfo> list(int pageNo, int pageSize) {
|
||||||
List<ExportTaskInfo> list = jdbcTemplate.query(
|
int page = Math.max(1, pageNo);
|
||||||
"SELECT id, task_code, biz_type, biz_id, file_name, file_oss_key, status, retry_count, IFNULL(download_count,0) AS download_count, " +
|
int size = Math.max(1, Math.min(1000, pageSize));
|
||||||
"DATE_FORMAT(download_token_expire_at, '%Y-%m-%d %H:%i:%s') AS token_expire_at, error_message, " +
|
int offset = (page - 1) * size;
|
||||||
"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",
|
Integer totalObj = jdbcTemplate.queryForObject(
|
||||||
ROW_MAPPER,
|
"SELECT COUNT(1) FROM export_task WHERE tenant_id=? AND is_deleted=0",
|
||||||
|
Integer.class,
|
||||||
tenantId()
|
tenantId()
|
||||||
);
|
);
|
||||||
return new PageResult<ExportTaskInfo>(list, list.size(), 1, 300);
|
int totalElements = totalObj == null ? 0 : totalObj;
|
||||||
|
|
||||||
|
List<ExportTaskInfo> 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<ExportTaskInfo>(list, totalElements, page, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
|||||||
@ -77,6 +77,15 @@ public class MeetingController {
|
|||||||
return ApiResponse.success(meetingService.list(query));
|
return ApiResponse.success(meetingService.list(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/default-budget")
|
||||||
|
@RequirePermission(value = "meeting.create", dataScope = DataScopeType.TENANT, auditAction = "MEETING_DEFAULT_BUDGET")
|
||||||
|
public ApiResponse<Map<String, Long>> getDefaultBudget(@RequestParam("projectId") Long projectId) {
|
||||||
|
long defaultBudgetCent = meetingService.getDefaultMeetingBudgetCent(projectId);
|
||||||
|
java.util.Map<String, Long> result = new java.util.LinkedHashMap<>();
|
||||||
|
result.put("defaultMeetingBudgetCent", defaultBudgetCent);
|
||||||
|
return ApiResponse.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/tenant-experts")
|
@GetMapping("/tenant-experts")
|
||||||
@RequirePermission(value = "meeting.material.read", dataScope = DataScopeType.TENANT, auditAction = "MEETING_TENANT_EXPERT_LIST")
|
@RequirePermission(value = "meeting.material.read", dataScope = DataScopeType.TENANT, auditAction = "MEETING_TENANT_EXPERT_LIST")
|
||||||
public ApiResponse<PageResult<ExpertInfo>> tenantExperts(@RequestParam(value = "keyword", required = false) String keyword) {
|
public ApiResponse<PageResult<ExpertInfo>> tenantExperts(@RequestParam(value = "keyword", required = false) String keyword) {
|
||||||
|
|||||||
@ -915,7 +915,7 @@ public class MeetingMaterialService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
String status = stringValue(issue.get("status")).toUpperCase(Locale.ROOT);
|
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) {
|
private String normalizeSupportedModuleCode(String moduleCode) {
|
||||||
@ -2106,7 +2106,7 @@ public class MeetingMaterialService {
|
|||||||
Map<String, Object> profileFile = asObjectMapOrEmpty(root.get("profileFile"));
|
Map<String, Object> profileFile = asObjectMapOrEmpty(root.get("profileFile"));
|
||||||
String ossKey = stringValue(firstNonNull(profileFile.get("ossKey"), root.get("ossKey")));
|
String ossKey = stringValue(firstNonNull(profileFile.get("ossKey"), root.get("ossKey")));
|
||||||
if (ossKey != null && !ossKey.isEmpty()) {
|
if (ossKey != null && !ossKey.isEmpty()) {
|
||||||
addReviewItem(items, "expert_profile_file", "涓撳绠€浠?涓插満鏂囦欢");
|
addReviewItem(items, "expert_profile_file", "专家简介/串场文件");
|
||||||
}
|
}
|
||||||
} else if ("EXPERT_LIST".equals(moduleCode)) {
|
} else if ("EXPERT_LIST".equals(moduleCode)) {
|
||||||
Map<String, Object> onsiteRoot = asObjectMapOrEmpty(root.get("onsitePhoto"));
|
Map<String, Object> onsiteRoot = asObjectMapOrEmpty(root.get("onsitePhoto"));
|
||||||
@ -2380,6 +2380,19 @@ public class MeetingMaterialService {
|
|||||||
}
|
}
|
||||||
addAttachmentNodeFromSingleFile(index, "signInSheet", "签到表", root.get("signInSheet"));
|
addAttachmentNodeFromSingleFile(index, "signInSheet", "签到表", root.get("signInSheet"));
|
||||||
addAttachmentNodeFromSingleFile(index, "themePhoto", "主题照片", root.get("themePhoto"));
|
addAttachmentNodeFromSingleFile(index, "themePhoto", "主题照片", root.get("themePhoto"));
|
||||||
|
Object videoObj = root.get("meetingVideo");
|
||||||
|
if (videoObj instanceof Map) {
|
||||||
|
Map<String, Object> 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");
|
Object invitationObj = root.get("invitation");
|
||||||
if (invitationObj instanceof Collection) {
|
if (invitationObj instanceof Collection) {
|
||||||
int idx = 1;
|
int idx = 1;
|
||||||
|
|||||||
@ -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());
|
long projectBudgetCent = Math.max(0L, project.getBudgetCent());
|
||||||
ProjectFeeSummary feeSummary = parseProjectFeeSummary(project.getProjectFeeJson());
|
ProjectFeeSummary feeSummary = parseProjectFeeSummary(project.getProjectFeeJson());
|
||||||
long distributableBudgetCent = projectBudgetCent - feeSummary.managementFeeCent - feeSummary.taxFeeCent - feeSummary.customFeeTotalCent;
|
long distributableBudgetCent = projectBudgetCent - feeSummary.managementFeeCent - feeSummary.taxFeeCent - feeSummary.customFeeTotalCent;
|
||||||
@ -737,8 +745,18 @@ public class MeetingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void updateAuditStatus(Long meetingId, MeetingAuditStatus status) {
|
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 meeting = getById(meetingId);
|
||||||
meeting.setAuditStatus(status);
|
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);
|
meetingRepository.save(meeting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,8 +30,11 @@ public class ProjectController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public ApiResponse<PageResult<Project>> list(@RequestParam(value = "parentOnly", required = false) Boolean parentOnly,
|
public ApiResponse<PageResult<Project>> list(@RequestParam(value = "parentOnly", required = false) Boolean parentOnly,
|
||||||
@RequestParam(value = "includeDeleted", required = false) Boolean includeDeleted) {
|
@RequestParam(value = "includeDeleted", required = false) Boolean includeDeleted,
|
||||||
return ApiResponse.success(projectService.list(parentOnly, 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")
|
@GetMapping("/{id}/children")
|
||||||
|
|||||||
@ -264,6 +264,10 @@ public class Project {
|
|||||||
return budgetCent;
|
return budgetCent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setBudgetCent(long budgetCent) {
|
||||||
|
this.budgetCent = budgetCent;
|
||||||
|
}
|
||||||
|
|
||||||
public int getMeetingTotal() {
|
public int getMeetingTotal() {
|
||||||
return meetingTotal;
|
return meetingTotal;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,13 +64,18 @@ public class ProjectService {
|
|||||||
this(projectRepository, null, null, null, null, null);
|
this(projectRepository, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResult<Project> list(Boolean parentOnly, Boolean includeDeleted) {
|
public PageResult<Project> list(Boolean parentOnly, Boolean includeDeleted, String keyword, Integer pageNo, Integer pageSize) {
|
||||||
List<Project> list = projectRepository.findAll(Boolean.TRUE.equals(includeDeleted));
|
List<Project> list = projectRepository.findAll(Boolean.TRUE.equals(includeDeleted));
|
||||||
if (Boolean.TRUE.equals(parentOnly)) {
|
if (Boolean.TRUE.equals(parentOnly)) {
|
||||||
list = list.stream()
|
list = list.stream()
|
||||||
.filter(project -> project.getParentProjectId() == null)
|
.filter(project -> project.getParentProjectId() == null)
|
||||||
.collect(Collectors.toList());
|
.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) {
|
if (dataPermissionService != null) {
|
||||||
DataPermissionService.DataScope scope = dataPermissionService.resolveCurrentUserScope();
|
DataPermissionService.DataScope scope = dataPermissionService.resolveCurrentUserScope();
|
||||||
final Map<Long, Long> creatorMap = scope.isProjectOwnerOnly()
|
final Map<Long, Long> creatorMap = scope.isProjectOwnerOnly()
|
||||||
@ -80,7 +85,14 @@ public class ProjectService {
|
|||||||
.filter(project -> dataPermissionService.canAccessProject(project.getId(), creatorMap.get(project.getId()), scope))
|
.filter(project -> dataPermissionService.canAccessProject(project.getId(), creatorMap.get(project.getId()), scope))
|
||||||
.collect(Collectors.toList());
|
.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<Project> pagedList = fromIndex < total ? list.subList(fromIndex, toIndex) : new ArrayList<>();
|
||||||
|
redactSensitiveData(pagedList);
|
||||||
|
return new PageResult<>(pagedList, total, page, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Project> listChildren(Long parentProjectId) {
|
public List<Project> listChildren(Long parentProjectId) {
|
||||||
@ -95,6 +107,7 @@ public class ProjectService {
|
|||||||
.filter(project -> dataPermissionService.canAccessProject(project.getId(), creatorMap.get(project.getId()), scope))
|
.filter(project -> dataPermissionService.canAccessProject(project.getId(), creatorMap.get(project.getId()), scope))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
redactSensitiveData(children);
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +123,7 @@ public class ProjectService {
|
|||||||
.filter(project -> dataPermissionService.canAccessProject(project.getId(), creatorMap.get(project.getId()), scope))
|
.filter(project -> dataPermissionService.canAccessProject(project.getId(), creatorMap.get(project.getId()), scope))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
redactSensitiveData(children);
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +148,7 @@ public class ProjectService {
|
|||||||
double budgetExecutionRatio = calculateBudgetExecutionRatio(request.getBudgetCent(), projectFee.totalCent);
|
double budgetExecutionRatio = calculateBudgetExecutionRatio(request.getBudgetCent(), projectFee.totalCent);
|
||||||
assertProjectBudgetConstraint(
|
assertProjectBudgetConstraint(
|
||||||
request.getAllowProjectOverBudget() != null && request.getAllowProjectOverBudget(),
|
request.getAllowProjectOverBudget() != null && request.getAllowProjectOverBudget(),
|
||||||
request.getOverBudgetThresholdRatio() == null ? 0.1d : request.getOverBudgetThresholdRatio(),
|
request.getOverBudgetThresholdRatio() == null ? 0.0d : request.getOverBudgetThresholdRatio(),
|
||||||
budgetExecutionRatio,
|
budgetExecutionRatio,
|
||||||
request.getBudgetCent(),
|
request.getBudgetCent(),
|
||||||
projectFee.totalCent
|
projectFee.totalCent
|
||||||
@ -157,7 +171,7 @@ public class ProjectService {
|
|||||||
request.getMeetingTotal(),
|
request.getMeetingTotal(),
|
||||||
0,
|
0,
|
||||||
request.getAllowMeetingOverBudget() != null && request.getAllowMeetingOverBudget(),
|
request.getAllowMeetingOverBudget() != null && request.getAllowMeetingOverBudget(),
|
||||||
request.getOverBudgetThresholdRatio() == null ? 0.1d : request.getOverBudgetThresholdRatio(),
|
request.getOverBudgetThresholdRatio() == null ? 0.0d : request.getOverBudgetThresholdRatio(),
|
||||||
request.getOverBudgetApprovalChainJson(),
|
request.getOverBudgetApprovalChainJson(),
|
||||||
budgetExecutionRatio,
|
budgetExecutionRatio,
|
||||||
null,
|
null,
|
||||||
@ -215,7 +229,7 @@ public class ProjectService {
|
|||||||
assertCurrentBudgetCanCoverChildren(projectId, request.getBudgetCent());
|
assertCurrentBudgetCanCoverChildren(projectId, request.getBudgetCent());
|
||||||
assertProjectBudgetConstraint(
|
assertProjectBudgetConstraint(
|
||||||
request.getAllowProjectOverBudget() != null && request.getAllowProjectOverBudget(),
|
request.getAllowProjectOverBudget() != null && request.getAllowProjectOverBudget(),
|
||||||
request.getOverBudgetThresholdRatio() == null ? 0.1d : request.getOverBudgetThresholdRatio(),
|
request.getOverBudgetThresholdRatio() == null ? 0.0d : request.getOverBudgetThresholdRatio(),
|
||||||
budgetExecutionRatio,
|
budgetExecutionRatio,
|
||||||
request.getBudgetCent(),
|
request.getBudgetCent(),
|
||||||
projectFee.totalCent
|
projectFee.totalCent
|
||||||
@ -238,7 +252,7 @@ public class ProjectService {
|
|||||||
request.getMeetingTotal(),
|
request.getMeetingTotal(),
|
||||||
existing.getMeetingCompletedCount(),
|
existing.getMeetingCompletedCount(),
|
||||||
request.getAllowMeetingOverBudget() != null && request.getAllowMeetingOverBudget(),
|
request.getAllowMeetingOverBudget() != null && request.getAllowMeetingOverBudget(),
|
||||||
request.getOverBudgetThresholdRatio() == null ? 0.1d : request.getOverBudgetThresholdRatio(),
|
request.getOverBudgetThresholdRatio() == null ? 0.0d : request.getOverBudgetThresholdRatio(),
|
||||||
request.getOverBudgetApprovalChainJson(),
|
request.getOverBudgetApprovalChainJson(),
|
||||||
budgetExecutionRatio,
|
budgetExecutionRatio,
|
||||||
existing.getRiskFlagsJson(),
|
existing.getRiskFlagsJson(),
|
||||||
@ -607,7 +621,7 @@ public class ProjectService {
|
|||||||
try {
|
try {
|
||||||
return new ProjectFeeSummary(
|
return new ProjectFeeSummary(
|
||||||
objectMapper.writeValueAsString(normalizedRoot),
|
objectMapper.writeValueAsString(normalizedRoot),
|
||||||
managementFeeCent + taxFeeCent + paidAmountCent + customTotalCent
|
managementFeeCent + taxFeeCent + customTotalCent
|
||||||
);
|
);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new BusinessException(10001, "项目费用配置序列化失败");
|
throw new BusinessException(10001, "项目费用配置序列化失败");
|
||||||
@ -704,10 +718,10 @@ public class ProjectService {
|
|||||||
double budgetExecutionRatio,
|
double budgetExecutionRatio,
|
||||||
Long budgetCent,
|
Long budgetCent,
|
||||||
long feeTotalCent) {
|
long feeTotalCent) {
|
||||||
if (allowProjectOverBudget) {
|
// 如果关闭了“允许超支”开关,强制阈值为 0
|
||||||
return;
|
double actualThreshold = allowProjectOverBudget ? Math.max(0d, thresholdRatio) : 0d;
|
||||||
}
|
double allowedRatio = 1d + actualThreshold;
|
||||||
double allowedRatio = 1d + Math.max(0d, thresholdRatio);
|
|
||||||
if (budgetExecutionRatio > allowedRatio) {
|
if (budgetExecutionRatio > allowedRatio) {
|
||||||
long budget = budgetCent == null ? 0L : budgetCent;
|
long budget = budgetCent == null ? 0L : budgetCent;
|
||||||
long allowedTotalCent = Math.round(budget * allowedRatio);
|
long allowedTotalCent = Math.round(budget * allowedRatio);
|
||||||
@ -893,6 +907,19 @@ public class ProjectService {
|
|||||||
}
|
}
|
||||||
return "项目变更";
|
return "项目变更";
|
||||||
}
|
}
|
||||||
|
private void redactSensitiveData(List<Project> 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 static class ProjectFeeSummary {
|
||||||
private final String normalizedJson;
|
private final String normalizedJson;
|
||||||
|
|||||||
@ -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
|
||||||
|
);
|
||||||
1
find_mojibake.js
Normal file
1
find_mojibake.js
Normal file
@ -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()); }); });
|
||||||
106
frontend/package-lock.json
generated
106
frontend/package-lock.json
generated
@ -13,7 +13,8 @@
|
|||||||
"element-plus": "^2.8.4",
|
"element-plus": "^2.8.4",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.10",
|
"vue": "^3.5.10",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.4.5",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
@ -1076,6 +1077,15 @@
|
|||||||
"url": "https://github.com/sponsors/antfu"
|
"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": {
|
"node_modules/async-validator": {
|
||||||
"version": "4.2.5",
|
"version": "4.2.5",
|
||||||
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
|
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
|
||||||
@ -1127,6 +1137,28 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@ -1164,6 +1196,18 @@
|
|||||||
"url": "https://github.com/sponsors/mesqueeb"
|
"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": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||||
@ -1362,6 +1406,15 @@
|
|||||||
"node": ">= 6"
|
"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": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@ -1747,6 +1800,18 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/superjson": {
|
||||||
"version": "2.2.6",
|
"version": "2.2.6",
|
||||||
"resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz",
|
"resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz",
|
||||||
@ -1868,6 +1933,45 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "^3.5.0"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,8 @@
|
|||||||
"element-plus": "^2.8.4",
|
"element-plus": "^2.8.4",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.10",
|
"vue": "^3.5.10",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.4.5",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export const logoutAuth = () => http.post("/auth/logout");
|
|||||||
export const logoutAllAuth = () => http.post("/auth/logout-all");
|
export const logoutAllAuth = () => http.post("/auth/logout-all");
|
||||||
export const fetchGlobalSearch = (params: { q: string; limitPerType?: number }) => http.get("/search/global", { params });
|
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 fetchProjectChildren = (id: number, params?: { includeDeleted?: boolean }) => http.get(`/projects/${id}/children`, { params });
|
||||||
export const createProject = (payload: {
|
export const createProject = (payload: {
|
||||||
name: string;
|
name: string;
|
||||||
@ -156,6 +156,8 @@ export const bindMeetingExperts = (meetingId: number, payload: { expertIds: numb
|
|||||||
http.post(`/meetings/${meetingId}/experts/bind`, payload);
|
http.post(`/meetings/${meetingId}/experts/bind`, payload);
|
||||||
export const unbindMeetingExpert = (meetingId: number, expertId: number) =>
|
export const unbindMeetingExpert = (meetingId: number, expertId: number) =>
|
||||||
http.delete(`/meetings/${meetingId}/experts/${expertId}`);
|
http.delete(`/meetings/${meetingId}/experts/${expertId}`);
|
||||||
|
export const fetchDefaultMeetingBudgetCent = (projectId: number) =>
|
||||||
|
http.get(`/meetings/default-budget?projectId=${projectId}`);
|
||||||
export const createMeeting = (payload: {
|
export const createMeeting = (payload: {
|
||||||
projectId: number;
|
projectId: number;
|
||||||
topic: string;
|
topic: string;
|
||||||
@ -1046,7 +1048,7 @@ export const fetchInAppNotificationSummary = (params?: { ts?: number }) =>
|
|||||||
http.get("/in-app-notifications/summary", { params });
|
http.get("/in-app-notifications/summary", { params });
|
||||||
export const markInAppNotificationRead = (id: number) => http.post(`/in-app-notifications/${id}/read`);
|
export const markInAppNotificationRead = (id: number) => http.post(`/in-app-notifications/${id}/read`);
|
||||||
export const markAllInAppNotificationsRead = () => http.post("/in-app-notifications/read-all");
|
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: {
|
export const createExportTask = (payload: {
|
||||||
idempotencyKey: string;
|
idempotencyKey: string;
|
||||||
taskCode: string;
|
taskCode: string;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export const PERMS = {
|
|||||||
archive: "project.archive",
|
archive: "project.archive",
|
||||||
bindUser: "project.bind.user",
|
bindUser: "project.bind.user",
|
||||||
bindExecutorUser: "project.bind.executor_user",
|
bindExecutorUser: "project.bind.executor_user",
|
||||||
|
sensitiveDataRead: "project.sensitive_data.read",
|
||||||
},
|
},
|
||||||
meeting: {
|
meeting: {
|
||||||
read: "meeting.read",
|
read: "meeting.read",
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export type AuthPayload = {
|
|||||||
roles?: string[];
|
roles?: string[];
|
||||||
userId?: number | string | null;
|
userId?: number | string | null;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
email?: string;
|
||||||
tenantId?: number | string | null;
|
tenantId?: number | string | null;
|
||||||
userName?: string;
|
userName?: string;
|
||||||
tenantName?: string;
|
tenantName?: string;
|
||||||
@ -37,6 +38,7 @@ type AuthState = {
|
|||||||
roles: string[];
|
roles: string[];
|
||||||
userId: number | null;
|
userId: number | null;
|
||||||
phone: string;
|
phone: string;
|
||||||
|
email: string;
|
||||||
tenantId: number | null;
|
tenantId: number | null;
|
||||||
userName: string;
|
userName: string;
|
||||||
tenantName: string;
|
tenantName: string;
|
||||||
@ -151,6 +153,7 @@ const readAuthStateFromStorage = (): AuthState => {
|
|||||||
roles: parseStringArray(localStorage.getItem("roles")),
|
roles: parseStringArray(localStorage.getItem("roles")),
|
||||||
userId: parseNumber(localStorage.getItem("userId")),
|
userId: parseNumber(localStorage.getItem("userId")),
|
||||||
phone: String(localStorage.getItem("phone") || "").trim(),
|
phone: String(localStorage.getItem("phone") || "").trim(),
|
||||||
|
email: String(localStorage.getItem("email") || "").trim(),
|
||||||
tenantId: parseNumber(localStorage.getItem("tenantId")),
|
tenantId: parseNumber(localStorage.getItem("tenantId")),
|
||||||
userName: String(localStorage.getItem("userName") || "").trim(),
|
userName: String(localStorage.getItem("userName") || "").trim(),
|
||||||
tenantName: String(localStorage.getItem("tenantName") || "").trim(),
|
tenantName: String(localStorage.getItem("tenantName") || "").trim(),
|
||||||
@ -170,6 +173,7 @@ const applyAuthState = (target: AuthState, source: AuthState) => {
|
|||||||
target.roles = source.roles;
|
target.roles = source.roles;
|
||||||
target.userId = source.userId;
|
target.userId = source.userId;
|
||||||
target.phone = source.phone;
|
target.phone = source.phone;
|
||||||
|
target.email = source.email;
|
||||||
target.tenantId = source.tenantId;
|
target.tenantId = source.tenantId;
|
||||||
target.userName = source.userName;
|
target.userName = source.userName;
|
||||||
target.tenantName = source.tenantName;
|
target.tenantName = source.tenantName;
|
||||||
@ -212,6 +216,7 @@ export const useAuthStore = defineStore("auth", {
|
|||||||
localStorage.setItem("token", String(data.token || ""));
|
localStorage.setItem("token", String(data.token || ""));
|
||||||
localStorage.setItem("userId", String(data.userId || ""));
|
localStorage.setItem("userId", String(data.userId || ""));
|
||||||
localStorage.setItem("phone", String(data.phone || ""));
|
localStorage.setItem("phone", String(data.phone || ""));
|
||||||
|
localStorage.setItem("email", String(data.email || ""));
|
||||||
localStorage.setItem("tenantId", String(data.tenantId || ""));
|
localStorage.setItem("tenantId", String(data.tenantId || ""));
|
||||||
localStorage.setItem("userName", String(data.userName || ""));
|
localStorage.setItem("userName", String(data.userName || ""));
|
||||||
localStorage.setItem("tenantName", String(data.tenantName || ""));
|
localStorage.setItem("tenantName", String(data.tenantName || ""));
|
||||||
@ -253,6 +258,7 @@ export const useAuthStore = defineStore("auth", {
|
|||||||
localStorage.removeItem("roles");
|
localStorage.removeItem("roles");
|
||||||
localStorage.removeItem("userId");
|
localStorage.removeItem("userId");
|
||||||
localStorage.removeItem("phone");
|
localStorage.removeItem("phone");
|
||||||
|
localStorage.removeItem("email");
|
||||||
localStorage.removeItem("tenantId");
|
localStorage.removeItem("tenantId");
|
||||||
localStorage.removeItem("userName");
|
localStorage.removeItem("userName");
|
||||||
localStorage.removeItem("tenantName");
|
localStorage.removeItem("tenantName");
|
||||||
|
|||||||
@ -310,6 +310,18 @@ const notifPageSize = ref(10);
|
|||||||
const notifDetailVisible = ref(false);
|
const notifDetailVisible = ref(false);
|
||||||
const currentNotifDetail = ref<Record<string, any> | null>(null);
|
const currentNotifDetail = ref<Record<string, any> | 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;
|
let suppressNextAuthRefresh = false;
|
||||||
|
|
||||||
const tenantSwitchSharedRoutePaths = new Set(["/dashboard", "/profile"]);
|
const tenantSwitchSharedRoutePaths = new Set(["/dashboard", "/profile"]);
|
||||||
@ -337,15 +349,21 @@ const escapeSvgText = (value: string) =>
|
|||||||
.replace(/"/g, """)
|
.replace(/"/g, """)
|
||||||
.replace(/'/g, "'");
|
.replace(/'/g, "'");
|
||||||
|
|
||||||
const createWatermarkImage = (lines: string[]) => {
|
const createWatermarkImage = (lines: string[], isDark: boolean) => {
|
||||||
const [line1 = "", line2 = ""] = lines.map((item) => escapeSvgText(item));
|
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 = `
|
const svg = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="320" height="240" viewBox="0 0 320 240">
|
<svg xmlns="http://www.w3.org/2000/svg" width="320" height="240" viewBox="0 0 320 240">
|
||||||
<g transform="rotate(-24 160 120)">
|
<g transform="rotate(-24 160 120)">
|
||||||
<text x="24" y="112" font-size="15" font-family="Arial, PingFang SC, Microsoft YaHei, sans-serif" fill="#0f172a" fill-opacity="0.1">
|
<text x="24" y="112" font-size="15" font-family="Arial, PingFang SC, Microsoft YaHei, sans-serif" fill="${textColor1}" fill-opacity="${opacity1}">
|
||||||
${line1}
|
${line1}
|
||||||
</text>
|
</text>
|
||||||
<text x="24" y="144" font-size="13" font-family="Arial, PingFang SC, Microsoft YaHei, sans-serif" fill="#334155" fill-opacity="0.14">
|
<text x="24" y="144" font-size="13" font-family="Arial, PingFang SC, Microsoft YaHei, sans-serif" fill="${textColor2}" fill-opacity="${opacity2}">
|
||||||
${line2}
|
${line2}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
@ -355,7 +373,7 @@ const createWatermarkImage = (lines: string[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const watermarkStyle = computed(() => ({
|
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));
|
const notifUnreadCount = computed(() => Number(unreadInAppCount.value || 0));
|
||||||
@ -606,6 +624,16 @@ onMounted(async () => {
|
|||||||
window.addEventListener("auth:token-updated", handleAuthTokenUpdated as EventListener);
|
window.addEventListener("auth:token-updated", handleAuthTokenUpdated as EventListener);
|
||||||
window.addEventListener("storage", handleAuthStorageChanged);
|
window.addEventListener("storage", handleAuthStorageChanged);
|
||||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
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 {
|
try {
|
||||||
await refreshLayoutState();
|
await refreshLayoutState();
|
||||||
@ -615,6 +643,13 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
if (mediaQueryList) {
|
||||||
|
if (mediaQueryList.removeEventListener) {
|
||||||
|
mediaQueryList.removeEventListener('change', updatePrefersDark);
|
||||||
|
} else {
|
||||||
|
mediaQueryList.removeListener(updatePrefersDark);
|
||||||
|
}
|
||||||
|
}
|
||||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||||
window.removeEventListener("auth:token-updated", handleAuthTokenUpdated as EventListener);
|
window.removeEventListener("auth:token-updated", handleAuthTokenUpdated as EventListener);
|
||||||
window.removeEventListener("storage", handleAuthStorageChanged);
|
window.removeEventListener("storage", handleAuthStorageChanged);
|
||||||
|
|||||||
@ -67,6 +67,7 @@
|
|||||||
:get-file-url="getFileUrl"
|
:get-file-url="getFileUrl"
|
||||||
:is-image-attachment="isImageAttachment"
|
:is-image-attachment="isImageAttachment"
|
||||||
:is-pdf-attachment="isPdfAttachment"
|
:is-pdf-attachment="isPdfAttachment"
|
||||||
|
:is-excel-attachment="isExcelAttachment"
|
||||||
:open-file-url="openFileUrl"
|
:open-file-url="openFileUrl"
|
||||||
:preview-oss-document="openAuditMaterialDocPreview"
|
:preview-oss-document="openAuditMaterialDocPreview"
|
||||||
:build-photo-item-key="buildPhotoItemKey"
|
:build-photo-item-key="buildPhotoItemKey"
|
||||||
@ -85,6 +86,7 @@
|
|||||||
<MeetingDocPreviewDialog
|
<MeetingDocPreviewDialog
|
||||||
v-model="auditMaterialPreviewVisible"
|
v-model="auditMaterialPreviewVisible"
|
||||||
:doc-preview-dialog-image-url="auditMaterialPreviewUrl"
|
:doc-preview-dialog-image-url="auditMaterialPreviewUrl"
|
||||||
|
:doc-preview-excel-html="auditMaterialPreviewExcelHtml"
|
||||||
:preview-kind="auditMaterialPreviewKind"
|
:preview-kind="auditMaterialPreviewKind"
|
||||||
/>
|
/>
|
||||||
<MeetingAuditProgressDialog
|
<MeetingAuditProgressDialog
|
||||||
@ -133,6 +135,8 @@ import { toZhAuditNode, toZhStatus } from "../../utils/status";
|
|||||||
import AuditListTable from "./audit-page/AuditListTable.vue";
|
import AuditListTable from "./audit-page/AuditListTable.vue";
|
||||||
import AuditMaterialDrawer from "./audit-page/AuditMaterialDrawer.vue";
|
import AuditMaterialDrawer from "./audit-page/AuditMaterialDrawer.vue";
|
||||||
import AuditQueryToolbar from "./audit-page/AuditQueryToolbar.vue";
|
import AuditQueryToolbar from "./audit-page/AuditQueryToolbar.vue";
|
||||||
|
import * as echarts from "echarts";
|
||||||
|
import * as XLSX from "xlsx";
|
||||||
import MeetingAuditProgressDialog from "./meeting-page/MeetingAuditProgressDialog.vue";
|
import MeetingAuditProgressDialog from "./meeting-page/MeetingAuditProgressDialog.vue";
|
||||||
import MeetingDocPreviewDialog from "./meeting-page/MeetingDocPreviewDialog.vue";
|
import MeetingDocPreviewDialog from "./meeting-page/MeetingDocPreviewDialog.vue";
|
||||||
|
|
||||||
@ -1787,9 +1791,30 @@ const openFileUrl = (ossKey: unknown) => {
|
|||||||
|
|
||||||
const auditMaterialPreviewVisible = ref(false);
|
const auditMaterialPreviewVisible = ref(false);
|
||||||
const auditMaterialPreviewUrl = ref("");
|
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<string>("");
|
||||||
|
|
||||||
|
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 openAuditMaterialDocPreview = async (ossKey: unknown, fileNameHint?: string) => {
|
||||||
const key = String(ossKey || "").trim();
|
const key = String(ossKey || "").trim();
|
||||||
if (!key) {
|
if (!key) {
|
||||||
@ -1811,8 +1836,12 @@ const openAuditMaterialDocPreview = async (ossKey: unknown, fileNameHint?: strin
|
|||||||
ElMessage.warning("暂未获取到文件查看链接");
|
ElMessage.warning("暂未获取到文件查看链接");
|
||||||
return;
|
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;
|
auditMaterialPreviewUrl.value = url;
|
||||||
|
if (isExcel) {
|
||||||
|
auditMaterialPreviewExcelHtml.value = await loadExcelHtmlFromUrl(url);
|
||||||
|
}
|
||||||
auditMaterialPreviewVisible.value = true;
|
auditMaterialPreviewVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1837,9 +1866,9 @@ const handleApproveCurrentModule = async () => {
|
|||||||
} else if (savedCount <= 0) {
|
} else if (savedCount <= 0) {
|
||||||
ElMessage.warning("未写入审核结果,请刷新后重试");
|
ElMessage.warning("未写入审核结果,请刷新后重试");
|
||||||
} else if (skippedRejected > 0) {
|
} else if (skippedRejected > 0) {
|
||||||
ElMessage.success(`本模块审核通过(${savedCount}/${itemCount}),${skippedRejected}项不通过已保留`);
|
ElMessage.success(`本模块审核通过,${skippedRejected}项不通过已保留`);
|
||||||
} else {
|
} else {
|
||||||
ElMessage.success(`本模块审核通过(${savedCount}/${itemCount})`);
|
ElMessage.success(`本模块审核通过`);
|
||||||
}
|
}
|
||||||
await refreshAuditTaskDetailCache();
|
await refreshAuditTaskDetailCache();
|
||||||
await loadMaterial();
|
await loadMaterial();
|
||||||
@ -1996,6 +2025,10 @@ const loadMaterial = async () => {
|
|||||||
itemKey: buildInvitationItemKey(x, i),
|
itemKey: buildInvitationItemKey(x, i),
|
||||||
}))
|
}))
|
||||||
.filter((x: { ossKey: string }) => !!x.ossKey),
|
.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 = {
|
expertProfileView.value = {
|
||||||
fileName: String(parsed?.profileFile?.name || parsed?.fileName || "").trim(),
|
fileName: String(parsed?.profileFile?.name || parsed?.fileName || "").trim(),
|
||||||
@ -2006,6 +2039,7 @@ const loadMaterial = async () => {
|
|||||||
{ ossKey: docView.value.signInOssKey },
|
{ ossKey: docView.value.signInOssKey },
|
||||||
{ ossKey: docView.value.themePhotoOssKey },
|
{ ossKey: docView.value.themePhotoOssKey },
|
||||||
...docView.value.invitations.map((x: any) => ({ ossKey: x?.ossKey || "" })),
|
...docView.value.invitations.map((x: any) => ({ ossKey: x?.ossKey || "" })),
|
||||||
|
{ ossKey: docView.value.videoOssKey },
|
||||||
].filter((x) => !!x.ossKey);
|
].filter((x) => !!x.ossKey);
|
||||||
await loadFileUrls(items);
|
await loadFileUrls(items);
|
||||||
} else if (materialModule.value === "EXPERT_PROFILE") {
|
} else if (materialModule.value === "EXPERT_PROFILE") {
|
||||||
|
|||||||
@ -26,6 +26,18 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
|
<div class="flex-end mt-md">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pageNo"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
:total="total"
|
||||||
|
@current-change="load"
|
||||||
|
@size-change="load"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-drawer v-model="createVisible" title="创建导出任务" :size="DRAWER_SIZE.md" destroy-on-close>
|
<el-drawer v-model="createVisible" title="创建导出任务" :size="DRAWER_SIZE.md" destroy-on-close>
|
||||||
<el-form ref="formRef" :model="form" label-position="left" :label-width="LABEL_WIDTH.md" :rules="formRules">
|
<el-form ref="formRef" :model="form" label-position="left" :label-width="LABEL_WIDTH.md" :rules="formRules">
|
||||||
<el-form-item label="任务编码" prop="taskCode"><el-input v-model="form.taskCode" /></el-form-item>
|
<el-form-item label="任务编码" prop="taskCode"><el-input v-model="form.taskCode" /></el-form-item>
|
||||||
@ -79,6 +91,9 @@ const canRead = computed(() => authStore.hasPermission(PERMS.exportTask.read));
|
|||||||
const canManage = computed(() => authStore.hasPermission(PERMS.exportTask.manage));
|
const canManage = computed(() => authStore.hasPermission(PERMS.exportTask.manage));
|
||||||
const rows = ref<any[]>([]);
|
const rows = ref<any[]>([]);
|
||||||
const tokens = ref<Record<number, string>>({});
|
const tokens = ref<Record<number, string>>({});
|
||||||
|
const pageNo = ref(1);
|
||||||
|
const pageSize = ref(10);
|
||||||
|
const total = ref(0);
|
||||||
const createVisible = ref(false);
|
const createVisible = ref(false);
|
||||||
const form = ref({
|
const form = ref({
|
||||||
taskCode: "EXPORT_LEDGER",
|
taskCode: "EXPORT_LEDGER",
|
||||||
@ -111,8 +126,9 @@ const openCreateDrawer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
const resp = await fetchExportTasks();
|
const resp = await fetchExportTasks({ pageNo: pageNo.value, pageSize: pageSize.value });
|
||||||
rows.value = resp?.data?.list || [];
|
rows.value = resp?.data?.list || [];
|
||||||
|
total.value = Number(resp?.data?.total || 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
@ -130,6 +146,7 @@ const handleCreate = async () => {
|
|||||||
});
|
});
|
||||||
ElMessage.success("导出任务创建成功");
|
ElMessage.success("导出任务创建成功");
|
||||||
createVisible.value = false;
|
createVisible.value = false;
|
||||||
|
pageNo.value = 1;
|
||||||
await load();
|
await load();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<PageContainer title="会议管理">
|
<PageContainer title="会议管理">
|
||||||
<MeetingQueryToolbar v-model:query-form="queryForm" @load="handleMeetingQueryLoad" @reset-query="resetQuery" />
|
<MeetingQueryToolbar v-model:query-form="queryForm" @load="handleMeetingQueryLoad" @reset-query="resetQuery" />
|
||||||
<MeetingListTable
|
<MeetingListTable
|
||||||
@ -62,6 +62,7 @@
|
|||||||
:meeting-location-options="meetingLocationOptions"
|
:meeting-location-options="meetingLocationOptions"
|
||||||
:max-labor-ratio="currentEditProjectLaborRatio"
|
:max-labor-ratio="currentEditProjectLaborRatio"
|
||||||
:max-catering-ratio="currentEditProjectCateringRatio"
|
:max-catering-ratio="currentEditProjectCateringRatio"
|
||||||
|
:last-reject-reason="currentEditLastRejectReason"
|
||||||
@save="handleUpdate"
|
@save="handleUpdate"
|
||||||
/>
|
/>
|
||||||
<MeetingDetailDrawer
|
<MeetingDetailDrawer
|
||||||
@ -89,6 +90,7 @@
|
|||||||
:material-submit-action-text="materialSubmitActionText"
|
:material-submit-action-text="materialSubmitActionText"
|
||||||
:material-resubmit-summary="materialResubmitPreview"
|
:material-resubmit-summary="materialResubmitPreview"
|
||||||
:material-budget-summary="materialBudgetSummary"
|
:material-budget-summary="materialBudgetSummary"
|
||||||
|
:last-reject-reason="materialLastRejectReason"
|
||||||
:basic-form="basicForm"
|
:basic-form="basicForm"
|
||||||
:bound-expert-rows="boundExpertRows"
|
:bound-expert-rows="boundExpertRows"
|
||||||
:is-review-approved="isReviewApproved"
|
:is-review-approved="isReviewApproved"
|
||||||
@ -111,6 +113,13 @@
|
|||||||
:handle-theme-photo-picture-change="handleThemePhotoPictureChange"
|
:handle-theme-photo-picture-change="handleThemePhotoPictureChange"
|
||||||
:handle-theme-photo-picture-preview="handleThemePhotoPicturePreview"
|
:handle-theme-photo-picture-preview="handleThemePhotoPicturePreview"
|
||||||
:handle-theme-photo-picture-remove="handleThemePhotoPictureRemove"
|
:handle-theme-photo-picture-remove="handleThemePhotoPictureRemove"
|
||||||
|
v-model:meeting-video-type="docForm.videoType"
|
||||||
|
v-model:meeting-video-link="docForm.videoLink"
|
||||||
|
:meeting-video-upload-file-list="meetingVideoUploadFileList"
|
||||||
|
:before-meeting-video-upload="beforeMeetingVideoUpload"
|
||||||
|
:handle-meeting-video-picture-change="handleMeetingVideoChange"
|
||||||
|
:handle-meeting-video-picture-remove="handleMeetingVideoRemove"
|
||||||
|
:meeting-form="resolveSelectedMeetingForm()"
|
||||||
:invitation-upload-file-list="invitationUploadFileList"
|
:invitation-upload-file-list="invitationUploadFileList"
|
||||||
:before-invitation-upload="beforeInvitationUpload"
|
:before-invitation-upload="beforeInvitationUpload"
|
||||||
:handle-invitation-picture-change="handleInvitationPictureChange"
|
:handle-invitation-picture-change="handleInvitationPictureChange"
|
||||||
@ -245,6 +254,7 @@
|
|||||||
<MeetingDocPreviewDialog
|
<MeetingDocPreviewDialog
|
||||||
v-model="docPreviewDialogVisible"
|
v-model="docPreviewDialogVisible"
|
||||||
:doc-preview-dialog-image-url="docPreviewDialogImageUrl"
|
:doc-preview-dialog-image-url="docPreviewDialogImageUrl"
|
||||||
|
:doc-preview-excel-html="docPreviewExcelHtml"
|
||||||
:preview-kind="docPreviewDialogKind"
|
:preview-kind="docPreviewDialogKind"
|
||||||
/>
|
/>
|
||||||
<MeetingOcrRawDialog
|
<MeetingOcrRawDialog
|
||||||
@ -292,6 +302,9 @@ import { useRoute } from "vue-router";
|
|||||||
import { Plus } from "@element-plus/icons-vue";
|
import { Plus } from "@element-plus/icons-vue";
|
||||||
import MeetingQueryToolbar from "./meeting-page/MeetingQueryToolbar.vue";
|
import MeetingQueryToolbar from "./meeting-page/MeetingQueryToolbar.vue";
|
||||||
import MeetingListTable from "./meeting-page/MeetingListTable.vue";
|
import MeetingListTable from "./meeting-page/MeetingListTable.vue";
|
||||||
|
import * as echarts from "echarts";
|
||||||
|
import * as XLSX from "xlsx";
|
||||||
|
|
||||||
import MeetingEditDrawer from "./meeting-page/MeetingEditDrawer.vue";
|
import MeetingEditDrawer from "./meeting-page/MeetingEditDrawer.vue";
|
||||||
import MeetingDetailDrawer from "./meeting-page/MeetingDetailDrawer.vue";
|
import MeetingDetailDrawer from "./meeting-page/MeetingDetailDrawer.vue";
|
||||||
import MeetingMaterialDrawer from "./meeting-page/MeetingMaterialDrawer.vue";
|
import MeetingMaterialDrawer from "./meeting-page/MeetingMaterialDrawer.vue";
|
||||||
@ -403,6 +416,8 @@ const editDrawerVisible = ref(false);
|
|||||||
const detailDrawerVisible = ref(false);
|
const detailDrawerVisible = ref(false);
|
||||||
const currentEditMeetingId = ref<number | null>(null);
|
const currentEditMeetingId = ref<number | null>(null);
|
||||||
const currentEditOriginalBudgetCent = ref(0);
|
const currentEditOriginalBudgetCent = ref(0);
|
||||||
|
const currentEditLastRejectReason = ref("");
|
||||||
|
const materialLastRejectReason = ref("");
|
||||||
const currentEditAllowMeetingOverBudget = ref(false);
|
const currentEditAllowMeetingOverBudget = ref(false);
|
||||||
const currentEditProjectLaborRatio = ref<number | null>(null);
|
const currentEditProjectLaborRatio = ref<number | null>(null);
|
||||||
const currentEditProjectCateringRatio = ref<number | null>(null);
|
const currentEditProjectCateringRatio = ref<number | null>(null);
|
||||||
@ -944,6 +959,10 @@ const docForm = ref({
|
|||||||
themePhotoName: "",
|
themePhotoName: "",
|
||||||
themePhotoOssKey: "",
|
themePhotoOssKey: "",
|
||||||
invitationLines: "",
|
invitationLines: "",
|
||||||
|
videoType: "file" as "file" | "link",
|
||||||
|
videoName: "",
|
||||||
|
videoOssKey: "",
|
||||||
|
videoLink: "",
|
||||||
});
|
});
|
||||||
const expertProfileForm = ref({
|
const expertProfileForm = ref({
|
||||||
fileName: "",
|
fileName: "",
|
||||||
@ -968,6 +987,7 @@ const agendaFile = ref<File | null>(null);
|
|||||||
const signInFile = ref<File | null>(null);
|
const signInFile = ref<File | null>(null);
|
||||||
const themePhotoFile = ref<File | null>(null);
|
const themePhotoFile = ref<File | null>(null);
|
||||||
const invitationFile = ref<File | null>(null);
|
const invitationFile = ref<File | null>(null);
|
||||||
|
const meetingVideoFile = ref<File | null>(null);
|
||||||
const photoAutoUploading = ref(false);
|
const photoAutoUploading = ref(false);
|
||||||
const laborProtocolAutoUploading = ref(false);
|
const laborProtocolAutoUploading = ref(false);
|
||||||
const laborInvoiceAutoUploading = ref(false);
|
const laborInvoiceAutoUploading = ref(false);
|
||||||
@ -1067,7 +1087,8 @@ const materialBudgetLoading = ref(false);
|
|||||||
const materialLoadRequestId = ref(0);
|
const materialLoadRequestId = ref(0);
|
||||||
const docPreviewDialogVisible = ref(false);
|
const docPreviewDialogVisible = ref(false);
|
||||||
const docPreviewDialogImageUrl = ref("");
|
const docPreviewDialogImageUrl = ref("");
|
||||||
const docPreviewDialogKind = ref<"image" | "pdf">("image");
|
const docPreviewDialogKind = ref<"image" | "pdf" | "excel">("image");
|
||||||
|
const docPreviewExcelHtml = ref<string>("");
|
||||||
const pendingIssueJumpState = ref<{
|
const pendingIssueJumpState = ref<{
|
||||||
meetingId: number;
|
meetingId: number;
|
||||||
moduleCode: MaterialModuleCode;
|
moduleCode: MaterialModuleCode;
|
||||||
@ -1425,6 +1446,11 @@ const buildAgendaJsonPayload = () =>
|
|||||||
}));
|
}));
|
||||||
const isImageDocFile = (name: string, ossKey: string) => isImageFile(name || "") || isImageFile(ossKey || "");
|
const isImageDocFile = (name: string, ossKey: string) => isImageFile(name || "") || isImageFile(ossKey || "");
|
||||||
const isPdfDocFile = (name: string, ossKey: string) => extractExt(name || "") === ".pdf" || extractExt(ossKey || "") === ".pdf";
|
const isPdfDocFile = (name: string, ossKey: string) => extractExt(name || "") === ".pdf" || extractExt(ossKey || "") === ".pdf";
|
||||||
|
const isExcelDocFile = (name: string, ossKey: string) => {
|
||||||
|
const ext1 = extractExt(name || "");
|
||||||
|
const ext2 = extractExt(ossKey || "");
|
||||||
|
return [".xls", ".xlsx"].includes(ext1) || [".xls", ".xlsx"].includes(ext2);
|
||||||
|
};
|
||||||
const agendaUploadFileList = computed<UploadUserFile[]>(() =>
|
const agendaUploadFileList = computed<UploadUserFile[]>(() =>
|
||||||
agendaDocRows.value.map((item, index) => {
|
agendaDocRows.value.map((item, index) => {
|
||||||
const name = item.name || `会议日程${index + 1}`;
|
const name = item.name || `会议日程${index + 1}`;
|
||||||
@ -1438,6 +1464,19 @@ const agendaUploadFileList = computed<UploadUserFile[]>(() =>
|
|||||||
} as UploadUserFile & { ossKey: string };
|
} as UploadUserFile & { ossKey: string };
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
const meetingVideoUploadFileList = computed<UploadUserFile[]>(() => {
|
||||||
|
if (!docForm.value.videoOssKey) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const name = displayDocName(docForm.value.videoName, docForm.value.videoOssKey, "会议视频");
|
||||||
|
return [{
|
||||||
|
name,
|
||||||
|
url: "",
|
||||||
|
status: "success",
|
||||||
|
uid: 2,
|
||||||
|
ossKey: docForm.value.videoOssKey,
|
||||||
|
} as UploadUserFile & { ossKey: string }];
|
||||||
|
});
|
||||||
const signInUploadFileList = computed<UploadUserFile[]>(() => {
|
const signInUploadFileList = computed<UploadUserFile[]>(() => {
|
||||||
if (!docForm.value.signInOssKey) {
|
if (!docForm.value.signInOssKey) {
|
||||||
return [];
|
return [];
|
||||||
@ -2623,6 +2662,7 @@ const openEditDrawer = async (row: any) => {
|
|||||||
}
|
}
|
||||||
currentEditMeetingId.value = Number(row.id);
|
currentEditMeetingId.value = Number(row.id);
|
||||||
currentEditOriginalBudgetCent.value = Math.max(0, Number(detail.budgetCent || 0));
|
currentEditOriginalBudgetCent.value = Math.max(0, Number(detail.budgetCent || 0));
|
||||||
|
currentEditLastRejectReason.value = String(detail.lastRejectReason || "");
|
||||||
currentEditAllowMeetingOverBudget.value = allowMeetingOverBudget;
|
currentEditAllowMeetingOverBudget.value = allowMeetingOverBudget;
|
||||||
meetingForm.value = {
|
meetingForm.value = {
|
||||||
projectId,
|
projectId,
|
||||||
@ -2699,7 +2739,6 @@ const REQUIRED_MODULES = [
|
|||||||
{ code: "WRITE_OFF_DOCS", name: "核销材料模块" },
|
{ code: "WRITE_OFF_DOCS", name: "核销材料模块" },
|
||||||
{ code: "EXPERT_PROFILE", name: "专家简介/串场模块" },
|
{ code: "EXPERT_PROFILE", name: "专家简介/串场模块" },
|
||||||
{ code: "EXPERT_LIST", name: "专家列表模块" },
|
{ code: "EXPERT_LIST", name: "专家列表模块" },
|
||||||
{ code: "MEETING_INVOICE", name: "会议发票模块" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const resolveMeetingMaterialEffectiveContentJson = (material: any) =>
|
const resolveMeetingMaterialEffectiveContentJson = (material: any) =>
|
||||||
@ -4275,9 +4314,7 @@ const showPendingIssueSubmitBlockDialog = async (meetingId: number, pendingIssue
|
|||||||
}, "当前会议仍有驳回项未处理完成,暂不能提交审核。"),
|
}, "当前会议仍有驳回项未处理完成,暂不能提交审核。"),
|
||||||
h("div", {
|
h("div", {
|
||||||
style: "margin-top:8px;font-size:13px;line-height:1.7;color:#606266;",
|
style: "margin-top:8px;font-size:13px;line-height:1.7;color:#606266;",
|
||||||
}, firstOpenIssue
|
}, "请先按驳回原因完成下方列出的问题修改后,再尝试提交审核。"),
|
||||||
? "请先按驳回原因完成修改;状态为“已修改,待审核确认”的问题,需要等待审核人员确认后才能再次提交。"
|
|
||||||
: "以下驳回项已修改但仍待审核人员确认,确认完成前暂不能再次提交。"),
|
|
||||||
h("div", {
|
h("div", {
|
||||||
style: "margin-top:16px;max-height:360px;overflow:auto;padding-right:6px;",
|
style: "margin-top:16px;max-height:360px;overflow:auto;padding-right:6px;",
|
||||||
}, groups.map((group) => h("div", {
|
}, groups.map((group) => h("div", {
|
||||||
@ -4451,6 +4488,33 @@ const syncWriteOffDocPreviewUrls = async () => {
|
|||||||
];
|
];
|
||||||
await Promise.allSettled(keys.map((key) => cacheDocPreviewUrl(key)));
|
await Promise.allSettled(keys.map((key) => cacheDocPreviewUrl(key)));
|
||||||
};
|
};
|
||||||
|
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 文件";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadExcelHtmlFromFile = async (file: File) => {
|
||||||
|
try {
|
||||||
|
const arrayBuffer = await file.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 文件";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openDocPicturePreview = async (ossKey: string, fileNameHint = "") => {
|
const openDocPicturePreview = async (ossKey: string, fileNameHint = "") => {
|
||||||
const key = String(ossKey || "").trim();
|
const key = String(ossKey || "").trim();
|
||||||
if (!key) {
|
if (!key) {
|
||||||
@ -4459,8 +4523,12 @@ const openDocPicturePreview = async (ossKey: string, fileNameHint = "") => {
|
|||||||
await cacheDocPreviewUrl(key);
|
await cacheDocPreviewUrl(key);
|
||||||
const url = String(docPreviewUrlMap.value[key] || "").trim();
|
const url = String(docPreviewUrlMap.value[key] || "").trim();
|
||||||
if (url) {
|
if (url) {
|
||||||
docPreviewDialogKind.value = isPdfDocFile(fileNameHint, key) ? "pdf" : "image";
|
const isExcel = isExcelDocFile(fileNameHint, key);
|
||||||
|
docPreviewDialogKind.value = isExcel ? "excel" : isPdfDocFile(fileNameHint, key) ? "pdf" : "image";
|
||||||
docPreviewDialogImageUrl.value = url;
|
docPreviewDialogImageUrl.value = url;
|
||||||
|
if (isExcel) {
|
||||||
|
docPreviewExcelHtml.value = await loadExcelHtmlFromUrl(url);
|
||||||
|
}
|
||||||
docPreviewDialogVisible.value = true;
|
docPreviewDialogVisible.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -4468,7 +4536,7 @@ const openDocPicturePreview = async (ossKey: string, fileNameHint = "") => {
|
|||||||
};
|
};
|
||||||
const handleDocPicturePreview = async (
|
const handleDocPicturePreview = async (
|
||||||
uploadFile: Parameters<NonNullable<UploadProps["onPreview"]>>[0],
|
uploadFile: Parameters<NonNullable<UploadProps["onPreview"]>>[0],
|
||||||
target: "agenda" | "signIn" | "themePhoto" | "invitation",
|
target: "agenda" | "signIn" | "themePhoto" | "invitation" | "meetingVideo",
|
||||||
) => {
|
) => {
|
||||||
const row = uploadFile as UploadUserFile & { ossKey?: string };
|
const row = uploadFile as UploadUserFile & { ossKey?: string };
|
||||||
const fallbackOssKey = target === "agenda"
|
const fallbackOssKey = target === "agenda"
|
||||||
@ -4486,11 +4554,15 @@ const handleDocPicturePreview = async (
|
|||||||
}
|
}
|
||||||
const url = String(uploadFile?.url || "").trim();
|
const url = String(uploadFile?.url || "").trim();
|
||||||
if (url) {
|
if (url) {
|
||||||
const rawMime = String((row?.raw as File | undefined)?.type || "").trim();
|
const rawFile = row?.raw as File | undefined;
|
||||||
const isPdf =
|
const rawMime = String(rawFile?.type || "").trim();
|
||||||
isPdfDocFile(nameHint, "") || rawMime === "application/pdf";
|
const isExcel = isExcelDocFile(nameHint, "");
|
||||||
docPreviewDialogKind.value = isPdf ? "pdf" : "image";
|
const isPdf = isPdfDocFile(nameHint, "") || rawMime === "application/pdf";
|
||||||
|
docPreviewDialogKind.value = isExcel ? "excel" : isPdf ? "pdf" : "image";
|
||||||
docPreviewDialogImageUrl.value = url;
|
docPreviewDialogImageUrl.value = url;
|
||||||
|
if (isExcel && rawFile) {
|
||||||
|
docPreviewExcelHtml.value = await loadExcelHtmlFromFile(rawFile);
|
||||||
|
}
|
||||||
docPreviewDialogVisible.value = true;
|
docPreviewDialogVisible.value = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -4604,7 +4676,7 @@ const removeInvitationDoc = (index: number) => {
|
|||||||
|
|
||||||
const MATERIAL_ACCEPT_EXT = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".pdf", ".tif", ".tiff", ".doc", ".docx", ".xls", ".xlsx", ".ofd"]);
|
const MATERIAL_ACCEPT_EXT = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".pdf", ".tif", ".tiff", ".doc", ".docx", ".xls", ".xlsx", ".ofd"]);
|
||||||
const EXPERT_PROFILE_ACCEPT_EXT = new Set([".pdf", ".ppt", ".pptx"]);
|
const EXPERT_PROFILE_ACCEPT_EXT = new Set([".pdf", ".ppt", ".pptx"]);
|
||||||
const MATERIAL_MAX_FILE_SIZE_MB = 2;
|
const MATERIAL_MAX_FILE_SIZE_MB = 5;
|
||||||
const MATERIAL_MAX_FILE_SIZE_BYTES = MATERIAL_MAX_FILE_SIZE_MB * 1024 * 1024;
|
const MATERIAL_MAX_FILE_SIZE_BYTES = MATERIAL_MAX_FILE_SIZE_MB * 1024 * 1024;
|
||||||
const isMaterialUploadAllowed = (file?: File | null) => {
|
const isMaterialUploadAllowed = (file?: File | null) => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
@ -4639,11 +4711,11 @@ const validateMaterialFile = (file: File | null, label: string, maxSizeMb = MATE
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!isMaterialUploadAllowed(file)) {
|
if (!isMaterialUploadAllowed(file)) {
|
||||||
ElMessage.warning(`${label}仅支持图片或PDF文件`);
|
ElMessage.warning(`${label}仅支持图片、PDF或Excel文件`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (Number(file.size || 0) > maxSizeMb * 1024 * 1024) {
|
if (Number(file.size || 0) > maxSizeMb * 1024 * 1024) {
|
||||||
ElMessage.warning(`${label}澶у皬涓嶈兘瓒呰繃${maxSizeMb}MB`);
|
ElMessage.warning(`${label}大小不能超过${maxSizeMb}MB`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -4666,6 +4738,50 @@ const validateExpertProfileFile = (file: File | null, maxSizeMb = MATERIAL_MAX_F
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateVideoFile = (file: File | null, label: string, maxSizeMb = 500) => {
|
||||||
|
if (!file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const ext = extractExt(file.name || "");
|
||||||
|
const mime = String(file.type || "").toLowerCase();
|
||||||
|
const allowedExts = new Set([".mp4", ".mov", ".avi", ".mkv"]);
|
||||||
|
if (!allowedExts.has(ext) && !mime.startsWith("video/")) {
|
||||||
|
ElMessage.warning(`${label}仅支持视频文件(mp4/mov/avi/mkv)`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Number(file.size || 0) > maxSizeMb * 1024 * 1024) {
|
||||||
|
ElMessage.warning(`${label}大小不能超过${maxSizeMb}MB`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeMeetingVideoUpload = (rawFile: any, maxSizeMb = 500) =>
|
||||||
|
validateVideoFile((rawFile as File) || null, "会议视频", maxSizeMb);
|
||||||
|
|
||||||
|
const handleMeetingVideoChange = async (uploadFile: any, uploadFiles: any, maxSizeMb = 500) => {
|
||||||
|
if (isReviewApproved("meetingVideo")) {
|
||||||
|
warnReviewedMaterialLocked();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = (uploadFile?.raw as File) || null;
|
||||||
|
meetingVideoFile.value = validateVideoFile(file, "会议视频", maxSizeMb) ? file : null;
|
||||||
|
if (!meetingVideoFile.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await uploadDocFile("meetingVideo");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMeetingVideoRemove = () => {
|
||||||
|
if (isReviewApproved("meetingVideo")) {
|
||||||
|
warnReviewedMaterialLocked();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
docForm.value.videoName = "";
|
||||||
|
docForm.value.videoOssKey = "";
|
||||||
|
};
|
||||||
|
|
||||||
const beforeAgendaUpload = (rawFile: any, maxSizeMb?: number) => {
|
const beforeAgendaUpload = (rawFile: any, maxSizeMb?: number) => {
|
||||||
if (!validateMaterialFile((rawFile as File) || null, "会议日程", maxSizeMb)) {
|
if (!validateMaterialFile((rawFile as File) || null, "会议日程", maxSizeMb)) {
|
||||||
return false;
|
return false;
|
||||||
@ -4771,7 +4887,7 @@ const handleExpertProfileFileChange = async (uploadFile: any, uploadFiles: any,
|
|||||||
const uploadUrl = signResp?.data?.uploadUrl;
|
const uploadUrl = signResp?.data?.uploadUrl;
|
||||||
const objectKey = signResp?.data?.objectKey;
|
const objectKey = signResp?.data?.objectKey;
|
||||||
if (!uploadUrl || !objectKey) {
|
if (!uploadUrl || !objectKey) {
|
||||||
ElMessage.error("鑾峰彇涓婁紶绛惧悕澶辫触");
|
ElMessage.error("获取上传签名失败");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await putToSignedUrl(uploadUrl, file, signResp?.data?.contentType || file.type);
|
await putToSignedUrl(uploadUrl, file, signResp?.data?.contentType || file.type);
|
||||||
@ -4891,7 +5007,7 @@ const handleLaborInvoiceFileRemove: UploadProps["onRemove"] = async (uploadFile)
|
|||||||
laborInvoiceUploadFiles.value = [];
|
laborInvoiceUploadFiles.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadDocFile = async (target: "agenda" | "signIn" | "themePhoto" | "invitation") => {
|
const uploadDocFile = async (target: "agenda" | "signIn" | "themePhoto" | "invitation" | "meetingVideo") => {
|
||||||
if (!selectedMeetingId.value) {
|
if (!selectedMeetingId.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -4901,7 +5017,9 @@ const uploadDocFile = async (target: "agenda" | "signIn" | "themePhoto" | "invit
|
|||||||
? signInFile.value
|
? signInFile.value
|
||||||
: target === "themePhoto"
|
: target === "themePhoto"
|
||||||
? themePhotoFile.value
|
? themePhotoFile.value
|
||||||
: invitationFile.value;
|
: target === "meetingVideo"
|
||||||
|
? meetingVideoFile.value
|
||||||
|
: invitationFile.value;
|
||||||
if (!file) {
|
if (!file) {
|
||||||
ElMessage.warning("请先选择文件");
|
ElMessage.warning("请先选择文件");
|
||||||
return;
|
return;
|
||||||
@ -4914,7 +5032,7 @@ const uploadDocFile = async (target: "agenda" | "signIn" | "themePhoto" | "invit
|
|||||||
const objectKey = signResp?.data?.objectKey;
|
const objectKey = signResp?.data?.objectKey;
|
||||||
const contentType = signResp?.data?.contentType || file.type || "application/octet-stream";
|
const contentType = signResp?.data?.contentType || file.type || "application/octet-stream";
|
||||||
if (!uploadUrl || !objectKey) {
|
if (!uploadUrl || !objectKey) {
|
||||||
ElMessage.error("鑾峰彇涓婁紶绛惧悕澶辫触");
|
ElMessage.error("获取上传签名失败");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await putToSignedUrl(uploadUrl, file, contentType);
|
await putToSignedUrl(uploadUrl, file, contentType);
|
||||||
@ -4930,6 +5048,10 @@ const uploadDocFile = async (target: "agenda" | "signIn" | "themePhoto" | "invit
|
|||||||
docForm.value.themePhotoName = file.name;
|
docForm.value.themePhotoName = file.name;
|
||||||
docForm.value.themePhotoOssKey = objectKey;
|
docForm.value.themePhotoOssKey = objectKey;
|
||||||
themePhotoFile.value = null;
|
themePhotoFile.value = null;
|
||||||
|
} else if (target === "meetingVideo") {
|
||||||
|
docForm.value.videoName = file.name;
|
||||||
|
docForm.value.videoOssKey = objectKey;
|
||||||
|
meetingVideoFile.value = null;
|
||||||
} else {
|
} else {
|
||||||
const line = `${file.name}|${objectKey}`;
|
const line = `${file.name}|${objectKey}`;
|
||||||
docForm.value.invitationLines = docForm.value.invitationLines ? `${docForm.value.invitationLines}\n${line}` : line;
|
docForm.value.invitationLines = docForm.value.invitationLines ? `${docForm.value.invitationLines}\n${line}` : line;
|
||||||
@ -5060,7 +5182,7 @@ const onMeetingInvoiceSectionFileChange = async (
|
|||||||
const uploadUrl = signResp?.data?.uploadUrl;
|
const uploadUrl = signResp?.data?.uploadUrl;
|
||||||
const objectKey = signResp?.data?.objectKey;
|
const objectKey = signResp?.data?.objectKey;
|
||||||
if (!uploadUrl || !objectKey) {
|
if (!uploadUrl || !objectKey) {
|
||||||
ElMessage.error("鑾峰彇涓婁紶绛惧悕澶辫触");
|
ElMessage.error("获取上传签名失败");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await putToSignedUrl(uploadUrl, file, signResp?.data?.contentType || file.type);
|
await putToSignedUrl(uploadUrl, file, signResp?.data?.contentType || file.type);
|
||||||
@ -5246,7 +5368,7 @@ const uploadLaborProtocolFile = async () => {
|
|||||||
const uploadUrl = signResp?.data?.uploadUrl;
|
const uploadUrl = signResp?.data?.uploadUrl;
|
||||||
const objectKey = signResp?.data?.objectKey;
|
const objectKey = signResp?.data?.objectKey;
|
||||||
if (!uploadUrl || !objectKey) {
|
if (!uploadUrl || !objectKey) {
|
||||||
ElMessage.error("鑾峰彇涓婁紶绛惧悕澶辫触");
|
ElMessage.error("获取上传签名失败");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await putToSignedUrl(uploadUrl, laborProtocolUploadFile.value, signResp?.data?.contentType || laborProtocolUploadFile.value.type);
|
await putToSignedUrl(uploadUrl, laborProtocolUploadFile.value, signResp?.data?.contentType || laborProtocolUploadFile.value.type);
|
||||||
@ -5284,7 +5406,7 @@ const uploadLaborInvoiceFile = async () => {
|
|||||||
const uploadUrl = signResp?.data?.uploadUrl;
|
const uploadUrl = signResp?.data?.uploadUrl;
|
||||||
const objectKey = signResp?.data?.objectKey;
|
const objectKey = signResp?.data?.objectKey;
|
||||||
if (!uploadUrl || !objectKey) {
|
if (!uploadUrl || !objectKey) {
|
||||||
ElMessage.error("鑾峰彇涓婁紶绛惧悕澶辫触");
|
ElMessage.error("获取上传签名失败");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await putToSignedUrl(uploadUrl, localFile, signResp?.data?.contentType || localFile.type);
|
await putToSignedUrl(uploadUrl, localFile, signResp?.data?.contentType || localFile.type);
|
||||||
@ -5463,7 +5585,7 @@ const resetMaterialForms = () => {
|
|||||||
improvementSuggestion: "",
|
improvementSuggestion: "",
|
||||||
meetingEffect: "",
|
meetingEffect: "",
|
||||||
};
|
};
|
||||||
docForm.value = { agendaLines: "", signInName: "", signInOssKey: "", themePhotoName: "", themePhotoOssKey: "", invitationLines: "" };
|
docForm.value = { agendaLines: "", signInName: "", signInOssKey: "", themePhotoName: "", themePhotoOssKey: "", invitationLines: "", videoType: "file", videoName: "", videoOssKey: "", videoLink: "" };
|
||||||
expertProfileForm.value = { fileName: "", ossKey: "" };
|
expertProfileForm.value = { fileName: "", ossKey: "" };
|
||||||
photoForm.value = { photoLines: "", summary: "" };
|
photoForm.value = { photoLines: "", summary: "" };
|
||||||
photoSummaryByExpert.value = {};
|
photoSummaryByExpert.value = {};
|
||||||
@ -5900,6 +6022,10 @@ const loadMaterialModule = async (
|
|||||||
docForm.value.signInOssKey = parsed.signInSheet?.ossKey || "";
|
docForm.value.signInOssKey = parsed.signInSheet?.ossKey || "";
|
||||||
docForm.value.themePhotoName = resolveStoredDocFileName(parsed.themePhoto);
|
docForm.value.themePhotoName = resolveStoredDocFileName(parsed.themePhoto);
|
||||||
docForm.value.themePhotoOssKey = parsed.themePhoto?.ossKey || "";
|
docForm.value.themePhotoOssKey = parsed.themePhoto?.ossKey || "";
|
||||||
|
docForm.value.videoType = parsed.meetingVideo?.type || "file";
|
||||||
|
docForm.value.videoName = resolveStoredDocFileName(parsed.meetingVideo) || String(parsed.meetingVideo?.name || "");
|
||||||
|
docForm.value.videoOssKey = parsed.meetingVideo?.ossKey || "";
|
||||||
|
docForm.value.videoLink = parsed.meetingVideo?.link || "";
|
||||||
expertProfileForm.value.fileName = resolveStoredDocFileName(parsed?.profileFile) || String(parsed?.fileName || "").trim();
|
expertProfileForm.value.fileName = resolveStoredDocFileName(parsed?.profileFile) || String(parsed?.fileName || "").trim();
|
||||||
expertProfileForm.value.ossKey = String(parsed?.profileFile?.ossKey || parsed?.ossKey || "").trim();
|
expertProfileForm.value.ossKey = String(parsed?.profileFile?.ossKey || parsed?.ossKey || "").trim();
|
||||||
const invitationList = Array.isArray(parsed.invitation) ? parsed.invitation : [];
|
const invitationList = Array.isArray(parsed.invitation) ? parsed.invitation : [];
|
||||||
@ -5913,6 +6039,10 @@ const loadMaterialModule = async (
|
|||||||
docForm.value.signInOssKey = parsed.signInSheet?.ossKey || "";
|
docForm.value.signInOssKey = parsed.signInSheet?.ossKey || "";
|
||||||
docForm.value.themePhotoName = resolveStoredDocFileName(parsed.themePhoto);
|
docForm.value.themePhotoName = resolveStoredDocFileName(parsed.themePhoto);
|
||||||
docForm.value.themePhotoOssKey = parsed.themePhoto?.ossKey || "";
|
docForm.value.themePhotoOssKey = parsed.themePhoto?.ossKey || "";
|
||||||
|
docForm.value.videoType = parsed.meetingVideo?.type || "file";
|
||||||
|
docForm.value.videoName = resolveStoredDocFileName(parsed.meetingVideo) || String(parsed.meetingVideo?.name || "");
|
||||||
|
docForm.value.videoOssKey = parsed.meetingVideo?.ossKey || "";
|
||||||
|
docForm.value.videoLink = parsed.meetingVideo?.link || "";
|
||||||
const invitationList = Array.isArray(parsed.invitation) ? parsed.invitation : [];
|
const invitationList = Array.isArray(parsed.invitation) ? parsed.invitation : [];
|
||||||
docForm.value.invitationLines = invitationList
|
docForm.value.invitationLines = invitationList
|
||||||
.map((x: any) => `${resolveStoredDocFileName(x)}|${x?.ossKey || ""}`.trim())
|
.map((x: any) => `${resolveStoredDocFileName(x)}|${x?.ossKey || ""}`.trim())
|
||||||
@ -6277,6 +6407,7 @@ const openMaterial = (row: any, moduleCode: MaterialModuleCode = "BASIC_INFO") =
|
|||||||
});
|
});
|
||||||
selectedMeetingId.value = meetingId;
|
selectedMeetingId.value = meetingId;
|
||||||
selectedModuleCode.value = initialModuleCode;
|
selectedModuleCode.value = initialModuleCode;
|
||||||
|
materialLastRejectReason.value = String(row?.lastRejectReason || "");
|
||||||
materialDialogVisible.value = true;
|
materialDialogVisible.value = true;
|
||||||
clearMaterialAuditTaskDetailCache();
|
clearMaterialAuditTaskDetailCache();
|
||||||
startMaterialModuleLoad(meetingId, initialModuleCode, { reloadExperts: true });
|
startMaterialModuleLoad(meetingId, initialModuleCode, { reloadExperts: true });
|
||||||
@ -6801,6 +6932,13 @@ const buildContentJson = () => {
|
|||||||
fileName: String(docForm.value.themePhotoName || "").trim(),
|
fileName: String(docForm.value.themePhotoName || "").trim(),
|
||||||
ossKey: String(docForm.value.themePhotoOssKey || "").trim(),
|
ossKey: String(docForm.value.themePhotoOssKey || "").trim(),
|
||||||
},
|
},
|
||||||
|
meetingVideo: {
|
||||||
|
type: docForm.value.videoType,
|
||||||
|
name: String(docForm.value.videoName || "").trim(),
|
||||||
|
fileName: String(docForm.value.videoName || "").trim(),
|
||||||
|
ossKey: String(docForm.value.videoOssKey || "").trim(),
|
||||||
|
link: String(docForm.value.videoLink || "").trim(),
|
||||||
|
},
|
||||||
invitation: (docForm.value.invitationLines || "")
|
invitation: (docForm.value.invitationLines || "")
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
@ -6848,6 +6986,13 @@ const buildContentJson = () => {
|
|||||||
fileName: String(docForm.value.themePhotoName || "").trim(),
|
fileName: String(docForm.value.themePhotoName || "").trim(),
|
||||||
ossKey: String(docForm.value.themePhotoOssKey || "").trim(),
|
ossKey: String(docForm.value.themePhotoOssKey || "").trim(),
|
||||||
},
|
},
|
||||||
|
meetingVideo: {
|
||||||
|
type: docForm.value.videoType,
|
||||||
|
name: String(docForm.value.videoName || "").trim(),
|
||||||
|
fileName: String(docForm.value.videoName || "").trim(),
|
||||||
|
ossKey: String(docForm.value.videoOssKey || "").trim(),
|
||||||
|
link: String(docForm.value.videoLink || "").trim(),
|
||||||
|
},
|
||||||
invitation: invitations,
|
invitation: invitations,
|
||||||
profileFile: {
|
profileFile: {
|
||||||
name: String(expertProfileForm.value.fileName || "").trim(),
|
name: String(expertProfileForm.value.fileName || "").trim(),
|
||||||
|
|||||||
@ -31,6 +31,7 @@
|
|||||||
<div class="avatar-text">
|
<div class="avatar-text">
|
||||||
<h3>{{ userName }}</h3>
|
<h3>{{ userName }}</h3>
|
||||||
<p>{{ phone }}</p>
|
<p>{{ phone }}</p>
|
||||||
|
<p style="margin:4px 0 0;font-size:13px;color:var(--el-text-color-secondary);">{{ email }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -197,7 +198,7 @@ import {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const appearanceStore = useAppearanceStore();
|
const appearanceStore = useAppearanceStore();
|
||||||
const { scope, tenantCode, tenantName: storedTenantName, userName: storedUserName, phone: storedPhone } = storeToRefs(authStore);
|
const { scope, tenantCode, tenantName: storedTenantName, userName: storedUserName, phone: storedPhone, email: storedEmail } = storeToRefs(authStore);
|
||||||
const { themeMode, density, themeScheme } = storeToRefs(appearanceStore);
|
const { themeMode, density, themeScheme } = storeToRefs(appearanceStore);
|
||||||
|
|
||||||
const authScope = computed(() => scope.value);
|
const authScope = computed(() => scope.value);
|
||||||
@ -205,6 +206,7 @@ const tenantCodeDisplay = computed(() => tenantCode.value || "-");
|
|||||||
const tenantName = computed(() => storedTenantName.value || (authScope.value === "PLATFORM" ? "平台侧" : "尚未关联"));
|
const tenantName = computed(() => storedTenantName.value || (authScope.value === "PLATFORM" ? "平台侧" : "尚未关联"));
|
||||||
const userName = computed(() => storedUserName.value || "未知用户");
|
const userName = computed(() => storedUserName.value || "未知用户");
|
||||||
const phone = computed(() => storedPhone.value || "未设置");
|
const phone = computed(() => storedPhone.value || "未设置");
|
||||||
|
const email = computed(() => storedEmail.value && storedEmail.value !== "null" ? storedEmail.value : "暂无邮箱");
|
||||||
|
|
||||||
const themeModeLabelMap: Record<string, string> = {
|
const themeModeLabelMap: Record<string, string> = {
|
||||||
SYSTEM: "跟随系统",
|
SYSTEM: "跟随系统",
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<PageContainer title="项目管理">
|
<PageContainer title="项目管理">
|
||||||
<QueryToolbar>
|
<QueryToolbar>
|
||||||
<el-form :inline="true" @submit.prevent class="project-page-toolbar">
|
<el-form :inline="true" @submit.prevent class="project-page-toolbar">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-input v-model="queryName" placeholder="搜索项目名称" clearable class="w-input-md" />
|
<el-input v-model="queryName" placeholder="搜索项目名称" clearable class="w-input-md" @clear="load" @keyup.enter="load" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<!-- <el-form-item>
|
<!-- <el-form-item>
|
||||||
<el-checkbox v-model="includeDeleted">包含已删除</el-checkbox>
|
<el-checkbox v-model="includeDeleted">包含已删除</el-checkbox>
|
||||||
</el-form-item> -->
|
</el-form-item> -->
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button v-if="canCreate" type="primary" @click="openCreateDrawer">新建项目</el-button>
|
<el-button v-if="canCreate" type="primary" @click="openCreateDrawer">新建项目</el-button>
|
||||||
|
<el-button type="primary" @click="load">查询</el-button>
|
||||||
<el-button @click="load">刷新</el-button>
|
<el-button @click="load">刷新</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@ -35,14 +36,14 @@
|
|||||||
</el-table-column> -->
|
</el-table-column> -->
|
||||||
<el-table-column prop="budgetCent" label="预算(元)" :formatter="budgetFormatter" />
|
<el-table-column prop="budgetCent" label="预算(元)" :formatter="budgetFormatter" />
|
||||||
<el-table-column label="费用合计(元)" :formatter="projectFeeTotalFormatter" />
|
<el-table-column label="费用合计(元)" :formatter="projectFeeTotalFormatter" />
|
||||||
<el-table-column prop="budgetExecutionRatio" label="预算执行率" :formatter="executionRatioFormatter" />
|
<!-- <el-table-column prop="budgetExecutionRatio" label="预算执行率" :formatter="executionRatioFormatter" /> -->
|
||||||
<el-table-column prop="meetingTotal" label="会议总期数" width="100"/>
|
<el-table-column prop="meetingTotal" label="会议总期数" width="100"/>
|
||||||
<el-table-column label="项目周期" width="200">
|
<el-table-column label="项目周期" width="200">
|
||||||
<template #default="{ row }">{{ row.startDate || "-" }} ~ {{ row.endDate || "-" }}</template>
|
<template #default="{ row }">{{ row.startDate || "-" }} ~ {{ row.endDate || "-" }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column prop="status" label="状态" :formatter="statusFormatter" width="100"/>
|
<el-table-column prop="status" label="状态" :formatter="statusFormatter" width="100"/>
|
||||||
<el-table-column label="操作" width="200">
|
<el-table-column label="操作" width="370">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button v-if="canCreate" size="small" type="primary" @click="openCreateDrawer(row)">添加子项目</el-button>
|
<el-button v-if="canCreate" size="small" type="primary" @click="openCreateDrawer(row)">添加子项目</el-button>
|
||||||
<el-button size="small" @click="openDetailDrawer(row)">详情</el-button>
|
<el-button size="small" @click="openDetailDrawer(row)">详情</el-button>
|
||||||
@ -56,7 +57,19 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<ProjectEditDrawer
|
<div class="mt-md flex justify-end" style="margin-top: 16px; display: flex; justify-content: flex-end;">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pageNo"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="total"
|
||||||
|
@size-change="load"
|
||||||
|
@current-change="load"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProjectEditDrawer
|
||||||
v-model="projectDrawerVisible"
|
v-model="projectDrawerVisible"
|
||||||
:mode="projectDrawerMode"
|
:mode="projectDrawerMode"
|
||||||
:source-row="projectDrawerSourceRow"
|
:source-row="projectDrawerSourceRow"
|
||||||
@ -87,17 +100,17 @@
|
|||||||
<el-descriptions-item label="合作企业负责人">{{ currentProject.partnerOwnerUsers || "-" }}</el-descriptions-item>
|
<el-descriptions-item label="合作企业负责人">{{ currentProject.partnerOwnerUsers || "-" }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="合作企业项目执行人">{{ currentProject.partnerExecutorUsers || "-" }}</el-descriptions-item>
|
<el-descriptions-item label="合作企业项目执行人">{{ currentProject.partnerExecutorUsers || "-" }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="项目周期">{{ currentProject.startDate || "-" }} ~ {{ currentProject.endDate || "-" }}</el-descriptions-item>
|
<el-descriptions-item label="项目周期">{{ currentProject.startDate || "-" }} ~ {{ currentProject.endDate || "-" }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="项目预算(元)">{{ toYuan(currentProject.budgetCent) }}</el-descriptions-item>
|
<el-descriptions-item label="项目预算(元)">{{ canSensitiveDataRead ? toYuan(currentProject.budgetCent) : '***' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="劳务费协议签署类型">{{ formatLaborAgreementSignType(currentProject.laborAgreementSignType) }}</el-descriptions-item>
|
<el-descriptions-item label="劳务费协议签署类型">{{ formatLaborAgreementSignType(currentProject.laborAgreementSignType) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="管理费(元)">{{ toYuan(currentProjectFee.managementFeeCent) }}</el-descriptions-item>
|
<el-descriptions-item label="管理费(元)">{{ canSensitiveDataRead ? toYuan(currentProjectFee.managementFeeCent) : '***' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="税费(元)">{{ toYuan(currentProjectFee.taxFeeCent) }}</el-descriptions-item>
|
<el-descriptions-item label="税费(元)">{{ canSensitiveDataRead ? toYuan(currentProjectFee.taxFeeCent) : '***' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="到款金额(元)">{{ toYuan(currentProjectFee.paidAmountCent) }}</el-descriptions-item>
|
<el-descriptions-item label="到款金额(元)">{{ canSensitiveDataRead ? toYuan(currentProjectFee.paidAmountCent) : '***' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="自定义费用(元)">{{ customFeeText }}</el-descriptions-item>
|
<el-descriptions-item label="自定义费用(元)">{{ customFeeText }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="会议总期数">{{ currentProject.meetingTotal || 0 }}</el-descriptions-item>
|
<el-descriptions-item label="会议总期数">{{ currentProject.meetingTotal || 0 }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="完成期数">{{ currentProject.meetingCompletedCount || 0 }}</el-descriptions-item>
|
<el-descriptions-item label="完成期数">{{ currentProject.meetingCompletedCount || 0 }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="项目状态">{{ toZhStatus(currentProject.status) }}</el-descriptions-item>
|
<el-descriptions-item label="项目状态">{{ toZhStatus(currentProject.status) }}</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="预算执行率">{{ formatExecutionRatio(currentProject.budgetExecutionRatio) }}</el-descriptions-item>
|
<!-- <el-descriptions-item label="预算执行率">{{ formatExecutionRatio(currentProject.budgetExecutionRatio) }}</el-descriptions-item> -->
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
<el-divider v-if="canKeyChangeLogRead">关键变更日志</el-divider>
|
<el-divider v-if="canKeyChangeLogRead">关键变更日志</el-divider>
|
||||||
<el-table v-if="canKeyChangeLogRead" :data="keyChangeLogs" style="width: 100%">
|
<el-table v-if="canKeyChangeLogRead" :data="keyChangeLogs" style="width: 100%">
|
||||||
@ -179,6 +192,7 @@ import ProjectEditDrawer from "./project-page/ProjectEditDrawer.vue";
|
|||||||
import MeetingEditDrawer from "./meeting-page/MeetingEditDrawer.vue";
|
import MeetingEditDrawer from "./meeting-page/MeetingEditDrawer.vue";
|
||||||
import {
|
import {
|
||||||
createMeeting,
|
createMeeting,
|
||||||
|
fetchDefaultMeetingBudgetCent,
|
||||||
fetchDictionaries,
|
fetchDictionaries,
|
||||||
fetchMeetings,
|
fetchMeetings,
|
||||||
fetchProjectBindingCandidates,
|
fetchProjectBindingCandidates,
|
||||||
@ -198,6 +212,9 @@ import { toZhStatus } from "../../utils/status";
|
|||||||
const rows = ref<any[]>([]);
|
const rows = ref<any[]>([]);
|
||||||
const queryName = ref("");
|
const queryName = ref("");
|
||||||
const includeDeleted = ref(false);
|
const includeDeleted = ref(false);
|
||||||
|
const pageNo = ref(1);
|
||||||
|
const pageSize = ref(20);
|
||||||
|
const total = ref(0);
|
||||||
const projectDrawerVisible = ref(false);
|
const projectDrawerVisible = ref(false);
|
||||||
const projectDrawerMode = ref<"create" | "edit">("create");
|
const projectDrawerMode = ref<"create" | "edit">("create");
|
||||||
const projectDrawerSourceRow = ref<any | null>(null);
|
const projectDrawerSourceRow = ref<any | null>(null);
|
||||||
@ -232,6 +249,7 @@ const canArchive = computed(() => authStore.hasPermission(PERMS.project.archive)
|
|||||||
const canBindUser = computed(() => authStore.hasPermission(PERMS.project.bindUser));
|
const canBindUser = computed(() => authStore.hasPermission(PERMS.project.bindUser));
|
||||||
const canBindExecutorUser = computed(() => authStore.hasPermission(PERMS.project.bindExecutorUser));
|
const canBindExecutorUser = computed(() => authStore.hasPermission(PERMS.project.bindExecutorUser));
|
||||||
const canKeyChangeLogRead = computed(() => authStore.hasPermission(PERMS.project.keyChangeLogRead));
|
const canKeyChangeLogRead = computed(() => authStore.hasPermission(PERMS.project.keyChangeLogRead));
|
||||||
|
const canSensitiveDataRead = computed(() => authStore.hasPermission(PERMS.project.sensitiveDataRead));
|
||||||
const currentRoles = computed(() => authStore.roles || []);
|
const currentRoles = computed(() => authStore.roles || []);
|
||||||
const currentUserId = computed(() => Number(authStore.userId || 0) || null);
|
const currentUserId = computed(() => Number(authStore.userId || 0) || null);
|
||||||
const isProjectExecutorRole = computed(() => currentRoles.value.includes("PROJECT_EXECUTOR"));
|
const isProjectExecutorRole = computed(() => currentRoles.value.includes("PROJECT_EXECUTOR"));
|
||||||
@ -285,13 +303,13 @@ const statusFormatter = (_row: any, _column: any, value: unknown) => toZhStatus(
|
|||||||
const toYuan = (cent: unknown) => Number(((Number(cent) || 0) / 100).toFixed(2));
|
const toYuan = (cent: unknown) => Number(((Number(cent) || 0) / 100).toFixed(2));
|
||||||
const formatLaborAgreementSignType = (value: unknown) => Number(value) === 2 ? "线下签" : "放心签";
|
const formatLaborAgreementSignType = (value: unknown) => Number(value) === 2 ? "线下签" : "放心签";
|
||||||
const formatExecutionRatio = (ratio: unknown) => `${((Number(ratio) || 0) * 100).toFixed(2)}%`;
|
const formatExecutionRatio = (ratio: unknown) => `${((Number(ratio) || 0) * 100).toFixed(2)}%`;
|
||||||
const budgetFormatter = (_row: any, _column: any, value: unknown) => toYuan(value);
|
const budgetFormatter = (_row: any, _column: any, value: unknown) => canSensitiveDataRead.value ? toYuan(value) : '***';
|
||||||
const executionRatioFormatter = (_row: any, _column: any, value: unknown) => formatExecutionRatio(value);
|
const executionRatioFormatter = (_row: any, _column: any, value: unknown) => formatExecutionRatio(value);
|
||||||
const projectFeeTotalFormatter = (row: any) => {
|
const projectFeeTotalFormatter = (row: any) => {
|
||||||
|
if (!canSensitiveDataRead.value) return '***';
|
||||||
const fee = parseProjectFee(row?.projectFeeJson);
|
const fee = parseProjectFee(row?.projectFeeJson);
|
||||||
const total = (fee.managementFeeCent || 0)
|
const total = (fee.managementFeeCent || 0)
|
||||||
+ (fee.taxFeeCent || 0)
|
+ (fee.taxFeeCent || 0)
|
||||||
+ (fee.paidAmountCent || 0)
|
|
||||||
+ (fee.customFees || []).reduce((acc: number, cur: { amountCent: number }) => acc + (cur.amountCent || 0), 0);
|
+ (fee.customFees || []).reduce((acc: number, cur: { amountCent: number }) => acc + (cur.amountCent || 0), 0);
|
||||||
return toYuan(total);
|
return toYuan(total);
|
||||||
};
|
};
|
||||||
@ -324,19 +342,18 @@ const parseProjectFee = (projectFeeJson: unknown) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const currentProjectFee = computed(() => parseProjectFee(currentProject.value?.projectFeeJson));
|
const currentProjectFee = computed(() => parseProjectFee(currentProject.value?.projectFeeJson));
|
||||||
const customFeeText = computed(() =>
|
const customFeeText = computed(() => {
|
||||||
currentProjectFee.value.customFees.length
|
if (!canSensitiveDataRead.value) return '***';
|
||||||
|
return currentProjectFee.value.customFees.length
|
||||||
? currentProjectFee.value.customFees.map((item: { name: string; amountCent: number }) => `${item.name}:${toYuan(item.amountCent)}元`).join(";")
|
? currentProjectFee.value.customFees.map((item: { name: string; amountCent: number }) => `${item.name}:${toYuan(item.amountCent)}元`).join(";")
|
||||||
: "-",
|
: "-";
|
||||||
);
|
});
|
||||||
const normalizeProjectRow = (row: any) => ({
|
const normalizeProjectRow = (row: any) => ({
|
||||||
...row,
|
...row,
|
||||||
hasChildren: Number(row?.subProjectCount || 0) > 0,
|
hasChildren: Number(row?.subProjectCount || 0) > 0,
|
||||||
});
|
});
|
||||||
const filteredRows = computed(() =>
|
const filteredRows = computed(() =>
|
||||||
rows.value
|
rows.value.map((row) => normalizeProjectRow(row))
|
||||||
.map((row) => normalizeProjectRow(row))
|
|
||||||
.filter((r) => !queryName.value || String(r.name || "").includes(queryName.value)),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const parseDateOnly = (value: unknown) => {
|
const parseDateOnly = (value: unknown) => {
|
||||||
@ -391,8 +408,9 @@ const meetingCreateFormTimeRange = computed<string[]>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
const resp = await fetchProjects({ parentOnly: true });
|
const resp = await fetchProjects({ parentOnly: true, keyword: queryName.value, pageNo: pageNo.value, pageSize: pageSize.value });
|
||||||
rows.value = resp?.data?.list || [];
|
rows.value = resp?.data?.list || [];
|
||||||
|
total.value = resp?.data?.total || 0;
|
||||||
childrenCache.clear();
|
childrenCache.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -471,13 +489,23 @@ const handleCreateMeeting = async (row: any) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fee = parseProjectFee(row?.projectFeeJson);
|
let defaultMeetingBudgetCent = 0;
|
||||||
const customFeeCent = (fee.customFees || []).reduce((acc: number, cur: { amountCent: number }) => acc + (Number(cur?.amountCent) || 0), 0);
|
if (canSensitiveDataRead.value) {
|
||||||
const distributableBudgetCent = (Number(row?.budgetCent) || 0) - (Number(fee.managementFeeCent) || 0) - (Number(fee.taxFeeCent) || 0) - customFeeCent;
|
const fee = parseProjectFee(row?.projectFeeJson);
|
||||||
const defaultMeetingBudgetCent = Math.floor(distributableBudgetCent / meetingTotal);
|
const customFeeCent = (fee.customFees || []).reduce((acc: number, cur: { amountCent: number }) => acc + (Number(cur?.amountCent) || 0), 0);
|
||||||
if (defaultMeetingBudgetCent <= 0) {
|
const distributableBudgetCent = (Number(row?.budgetCent) || 0) - (Number(fee.managementFeeCent) || 0) - (Number(fee.taxFeeCent) || 0) - customFeeCent;
|
||||||
ElMessage.warning("项目可分配会议预算不足,请先检查项目预算或费用设置");
|
defaultMeetingBudgetCent = Math.floor(distributableBudgetCent / meetingTotal);
|
||||||
return;
|
if (defaultMeetingBudgetCent <= 0) {
|
||||||
|
ElMessage.warning(`项目可分配会议预算不足(剩余可用: ${distributableBudgetCent / 100}元),请先检查项目预算或费用设置`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const resp = await fetchDefaultMeetingBudgetCent(row.id);
|
||||||
|
defaultMeetingBudgetCent = resp?.data?.defaultMeetingBudgetCent || 0;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to fetch default budget", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|||||||
@ -17,6 +17,7 @@ const props = defineProps<{
|
|||||||
getFileUrl: (ossKey: unknown) => string;
|
getFileUrl: (ossKey: unknown) => string;
|
||||||
isImageAttachment: (name: unknown, ossKey: unknown) => boolean;
|
isImageAttachment: (name: unknown, ossKey: unknown) => boolean;
|
||||||
isPdfAttachment: (name: unknown, ossKey: unknown) => boolean;
|
isPdfAttachment: (name: unknown, ossKey: unknown) => boolean;
|
||||||
|
isExcelAttachment: (name: unknown, ossKey: unknown) => boolean;
|
||||||
openFileUrl: (ossKey: unknown) => void;
|
openFileUrl: (ossKey: unknown) => void;
|
||||||
previewOssDocument: (
|
previewOssDocument: (
|
||||||
ossKey: string,
|
ossKey: string,
|
||||||
@ -45,7 +46,10 @@ const previewProfileFile = () => {
|
|||||||
if (!fileKey.value) {
|
if (!fileKey.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (props.isPdfAttachment(props.expertProfileView?.fileName, fileKey.value)) {
|
if (
|
||||||
|
props.isPdfAttachment(props.expertProfileView?.fileName, fileKey.value) ||
|
||||||
|
props.isExcelAttachment(props.expertProfileView?.fileName, fileKey.value)
|
||||||
|
) {
|
||||||
void props.previewOssDocument(fileKey.value, fileName.value);
|
void props.previewOssDocument(fileKey.value, fileName.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -114,6 +118,16 @@ const previewProfileFile = () => {
|
|||||||
<el-icon class="audit-profile-pdf__icon"><Document /></el-icon>
|
<el-icon class="audit-profile-pdf__icon"><Document /></el-icon>
|
||||||
<span class="audit-profile-pdf__label">PDF</span>
|
<span class="audit-profile-pdf__label">PDF</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-else-if="isExcelAttachment(expertProfileView.fileName, fileKey)"
|
||||||
|
class="audit-profile-pdf"
|
||||||
|
type="button"
|
||||||
|
:title="fileName"
|
||||||
|
@click.stop="previewProfileFile"
|
||||||
|
>
|
||||||
|
<el-icon class="audit-profile-pdf__icon"><Document /></el-icon>
|
||||||
|
<span class="audit-profile-pdf__label">EXCEL</span>
|
||||||
|
</button>
|
||||||
<div v-else class="audit-profile-file">FILE</div>
|
<div v-else class="audit-profile-file">FILE</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,7 @@ const props = defineProps<{
|
|||||||
getFileUrl: (ossKey: unknown) => string;
|
getFileUrl: (ossKey: unknown) => string;
|
||||||
isImageAttachment: (name: unknown, ossKey: unknown) => boolean;
|
isImageAttachment: (name: unknown, ossKey: unknown) => boolean;
|
||||||
isPdfAttachment: (name: unknown, ossKey: unknown) => boolean;
|
isPdfAttachment: (name: unknown, ossKey: unknown) => boolean;
|
||||||
|
isExcelAttachment: (name: unknown, ossKey: unknown) => boolean;
|
||||||
openFileUrl: (ossKey: unknown) => void;
|
openFileUrl: (ossKey: unknown) => void;
|
||||||
previewOssDocument: (
|
previewOssDocument: (
|
||||||
ossKey: string,
|
ossKey: string,
|
||||||
@ -289,7 +290,6 @@ const previewLaborInvoice = (row: Record<string, unknown>) => {
|
|||||||
v-if="canReject && !isHigherReview"
|
v-if="canReject && !isHigherReview"
|
||||||
size="small"
|
size="small"
|
||||||
type="danger"
|
type="danger"
|
||||||
link
|
|
||||||
@click.stop="handleRejectMaterialItem(`onsite_summary:${selectedAuditExpertId}`, '现场说明')"
|
@click.stop="handleRejectMaterialItem(`onsite_summary:${selectedAuditExpertId}`, '现场说明')"
|
||||||
>
|
>
|
||||||
不通过
|
不通过
|
||||||
@ -403,7 +403,6 @@ const previewLaborInvoice = (row: Record<string, unknown>) => {
|
|||||||
v-if="canReject && !isHigherReview"
|
v-if="canReject && !isHigherReview"
|
||||||
size="small"
|
size="small"
|
||||||
type="danger"
|
type="danger"
|
||||||
link
|
|
||||||
@click.stop="handleRejectMaterialItem(buildLaborItemKey(currentLaborRow, currentLaborRow.__auditIndex), `劳务协议-${selectedAuditExpertName || ''}${currentLaborRow.__roleLabel ? `-${currentLaborRow.__roleLabel}` : ''}`)"
|
@click.stop="handleRejectMaterialItem(buildLaborItemKey(currentLaborRow, currentLaborRow.__auditIndex), `劳务协议-${selectedAuditExpertName || ''}${currentLaborRow.__roleLabel ? `-${currentLaborRow.__roleLabel}` : ''}`)"
|
||||||
>
|
>
|
||||||
不通过
|
不通过
|
||||||
@ -439,6 +438,18 @@ const previewLaborInvoice = (row: Record<string, unknown>) => {
|
|||||||
<el-icon class="audit-preview-pdf__icon"><Document /></el-icon>
|
<el-icon class="audit-preview-pdf__icon"><Document /></el-icon>
|
||||||
<span class="audit-preview-pdf__label">PDF</span>
|
<span class="audit-preview-pdf__label">PDF</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="audit-preview-pdf"
|
||||||
|
:title="laborProtocolFileLabel(currentLaborRow)"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click.stop="previewLaborProtocol(currentLaborRow)"
|
||||||
|
@keydown.enter.prevent="previewLaborProtocol(currentLaborRow)"
|
||||||
|
>
|
||||||
|
<el-icon class="audit-preview-pdf__icon"><Document /></el-icon>
|
||||||
|
<span class="audit-preview-pdf__label">DOC</span>
|
||||||
|
</div>
|
||||||
<div class="audit-attachment-panel__name" :title="laborProtocolFileLabel(currentLaborRow)">
|
<div class="audit-attachment-panel__name" :title="laborProtocolFileLabel(currentLaborRow)">
|
||||||
{{ laborProtocolFileLabel(currentLaborRow) }}
|
{{ laborProtocolFileLabel(currentLaborRow) }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -102,6 +102,7 @@ const props = defineProps<{
|
|||||||
getFileUrl: (ossKey: unknown) => string;
|
getFileUrl: (ossKey: unknown) => string;
|
||||||
isImageAttachment: (name: unknown, ossKey: unknown) => boolean;
|
isImageAttachment: (name: unknown, ossKey: unknown) => boolean;
|
||||||
isPdfAttachment: (name: unknown, ossKey: unknown) => boolean;
|
isPdfAttachment: (name: unknown, ossKey: unknown) => boolean;
|
||||||
|
isExcelAttachment: (name: unknown, ossKey: unknown) => boolean;
|
||||||
openFileUrl: (ossKey: unknown) => void;
|
openFileUrl: (ossKey: unknown) => void;
|
||||||
previewOssDocument: (
|
previewOssDocument: (
|
||||||
ossKey: string,
|
ossKey: string,
|
||||||
@ -666,11 +667,11 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="audit-full-material-section">
|
<div class="audit-full-material-section">
|
||||||
<div class="audit-full-material-section__head">
|
<!-- <div class="audit-full-material-section__head">
|
||||||
<div>
|
<div>
|
||||||
<div class="audit-full-material-section__title">完整资料</div>
|
<div class="audit-full-material-section__title"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<el-tabs
|
<el-tabs
|
||||||
v-model="materialModule"
|
v-model="materialModule"
|
||||||
@ -738,6 +739,7 @@ watch(
|
|||||||
:get-file-url="getFileUrl"
|
:get-file-url="getFileUrl"
|
||||||
:is-image-attachment="isImageAttachment"
|
:is-image-attachment="isImageAttachment"
|
||||||
:is-pdf-attachment="isPdfAttachment"
|
:is-pdf-attachment="isPdfAttachment"
|
||||||
|
:is-excel-attachment="isExcelAttachment"
|
||||||
:open-file-url="openFileUrl"
|
:open-file-url="openFileUrl"
|
||||||
:preview-oss-document="previewOssDocument"
|
:preview-oss-document="previewOssDocument"
|
||||||
:handle-reject-material-item="handleRejectMaterialItem"
|
:handle-reject-material-item="handleRejectMaterialItem"
|
||||||
@ -757,6 +759,7 @@ watch(
|
|||||||
:get-file-url="getFileUrl"
|
:get-file-url="getFileUrl"
|
||||||
:is-image-attachment="isImageAttachment"
|
:is-image-attachment="isImageAttachment"
|
||||||
:is-pdf-attachment="isPdfAttachment"
|
:is-pdf-attachment="isPdfAttachment"
|
||||||
|
:is-excel-attachment="isExcelAttachment"
|
||||||
:open-file-url="openFileUrl"
|
:open-file-url="openFileUrl"
|
||||||
:preview-oss-document="previewOssDocument"
|
:preview-oss-document="previewOssDocument"
|
||||||
:handle-reject-material-item="handleRejectMaterialItem"
|
:handle-reject-material-item="handleRejectMaterialItem"
|
||||||
@ -784,6 +787,7 @@ watch(
|
|||||||
:get-file-url="getFileUrl"
|
:get-file-url="getFileUrl"
|
||||||
:is-image-attachment="isImageAttachment"
|
:is-image-attachment="isImageAttachment"
|
||||||
:is-pdf-attachment="isPdfAttachment"
|
:is-pdf-attachment="isPdfAttachment"
|
||||||
|
:is-excel-attachment="isExcelAttachment"
|
||||||
:open-file-url="openFileUrl"
|
:open-file-url="openFileUrl"
|
||||||
:preview-oss-document="previewOssDocument"
|
:preview-oss-document="previewOssDocument"
|
||||||
:build-photo-item-key="buildPhotoItemKey"
|
:build-photo-item-key="buildPhotoItemKey"
|
||||||
@ -882,7 +886,6 @@ watch(
|
|||||||
v-if="canReject"
|
v-if="canReject"
|
||||||
size="small"
|
size="small"
|
||||||
type="danger"
|
type="danger"
|
||||||
link
|
|
||||||
@click.stop="handleRejectMaterialItem(buildMeetingInvoiceFieldItemKey(section.sectionCode, file.fieldKey), `${section.sectionTitle}-${file.label}`)"
|
@click.stop="handleRejectMaterialItem(buildMeetingInvoiceFieldItemKey(section.sectionCode, file.fieldKey), `${section.sectionTitle}-${file.label}`)"
|
||||||
>
|
>
|
||||||
不通过
|
不通过
|
||||||
@ -919,7 +922,6 @@ watch(
|
|||||||
v-if="canReject && !isHigherReview"
|
v-if="canReject && !isHigherReview"
|
||||||
size="small"
|
size="small"
|
||||||
type="danger"
|
type="danger"
|
||||||
link
|
|
||||||
@click.stop="handleRejectMaterialItem(buildMeetingInvoiceAmountItemKey(section.sectionCode), `${section.sectionTitle}-总费用金额`)"
|
@click.stop="handleRejectMaterialItem(buildMeetingInvoiceAmountItemKey(section.sectionCode), `${section.sectionTitle}-总费用金额`)"
|
||||||
>
|
>
|
||||||
不通过
|
不通过
|
||||||
|
|||||||
@ -9,6 +9,7 @@ type ReviewDocItem = {
|
|||||||
reviewKey: string | string[];
|
reviewKey: string | string[];
|
||||||
rejectKey: string;
|
rejectKey: string;
|
||||||
rejectLabel: string;
|
rejectLabel: string;
|
||||||
|
link?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -26,6 +27,7 @@ const props = defineProps<{
|
|||||||
getFileUrl: (ossKey: unknown) => string;
|
getFileUrl: (ossKey: unknown) => string;
|
||||||
isImageAttachment: (name: unknown, ossKey: unknown) => boolean;
|
isImageAttachment: (name: unknown, ossKey: unknown) => boolean;
|
||||||
isPdfAttachment: (name: unknown, ossKey: unknown) => boolean;
|
isPdfAttachment: (name: unknown, ossKey: unknown) => boolean;
|
||||||
|
isExcelAttachment: (name: unknown, ossKey: unknown) => boolean;
|
||||||
openFileUrl: (ossKey: unknown) => void;
|
openFileUrl: (ossKey: unknown) => void;
|
||||||
previewOssDocument: (
|
previewOssDocument: (
|
||||||
ossKey: string,
|
ossKey: string,
|
||||||
@ -44,16 +46,26 @@ const fileDisplayName = (name: unknown, ossKey: unknown) => {
|
|||||||
return tail || "附件";
|
return tail || "附件";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const openLink = (link?: string) => {
|
||||||
|
if (link) {
|
||||||
|
const url = link.startsWith('http') ? link : `https://${link}`;
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const previewFile = (name: unknown, ossKey: unknown) => {
|
const previewFile = (name: unknown, ossKey: unknown) => {
|
||||||
const key = String(ossKey ?? "").trim();
|
const key = String(ossKey || "").trim();
|
||||||
if (!key) {
|
if (!key) return;
|
||||||
return;
|
if (
|
||||||
}
|
props.isImageAttachment(name, key) ||
|
||||||
if (props.isPdfAttachment(name, key)) {
|
props.isPdfAttachment(name, key) ||
|
||||||
|
props.isExcelAttachment(name, key)
|
||||||
|
) {
|
||||||
void props.previewOssDocument(key, fileDisplayName(name, key));
|
void props.previewOssDocument(key, fileDisplayName(name, key));
|
||||||
return;
|
} else {
|
||||||
|
props.openFileUrl(key);
|
||||||
}
|
}
|
||||||
props.openFileUrl(key);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildAgendaReviewKey = (item: any, index: number) => {
|
const buildAgendaReviewKey = (item: any, index: number) => {
|
||||||
@ -101,9 +113,10 @@ const invitationItems = computed<ReviewDocItem[]>(() =>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const sections = computed(() => [
|
const sections = computed(() => {
|
||||||
{
|
const baseSections = [
|
||||||
key: "agenda",
|
{
|
||||||
|
key: "agenda",
|
||||||
title: "会议日程",
|
title: "会议日程",
|
||||||
description: "支持多份材料,按单份逐项审核。",
|
description: "支持多份材料,按单份逐项审核。",
|
||||||
items: agendaItems.value,
|
items: agendaItems.value,
|
||||||
@ -150,7 +163,29 @@ const sections = computed(() => [
|
|||||||
description: "支持多份材料,按单份逐项审核。",
|
description: "支持多份材料,按单份逐项审核。",
|
||||||
items: invitationItems.value,
|
items: invitationItems.value,
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
if (props.docView?.videoType || props.docView?.videoOssKey || props.docView?.videoLink) {
|
||||||
|
baseSections.push({
|
||||||
|
key: "meetingVideo",
|
||||||
|
title: "会议视频",
|
||||||
|
description: "核对线上会议视频内容或网盘链接。",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "meetingVideo",
|
||||||
|
name: props.docView?.videoType === 'link' ? '网盘链接' : fileDisplayName(props.docView?.videoName, props.docView?.videoOssKey),
|
||||||
|
ossKey: props.docView?.videoType === 'link' ? '' : String(props.docView?.videoOssKey ?? "").trim(),
|
||||||
|
link: props.docView?.videoType === 'link' ? String(props.docView?.videoLink ?? "").trim() : '',
|
||||||
|
reviewKey: "meetingVideo",
|
||||||
|
rejectKey: "meetingVideo",
|
||||||
|
rejectLabel: "会议视频",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseSections;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -180,7 +215,11 @@ const sections = computed(() => [
|
|||||||
@click="handleChangeAnchorClick(item.reviewKey, $event)"
|
@click="handleChangeAnchorClick(item.reviewKey, $event)"
|
||||||
>
|
>
|
||||||
<div class="audit-doc-card__preview">
|
<div class="audit-doc-card__preview">
|
||||||
<div v-if="!item.ossKey" class="audit-doc-card__empty">未上传</div>
|
<div v-if="item.link" class="audit-doc-card__empty" style="flex-direction: column; gap: 8px;">
|
||||||
|
<span style="font-size: 24px;">🔗</span>
|
||||||
|
<span style="color: #409eff; cursor: pointer; font-size: 13px;" @click.stop="openLink(item.link)">打开网盘链接</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!item.ossKey" class="audit-doc-card__empty">未上传</div>
|
||||||
<el-image
|
<el-image
|
||||||
v-else-if="
|
v-else-if="
|
||||||
isImageAttachment(item.name, item.ossKey) &&
|
isImageAttachment(item.name, item.ossKey) &&
|
||||||
@ -202,6 +241,16 @@ const sections = computed(() => [
|
|||||||
<el-icon class="audit-doc-card__pdf-icon"><Document /></el-icon>
|
<el-icon class="audit-doc-card__pdf-icon"><Document /></el-icon>
|
||||||
<span class="audit-doc-card__pdf-label">PDF</span>
|
<span class="audit-doc-card__pdf-label">PDF</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-else-if="isExcelAttachment(item.name, item.ossKey)"
|
||||||
|
class="audit-doc-card__pdf"
|
||||||
|
type="button"
|
||||||
|
:title="item.name"
|
||||||
|
@click.stop="previewFile(item.name, item.ossKey)"
|
||||||
|
>
|
||||||
|
<el-icon class="audit-doc-card__pdf-icon"><Document /></el-icon>
|
||||||
|
<span class="audit-doc-card__pdf-label">EXCEL</span>
|
||||||
|
</button>
|
||||||
<el-button
|
<el-button
|
||||||
v-else
|
v-else
|
||||||
size="small"
|
size="small"
|
||||||
@ -230,8 +279,8 @@ const sections = computed(() => [
|
|||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
plain
|
plain
|
||||||
:disabled="!item.ossKey"
|
:disabled="!item.ossKey && !item.link"
|
||||||
@click.stop="previewFile(item.name, item.ossKey)"
|
@click.stop="item.link ? openLink(item.link) : previewFile(item.name, item.ossKey)"
|
||||||
>
|
>
|
||||||
查看
|
查看
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|||||||
@ -5,19 +5,24 @@ const visible = defineModel<boolean>({ required: true });
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
docPreviewDialogImageUrl: string;
|
docPreviewDialogImageUrl: string;
|
||||||
/** pdf 使用 iframe,图片使用 img */
|
docPreviewExcelHtml?: string;
|
||||||
previewKind?: "image" | "pdf";
|
/** pdf 使用 iframe,图片使用 img,excel 使用 table HTML */
|
||||||
|
previewKind?: "image" | "pdf" | "excel";
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const kind = computed(() => props.previewKind || "image");
|
const kind = computed(() => props.previewKind || "image");
|
||||||
const dialogTitle = computed(() => (kind.value === "pdf" ? "PDF 预览" : "图片预览"));
|
const dialogTitle = computed(() => {
|
||||||
|
if (kind.value === "pdf") return "PDF 预览";
|
||||||
|
if (kind.value === "excel") return "Excel 预览";
|
||||||
|
return "图片预览";
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
:title="dialogTitle"
|
:title="dialogTitle"
|
||||||
width="60%"
|
width="80%"
|
||||||
append-to-body
|
append-to-body
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
@ -27,6 +32,7 @@ const dialogTitle = computed(() => (kind.value === "pdf" ? "PDF 预览" : "图
|
|||||||
title="PDF 预览"
|
title="PDF 预览"
|
||||||
class="doc-preview-pdf-frame"
|
class="doc-preview-pdf-frame"
|
||||||
/>
|
/>
|
||||||
|
<div v-else-if="kind === 'excel'" class="doc-preview-excel-container" v-html="docPreviewExcelHtml"></div>
|
||||||
<div v-else-if="docPreviewDialogImageUrl" class="doc-preview-image-container">
|
<div v-else-if="docPreviewDialogImageUrl" class="doc-preview-image-container">
|
||||||
<img
|
<img
|
||||||
:src="docPreviewDialogImageUrl"
|
:src="docPreviewDialogImageUrl"
|
||||||
@ -58,4 +64,22 @@ const dialogTitle = computed(() => (kind.value === "pdf" ? "PDF 预览" : "图
|
|||||||
max-height: 75vh;
|
max-height: 75vh;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
.doc-preview-excel-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 75vh;
|
||||||
|
overflow: auto;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.doc-preview-excel-container :deep(table) {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.doc-preview-excel-container :deep(th),
|
||||||
|
.doc-preview-excel-container :deep(td) {
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -37,10 +37,12 @@ const props = withDefaults(defineProps<{
|
|||||||
title?: string;
|
title?: string;
|
||||||
maxLaborRatio?: RatioLimit;
|
maxLaborRatio?: RatioLimit;
|
||||||
maxCateringRatio?: RatioLimit;
|
maxCateringRatio?: RatioLimit;
|
||||||
|
lastRejectReason?: string;
|
||||||
}>(), {
|
}>(), {
|
||||||
title: "编辑会议",
|
title: "编辑会议",
|
||||||
maxLaborRatio: null,
|
maxLaborRatio: null,
|
||||||
maxCateringRatio: null,
|
maxCateringRatio: null,
|
||||||
|
lastRejectReason: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -109,6 +111,11 @@ watch(locationPath, (value) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
meetingForm.value.location = normalizeLocationText(Array.isArray(value) ? value : []);
|
meetingForm.value.location = normalizeLocationText(Array.isArray(value) ? value : []);
|
||||||
|
if (formRef.value) {
|
||||||
|
setTimeout(() => {
|
||||||
|
formRef.value?.clearValidate("location");
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -187,6 +194,14 @@ const handleSave = async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-drawer v-model="visible" :title="props.title" :size="DRAWER_SIZE.xl" destroy-on-close>
|
<el-drawer v-model="visible" :title="props.title" :size="DRAWER_SIZE.xl" destroy-on-close>
|
||||||
|
<el-alert
|
||||||
|
v-if="props.lastRejectReason"
|
||||||
|
:title="`[审核被驳回] 驳回原因:${props.lastRejectReason}`"
|
||||||
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 12px;"
|
||||||
|
/>
|
||||||
<el-form ref="formRef" :model="meetingForm" label-position="left" :label-width="LABEL_WIDTH.md" :rules="formRules">
|
<el-form ref="formRef" :model="meetingForm" label-position="left" :label-width="LABEL_WIDTH.md" :rules="formRules">
|
||||||
<el-form-item prop="projectName" label="项目名称">
|
<el-form-item prop="projectName" label="项目名称">
|
||||||
<span>{{ meetingForm.projectName || "-" }}</span>
|
<span>{{ meetingForm.projectName || "-" }}</span>
|
||||||
|
|||||||
@ -72,7 +72,22 @@ const summaryStatusText = (rowId: number, statusFormatter: (row: any, column: an
|
|||||||
<el-table-column label="预算(元)" width="100" >
|
<el-table-column label="预算(元)" width="100" >
|
||||||
<template #default="{ row }">{{ toYuan(row.budgetCent) }}</template>
|
<template #default="{ row }">{{ toYuan(row.budgetCent) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="auditStatus" label="审核状态" width="140" :formatter="statusFormatter" />
|
<el-table-column prop="auditStatus" label="审核状态" width="140">
|
||||||
|
<template #default="{ row, column }">
|
||||||
|
<el-tooltip
|
||||||
|
v-if="(normalizeText(row.auditStatus) === 'REJECTED' || (normalizeText(row.auditStatus) === 'PENDING' && row.lastRejectReason)) && row.lastRejectReason"
|
||||||
|
effect="dark"
|
||||||
|
:content="`驳回原因:${row.lastRejectReason}`"
|
||||||
|
placement="top"
|
||||||
|
:show-after="200"
|
||||||
|
>
|
||||||
|
<el-tag :type="normalizeText(row.auditStatus) === 'REJECTED' ? 'danger' : 'warning'">
|
||||||
|
{{ normalizeText(row.auditStatus) === 'PENDING' ? '已退回' : statusFormatter(row, column, row.auditStatus) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-tooltip>
|
||||||
|
<span v-else>{{ statusFormatter(row, column, row.auditStatus) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<!-- <el-table-column prop="currentAuditNode" label="当前审核节点" width="160" :formatter="auditNodeFormatter" />
|
<!-- <el-table-column prop="currentAuditNode" label="当前审核节点" width="160" :formatter="auditNodeFormatter" />
|
||||||
<el-table-column prop="currentAuditorUserId" label="当前审核节点" width="160" :formatter="auditorNameFormatter" /> -->
|
<el-table-column prop="currentAuditorUserId" label="当前审核节点" width="160" :formatter="auditorNameFormatter" /> -->
|
||||||
<el-table-column label="操作" width="320">
|
<el-table-column label="操作" width="320">
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DRAWER_SIZE, LABEL_WIDTH } from "../../../constants/ui";
|
import { DRAWER_SIZE, LABEL_WIDTH } from "../../../constants/ui";
|
||||||
import { computed, ref, nextTick, watch } from "vue";
|
import { computed, ref, nextTick, watch } from "vue";
|
||||||
import { Plus, Picture } from "@element-plus/icons-vue";
|
import { Plus, Picture } from "@element-plus/icons-vue";
|
||||||
@ -101,6 +101,8 @@ const selectedModuleCode = defineModel<string>("selectedModuleCode", { required:
|
|||||||
const expertSubModule = defineModel<string>("expertSubModule", { required: true });
|
const expertSubModule = defineModel<string>("expertSubModule", { required: true });
|
||||||
const selectedExpertLaborPreTaxAmountYuan = defineModel<number>("selectedExpertLaborPreTaxAmountYuan", { required: true });
|
const selectedExpertLaborPreTaxAmountYuan = defineModel<number>("selectedExpertLaborPreTaxAmountYuan", { required: true });
|
||||||
const selectedExpertLaborAfterTaxAmountYuan = defineModel<number>("selectedExpertLaborAfterTaxAmountYuan", { required: true });
|
const selectedExpertLaborAfterTaxAmountYuan = defineModel<number>("selectedExpertLaborAfterTaxAmountYuan", { required: true });
|
||||||
|
const meetingVideoType = defineModel<"file" | "link">("meetingVideoType", { default: "file" });
|
||||||
|
const meetingVideoLink = defineModel<string>("meetingVideoLink", { default: "" });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
materialDialogTitle: string;
|
materialDialogTitle: string;
|
||||||
@ -147,6 +149,11 @@ const props = defineProps<{
|
|||||||
handleThemePhotoPictureChange: (uploadFile: any, uploadFiles: any, maxSizeMb?: number) => void;
|
handleThemePhotoPictureChange: (uploadFile: any, uploadFiles: any, maxSizeMb?: number) => void;
|
||||||
handleThemePhotoPicturePreview: UploadProps["onPreview"];
|
handleThemePhotoPicturePreview: UploadProps["onPreview"];
|
||||||
handleThemePhotoPictureRemove: UploadProps["onRemove"];
|
handleThemePhotoPictureRemove: UploadProps["onRemove"];
|
||||||
|
meetingVideoUploadFileList: any[];
|
||||||
|
beforeMeetingVideoUpload: (rawFile: any, maxSizeMb?: number) => boolean | Promise<any>;
|
||||||
|
handleMeetingVideoPictureChange: (uploadFile: any, uploadFiles: any, maxSizeMb?: number) => void;
|
||||||
|
handleMeetingVideoPictureRemove: UploadProps["onRemove"];
|
||||||
|
meetingForm?: string;
|
||||||
invitationUploadFileList: any[];
|
invitationUploadFileList: any[];
|
||||||
beforeInvitationUpload: (rawFile: any, maxSizeMb?: number) => boolean | Promise<any>;
|
beforeInvitationUpload: (rawFile: any, maxSizeMb?: number) => boolean | Promise<any>;
|
||||||
handleInvitationPictureChange: (uploadFile: any, uploadFiles: any, maxSizeMb?: number) => void;
|
handleInvitationPictureChange: (uploadFile: any, uploadFiles: any, maxSizeMb?: number) => void;
|
||||||
@ -206,6 +213,7 @@ const props = defineProps<{
|
|||||||
checkFormChanged?: () => boolean;
|
checkFormChanged?: () => boolean;
|
||||||
doSaveMaterial?: () => Promise<boolean>;
|
doSaveMaterial?: () => Promise<boolean>;
|
||||||
jumpToIssueModify?: (issue: any) => void;
|
jumpToIssueModify?: (issue: any) => void;
|
||||||
|
lastRejectReason?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const protocolIsPdf = computed(() =>
|
const protocolIsPdf = computed(() =>
|
||||||
@ -365,6 +373,7 @@ const isMaterialFieldDisabled = (itemKey: string | string[]) =>
|
|||||||
const canEditReviewedItem = (itemKey: string | string[]) => !isMaterialFieldDisabled(itemKey);
|
const canEditReviewedItem = (itemKey: string | string[]) => !isMaterialFieldDisabled(itemKey);
|
||||||
const canEditAgenda = computed(() => canEditReviewedItem("agenda"));
|
const canEditAgenda = computed(() => canEditReviewedItem("agenda"));
|
||||||
const canEditSignInSheet = computed(() => canEditReviewedItem("signInSheet"));
|
const canEditSignInSheet = computed(() => canEditReviewedItem("signInSheet"));
|
||||||
|
const canEditMeetingVideo = computed(() => canEditReviewedItem("meetingVideo"));
|
||||||
const canEditThemePhoto = computed(() => canEditReviewedItem("themePhoto"));
|
const canEditThemePhoto = computed(() => canEditReviewedItem("themePhoto"));
|
||||||
const canEditInvitation = computed(() => !props.materialReadonly);
|
const canEditInvitation = computed(() => !props.materialReadonly);
|
||||||
const canEditExpertProfile = computed(() =>
|
const canEditExpertProfile = computed(() =>
|
||||||
@ -613,8 +622,17 @@ watch(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-drawer v-model="materialDialogVisible" size="80vw" :with-header="false" :before-close="handleBeforeCloseDrawer" destroy-on-close class="custom-material-drawer">
|
<el-drawer v-model="materialDialogVisible" size="80vw" :with-header="false" :before-close="handleBeforeCloseDrawer" destroy-on-close class="custom-material-drawer">
|
||||||
<div class="material-layout">
|
<div style="display: flex; flex-direction: column; height: 100%;">
|
||||||
<div class="material-sidebar">
|
<el-alert
|
||||||
|
v-if="props.lastRejectReason"
|
||||||
|
:title="`[审核被驳回] 驳回原因:${props.lastRejectReason}`"
|
||||||
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
style="flex-shrink: 0;"
|
||||||
|
/>
|
||||||
|
<div class="material-layout" style="flex: 1; height: 0;">
|
||||||
|
<div class="material-sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<div class="sidebar-title">{{ materialDialogTitle }}</div>
|
<div class="sidebar-title">{{ materialDialogTitle }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -904,7 +922,6 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
<div class="material-module-card" v-else-if="selectedModuleCode === 'WRITE_OFF_DOCS'">
|
<div class="material-module-card" v-else-if="selectedModuleCode === 'WRITE_OFF_DOCS'">
|
||||||
<el-form label-position="left" :label-width="LABEL_WIDTH.lg">
|
<el-form label-position="left" :label-width="LABEL_WIDTH.lg">
|
||||||
<el-alert title="核销材料按会议实际上传,不再套用模板" type="info" :closable="false" class="mb-md" />
|
|
||||||
<el-form-item label="上传会议日程">
|
<el-form-item label="上传会议日程">
|
||||||
<div class="flex gap-md w-full">
|
<div class="flex gap-md w-full">
|
||||||
<div>
|
<div>
|
||||||
@ -983,7 +1000,7 @@ watch(
|
|||||||
:on-change="(f, l) => handleUploadChangeWithCompression(f, l, (uf: any, ufs: any) => handleSignInPictureChange(uf, ufs, 2))"
|
:on-change="(f, l) => handleUploadChangeWithCompression(f, l, (uf: any, ufs: any) => handleSignInPictureChange(uf, ufs, 2))"
|
||||||
:on-preview="handleSignInPicturePreview"
|
:on-preview="handleSignInPicturePreview"
|
||||||
:on-remove="handleSignInPictureRemove"
|
:on-remove="handleSignInPictureRemove"
|
||||||
accept=".jpg,.jpeg,.png,.webp,.gif,.bmp,.pdf"
|
accept=".jpg,.jpeg,.png,.webp,.gif,.bmp,.pdf,.xls,.xlsx"
|
||||||
>
|
>
|
||||||
<template #file="{ file }">
|
<template #file="{ file }">
|
||||||
<MaterialPictureCardFileItem
|
<MaterialPictureCardFileItem
|
||||||
@ -1096,6 +1113,52 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="上传会议视频" v-if="meetingForm === '线上' || meetingForm === '线上+线下'">
|
||||||
|
<div class="flex-col gap-md w-full">
|
||||||
|
<el-radio-group v-model="meetingVideoType" :disabled="!canEditMeetingVideo">
|
||||||
|
<el-radio value="file">上传视频文件</el-radio>
|
||||||
|
<el-radio value="link">填写网盘链接</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
|
||||||
|
<div v-if="meetingVideoType === 'file'" class="mt-sm">
|
||||||
|
<el-upload
|
||||||
|
:class="{ 'expert-profile-upload--limit': meetingVideoUploadFileList.length >= 1 }"
|
||||||
|
:file-list="meetingVideoUploadFileList"
|
||||||
|
list-type="picture-card"
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="1"
|
||||||
|
:disabled="!canEditMeetingVideo"
|
||||||
|
:before-upload="(f) => beforeMeetingVideoUpload(f, 500)"
|
||||||
|
:on-change="(f, l) => handleUploadChangeWithCompression(f, l, (uf: any, ufs: any) => handleMeetingVideoPictureChange(uf, ufs, 500))"
|
||||||
|
:on-remove="handleMeetingVideoPictureRemove"
|
||||||
|
accept=".mp4,.mov,.avi,.mkv"
|
||||||
|
>
|
||||||
|
<template #file="{ file }">
|
||||||
|
<MaterialPictureCardFileItem
|
||||||
|
:file="file"
|
||||||
|
:disabled="!canEditMaterial || !canEditMeetingVideo"
|
||||||
|
@remove-file="runMaterialUploadRemove(handleMeetingVideoPictureRemove, $event)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<el-icon v-if="canEditMeetingVideo && meetingVideoUploadFileList.length < 1"><Plus /></el-icon>
|
||||||
|
</el-upload>
|
||||||
|
<div class="text-secondary mt-xs" style="font-size: 12px">支持 mp4/mov/avi/mkv 格式,大小不超过 500MB</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="meetingVideoType === 'link'" class="mt-sm">
|
||||||
|
<el-input
|
||||||
|
v-model="meetingVideoLink"
|
||||||
|
placeholder="请输入百度网盘或外部视频链接"
|
||||||
|
:disabled="!canEditMeetingVideo"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full mt-sm">
|
||||||
|
<el-tag v-if="toReviewResultText('meetingVideo')" size="small" :type="toReviewTagType('meetingVideo')">{{ toReviewResultText('meetingVideo') }}</el-tag>
|
||||||
|
<span v-if="toReviewReasonText('meetingVideo')" class="ml-sm-text-danger">不通过原因:{{ toReviewReasonText("meetingVideo") }}</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<div class="material-module-card" v-else-if="selectedModuleCode === 'EXPERT_PROFILE'">
|
<div class="material-module-card" v-else-if="selectedModuleCode === 'EXPERT_PROFILE'">
|
||||||
@ -1320,8 +1383,8 @@ watch(
|
|||||||
<!-- LABOR_PROTOCOL sub-module -->
|
<!-- LABOR_PROTOCOL sub-module -->
|
||||||
<template v-else-if="expertSubModule === 'LABOR_PROTOCOL'">
|
<template v-else-if="expertSubModule === 'LABOR_PROTOCOL'">
|
||||||
<div class="section-card-flat mt-md" v-bind="issueFocusAttrs(currentLaborReviewKey)">
|
<div class="section-card-flat mt-md" v-bind="issueFocusAttrs(currentLaborReviewKey)">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20" style="row-gap: 16px;">
|
||||||
<el-col :span="12">
|
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="12">
|
||||||
<div class="attachment-panel">
|
<div class="attachment-panel">
|
||||||
<div class="attachment-header">
|
<div class="attachment-header">
|
||||||
<span class="attachment-title">劳务协议附件</span>
|
<span class="attachment-title">劳务协议附件</span>
|
||||||
@ -1335,7 +1398,7 @@ watch(
|
|||||||
|
|
||||||
<div class="attachment-body">
|
<div class="attachment-body">
|
||||||
<div v-if="laborInfoRestrictedByAgreementOcr" class="attachment-restricted-state">
|
<div v-if="laborInfoRestrictedByAgreementOcr" class="attachment-restricted-state">
|
||||||
当前劳务信息由“上传劳务费协议”识别导入,不允许查看和修改劳务协议附件。
|
劳务费协议附件已上传
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="laborProtocolUploadFileList.length || selectedExpertLabor.protocolOssKey" class="file-preview-card">
|
<div v-else-if="laborProtocolUploadFileList.length || selectedExpertLabor.protocolOssKey" class="file-preview-card">
|
||||||
<el-image
|
<el-image
|
||||||
@ -1398,14 +1461,14 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<el-col :span="12">
|
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="12">
|
||||||
<div class="attachment-panel" style="height: 100%;">
|
<div class="attachment-panel" style="height: 100%;">
|
||||||
<div class="attachment-header">
|
<div class="attachment-header">
|
||||||
<span class="attachment-title">劳务金额信息</span>
|
<span class="attachment-title">劳务金额信息</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="attachment-body" style="justify-content: flex-start; padding: 20px;">
|
<div class="attachment-body" style="justify-content: flex-start; padding: 20px;">
|
||||||
<div v-if="laborInfoRestrictedByAgreementOcr" class="attachment-restricted-state attachment-restricted-state--compact">
|
<div v-if="laborInfoRestrictedByAgreementOcr" class="attachment-restricted-state attachment-restricted-state--compact">
|
||||||
当前劳务信息由“上传劳务费协议”识别导入,不允许查看和修改劳务金额信息。
|
劳务金额信息已填写
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="amount-input-group">
|
<div class="amount-input-group">
|
||||||
@ -1495,7 +1558,6 @@ watch(
|
|||||||
|
|
||||||
<div class="material-module-card" v-else-if="selectedModuleCode === 'MEETING_INVOICE'">
|
<div class="material-module-card" v-else-if="selectedModuleCode === 'MEETING_INVOICE'">
|
||||||
<el-form label-position="left" :label-width="LABEL_WIDTH.lg">
|
<el-form label-position="left" :label-width="LABEL_WIDTH.lg">
|
||||||
<el-alert title="会议发票按分项上传,单个上传位文件数量以分项配置为准,单文件不超2MB" type="info" :closable="false" class="mb-md" />
|
|
||||||
<div
|
<div
|
||||||
v-for="(section, sectionIndex) in meetingInvoiceSectionDefs"
|
v-for="(section, sectionIndex) in meetingInvoiceSectionDefs"
|
||||||
:key="section.code"
|
:key="section.code"
|
||||||
@ -1688,6 +1750,7 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -103,7 +103,7 @@
|
|||||||
<el-form-item label="允许项目超支">
|
<el-form-item label="允许项目超支">
|
||||||
<el-switch v-model="projectForm.allowProjectOverBudget" />
|
<el-switch v-model="projectForm.allowProjectOverBudget" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="超支阈值">
|
<el-form-item v-if="projectForm.allowProjectOverBudget" label="超支阈值">
|
||||||
<el-input-number v-model="projectForm.overBudgetThresholdRatio" :min="0" :max="1" :step="0.01" class="project-number-input" />
|
<el-input-number v-model="projectForm.overBudgetThresholdRatio" :min="0" :max="1" :step="0.01" class="project-number-input" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="发票信息">
|
<el-form-item label="发票信息">
|
||||||
@ -172,7 +172,7 @@ const defaultForm = () => ({
|
|||||||
laborAgreementSignType: 1 as 1 | 2,
|
laborAgreementSignType: 1 as 1 | 2,
|
||||||
allowMeetingOverBudget: false,
|
allowMeetingOverBudget: false,
|
||||||
allowProjectOverBudget: false,
|
allowProjectOverBudget: false,
|
||||||
overBudgetThresholdRatio: 0.1,
|
overBudgetThresholdRatio: 0,
|
||||||
invoiceInfo: "",
|
invoiceInfo: "",
|
||||||
hostOwnerUsers: "",
|
hostOwnerUsers: "",
|
||||||
hostExecutorUsers: "",
|
hostExecutorUsers: "",
|
||||||
|
|||||||
BIN
patch1.txt
Normal file
BIN
patch1.txt
Normal file
Binary file not shown.
BIN
patch2.txt
Normal file
BIN
patch2.txt
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user