feat: 会议劳务/会务材料分轨审核 + 注册页标题 + 下拉空值修复
会议材料分轨 (material_audit_stage 单轨 → labor/service 双轨): - biz_meeting: labor_audit_stage/labor_compliance_approved + service_audit_stage/service_compliance_approved 四列替换原单轨两列 - StageDeriver: 单轨状态机(R/N/C0/C1/A) + 角色优先级选代表轨 + 会议物理态取两轨最小进度 - MeetingAuditStageEnum / 前端 meetingStage.js 镜像: 分轨推导 + 颜色跟随角色展示阶段(避免文案与颜色错位) - BizMeetingController/ServiceImpl/AuditLog + mapper + MeetingStageScheduler 同步分轨字段 - MeetingDetail/Meetings/doctor/executor Meetings/ManagerProjectDetail/ManagerProjectsAssign/PublicityDetail 同步双轨 其它: - 注册页 (执行单位/专家/支持单位) 加居中页标题 - DoctorDeptSelect/DoctorTitleSelect 空字符串统一 null, 避免挂载必填项飘红 - BizOrgMapper sponsor/executor options 加 status 字段 - sponsor/NewPerson 编辑态 orgName 只读
This commit is contained in:
+158
-69
@@ -31,6 +31,7 @@ import com.ruoyi.business.notify.BizNotifyService;
|
|||||||
import com.ruoyi.business.service.IBizMeetingService;
|
import com.ruoyi.business.service.IBizMeetingService;
|
||||||
import com.ruoyi.business.service.IBizOrgService;
|
import com.ruoyi.business.service.IBizOrgService;
|
||||||
import com.ruoyi.business.service.IBizProjectService;
|
import com.ruoyi.business.service.IBizProjectService;
|
||||||
|
import com.ruoyi.business.service.IBizProjectSponsorAssignService;
|
||||||
import com.ruoyi.business.service.IBizMeetingAttendeeService;
|
import com.ruoyi.business.service.IBizMeetingAttendeeService;
|
||||||
import com.ruoyi.business.service.StageDeriver;
|
import com.ruoyi.business.service.StageDeriver;
|
||||||
import com.ruoyi.business.service.PosterService;
|
import com.ruoyi.business.service.PosterService;
|
||||||
@@ -63,6 +64,8 @@ public class BizMeetingController extends BaseController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private IBizProjectService bizProjectService;
|
private IBizProjectService bizProjectService;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
private IBizProjectSponsorAssignService bizProjectSponsorAssignService;
|
||||||
|
@Autowired
|
||||||
private IBizOrgService bizOrgService;
|
private IBizOrgService bizOrgService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private StageDeriver stageDeriver;
|
private StageDeriver stageDeriver;
|
||||||
@@ -87,12 +90,12 @@ public class BizMeetingController extends BaseController {
|
|||||||
bizMeeting.getParams().put("sponsorAdminUserId", uid);
|
bizMeeting.getParams().put("sponsorAdminUserId", uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// executor 数据权限: 只看"我的项目"下的会议 (与项目列表 selectExecutorList/selectExecutorStaffList 同源).
|
// executor 数据权限: MAIN 看"我的执行单位"下的会议 (走 biz_project_assign.execution_unit_id);
|
||||||
// MAIN 走 biz_project_assign (execution_unit_id), SUB(执行人) 走 biz_project_executor_assign.staff_user_id.
|
// SUB(执行人) 只看"本人创建的会议" (按 create_by 过滤, 不再看整个分配项目的会议).
|
||||||
else if ("executor".equals(roleType)) {
|
else if ("executor".equals(roleType)) {
|
||||||
SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid);
|
SysUser current = uid == null ? null : sysUserMapper.selectUserById(uid);
|
||||||
if (current != null && "SUB".equals(current.getAccountType())) {
|
if (current != null && "SUB".equals(current.getAccountType())) {
|
||||||
bizMeeting.getParams().put("executorStaffUserId", uid);
|
bizMeeting.getParams().put("executorCreatorUsername", SecurityUtils.getUsername());
|
||||||
} else {
|
} else {
|
||||||
bizMeeting.getParams().put("executorUserId", uid);
|
bizMeeting.getParams().put("executorUserId", uid);
|
||||||
}
|
}
|
||||||
@@ -233,18 +236,19 @@ public class BizMeetingController extends BaseController {
|
|||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行人员提交材料
|
* 执行人员提交材料 (分轨: 劳务 LABOR / 会务 SERVICE 可多选, 每轨独立提交).
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>校验 1: 当前用户是该会议执行方 (项目级归属, 强校验)</li>
|
* <li>校验 1: 当前用户是该会议执行方 (项目级归属, 强校验)</li>
|
||||||
* <li>校验 2: 已执行 (is_executed=1) 且未冻结</li>
|
* <li>校验 2: 已执行 (is_executed=1) 且未冻结</li>
|
||||||
* <li>校验 3: material_audit_stage ∈ {NOT_SUBMITTED, REJECTED}</li>
|
* <li>校验 3: 所选轨 audit_stage ∈ {NOT_SUBMITTED, REJECTED}</li>
|
||||||
* <li>校验 4: biz_meeting_material 劳务(L_*)与会务(M_*)各至少 1 条, 不必全部子类型填满</li>
|
* <li>校验 4: 所选轨在 biz_meeting_material 各至少 1 条 (劳务 L_* / 会务 M_*)</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* 通过后 material → SUBMITTED (compliance_approved=0), 记 audit_log.
|
* body: { "types": ["LABOR","SERVICE"] }
|
||||||
|
* 通过后该轨 → SUBMITTED (compliance_approved=0), 每轨记一条 audit_log.
|
||||||
*/
|
*/
|
||||||
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
||||||
@PostMapping("/{meetingId}/submit-material")
|
@PostMapping("/{meetingId}/submit-material")
|
||||||
public AjaxResult submitMaterial(@PathVariable("meetingId") Long meetingId) {
|
public AjaxResult submitMaterial(@PathVariable("meetingId") Long meetingId, @RequestBody SubmitBody body) {
|
||||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||||
if (m == null) throw new ServiceException("会议不存在");
|
if (m == null) throw new ServiceException("会议不存在");
|
||||||
|
|
||||||
@@ -254,33 +258,52 @@ public class BizMeetingController extends BaseController {
|
|||||||
}
|
}
|
||||||
if (!isExecuted(m)) throw new ServiceException("会议尚未执行, 不能提交材料");
|
if (!isExecuted(m)) throw new ServiceException("会议尚未执行, 不能提交材料");
|
||||||
if (isFrozen(m)) throw new ServiceException("会议已冻结, 不能提交材料");
|
if (isFrozen(m)) throw new ServiceException("会议已冻结, 不能提交材料");
|
||||||
String stage = m.getMaterialAuditStage();
|
|
||||||
if (!"NOT_SUBMITTED".equals(stage) && !"REJECTED".equals(stage)) {
|
List<String> types = body == null ? null : body.getTypes();
|
||||||
throw new ServiceException("当前阶段 (" + stage + ") 不允许提交材料");
|
if (types == null || types.isEmpty()) throw new ServiceException("请选择要提交的材料类型");
|
||||||
|
for (String type : types) {
|
||||||
|
if (!"LABOR".equals(type) && !"SERVICE".equals(type)) throw new ServiceException("未知材料类型: " + type);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<BizMeetingMaterial> mats = bizMeetingMaterialService.selectByMeetingId(meetingId);
|
List<BizMeetingMaterial> mats = bizMeetingMaterialService.selectByMeetingId(meetingId);
|
||||||
boolean hasLabor = mats.stream().anyMatch(x -> x.getSubType() != null && x.getSubType().startsWith("L_"));
|
boolean hasLabor = mats.stream().anyMatch(x -> x.getSubType() != null && x.getSubType().startsWith("L_"));
|
||||||
boolean hasService = mats.stream().anyMatch(x -> x.getSubType() != null && x.getSubType().startsWith("M_"));
|
boolean hasService = mats.stream().anyMatch(x -> x.getSubType() != null && x.getSubType().startsWith("M_"));
|
||||||
if (!hasLabor || !hasService) {
|
|
||||||
throw new ServiceException("劳务材料和会务材料各至少上传一条");
|
for (String type : types) {
|
||||||
|
String label = "LABOR".equals(type) ? "劳务材料" : "会务材料";
|
||||||
|
String stage = "LABOR".equals(type) ? m.getLaborAuditStage() : m.getServiceAuditStage();
|
||||||
|
if (!"NOT_SUBMITTED".equals(stage) && !"REJECTED".equals(stage)) {
|
||||||
|
throw new ServiceException(label + "当前阶段 (" + stage + ") 不允许提交");
|
||||||
|
}
|
||||||
|
boolean has = "LABOR".equals(type) ? hasLabor : hasService;
|
||||||
|
if (!has) throw new ServiceException(label + "至少上传一条");
|
||||||
}
|
}
|
||||||
|
|
||||||
m.setMaterialAuditStage("SUBMITTED");
|
for (String type : types) {
|
||||||
m.setMaterialComplianceApproved(0);
|
if ("LABOR".equals(type)) {
|
||||||
|
m.setLaborAuditStage("SUBMITTED");
|
||||||
|
m.setLaborComplianceApproved(0);
|
||||||
|
} else {
|
||||||
|
m.setServiceAuditStage("SUBMITTED");
|
||||||
|
m.setServiceComplianceApproved(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
m.setMaterialAuditTime(new Date());
|
m.setMaterialAuditTime(new Date());
|
||||||
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
||||||
bizMeetingService.updateByPrimaryKey(m);
|
bizMeetingService.updateByPrimaryKey(m);
|
||||||
appendAuditLog(m, "MATERIAL", "SUBMITTED", "执行人员提交材料");
|
for (String type : types) {
|
||||||
|
appendAuditLog(m, "MATERIAL", "SUBMITTED", "执行人员提交" + ("LABOR".equals(type) ? "劳务" : "会务") + "材料", type);
|
||||||
|
}
|
||||||
return success("SUBMITTED");
|
return success("SUBMITTED");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 合规审核 (role_type=manager), 材料两级审核中的第一级.
|
* 合规审核 (role_type=manager), 材料两级审核中的第一级 (分轨).
|
||||||
* <p>
|
* <p>
|
||||||
* body: { "approved": true|false, "opinion": "..." }
|
* body: { "items": [{ "type": "LABOR|SERVICE", "approved": true|false }], "opinion": "..." }
|
||||||
* <p>合规审中判据: material_audit_stage=SUBMITTED 且 compliance_approved=0.
|
* <p>逐轨审核: 该轨 C0 (SUBMITTED 且 compliance_approved=0) 才可审;
|
||||||
* 通过 → compliance_approved=1 (转入支持方审), 拒绝 → REJECTED (退回执行方).
|
* 通过 → compliance_approved=1 (转支持方审), 拒绝 → REJECTED (退回执行方).
|
||||||
|
* 一轨给通过、另一轨给拒绝 在同一 body 里各自表达.
|
||||||
*/
|
*/
|
||||||
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
||||||
@PostMapping("/{meetingId}/audit-compliance")
|
@PostMapping("/{meetingId}/audit-compliance")
|
||||||
@@ -290,17 +313,25 @@ public class BizMeetingController extends BaseController {
|
|||||||
}
|
}
|
||||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||||
if (m == null) throw new ServiceException("会议不存在");
|
if (m == null) throw new ServiceException("会议不存在");
|
||||||
boolean approved = Boolean.TRUE.equals(body.getApproved());
|
List<AuditItem> items = body == null ? null : body.getItems();
|
||||||
if (!approved && (body.getOpinion() == null || body.getOpinion().isEmpty())) {
|
if (items == null || items.isEmpty()) throw new ServiceException("请选择要审核的材料类型");
|
||||||
|
for (AuditItem it : items) {
|
||||||
|
if (!"LABOR".equals(it.getType()) && !"SERVICE".equals(it.getType())) throw new ServiceException("未知材料类型: " + it.getType());
|
||||||
|
if (!Boolean.TRUE.equals(it.getApproved()) && (body.getOpinion() == null || body.getOpinion().isEmpty())) {
|
||||||
throw new ServiceException("拒绝时意见不能为空");
|
throw new ServiceException("拒绝时意见不能为空");
|
||||||
}
|
}
|
||||||
return success(doComplianceAudit(m, approved, body.getOpinion()));
|
}
|
||||||
|
for (AuditItem it : items) {
|
||||||
|
doComplianceAuditTrack(m, it.getType(), Boolean.TRUE.equals(it.getApproved()), body.getOpinion());
|
||||||
|
}
|
||||||
|
return success("OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量合规审核 (role_type=manager): 一次对多个会议执行材料一级审核.
|
* 批量合规审核 (role_type=manager): 一次对多个会议执行材料一级审核.
|
||||||
* <p>
|
* <p>
|
||||||
* body: { "meetingIds": [..], "approved": true|false, "opinion": "..." }
|
* body: { "meetingIds": [..], "approved": true|false, "opinion": "..." }
|
||||||
|
* <p>分轨简化: 一个结果应用到每个会议所有处于合规审中 (C0) 的轨 (劳务/会务), 不做逐轨不同结果.
|
||||||
* <p>best-effort: 逐条审核, 状态不符的会议跳过并回执失败原因, 不整批回滚.
|
* <p>best-effort: 逐条审核, 状态不符的会议跳过并回执失败原因, 不整批回滚.
|
||||||
*/
|
*/
|
||||||
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
||||||
@@ -325,7 +356,7 @@ public class BizMeetingController extends BaseController {
|
|||||||
try {
|
try {
|
||||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||||
if (m == null) throw new ServiceException("会议不存在");
|
if (m == null) throw new ServiceException("会议不存在");
|
||||||
doComplianceAudit(m, approved, body.getOpinion());
|
doComplianceAuditAllC0(m, approved, body.getOpinion());
|
||||||
successIds.add(meetingId);
|
successIds.add(meetingId);
|
||||||
} catch (ServiceException e) {
|
} catch (ServiceException e) {
|
||||||
Map<String, Object> f = new HashMap<>();
|
Map<String, Object> f = new HashMap<>();
|
||||||
@@ -343,35 +374,53 @@ public class BizMeetingController extends BaseController {
|
|||||||
return success(data);
|
return success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 某轨是否处于合规审中 (C0): SUBMITTED 且 compliance_approved != 1. */
|
||||||
|
private static boolean isCompliancePending(String stage, Integer compliance) {
|
||||||
|
return "SUBMITTED".equals(stage) && (compliance == null || compliance != 1);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 内部: 合规审核核心流转 (阶段校验 + 状态写入 + audit_log). 单条/批量共用.
|
* 内部: 合规审核单轨核心流转 (阶段校验 + 状态写入 + audit_log). 单条/批量共用.
|
||||||
*/
|
*/
|
||||||
private String doComplianceAudit(BizMeeting m, boolean approved, String opinion) {
|
private void doComplianceAuditTrack(BizMeeting m, String type, boolean approved, String opinion) {
|
||||||
Integer compliance = m.getMaterialComplianceApproved();
|
boolean labor = "LABOR".equals(type);
|
||||||
if (!"SUBMITTED".equals(m.getMaterialAuditStage()) || (compliance != null && compliance == 1)) {
|
String stage = labor ? m.getLaborAuditStage() : m.getServiceAuditStage();
|
||||||
throw new ServiceException("当前阶段不允许合规审核");
|
Integer compliance = labor ? m.getLaborComplianceApproved() : m.getServiceComplianceApproved();
|
||||||
|
if (!isCompliancePending(stage, compliance)) {
|
||||||
|
throw new ServiceException((labor ? "劳务材料" : "会务材料") + "当前阶段不允许合规审核");
|
||||||
}
|
}
|
||||||
String result = approved ? "APPROVED" : "REJECTED";
|
|
||||||
if (approved) {
|
if (approved) {
|
||||||
m.setMaterialComplianceApproved(1);
|
if (labor) m.setLaborComplianceApproved(1); else m.setServiceComplianceApproved(1);
|
||||||
} else {
|
} else {
|
||||||
m.setMaterialAuditStage("REJECTED");
|
if (labor) m.setLaborAuditStage("REJECTED"); else m.setServiceAuditStage("REJECTED");
|
||||||
// 退回 → 提交截止时间重新计算 (now + 项目天数)
|
// 退回 → 提交截止时间重新计算 (now + 项目天数)
|
||||||
m.setSubmitDeadline(computeSubmitDeadline(m.getProjectId(), new Date()));
|
m.setSubmitDeadline(computeSubmitDeadline(m.getProjectId(), new Date()));
|
||||||
}
|
}
|
||||||
m.setMaterialAuditTime(new Date());
|
m.setMaterialAuditTime(new Date());
|
||||||
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
||||||
bizMeetingService.updateByPrimaryKey(m);
|
bizMeetingService.updateByPrimaryKey(m);
|
||||||
appendAuditLog(m, "MATERIAL", result, opinion);
|
appendAuditLog(m, "MATERIAL", approved ? "APPROVED" : "REJECTED", opinion, type);
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支持方(监察员) 审核, 材料两级审核中的第二级.
|
* 内部: 批量合规审核 — 一个结果应用到该会议所有处于合规审中 (C0) 的轨 (简化, 不做逐轨不同结果).
|
||||||
|
*/
|
||||||
|
private void doComplianceAuditAllC0(BizMeeting m, boolean approved, String opinion) {
|
||||||
|
List<String> targets = new ArrayList<>();
|
||||||
|
if (isCompliancePending(m.getLaborAuditStage(), m.getLaborComplianceApproved())) targets.add("LABOR");
|
||||||
|
if (isCompliancePending(m.getServiceAuditStage(), m.getServiceComplianceApproved())) targets.add("SERVICE");
|
||||||
|
if (targets.isEmpty()) throw new ServiceException("当前阶段不允许合规审核");
|
||||||
|
for (String type : targets) {
|
||||||
|
doComplianceAuditTrack(m, type, approved, opinion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持方(监察员) 审核, 材料两级审核中的第二级 (分轨).
|
||||||
* <p>
|
* <p>
|
||||||
* body: { "approved": true|false, "opinion": "..." }
|
* body: { "items": [{ "type": "LABOR|SERVICE", "approved": true|false }], "opinion": "..." }
|
||||||
* <p>支持方审中判据: material_audit_stage=SUBMITTED 且 compliance_approved=1.
|
* <p>逐轨审核: 该轨 C1 (SUBMITTED 且 compliance_approved=1) 才可审;
|
||||||
* 通过 → APPROVED (一并写监管意见), 拒绝 → REJECTED (退回执行方).
|
* 通过 → APPROVED (任一轨通过即写监管意见), 拒绝 → REJECTED (退回执行方 + 通知).
|
||||||
*/
|
*/
|
||||||
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
@Log(title = "会议审核", businessType = BusinessType.UPDATE)
|
||||||
@PostMapping("/{meetingId}/audit-supervision")
|
@PostMapping("/{meetingId}/audit-supervision")
|
||||||
@@ -382,27 +431,57 @@ public class BizMeetingController extends BaseController {
|
|||||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||||
if (m == null) throw new ServiceException("会议不存在");
|
if (m == null) throw new ServiceException("会议不存在");
|
||||||
|
|
||||||
// 授权: 监察员 (biz_meeting_supervisor.sponsor_org_id) 或 支持方企业 (biz_project.sponsor_org_id) 均可审
|
// 授权 (与列表可见性同源, 原则: 能看到就能监察):
|
||||||
|
// 1) 项目级监察员 (biz_project_sponsor_assign.monitor_user_id = 当前 user_id) — SUB 监察员列表同源
|
||||||
|
// 2) 会议级监察员机构 (biz_meeting_supervisor.sponsor_org_id = 我的 org_id)
|
||||||
|
// 3) 支持方主账号 (biz_project.sponsor_org_id = 我的 org_id)
|
||||||
|
BizProject project = m.getProjectId() == null ? null : bizProjectService.getById(m.getProjectId());
|
||||||
|
boolean isProjectMonitor = m.getProjectId() != null && userId != null
|
||||||
|
&& bizProjectSponsorAssignService.listByProjectId(String.valueOf(m.getProjectId())).stream()
|
||||||
|
.anyMatch(a -> userId.equals(a.getMonitorUserId()));
|
||||||
boolean isSupervisor = bizMeetingSupervisorService.selectByMeetingId(meetingId).stream()
|
boolean isSupervisor = bizMeetingSupervisorService.selectByMeetingId(meetingId).stream()
|
||||||
.anyMatch(s -> myOrgId != null && myOrgId.equals(s.getSponsorOrgId()));
|
.anyMatch(s -> myOrgId != null && myOrgId.equals(s.getSponsorOrgId()));
|
||||||
BizProject project = m.getProjectId() == null ? null : bizProjectService.getById(m.getProjectId());
|
|
||||||
boolean isSponsorMain = project != null && myOrgId != null && myOrgId.equals(project.getSponsorOrgId());
|
boolean isSponsorMain = project != null && myOrgId != null && myOrgId.equals(project.getSponsorOrgId());
|
||||||
if (!isSupervisor && !isSponsorMain) {
|
if (!isProjectMonitor && !isSupervisor && !isSponsorMain) {
|
||||||
throw new ServiceException("您不是该会议监察员, 无权监察");
|
throw new ServiceException("您不是该会议监察员, 无权监察");
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean approved = Boolean.TRUE.equals(body.getApproved());
|
List<AuditItem> items = body == null ? null : body.getItems();
|
||||||
if (!approved && (body.getOpinion() == null || body.getOpinion().isEmpty())) {
|
if (items == null || items.isEmpty()) throw new ServiceException("请选择要审核的材料类型");
|
||||||
|
boolean anyRejected = false;
|
||||||
|
for (AuditItem it : items) {
|
||||||
|
if (!"LABOR".equals(it.getType()) && !"SERVICE".equals(it.getType())) throw new ServiceException("未知材料类型: " + it.getType());
|
||||||
|
if (!Boolean.TRUE.equals(it.getApproved()) && (body.getOpinion() == null || body.getOpinion().isEmpty())) {
|
||||||
throw new ServiceException("拒绝时意见不能为空");
|
throw new ServiceException("拒绝时意见不能为空");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Integer compliance = m.getMaterialComplianceApproved();
|
for (AuditItem it : items) {
|
||||||
if (!"SUBMITTED".equals(m.getMaterialAuditStage()) || compliance == null || compliance != 1) {
|
boolean approved = Boolean.TRUE.equals(it.getApproved());
|
||||||
throw new ServiceException("当前阶段不允许监察审核");
|
doSupervisionAuditTrack(m, it.getType(), approved, body.getOpinion());
|
||||||
|
if (!approved) anyRejected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
String result = approved ? "APPROVED" : "REJECTED";
|
// 退回 → 通知执行方 (待整改 + 说明, 任一轨被拒即通知一次)
|
||||||
m.setMaterialAuditStage(approved ? "APPROVED" : "REJECTED");
|
if (anyRejected) {
|
||||||
|
for (BizMeetingExecutor e : bizMeetingExecutorService.selectByMeetingId(meetingId)) {
|
||||||
|
bizNotifyService.meetingSupervisionRejected(resolveMainUserIdByOrgId(e.getExecutorOrgId()), meetingId, m.getMeetingName(), body.getOpinion());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return success("OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部: 支持方审核单轨核心流转 (阶段校验 + 状态写入 + audit_log).
|
||||||
|
*/
|
||||||
|
private void doSupervisionAuditTrack(BizMeeting m, String type, boolean approved, String opinion) {
|
||||||
|
boolean labor = "LABOR".equals(type);
|
||||||
|
String stage = labor ? m.getLaborAuditStage() : m.getServiceAuditStage();
|
||||||
|
Integer compliance = labor ? m.getLaborComplianceApproved() : m.getServiceComplianceApproved();
|
||||||
|
if (!"SUBMITTED".equals(stage) || compliance == null || compliance != 1) {
|
||||||
|
throw new ServiceException((labor ? "劳务材料" : "会务材料") + "当前阶段不允许监察审核");
|
||||||
|
}
|
||||||
|
if (labor) m.setLaborAuditStage(approved ? "APPROVED" : "REJECTED");
|
||||||
|
else m.setServiceAuditStage(approved ? "APPROVED" : "REJECTED");
|
||||||
// 退回 → 提交截止时间重新计算 (now + 项目天数)
|
// 退回 → 提交截止时间重新计算 (now + 项目天数)
|
||||||
if (!approved) {
|
if (!approved) {
|
||||||
m.setSubmitDeadline(computeSubmitDeadline(m.getProjectId(), new Date()));
|
m.setSubmitDeadline(computeSubmitDeadline(m.getProjectId(), new Date()));
|
||||||
@@ -410,21 +489,13 @@ public class BizMeetingController extends BaseController {
|
|||||||
m.setMaterialAuditTime(new Date());
|
m.setMaterialAuditTime(new Date());
|
||||||
// 材料通过 → 写监管意见 (支持方的书面意见)
|
// 材料通过 → 写监管意见 (支持方的书面意见)
|
||||||
if (approved) {
|
if (approved) {
|
||||||
m.setSupervisionOpinion(body.getOpinion());
|
m.setSupervisionOpinion(opinion);
|
||||||
m.setSupervisionBy(SecurityUtils.getUsername());
|
m.setSupervisionBy(SecurityUtils.getUsername());
|
||||||
m.setSupervisionTime(new Date());
|
m.setSupervisionTime(new Date());
|
||||||
}
|
}
|
||||||
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
||||||
bizMeetingService.updateByPrimaryKey(m);
|
bizMeetingService.updateByPrimaryKey(m);
|
||||||
appendAuditLog(m, "MATERIAL", result, body.getOpinion());
|
appendAuditLog(m, "MATERIAL", approved ? "APPROVED" : "REJECTED", opinion, type);
|
||||||
|
|
||||||
// 退回 → 通知执行方 (待整改 + 说明)
|
|
||||||
if (!approved) {
|
|
||||||
for (BizMeetingExecutor e : bizMeetingExecutorService.selectByMeetingId(meetingId)) {
|
|
||||||
bizNotifyService.meetingSupervisionRejected(resolveMainUserIdByOrgId(e.getExecutorOrgId()), meetingId, m.getMeetingName(), body.getOpinion());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return success(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -443,8 +514,8 @@ public class BizMeetingController extends BaseController {
|
|||||||
}
|
}
|
||||||
BizMeeting m = bizMeetingService.getById(meetingId);
|
BizMeeting m = bizMeetingService.getById(meetingId);
|
||||||
if (m == null) throw new ServiceException("会议不存在");
|
if (m == null) throw new ServiceException("会议不存在");
|
||||||
if (!"APPROVED".equals(m.getMaterialAuditStage())) {
|
if (!"APPROVED".equals(m.getLaborAuditStage()) || !"APPROVED".equals(m.getServiceAuditStage())) {
|
||||||
throw new ServiceException("材料审核通过后才能结算");
|
throw new ServiceException("劳务与会务材料都审核通过后才能结算");
|
||||||
}
|
}
|
||||||
if (isSettled(m)) throw new ServiceException("会议已结算");
|
if (isSettled(m)) throw new ServiceException("会议已结算");
|
||||||
// 费用未汇总完 (fee_calc_status=0) 禁止结算: 此时 labor_fee/meeting_fee 可能为旧值/0, 直接回写会污染项目金额.
|
// 费用未汇总完 (fee_calc_status=0) 禁止结算: 此时 labor_fee/meeting_fee 可能为旧值/0, 直接回写会污染项目金额.
|
||||||
@@ -465,7 +536,7 @@ public class BizMeetingController extends BaseController {
|
|||||||
m.setSettleTime(new Date());
|
m.setSettleTime(new Date());
|
||||||
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
||||||
bizMeetingService.updateByPrimaryKey(m);
|
bizMeetingService.updateByPrimaryKey(m);
|
||||||
appendAuditLog(m, "SETTLE", "APPROVED", "会议结算");
|
appendAuditLog(m, "SETTLE", "APPROVED", "会议结算", null);
|
||||||
return success("SETTLED");
|
return success("SETTLED");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,7 +558,7 @@ public class BizMeetingController extends BaseController {
|
|||||||
m.setFinishTime(new Date());
|
m.setFinishTime(new Date());
|
||||||
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
m.setCurrentStage(stageDeriver.derivePhysicalStage(m));
|
||||||
bizMeetingService.updateByPrimaryKey(m);
|
bizMeetingService.updateByPrimaryKey(m);
|
||||||
appendAuditLog(m, "FINISH", "APPROVED", "会议完结");
|
appendAuditLog(m, "FINISH", "APPROVED", "会议完结", null);
|
||||||
return success("FINISHED");
|
return success("FINISHED");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,13 +630,14 @@ public class BizMeetingController extends BaseController {
|
|||||||
/**
|
/**
|
||||||
* 内部: 写一条 audit_log (4 列角色展示状态由 post-transition 事实推导).
|
* 内部: 写一条 audit_log (4 列角色展示状态由 post-transition 事实推导).
|
||||||
*/
|
*/
|
||||||
private void appendAuditLog(BizMeeting m, String auditType, String result, String opinion) {
|
private void appendAuditLog(BizMeeting m, String auditType, String result, String opinion, String materialType) {
|
||||||
BizMeetingAuditLog log = new BizMeetingAuditLog();
|
BizMeetingAuditLog log = new BizMeetingAuditLog();
|
||||||
log.setMeetingId(m.getMeetingId());
|
log.setMeetingId(m.getMeetingId());
|
||||||
log.setAuditor(SecurityUtils.getUsername());
|
log.setAuditor(SecurityUtils.getUsername());
|
||||||
log.setAuditType(auditType);
|
log.setAuditType(auditType);
|
||||||
log.setAuditResult(result);
|
log.setAuditResult(result);
|
||||||
log.setOpinion(opinion);
|
log.setOpinion(opinion);
|
||||||
|
log.setMaterialType(materialType);
|
||||||
log.setCreateTime(new Date());
|
log.setCreateTime(new Date());
|
||||||
log.setAuditTime(new Date());
|
log.setAuditTime(new Date());
|
||||||
log.setExecutorStage(stageDeriver.deriveDisplay("executor", m));
|
log.setExecutorStage(stageDeriver.deriveDisplay("executor", m));
|
||||||
@@ -575,16 +647,33 @@ public class BizMeetingController extends BaseController {
|
|||||||
bizMeetingAuditLogService.insert(log);
|
bizMeetingAuditLogService.insert(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** request body for audit endpoints */
|
/** request body for audit endpoints (逐轨: 每项一个材料类型 + 通过/拒绝) */
|
||||||
public static class AuditBody {
|
public static class AuditBody {
|
||||||
private Boolean approved; // true=通过 false=拒绝
|
private List<AuditItem> items; // 逐轨审核项
|
||||||
private String opinion; // 意见
|
private String opinion; // 意见 (拒绝时必填)
|
||||||
public Boolean getApproved() { return approved; }
|
public List<AuditItem> getItems() { return items; }
|
||||||
public void setApproved(Boolean approved) { this.approved = approved; }
|
public void setItems(List<AuditItem> items) { this.items = items; }
|
||||||
public String getOpinion() { return opinion; }
|
public String getOpinion() { return opinion; }
|
||||||
public void setOpinion(String opinion) { this.opinion = opinion; }
|
public void setOpinion(String opinion) { this.opinion = opinion; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 单轨审核项 */
|
||||||
|
public static class AuditItem {
|
||||||
|
private String type; // LABOR / SERVICE
|
||||||
|
private Boolean approved; // true=通过 false=拒绝
|
||||||
|
public String getType() { return type; }
|
||||||
|
public void setType(String type) { this.type = type; }
|
||||||
|
public Boolean getApproved() { return approved; }
|
||||||
|
public void setApproved(Boolean approved) { this.approved = approved; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** request body for submit-material (分轨多选) */
|
||||||
|
public static class SubmitBody {
|
||||||
|
private List<String> types; // ["LABOR","SERVICE"]
|
||||||
|
public List<String> getTypes() { return types; }
|
||||||
|
public void setTypes(List<String> types) { this.types = types; }
|
||||||
|
}
|
||||||
|
|
||||||
/** request body for batch audit endpoint */
|
/** request body for batch audit endpoint */
|
||||||
public static class BatchAuditBody {
|
public static class BatchAuditBody {
|
||||||
private List<Long> meetingIds;
|
private List<Long> meetingIds;
|
||||||
|
|||||||
@@ -72,8 +72,10 @@ public class BizMeeting extends BaseEntity {
|
|||||||
/** 监察时间 (与 DB datetime 对齐) */
|
/** 监察时间 (与 DB datetime 对齐) */
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private Date supervisionTime;
|
private Date supervisionTime;
|
||||||
/** 材料审核阶段 (NOT_SUBMITTED/SUBMITTED/APPROVED/REJECTED, 见 MeetingAuditStageEnum) */
|
/** 劳务材料审核阶段 (NOT_SUBMITTED/SUBMITTED/APPROVED/REJECTED, 见 MeetingAuditStageEnum) */
|
||||||
private String materialAuditStage;
|
private String laborAuditStage;
|
||||||
|
/** 会务材料审核阶段 (NOT_SUBMITTED/SUBMITTED/APPROVED/REJECTED, 见 MeetingAuditStageEnum) */
|
||||||
|
private String serviceAuditStage;
|
||||||
/** 是否执行 0否1是 (会议开始时间到, scheduler 置1) */
|
/** 是否执行 0否1是 (会议开始时间到, scheduler 置1) */
|
||||||
private Integer isExecuted;
|
private Integer isExecuted;
|
||||||
/** 执行时间 */
|
/** 执行时间 */
|
||||||
@@ -97,8 +99,10 @@ public class BizMeeting extends BaseEntity {
|
|||||||
/** 材料最近一次审核动作时间 */
|
/** 材料最近一次审核动作时间 */
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private Date materialAuditTime;
|
private Date materialAuditTime;
|
||||||
/** 材料合规是否已通过 0否1是 (区分 SUBMITTED 内合规审/支持方审) */
|
/** 劳务材料合规是否已通过 0否1是 (区分 SUBMITTED 内合规审/支持方审) */
|
||||||
private Integer materialComplianceApproved;
|
private Integer laborComplianceApproved;
|
||||||
|
/** 会务材料合规是否已通过 0否1是 (区分 SUBMITTED 内合规审/支持方审) */
|
||||||
|
private Integer serviceComplianceApproved;
|
||||||
/** 提交截止时间 (建会 = end_time + 项目 submit_deadline_days 天; 退回/解冻 = now + 天数; null = 项目未设天数, 永不冻结) */
|
/** 提交截止时间 (建会 = end_time + 项目 submit_deadline_days 天; 退回/解冻 = now + 天数; null = 项目未设天数, 永不冻结) */
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private Date submitDeadline;
|
private Date submitDeadline;
|
||||||
@@ -189,8 +193,10 @@ public class BizMeeting extends BaseEntity {
|
|||||||
public void setPosterUrl(String posterUrl) { this.posterUrl = posterUrl; }
|
public void setPosterUrl(String posterUrl) { this.posterUrl = posterUrl; }
|
||||||
public String getLaborSigned() { return laborSigned; }
|
public String getLaborSigned() { return laborSigned; }
|
||||||
public void setLaborSigned(String laborSigned) { this.laborSigned = laborSigned; }
|
public void setLaborSigned(String laborSigned) { this.laborSigned = laborSigned; }
|
||||||
public String getMaterialAuditStage() { return materialAuditStage; }
|
public String getLaborAuditStage() { return laborAuditStage; }
|
||||||
public void setMaterialAuditStage(String materialAuditStage) { this.materialAuditStage = materialAuditStage; }
|
public void setLaborAuditStage(String laborAuditStage) { this.laborAuditStage = laborAuditStage; }
|
||||||
|
public String getServiceAuditStage() { return serviceAuditStage; }
|
||||||
|
public void setServiceAuditStage(String serviceAuditStage) { this.serviceAuditStage = serviceAuditStage; }
|
||||||
public Integer getIsExecuted() { return isExecuted; }
|
public Integer getIsExecuted() { return isExecuted; }
|
||||||
public void setIsExecuted(Integer isExecuted) { this.isExecuted = isExecuted; }
|
public void setIsExecuted(Integer isExecuted) { this.isExecuted = isExecuted; }
|
||||||
public Date getExecuteTime() { return executeTime; }
|
public Date getExecuteTime() { return executeTime; }
|
||||||
@@ -209,8 +215,10 @@ public class BizMeeting extends BaseEntity {
|
|||||||
public void setFreezeTime(Date freezeTime) { this.freezeTime = freezeTime; }
|
public void setFreezeTime(Date freezeTime) { this.freezeTime = freezeTime; }
|
||||||
public Date getMaterialAuditTime() { return materialAuditTime; }
|
public Date getMaterialAuditTime() { return materialAuditTime; }
|
||||||
public void setMaterialAuditTime(Date materialAuditTime) { this.materialAuditTime = materialAuditTime; }
|
public void setMaterialAuditTime(Date materialAuditTime) { this.materialAuditTime = materialAuditTime; }
|
||||||
public Integer getMaterialComplianceApproved() { return materialComplianceApproved; }
|
public Integer getLaborComplianceApproved() { return laborComplianceApproved; }
|
||||||
public void setMaterialComplianceApproved(Integer materialComplianceApproved) { this.materialComplianceApproved = materialComplianceApproved; }
|
public void setLaborComplianceApproved(Integer laborComplianceApproved) { this.laborComplianceApproved = laborComplianceApproved; }
|
||||||
|
public Integer getServiceComplianceApproved() { return serviceComplianceApproved; }
|
||||||
|
public void setServiceComplianceApproved(Integer serviceComplianceApproved) { this.serviceComplianceApproved = serviceComplianceApproved; }
|
||||||
public Date getSubmitDeadline() { return submitDeadline; }
|
public Date getSubmitDeadline() { return submitDeadline; }
|
||||||
public void setSubmitDeadline(Date submitDeadline) { this.submitDeadline = submitDeadline; }
|
public void setSubmitDeadline(Date submitDeadline) { this.submitDeadline = submitDeadline; }
|
||||||
public Long getUserId() { return userId; }
|
public Long getUserId() { return userId; }
|
||||||
|
|||||||
+7
-1
@@ -7,7 +7,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
|
|||||||
* 会议审核流程日志对象 biz_meeting_audit_log
|
* 会议审核流程日志对象 biz_meeting_audit_log
|
||||||
* <p>
|
* <p>
|
||||||
* 记录会议审核的每一次流转: 谁、什么时间、什么意见、当前阶段.
|
* 记录会议审核的每一次流转: 谁、什么时间、什么意见、当前阶段.
|
||||||
* 与 biz_meeting.material_audit_stage 配合, 还原审核轨迹.
|
* 与 biz_meeting 劳务/会务两轨 audit_stage 配合, 还原审核轨迹.
|
||||||
*/
|
*/
|
||||||
public class BizMeetingAuditLog {
|
public class BizMeetingAuditLog {
|
||||||
|
|
||||||
@@ -51,6 +51,9 @@ public class BizMeetingAuditLog {
|
|||||||
/** 审核结果 (APPROVED / REJECTED) */
|
/** 审核结果 (APPROVED / REJECTED) */
|
||||||
private String auditResult;
|
private String auditResult;
|
||||||
|
|
||||||
|
/** 材料类型 (LABOR / SERVICE; NULL = 会议级操作如结算/完结, 或历史数据) */
|
||||||
|
private String materialType;
|
||||||
|
|
||||||
/** 软删除标记 0否1是 (admin 删除会议时级联置1, 查询需 WHERE is_deleted=0) */
|
/** 软删除标记 0否1是 (admin 删除会议时级联置1, 查询需 WHERE is_deleted=0) */
|
||||||
private Integer isDeleted;
|
private Integer isDeleted;
|
||||||
|
|
||||||
@@ -90,6 +93,9 @@ public class BizMeetingAuditLog {
|
|||||||
public String getAuditResult() { return auditResult; }
|
public String getAuditResult() { return auditResult; }
|
||||||
public void setAuditResult(String auditResult) { this.auditResult = auditResult; }
|
public void setAuditResult(String auditResult) { this.auditResult = auditResult; }
|
||||||
|
|
||||||
|
public String getMaterialType() { return materialType; }
|
||||||
|
public void setMaterialType(String materialType) { this.materialType = materialType; }
|
||||||
|
|
||||||
public Integer getIsDeleted() { return isDeleted; }
|
public Integer getIsDeleted() { return isDeleted; }
|
||||||
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
|
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
|
||||||
}
|
}
|
||||||
+5
-6
@@ -10,12 +10,11 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
* 会议事实/阶段 自动流转调度器 (每分钟一次).
|
* 会议事实/阶段 自动流转调度器 (每分钟一次).
|
||||||
* <p>
|
* <p>
|
||||||
* 状态机已改为「事实 + 推导」模型 (见 {@code StageDeriver}): biz_meeting 存事实
|
* 状态机已改为「事实 + 推导」模型 (见 {@code StageDeriver}): biz_meeting 存事实
|
||||||
* (is_executed / is_frozen / material_audit_stage / 审核时间 …),
|
* (is_executed / is_frozen / 劳务·会务两轨 audit_stage / 审核时间 …),
|
||||||
* 各角色看到的阶段名称由推导得出. 本调度器只负责「时间驱动」的三类事实落地:
|
* 各角色看到的阶段名称由推导得出. 本调度器只负责「时间驱动」的两类事实落地:
|
||||||
* <pre>
|
* <pre>
|
||||||
* 1) start_time 到 → is_executed=1 (执行中)
|
* 1) start_time 到 → is_executed=1 (执行中)
|
||||||
* 2) end_time + submit_deadline_days 到 且 material 未提交 → is_frozen=1 (冻结)
|
* 2) submit_deadline 到 且 任一轨未提交/已退回 → is_frozen=1 (冻结)
|
||||||
* 3) material 通过 且 审核时间过 24h → 待结算 (current_stage 缓存翻 AWAITING_SETTLEMENT)
|
|
||||||
* </pre>
|
* </pre>
|
||||||
* 其余阶段流转由执行方提交 / 审核动作触发 (BizMeetingController), 不在此调度器范围.
|
* 其余阶段流转由执行方提交 / 审核动作触发 (BizMeetingController), 不在此调度器范围.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -29,7 +28,7 @@ public class MeetingStageScheduler
|
|||||||
private BizMeetingMapper meetingMapper;
|
private BizMeetingMapper meetingMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 每分钟: start_time 已过 且 material 未提交 且未执行 → 置执行中.
|
* 每分钟: start_time 已过 且 劳务·会务两轨均未提交 且未执行 → 置执行中.
|
||||||
*/
|
*/
|
||||||
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
||||||
public void markExecuted()
|
public void markExecuted()
|
||||||
@@ -49,7 +48,7 @@ public class MeetingStageScheduler
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 每分钟: material 未提交 且 提交截止时间 (end_time + submit_deadline_days) 已过 → 冻结.
|
* 每分钟: 任一轨未提交/已退回 且 提交截止时间已过 → 冻结 (会议级).
|
||||||
*/
|
*/
|
||||||
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
@Scheduled(fixedRate = 60_000, initialDelay = 30_000)
|
||||||
public void markFrozen()
|
public void markFrozen()
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import com.ruoyi.business.domain.BizMeeting;
|
|||||||
* 会议阶段推导器 (单一可信源).
|
* 会议阶段推导器 (单一可信源).
|
||||||
* <p>
|
* <p>
|
||||||
* 事实与展示分离: {@code biz_meeting} 只存事实 (is_executed/is_settled/is_finished/is_frozen
|
* 事实与展示分离: {@code biz_meeting} 只存事实 (is_executed/is_settled/is_finished/is_frozen
|
||||||
* + material_audit_stage + 审核时间 + compliance_approved), 各角色看到的
|
* + 劳务/会务两轨各自 audit_stage + compliance_approved + 审核时间), 各角色看到的
|
||||||
* 「阶段名称」由本类实时计算.
|
* 「阶段名称」由本类实时计算.
|
||||||
|
* <p>
|
||||||
|
* 劳务/会务材料分轨后, 每条轨道独立走 N/C0/C1/A/R 状态机, 会议级阶段由两轨汇合:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>{@link #derivePhysicalStage(BizMeeting)}: 10 值物理阶段 (current_stage 缓存 + 列表筛选).</li>
|
* <li>{@link #derivePhysicalStage(BizMeeting)}: 10 值物理阶段 = 两轨最小进度 (current_stage 缓存 + 列表筛选).</li>
|
||||||
* <li>{@link #deriveDisplay(String, BizMeeting)}: 各角色展示名 (audit_log 4 列 + 前端镜像).</li>
|
* <li>{@link #deriveDisplay(String, BizMeeting)}: 各角色展示名 (audit_log 4 列 + 前端镜像),
|
||||||
|
* 按角色优先级选代表轨, 再用单轨措辞渲染.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @author guoju
|
* @author guoju
|
||||||
@@ -24,10 +27,29 @@ public class StageDeriver
|
|||||||
return v != null && v == 1;
|
return v != null && v == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 单轨状态码: R=0, N=1, C0=2, C1=3, A=4 (数值越小 = 流程越靠前). */
|
||||||
|
private static int stateOf(String stage, Integer complianceApproved)
|
||||||
|
{
|
||||||
|
if ("REJECTED".equals(stage)) return 0;
|
||||||
|
if ("SUBMITTED".equals(stage)) return t(complianceApproved) ? 3 : 2;
|
||||||
|
if ("APPROVED".equals(stage)) return 4;
|
||||||
|
return 1; // NOT_SUBMITTED (或 null 兜底)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int laborState(BizMeeting m)
|
||||||
|
{
|
||||||
|
return stateOf(m.getLaborAuditStage(), m.getLaborComplianceApproved());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int serviceState(BizMeeting m)
|
||||||
|
{
|
||||||
|
return stateOf(m.getServiceAuditStage(), m.getServiceComplianceApproved());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 10 值物理阶段 (NOT_STARTED/RUNNING/AWAITING_COMPLIANCE/AWAITING_SUPERVISION/
|
* 10 值物理阶段 (NOT_STARTED/RUNNING/AWAITING_COMPLIANCE/AWAITING_SUPERVISION/
|
||||||
* SUPERVISION_APPROVED/RECTIFYING/AWAITING_SETTLEMENT/SETTLED/FINISHED/FROZEN), 由事实推导.
|
* SUPERVISION_APPROVED/RECTIFYING/AWAITING_SETTLEMENT/SETTLED/FINISHED/FROZEN), 由事实推导.
|
||||||
* 用于 current_stage 缓存 (列表筛选仍按物理态精确匹配).
|
* 两轨取最小进度 (流程最靠前的一轨决定会议物理态), 用于 current_stage 缓存 (列表筛选按物理态精确匹配).
|
||||||
*/
|
*/
|
||||||
public String derivePhysicalStage(BizMeeting m)
|
public String derivePhysicalStage(BizMeeting m)
|
||||||
{
|
{
|
||||||
@@ -35,26 +57,25 @@ public class StageDeriver
|
|||||||
if (t(m.getIsFinished())) return "FINISHED";
|
if (t(m.getIsFinished())) return "FINISHED";
|
||||||
if (t(m.getIsSettled())) return "SETTLED";
|
if (t(m.getIsSettled())) return "SETTLED";
|
||||||
|
|
||||||
String material = m.getMaterialAuditStage();
|
int s = Math.min(laborState(m), serviceState(m));
|
||||||
if ("REJECTED".equals(material)) return "RECTIFYING";
|
switch (s)
|
||||||
if ("APPROVED".equals(material))
|
|
||||||
{
|
{
|
||||||
return "AWAITING_SETTLEMENT";
|
case 0: return "RECTIFYING";
|
||||||
|
case 2: return "AWAITING_COMPLIANCE";
|
||||||
|
case 3: return "AWAITING_SUPERVISION";
|
||||||
|
case 4: return "AWAITING_SETTLEMENT";
|
||||||
|
default: return t(m.getIsExecuted()) ? "RUNNING" : "NOT_STARTED"; // s = 1 (N)
|
||||||
}
|
}
|
||||||
if ("SUBMITTED".equals(material))
|
|
||||||
{
|
|
||||||
return t(m.getMaterialComplianceApproved()) ? "AWAITING_SUPERVISION" : "AWAITING_COMPLIANCE";
|
|
||||||
}
|
|
||||||
// NOT_SUBMITTED (或 null 兜底)
|
|
||||||
return t(m.getIsExecuted()) ? "RUNNING" : "NOT_STARTED";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 各角色展示阶段名. role ∈ {executor, sponsor, manager, admin}; doctor/expert 等非流程角色按 admin 中性.
|
* 各角色展示阶段名. role ∈ {executor, sponsor, manager, admin}; doctor/expert 等非流程角色按 admin 中性.
|
||||||
* <p>
|
* <p>
|
||||||
* 优先级自上而下命中即返回:
|
* 固定顶格: 冻结中 → 已完结 → 已结算. 否则按角色优先级选「代表轨」再渲染单轨措辞:
|
||||||
* <pre>
|
* <pre>
|
||||||
* 冻结 → 完结 → 已结算 → (材料) 审核驳回 → 审核通过 → (材料) APPROVED → SUBMITTED → NOT_SUBMITTED
|
* 默认 (executor/admin/doctor): 最小进度 (R > N > C0 > C1 > A)
|
||||||
|
* manager: 优先 C0(待合规审核) > R > N > C1 > A → 有活「待审核」, 无活按最早环节优先
|
||||||
|
* sponsor: 优先 C1(待支持方审核) > R > N > C0 > A → 有活「待审核」, 无活按最早环节优先
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public String deriveDisplay(String role, BizMeeting m)
|
public String deriveDisplay(String role, BizMeeting m)
|
||||||
@@ -63,44 +84,69 @@ public class StageDeriver
|
|||||||
if (t(m.getIsFinished())) return "已完结";
|
if (t(m.getIsFinished())) return "已完结";
|
||||||
if (t(m.getIsSettled())) return "已结算";
|
if (t(m.getIsSettled())) return "已结算";
|
||||||
|
|
||||||
String material = m.getMaterialAuditStage();
|
int chosen = chooseState(role, laborState(m), serviceState(m));
|
||||||
|
return render(role, chosen, t(m.getIsExecuted()));
|
||||||
// 退回: 执行方看「已退回」, 其他方看「待整改」
|
|
||||||
if ("REJECTED".equals(material))
|
|
||||||
{
|
|
||||||
return "executor".equals(role) ? "已退回" : "待整改";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 材料已支持方通过 → 待结算 (支持方审通过即待结算, 不再有 24h 慢路径)
|
/** 按角色优先级选代表轨 (两轨中优先级更高的那轨). */
|
||||||
if ("APPROVED".equals(material))
|
private static int chooseState(String role, int labor, int service)
|
||||||
{
|
{
|
||||||
|
if ("manager".equals(role))
|
||||||
|
{
|
||||||
|
return managerPriority(labor) <= managerPriority(service) ? labor : service;
|
||||||
|
}
|
||||||
|
if ("sponsor".equals(role))
|
||||||
|
{
|
||||||
|
return sponsorPriority(labor) <= sponsorPriority(service) ? labor : service;
|
||||||
|
}
|
||||||
|
// 默认: 最小进度 (状态码本身即 R<N<C0<C1<A 顺序)
|
||||||
|
return Math.min(labor, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** manager 优先级: C0=0, R=1, N=2, C1=3, A=4 (越小越优先). C0=有活最优先; 无活时按最早环节优先(R<N<C1<A). */
|
||||||
|
private static int managerPriority(int s)
|
||||||
|
{
|
||||||
|
switch (s)
|
||||||
|
{
|
||||||
|
case 2: return 0; // C0 待合规审核 (有活)
|
||||||
|
case 0: return 1; // R 待整改 (无活时最早)
|
||||||
|
case 1: return 2; // N 未提交
|
||||||
|
case 3: return 3; // C1 待支持方审核
|
||||||
|
default: return 4; // A 已通过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** sponsor 优先级: C1=0, R=1, N=2, C0=3, A=4 (越小越优先). C1=有活最优先; 无活时按最早环节优先(R<N<C0<A). */
|
||||||
|
private static int sponsorPriority(int s)
|
||||||
|
{
|
||||||
|
switch (s)
|
||||||
|
{
|
||||||
|
case 3: return 0; // C1 待支持方审核 (有活)
|
||||||
|
case 0: return 1; // R 待整改 (无活时最早)
|
||||||
|
case 1: return 2; // N 未提交
|
||||||
|
case 2: return 3; // C0 待合规审核
|
||||||
|
default: return 4; // A 已通过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单轨措辞 (代表轨状态 + 角色 → 展示名). */
|
||||||
|
private static String render(String role, int s, boolean executed)
|
||||||
|
{
|
||||||
|
switch (s)
|
||||||
|
{
|
||||||
|
case 0: // R 退回
|
||||||
|
return "executor".equals(role) ? "已退回" : "待整改";
|
||||||
|
case 1: // N 未提交
|
||||||
|
if (!executed) return "未执行";
|
||||||
|
return "executor".equals(role) ? "执行中" : "已执行未传材料";
|
||||||
|
case 2: // C0 合规审中
|
||||||
|
if ("sponsor".equals(role)) return "已执行未传材料"; // 只读
|
||||||
|
return "待审核"; // executor / manager / admin
|
||||||
|
case 3: // C1 支持方审中
|
||||||
|
if ("manager".equals(role)) return "审核通过";
|
||||||
|
return "待审核"; // executor / sponsor / admin
|
||||||
|
default: // 4 = A 已通过
|
||||||
return "待结算";
|
return "待结算";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 材料在审 (SUBMITTED)
|
|
||||||
if ("SUBMITTED".equals(material))
|
|
||||||
{
|
|
||||||
if (t(m.getMaterialComplianceApproved()))
|
|
||||||
{
|
|
||||||
// 支持方审中
|
|
||||||
if ("manager".equals(role)) return "审核通过";
|
|
||||||
if ("sponsor".equals(role)) return "待审核";
|
|
||||||
return "待审核"; // executor / admin
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// 合规审中
|
|
||||||
if ("sponsor".equals(role)) return "已执行未传材料"; // 只读
|
|
||||||
if ("manager".equals(role)) return "待审核";
|
|
||||||
return "待审核"; // executor / admin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 未提交
|
|
||||||
if (t(m.getIsExecuted()))
|
|
||||||
{
|
|
||||||
return "executor".equals(role) ? "执行中" : "已执行未传材料";
|
|
||||||
}
|
|
||||||
return "未执行";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -61,8 +61,11 @@ public class BizMeetingServiceImpl implements IBizMeetingService
|
|||||||
if (entity.getCurrentStage() == null || entity.getCurrentStage().isEmpty()) {
|
if (entity.getCurrentStage() == null || entity.getCurrentStage().isEmpty()) {
|
||||||
entity.setCurrentStage(BizMeetingStageEnum.NOT_STARTED.getCode());
|
entity.setCurrentStage(BizMeetingStageEnum.NOT_STARTED.getCode());
|
||||||
}
|
}
|
||||||
if (entity.getMaterialAuditStage() == null || entity.getMaterialAuditStage().isEmpty()) {
|
if (entity.getLaborAuditStage() == null || entity.getLaborAuditStage().isEmpty()) {
|
||||||
entity.setMaterialAuditStage("NOT_SUBMITTED");
|
entity.setLaborAuditStage("NOT_SUBMITTED");
|
||||||
|
}
|
||||||
|
if (entity.getServiceAuditStage() == null || entity.getServiceAuditStage().isEmpty()) {
|
||||||
|
entity.setServiceAuditStage("NOT_SUBMITTED");
|
||||||
}
|
}
|
||||||
return bizMeetingMapper.insert(entity);
|
return bizMeetingMapper.insert(entity);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -15,11 +15,12 @@
|
|||||||
<result property="auditTime" column="audit_time" />
|
<result property="auditTime" column="audit_time" />
|
||||||
<result property="auditType" column="audit_type" />
|
<result property="auditType" column="audit_type" />
|
||||||
<result property="auditResult" column="audit_result" />
|
<result property="auditResult" column="audit_result" />
|
||||||
|
<result property="materialType" column="material_type" />
|
||||||
<result property="isDeleted" column="is_deleted" />
|
<result property="isDeleted" column="is_deleted" />
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="selectFields">
|
<sql id="selectFields">
|
||||||
select id, meeting_id, auditor, opinion, executor_stage, sponsor_stage, manager_stage, admin_stage, create_time, audit_time, audit_type, audit_result, is_deleted
|
select id, meeting_id, auditor, opinion, executor_stage, sponsor_stage, manager_stage, admin_stage, create_time, audit_time, audit_type, audit_result, material_type, is_deleted
|
||||||
from biz_meeting_audit_log
|
from biz_meeting_audit_log
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
<if test="auditTime != null">audit_time,</if>
|
<if test="auditTime != null">audit_time,</if>
|
||||||
<if test="auditType != null and auditType != ''">audit_type,</if>
|
<if test="auditType != null and auditType != ''">audit_type,</if>
|
||||||
<if test="auditResult != null and auditResult != ''">audit_result,</if>
|
<if test="auditResult != null and auditResult != ''">audit_result,</if>
|
||||||
|
<if test="materialType != null and materialType != ''">material_type,</if>
|
||||||
</trim>
|
</trim>
|
||||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||||
<if test="meetingId != null">#{meetingId},</if>
|
<if test="meetingId != null">#{meetingId},</if>
|
||||||
@@ -65,6 +67,7 @@
|
|||||||
<if test="auditTime != null">#{auditTime},</if>
|
<if test="auditTime != null">#{auditTime},</if>
|
||||||
<if test="auditType != null and auditType != ''">#{auditType},</if>
|
<if test="auditType != null and auditType != ''">#{auditType},</if>
|
||||||
<if test="auditResult != null and auditResult != ''">#{auditResult},</if>
|
<if test="auditResult != null and auditResult != ''">#{auditResult},</if>
|
||||||
|
<if test="materialType != null and materialType != ''">#{materialType},</if>
|
||||||
</trim>
|
</trim>
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
<result property="supervisionOpinion" column="supervision_opinion" />
|
<result property="supervisionOpinion" column="supervision_opinion" />
|
||||||
<result property="supervisionBy" column="supervision_by" />
|
<result property="supervisionBy" column="supervision_by" />
|
||||||
<result property="supervisionTime" column="supervision_time" />
|
<result property="supervisionTime" column="supervision_time" />
|
||||||
<result property="materialAuditStage" column="material_audit_stage" />
|
<result property="laborAuditStage" column="labor_audit_stage" />
|
||||||
|
<result property="serviceAuditStage" column="service_audit_stage" />
|
||||||
<result property="isExecuted" column="is_executed" />
|
<result property="isExecuted" column="is_executed" />
|
||||||
<result property="executeTime" column="execute_time" />
|
<result property="executeTime" column="execute_time" />
|
||||||
<result property="isSettled" column="is_settled" />
|
<result property="isSettled" column="is_settled" />
|
||||||
@@ -31,7 +32,8 @@
|
|||||||
<result property="isFrozen" column="is_frozen" />
|
<result property="isFrozen" column="is_frozen" />
|
||||||
<result property="freezeTime" column="freeze_time" />
|
<result property="freezeTime" column="freeze_time" />
|
||||||
<result property="materialAuditTime" column="material_audit_time" />
|
<result property="materialAuditTime" column="material_audit_time" />
|
||||||
<result property="materialComplianceApproved" column="material_compliance_approved" />
|
<result property="laborComplianceApproved" column="labor_compliance_approved" />
|
||||||
|
<result property="serviceComplianceApproved" column="service_compliance_approved" />
|
||||||
<result property="submitDeadline" column="submit_deadline" />
|
<result property="submitDeadline" column="submit_deadline" />
|
||||||
<result property="invitationUrl" column="invitation_url" />
|
<result property="invitationUrl" column="invitation_url" />
|
||||||
<result property="projectInvitationUrl" column="project_invitation_url" />
|
<result property="projectInvitationUrl" column="project_invitation_url" />
|
||||||
@@ -51,7 +53,7 @@
|
|||||||
<result property="isDeleted" column="is_deleted" />
|
<result property="isDeleted" column="is_deleted" />
|
||||||
</resultMap>
|
</resultMap>
|
||||||
<sql id="selectFields">
|
<sql id="selectFields">
|
||||||
meeting_id, business_id, project_id, execution_unit_id, project_no, project_name, meeting_name, period_no, total_periods, project_form, start_time, end_time, address, remark, current_stage, supervision_opinion, supervision_by, supervision_time, material_audit_stage, is_executed, execute_time, is_settled, settle_time, is_finished, finish_time, is_frozen, freeze_time, material_audit_time, material_compliance_approved, submit_deadline, invitation_url, schedule_url, poster_url, labor_signed, labor_fee, meeting_fee, total_fee, fee_calc_status, create_by, create_time, update_by, update_time, is_deleted
|
meeting_id, business_id, project_id, execution_unit_id, project_no, project_name, meeting_name, period_no, total_periods, project_form, start_time, end_time, address, remark, current_stage, supervision_opinion, supervision_by, supervision_time, labor_audit_stage, service_audit_stage, is_executed, execute_time, is_settled, settle_time, is_finished, finish_time, is_frozen, freeze_time, material_audit_time, labor_compliance_approved, service_compliance_approved, submit_deadline, invitation_url, schedule_url, poster_url, labor_signed, labor_fee, meeting_fee, total_fee, fee_calc_status, create_by, create_time, update_by, update_time, is_deleted
|
||||||
</sql>
|
</sql>
|
||||||
<select id="selectByPrimaryKey" resultMap="BizMeetingResult" parameterType="Long">
|
<select id="selectByPrimaryKey" resultMap="BizMeetingResult" parameterType="Long">
|
||||||
select
|
select
|
||||||
@@ -93,16 +95,13 @@
|
|||||||
and p2.is_finished = '1' and p2.open_status = 'N'
|
and p2.is_finished = '1' and p2.open_status = 'N'
|
||||||
)
|
)
|
||||||
</if>
|
</if>
|
||||||
<!-- executor 数据权限: 只看"我的项目"下的会议. MAIN 走 biz_project_assign (execution_unit_id), SUB(执行人) 走 biz_project_executor_assign.staff_user_id -->
|
<!-- executor 数据权限: MAIN 看本执行单位项目下的会议 (biz_project_assign.execution_unit_id), SUB(执行人) 只看本人创建的会议 (create_by) -->
|
||||||
<if test="params.executorUserId != null">and project_id in (
|
<if test="params.executorUserId != null">and project_id in (
|
||||||
select distinct a.project_id from biz_project_assign a
|
select distinct a.project_id from biz_project_assign a
|
||||||
where a.is_deleted = 0
|
where a.is_deleted = 0
|
||||||
and a.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')
|
and a.execution_unit_id = (select org_id from biz_org where user_id = #{params.executorUserId} and org_type = 'executor')
|
||||||
)</if>
|
)</if>
|
||||||
<if test="params.executorStaffUserId != null">and project_id in (
|
<if test="params.executorCreatorUsername != null and params.executorCreatorUsername != ''">and create_by = #{params.executorCreatorUsername}</if>
|
||||||
select distinct a.project_id from biz_project_executor_assign a
|
|
||||||
where a.is_deleted = 0 and a.staff_user_id = #{params.executorStaffUserId}
|
|
||||||
)</if>
|
|
||||||
<!-- 合规人员(manager) 数据权限: 只看本人创建项目的会议 -->
|
<!-- 合规人员(manager) 数据权限: 只看本人创建项目的会议 -->
|
||||||
<if test="params.managerCreateUserId != null">and project_id in (
|
<if test="params.managerCreateUserId != null">and project_id in (
|
||||||
select project_id from biz_project where create_user_id = #{params.managerCreateUserId} and is_deleted = 0
|
select project_id from biz_project where create_user_id = #{params.managerCreateUserId} and is_deleted = 0
|
||||||
@@ -131,7 +130,8 @@
|
|||||||
<if test="supervisionOpinion != null and supervisionOpinion != ''">supervision_opinion,</if>
|
<if test="supervisionOpinion != null and supervisionOpinion != ''">supervision_opinion,</if>
|
||||||
<if test="supervisionBy != null and supervisionBy != ''">supervision_by,</if>
|
<if test="supervisionBy != null and supervisionBy != ''">supervision_by,</if>
|
||||||
<if test="supervisionTime != null">supervision_time,</if>
|
<if test="supervisionTime != null">supervision_time,</if>
|
||||||
<if test="materialAuditStage != null and materialAuditStage != ''">material_audit_stage,</if>
|
<if test="laborAuditStage != null and laborAuditStage != ''">labor_audit_stage,</if>
|
||||||
|
<if test="serviceAuditStage != null and serviceAuditStage != ''">service_audit_stage,</if>
|
||||||
<if test="submitDeadline != null">submit_deadline,</if>
|
<if test="submitDeadline != null">submit_deadline,</if>
|
||||||
<if test="invitationUrl != null and invitationUrl != ''">invitation_url,</if>
|
<if test="invitationUrl != null and invitationUrl != ''">invitation_url,</if>
|
||||||
<if test="scheduleUrl != null and scheduleUrl != ''">schedule_url,</if>
|
<if test="scheduleUrl != null and scheduleUrl != ''">schedule_url,</if>
|
||||||
@@ -161,7 +161,8 @@
|
|||||||
<if test="supervisionOpinion != null and supervisionOpinion != ''">#{supervisionOpinion},</if>
|
<if test="supervisionOpinion != null and supervisionOpinion != ''">#{supervisionOpinion},</if>
|
||||||
<if test="supervisionBy != null and supervisionBy != ''">#{supervisionBy},</if>
|
<if test="supervisionBy != null and supervisionBy != ''">#{supervisionBy},</if>
|
||||||
<if test="supervisionTime != null">#{supervisionTime},</if>
|
<if test="supervisionTime != null">#{supervisionTime},</if>
|
||||||
<if test="materialAuditStage != null and materialAuditStage != ''">#{materialAuditStage},</if>
|
<if test="laborAuditStage != null and laborAuditStage != ''">#{laborAuditStage},</if>
|
||||||
|
<if test="serviceAuditStage != null and serviceAuditStage != ''">#{serviceAuditStage},</if>
|
||||||
<if test="submitDeadline != null">#{submitDeadline},</if>
|
<if test="submitDeadline != null">#{submitDeadline},</if>
|
||||||
<if test="invitationUrl != null and invitationUrl != ''">#{invitationUrl},</if>
|
<if test="invitationUrl != null and invitationUrl != ''">#{invitationUrl},</if>
|
||||||
<if test="scheduleUrl != null and scheduleUrl != ''">#{scheduleUrl},</if>
|
<if test="scheduleUrl != null and scheduleUrl != ''">#{scheduleUrl},</if>
|
||||||
@@ -194,7 +195,8 @@
|
|||||||
<if test="supervisionOpinion != null and supervisionOpinion != ''">supervision_opinion = #{supervisionOpinion},</if>
|
<if test="supervisionOpinion != null and supervisionOpinion != ''">supervision_opinion = #{supervisionOpinion},</if>
|
||||||
<if test="supervisionBy != null and supervisionBy != ''">supervision_by = #{supervisionBy},</if>
|
<if test="supervisionBy != null and supervisionBy != ''">supervision_by = #{supervisionBy},</if>
|
||||||
<if test="supervisionTime != null">supervision_time = #{supervisionTime},</if>
|
<if test="supervisionTime != null">supervision_time = #{supervisionTime},</if>
|
||||||
<if test="materialAuditStage != null and materialAuditStage != ''">material_audit_stage = #{materialAuditStage},</if>
|
<if test="laborAuditStage != null and laborAuditStage != ''">labor_audit_stage = #{laborAuditStage},</if>
|
||||||
|
<if test="serviceAuditStage != null and serviceAuditStage != ''">service_audit_stage = #{serviceAuditStage},</if>
|
||||||
<if test="isExecuted != null">is_executed = #{isExecuted},</if>
|
<if test="isExecuted != null">is_executed = #{isExecuted},</if>
|
||||||
<if test="executeTime != null">execute_time = #{executeTime},</if>
|
<if test="executeTime != null">execute_time = #{executeTime},</if>
|
||||||
<if test="isSettled != null">is_settled = #{isSettled},</if>
|
<if test="isSettled != null">is_settled = #{isSettled},</if>
|
||||||
@@ -204,7 +206,8 @@
|
|||||||
<if test="isFrozen != null">is_frozen = #{isFrozen},</if>
|
<if test="isFrozen != null">is_frozen = #{isFrozen},</if>
|
||||||
<if test="freezeTime != null">freeze_time = #{freezeTime},</if>
|
<if test="freezeTime != null">freeze_time = #{freezeTime},</if>
|
||||||
<if test="materialAuditTime != null">material_audit_time = #{materialAuditTime},</if>
|
<if test="materialAuditTime != null">material_audit_time = #{materialAuditTime},</if>
|
||||||
<if test="materialComplianceApproved != null">material_compliance_approved = #{materialComplianceApproved},</if>
|
<if test="laborComplianceApproved != null">labor_compliance_approved = #{laborComplianceApproved},</if>
|
||||||
|
<if test="serviceComplianceApproved != null">service_compliance_approved = #{serviceComplianceApproved},</if>
|
||||||
<if test="submitDeadline != null">submit_deadline = #{submitDeadline},</if>
|
<if test="submitDeadline != null">submit_deadline = #{submitDeadline},</if>
|
||||||
<if test="invitationUrl != null and invitationUrl != ''">invitation_url = #{invitationUrl},</if>
|
<if test="invitationUrl != null and invitationUrl != ''">invitation_url = #{invitationUrl},</if>
|
||||||
<if test="scheduleUrl != null and scheduleUrl != ''">schedule_url = #{scheduleUrl},</if>
|
<if test="scheduleUrl != null and scheduleUrl != ''">schedule_url = #{scheduleUrl},</if>
|
||||||
@@ -258,7 +261,8 @@
|
|||||||
and is_frozen = 0
|
and is_frozen = 0
|
||||||
and start_time is not null
|
and start_time is not null
|
||||||
and start_time <= NOW()
|
and start_time <= NOW()
|
||||||
and material_audit_stage = 'NOT_SUBMITTED'
|
and labor_audit_stage = 'NOT_SUBMITTED'
|
||||||
|
and service_audit_stage = 'NOT_SUBMITTED'
|
||||||
</update>
|
</update>
|
||||||
<!-- 自动流转 (MeetingStageScheduler 每分钟调): material 未提交/已退回 且 submit_deadline 已过 → 冻结.
|
<!-- 自动流转 (MeetingStageScheduler 每分钟调): material 未提交/已退回 且 submit_deadline 已过 → 冻结.
|
||||||
submit_deadline 由建会/退回/解冻 时写入 (end_time 或 now + 项目 submit_deadline_days 天), null = 项目未设天数 → 永不冻结. -->
|
submit_deadline 由建会/退回/解冻 时写入 (end_time 或 now + 项目 submit_deadline_days 天), null = 项目未设天数 → 永不冻结. -->
|
||||||
@@ -269,7 +273,8 @@
|
|||||||
current_stage = 'FROZEN'
|
current_stage = 'FROZEN'
|
||||||
where is_deleted = 0
|
where is_deleted = 0
|
||||||
and is_frozen = 0
|
and is_frozen = 0
|
||||||
and material_audit_stage in ('NOT_SUBMITTED', 'REJECTED')
|
and (labor_audit_stage in ('NOT_SUBMITTED', 'REJECTED')
|
||||||
|
or service_audit_stage in ('NOT_SUBMITTED', 'REJECTED'))
|
||||||
and submit_deadline is not null
|
and submit_deadline is not null
|
||||||
and submit_deadline <= NOW()
|
and submit_deadline <= NOW()
|
||||||
</update>
|
</update>
|
||||||
|
|||||||
@@ -111,6 +111,7 @@
|
|||||||
<select id="selectSponsorOrgOptions" parameterType="BizOrg" resultType="java.util.LinkedHashMap">
|
<select id="selectSponsorOrgOptions" parameterType="BizOrg" resultType="java.util.LinkedHashMap">
|
||||||
select o.org_id as orgId,
|
select o.org_id as orgId,
|
||||||
o.org_name as orgName,
|
o.org_name as orgName,
|
||||||
|
o.status as status,
|
||||||
u.user_name as userName
|
u.user_name as userName
|
||||||
from biz_org o
|
from biz_org o
|
||||||
join sys_user u on u.user_id = o.user_id
|
join sys_user u on u.user_id = o.user_id
|
||||||
@@ -135,6 +136,7 @@
|
|||||||
<select id="selectExecutorOrgOptions" parameterType="BizOrg" resultType="java.util.LinkedHashMap">
|
<select id="selectExecutorOrgOptions" parameterType="BizOrg" resultType="java.util.LinkedHashMap">
|
||||||
select o.org_id as orgId,
|
select o.org_id as orgId,
|
||||||
o.org_name as orgName,
|
o.org_name as orgName,
|
||||||
|
o.status as status,
|
||||||
u.user_name as userName
|
u.user_name as userName
|
||||||
from biz_org o
|
from biz_org o
|
||||||
join sys_user u on u.user_id = o.user_id
|
join sys_user u on u.user_id = o.user_id
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package com.ruoyi.common.enums;
|
package com.ruoyi.common.enums;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 材料 审核阶段 (biz_meeting.material_audit_stage)
|
* 材料 审核阶段 (单轨 4 值, 用于 biz_meeting.labor_audit_stage / service_audit_stage)
|
||||||
* <p>
|
* <p>
|
||||||
* 4 值 (用户拍板「已提交/待审核」合并): 提交后进入 SUBMITTED, 两级审核(合规先-支持方后)
|
* 劳务/会务两轨各自独立走这条状态机. 4 值 (用户拍板「已提交/待审核」合并):
|
||||||
* 用 material_compliance_approved 布尔区分「合规审中 vs 支持方审中」.
|
* 提交后进入 SUBMITTED, 两级审核(合规先-支持方后)用各自 compliance_approved 布尔区分
|
||||||
|
* 「合规审中 vs 支持方审中」.
|
||||||
* <pre>
|
* <pre>
|
||||||
* NOT_SUBMITTED 未提交 执行方还没交
|
* NOT_SUBMITTED 未提交 执行方还没交
|
||||||
* ↓ (执行方提交)
|
* ↓ (执行方提交)
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ const rawOptions = ref([])
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
// el-select 内部绑定的 key (可能 = 字典值, 可能 = OTHER_KEY)
|
// el-select 内部绑定的 key (可能 = 字典值, 可能 = OTHER_KEY)
|
||||||
const selectedKey = ref(props.modelValue ?? null)
|
// 空字符串统一成 null, 避免挂载时 '' → null 触发 el-select 的 watch→validate('change') 导致必填项直接飘红
|
||||||
|
const selectedKey = ref((props.modelValue == null || props.modelValue === '') ? null : props.modelValue)
|
||||||
|
|
||||||
// "其他" 输入框独立内容
|
// "其他" 输入框独立内容
|
||||||
const otherText = ref(props.modelValue ?? '')
|
const otherText = ref(props.modelValue ?? '')
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ const rawOptions = ref([])
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
// el-select 内部绑定的 key
|
// el-select 内部绑定的 key
|
||||||
const selectedKey = ref(props.modelValue ?? null)
|
// 空字符串统一成 null, 避免挂载时 '' → null 触发 el-select 的 watch→validate('change') 导致必填项直接飘红
|
||||||
|
const selectedKey = ref((props.modelValue == null || props.modelValue === '') ? null : props.modelValue)
|
||||||
|
|
||||||
// "其他" 输入框独立内容
|
// "其他" 输入框独立内容
|
||||||
const otherText = ref(props.modelValue ?? '')
|
const otherText = ref(props.modelValue ?? '')
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
* 会议阶段显示 (事实驱动, 与后端 StageDeriver 镜像).
|
* 会议阶段显示 (事实驱动, 与后端 StageDeriver 镜像).
|
||||||
*
|
*
|
||||||
* biz_meeting 现在存「事实」: is_executed / is_frozen / is_settled / is_finished
|
* biz_meeting 现在存「事实」: is_executed / is_frozen / is_settled / is_finished
|
||||||
* + material_audit_stage (4 值: NOT_SUBMITTED/SUBMITTED/APPROVED/REJECTED)
|
* + 劳务/会务两轨各自 audit_stage (4 值: NOT_SUBMITTED/SUBMITTED/APPROVED/REJECTED)
|
||||||
* + material_compliance_approved (区分两级审核) + 审核时间.
|
* + 各自 compliance_approved (区分两级审核) + 审核时间.
|
||||||
*
|
*
|
||||||
* 各角色看到的「阶段名称」由这些事实实时推导, 不再是单一 current_stage 枚举投影.
|
* 各角色看到的「阶段名称」由这些事实实时推导, 不再是单一 current_stage 枚举投影.
|
||||||
* current_stage 仍是物理阶段缓存 (10 值), 仅供列表筛选精确匹配.
|
* current_stage 仍是物理阶段缓存 (10 值), 仅供列表筛选精确匹配.
|
||||||
@@ -13,21 +13,87 @@ function isTrue(v) {
|
|||||||
return v === 1 || v === '1' || v === true
|
return v === 1 || v === '1' || v === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 单轨状态码: R=0, N=1, C0=2, C1=3, A=4 (数值越小 = 流程越靠前). */
|
||||||
|
function stateOf(stage, complianceApproved) {
|
||||||
|
if (stage === 'REJECTED') return 0
|
||||||
|
if (stage === 'SUBMITTED') return isTrue(complianceApproved) ? 3 : 2
|
||||||
|
if (stage === 'APPROVED') return 4
|
||||||
|
return 1 // NOT_SUBMITTED (或空兜底)
|
||||||
|
}
|
||||||
|
|
||||||
|
function laborState(row) {
|
||||||
|
return stateOf(row.laborAuditStage, row.laborComplianceApproved)
|
||||||
|
}
|
||||||
|
|
||||||
|
function serviceState(row) {
|
||||||
|
return stateOf(row.serviceAuditStage, row.serviceComplianceApproved)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 10 值物理阶段 (镜像后端 StageDeriver.derivePhysicalStage), 用于颜色/筛选.
|
* 10 值物理阶段 (镜像后端 StageDeriver.derivePhysicalStage), 用于颜色/筛选.
|
||||||
|
* 两轨取最小进度 (流程最靠前的一轨决定会议物理态).
|
||||||
*/
|
*/
|
||||||
export function derivePhysicalStage(row) {
|
export function derivePhysicalStage(row) {
|
||||||
if (!row) return 'NOT_STARTED'
|
if (!row) return 'NOT_STARTED'
|
||||||
if (isTrue(row.isFrozen)) return 'FROZEN'
|
if (isTrue(row.isFrozen)) return 'FROZEN'
|
||||||
if (isTrue(row.isFinished)) return 'FINISHED'
|
if (isTrue(row.isFinished)) return 'FINISHED'
|
||||||
if (isTrue(row.isSettled)) return 'SETTLED'
|
if (isTrue(row.isSettled)) return 'SETTLED'
|
||||||
const material = row.materialAuditStage
|
const s = Math.min(laborState(row), serviceState(row))
|
||||||
if (material === 'REJECTED') return 'RECTIFYING'
|
if (s === 0) return 'RECTIFYING'
|
||||||
if (material === 'APPROVED') return 'AWAITING_SETTLEMENT'
|
if (s === 2) return 'AWAITING_COMPLIANCE'
|
||||||
if (material === 'SUBMITTED') return isTrue(row.materialComplianceApproved) ? 'AWAITING_SUPERVISION' : 'AWAITING_COMPLIANCE'
|
if (s === 3) return 'AWAITING_SUPERVISION'
|
||||||
|
if (s === 4) return 'AWAITING_SETTLEMENT'
|
||||||
return isTrue(row.isExecuted) ? 'RUNNING' : 'NOT_STARTED'
|
return isTrue(row.isExecuted) ? 'RUNNING' : 'NOT_STARTED'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** manager 优先级: C0=0, R=1, N=2, C1=3, A=4 (越小越优先). C0=有活最优先; 无活时按最早环节优先(R<N<C1<A). */
|
||||||
|
function managerPriority(s) {
|
||||||
|
switch (s) {
|
||||||
|
case 2: return 0
|
||||||
|
case 0: return 1
|
||||||
|
case 1: return 2
|
||||||
|
case 3: return 3
|
||||||
|
default: return 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** sponsor 优先级: C1=0, R=1, N=2, C0=3, A=4 (越小越优先). C1=有活最优先; 无活时按最早环节优先(R<N<C0<A). */
|
||||||
|
function sponsorPriority(s) {
|
||||||
|
switch (s) {
|
||||||
|
case 3: return 0
|
||||||
|
case 0: return 1
|
||||||
|
case 1: return 2
|
||||||
|
case 2: return 3
|
||||||
|
default: return 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按角色优先级选代表轨 (两轨中优先级更高的那轨). */
|
||||||
|
function chooseState(role, labor, service) {
|
||||||
|
if (role === 'manager') return managerPriority(labor) <= managerPriority(service) ? labor : service
|
||||||
|
if (role === 'sponsor') return sponsorPriority(labor) <= sponsorPriority(service) ? labor : service
|
||||||
|
return Math.min(labor, service)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单轨措辞 (代表轨状态 + 角色 → 展示名). */
|
||||||
|
function render(role, s, executed) {
|
||||||
|
switch (s) {
|
||||||
|
case 0: // R 退回
|
||||||
|
return role === 'executor' ? '已退回' : '待整改'
|
||||||
|
case 1: // N 未提交
|
||||||
|
if (!executed) return '未执行'
|
||||||
|
return role === 'executor' ? '执行中' : '已执行未传材料'
|
||||||
|
case 2: // C0 合规审中
|
||||||
|
if (role === 'sponsor') return '已执行未传材料' // 只读
|
||||||
|
return '待审核'
|
||||||
|
case 3: // C1 支持方审中
|
||||||
|
if (role === 'manager') return '审核通过'
|
||||||
|
return '待审核'
|
||||||
|
default: // 4 = A 已通过
|
||||||
|
return '待结算'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 各角色展示阶段名 (镜像后端 StageDeriver.deriveDisplay).
|
* 各角色展示阶段名 (镜像后端 StageDeriver.deriveDisplay).
|
||||||
* role ∈ {executor, sponsor, manager, admin, doctor, expert}; 非流程角色回退 admin 中性.
|
* role ∈ {executor, sponsor, manager, admin, doctor, expert}; 非流程角色回退 admin 中性.
|
||||||
@@ -37,34 +103,8 @@ export function deriveStage(role, row) {
|
|||||||
if (isTrue(row.isFrozen)) return '冻结中'
|
if (isTrue(row.isFrozen)) return '冻结中'
|
||||||
if (isTrue(row.isFinished)) return '已完结'
|
if (isTrue(row.isFinished)) return '已完结'
|
||||||
if (isTrue(row.isSettled)) return '已结算'
|
if (isTrue(row.isSettled)) return '已结算'
|
||||||
|
const chosen = chooseState(role, laborState(row), serviceState(row))
|
||||||
const material = row.materialAuditStage
|
return render(role, chosen, isTrue(row.isExecuted))
|
||||||
|
|
||||||
// 退回: 执行方看「已退回」, 其他方看「待整改」
|
|
||||||
if (material === 'REJECTED') {
|
|
||||||
return role === 'executor' ? '已退回' : '待整改'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 材料已支持方通过 → 待结算 (不再有 24h 慢路径)
|
|
||||||
if (material === 'APPROVED') {
|
|
||||||
return '待结算'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 材料在审 (SUBMITTED)
|
|
||||||
if (material === 'SUBMITTED') {
|
|
||||||
if (isTrue(row.materialComplianceApproved)) {
|
|
||||||
// 支持方审中
|
|
||||||
return role === 'manager' ? '审核通过' : '待审核'
|
|
||||||
}
|
|
||||||
// 合规审中
|
|
||||||
return role === 'sponsor' ? '已执行未传材料' : '待审核'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 未提交
|
|
||||||
if (isTrue(row.isExecuted)) {
|
|
||||||
return role === 'executor' ? '执行中' : '已执行未传材料'
|
|
||||||
}
|
|
||||||
return '未执行'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,31 +114,38 @@ export function stageLabel(role, row) {
|
|||||||
return deriveStage(role, row)
|
return deriveStage(role, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 颜色 class (业务专用 tag), 基于物理阶段 */
|
/**
|
||||||
export function stageClass(row) {
|
* 展示阶段名 → 颜色映射 (class + el-tag type), 与 render() 措辞一一对应.
|
||||||
const s = derivePhysicalStage(row)
|
* 颜色跟随「各角色看到的展示阶段」而非物理阶段, 避免文案与颜色错位
|
||||||
if (s === 'NOT_STARTED') return 'pending'
|
* (如 sponsor 看「待审核」却因物理阶段 RECTIFYING 显示红色).
|
||||||
if (s === 'RUNNING') return 'running'
|
* 规则: 待整改/已退回=红, 未执行=灰, 执行中/已执行未传材料=蓝, 待审核=橙, 通过/待结算/完结=绿.
|
||||||
if (s === 'AWAITING_COMPLIANCE' || s === 'AWAITING_SUPERVISION') return 'reviewing'
|
*/
|
||||||
if (s === 'SUPERVISION_APPROVED') return 'done'
|
const STAGE_STYLE = {
|
||||||
if (s === 'RECTIFYING') return 'waiting'
|
'冻结中': { cls: 'frozen', tag: 'info' },
|
||||||
if (s === 'AWAITING_SETTLEMENT') return 'waiting'
|
'已完结': { cls: 'done', tag: 'success' },
|
||||||
if (s === 'SETTLED') return 'done'
|
'已结算': { cls: 'done', tag: 'success' },
|
||||||
if (s === 'FINISHED') return 'done'
|
'待整改': { cls: 'waiting', tag: 'danger' },
|
||||||
if (s === 'FROZEN') return 'frozen'
|
'已退回': { cls: 'waiting', tag: 'danger' },
|
||||||
return 'default'
|
'未执行': { cls: 'pending', tag: 'info' },
|
||||||
|
'执行中': { cls: 'running', tag: 'primary' },
|
||||||
|
'已执行未传材料': { cls: 'running', tag: 'primary' },
|
||||||
|
'待审核': { cls: 'reviewing', tag: 'warning' },
|
||||||
|
'审核通过': { cls: 'done', tag: 'success' },
|
||||||
|
'待结算': { cls: 'done', tag: 'success' },
|
||||||
}
|
}
|
||||||
|
|
||||||
/** el-tag type (doctor 列表用), 基于物理阶段 */
|
function stageStyle(role, row) {
|
||||||
export function stageTag(row) {
|
return STAGE_STYLE[deriveStage(role, row)] || { cls: 'default', tag: 'info' }
|
||||||
const s = derivePhysicalStage(row)
|
}
|
||||||
if (s === 'NOT_STARTED') return 'info'
|
|
||||||
if (s === 'RUNNING' || s === 'RECTIFYING') return 'primary'
|
/** 颜色 class (业务专用 tag), 基于角色展示阶段 (与 stageLabel 文案一致). */
|
||||||
if (s === 'AWAITING_COMPLIANCE' || s === 'AWAITING_SUPERVISION') return 'warning'
|
export function stageClass(role, row) {
|
||||||
if (s === 'SUPERVISION_APPROVED' || s === 'SETTLED' || s === 'FINISHED') return 'success'
|
return stageStyle(role, row).cls
|
||||||
if (s === 'AWAITING_SETTLEMENT') return 'warning'
|
}
|
||||||
if (s === 'FROZEN') return 'info'
|
|
||||||
return 'info'
|
/** el-tag type (doctor 列表用), 基于角色展示阶段. */
|
||||||
|
export function stageTag(role, row) {
|
||||||
|
return stageStyle(role, row).tag
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
<div class="page-wrap">
|
<div class="page-wrap">
|
||||||
<div class="page-card">
|
<div class="page-card">
|
||||||
|
|
||||||
|
<h2 class="page-title">项目执行单位注册</h2>
|
||||||
|
|
||||||
<el-steps :active="step" finish-status="success" simple class="steps">
|
<el-steps :active="step" finish-status="success" simple class="steps">
|
||||||
<el-step title="基本信息" />
|
<el-step title="基本信息" />
|
||||||
<el-step title="注册成功" />
|
<el-step title="注册成功" />
|
||||||
@@ -196,6 +198,7 @@ onUnmounted(() => { if (smsTimer) clearInterval(smsTimer) })
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page-card { max-width: 1200px; margin: 0 auto; }
|
.page-card { max-width: 1200px; margin: 0 auto; }
|
||||||
|
.page-title { text-align: center; font-size: 24px; font-weight: 600; color: #303133; margin: 24px 0 0; }
|
||||||
.steps { max-width: 720px; margin: 24px auto 0; }
|
.steps { max-width: 720px; margin: 24px auto 0; }
|
||||||
.step-body { max-width: 640px; margin: 32px auto 0; }
|
.step-body { max-width: 640px; margin: 32px auto 0; }
|
||||||
.sms-row { display: flex; gap: 12px; width: 100%; }
|
.sms-row { display: flex; gap: 12px; width: 100%; }
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
<div class="page-wrap">
|
<div class="page-wrap">
|
||||||
<div class="page-card">
|
<div class="page-card">
|
||||||
|
|
||||||
|
<h2 class="page-title">项目参与专家注册</h2>
|
||||||
|
|
||||||
<el-steps :active="step" finish-status="success" simple class="steps">
|
<el-steps :active="step" finish-status="success" simple class="steps">
|
||||||
<el-step title="基本信息" />
|
<el-step title="基本信息" />
|
||||||
<el-step title="注册成功" />
|
<el-step title="注册成功" />
|
||||||
@@ -185,6 +187,7 @@ onUnmounted(() => { if (smsTimer) clearInterval(smsTimer) })
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page-card { max-width: 1200px; margin: 0 auto; }
|
.page-card { max-width: 1200px; margin: 0 auto; }
|
||||||
|
.page-title { text-align: center; font-size: 24px; font-weight: 600; color: #303133; margin: 24px 0 0; }
|
||||||
.steps { max-width: 720px; margin: 24px auto 0; }
|
.steps { max-width: 720px; margin: 24px auto 0; }
|
||||||
.step-body { max-width: 640px; margin: 32px auto 0; }
|
.step-body { max-width: 640px; margin: 32px auto 0; }
|
||||||
.sms-row { display: flex; gap: 12px; width: 100%; }
|
.sms-row { display: flex; gap: 12px; width: 100%; }
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
<div class="page-wrap">
|
<div class="page-wrap">
|
||||||
<div class="page-card">
|
<div class="page-card">
|
||||||
|
|
||||||
|
<h2 class="page-title">项目支持单位注册</h2>
|
||||||
|
|
||||||
<el-steps :active="step" finish-status="success" simple class="steps">
|
<el-steps :active="step" finish-status="success" simple class="steps">
|
||||||
<el-step title="基本信息" />
|
<el-step title="基本信息" />
|
||||||
<el-step title="注册成功" />
|
<el-step title="注册成功" />
|
||||||
@@ -210,6 +212,7 @@ onUnmounted(() => { if (smsTimer) clearInterval(smsTimer) })
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page-card { max-width: 1200px; margin: 0 auto; }
|
.page-card { max-width: 1200px; margin: 0 auto; }
|
||||||
|
.page-title { text-align: center; font-size: 24px; font-weight: 600; color: #303133; margin: 24px 0 0; }
|
||||||
.steps { max-width: 720px; margin: 24px auto 0; }
|
.steps { max-width: 720px; margin: 24px auto 0; }
|
||||||
.step-body { max-width: 640px; margin: 32px auto 0; }
|
.step-body { max-width: 640px; margin: 32px auto 0; }
|
||||||
.sms-row { display: flex; gap: 12px; width: 100%; }
|
.sms-row { display: flex; gap: 12px; width: 100%; }
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<el-table-column prop="meetingName" label="会议名称" min-width="280" show-overflow-tooltip />
|
<el-table-column prop="meetingName" label="会议名称" min-width="280" show-overflow-tooltip />
|
||||||
<el-table-column prop="currentStage" label="当前阶段" width="140">
|
<el-table-column prop="currentStage" label="当前阶段" width="140">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="stageTag(row)" size="small">{{ stageLabel('doctor', row) }}</el-tag>
|
<el-tag :type="stageTag('doctor', row)" size="small">{{ stageLabel('doctor', row) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="日程" width="130" align="center">
|
<el-table-column label="日程" width="130" align="center">
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="currentStage" label="当前阶段" width="140" align="center">
|
<el-table-column prop="currentStage" label="当前阶段" width="140" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span :class="['stage-tag', 'stage-' + stageClass(row)]">{{ stageLabel('executor', row) }}</span>
|
<span :class="['stage-tag', 'stage-' + stageClass('executor', row)]">{{ stageLabel('executor', row) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||||
|
|||||||
@@ -44,10 +44,10 @@
|
|||||||
<el-table-column prop="orgName" label="名称" min-width="160" show-overflow-tooltip />
|
<el-table-column prop="orgName" label="名称" min-width="160" show-overflow-tooltip />
|
||||||
<el-table-column prop="sessions" label="场次" width="90" align="right" />
|
<el-table-column prop="sessions" label="场次" width="90" align="right" />
|
||||||
<el-table-column label="金额" width="130" align="right">
|
<el-table-column label="金额" width="130" align="right">
|
||||||
<template #default="{ row }">¥ {{ fmtMoney(row.amount) }}</template>
|
<template #default="{ row }"><span class="money">¥ {{ fmtMoney(row.amount) }}</span></template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<span style="color:#c0c4cc">暂无分配 (到 项目管理 / 项目分配 添加)</span>
|
<span style="color:#909399">暂无分配 (到 项目管理 / 项目分配 添加)</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(r, idx) in form.roleRows" :key="idx">
|
<tr v-for="(r, idx) in form.roleRows" :key="idx">
|
||||||
<td>{{ r.role || '-' }}</td>
|
<td>{{ r.role || '-' }}</td>
|
||||||
<td style="text-align:right">¥ {{ fmtMoney(r.amount) }}</td>
|
<td class="money" style="text-align:right">¥ {{ fmtMoney(r.amount) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="!form.roleRows.length"><td colspan="2" class="empty-row">暂无角色</td></tr>
|
<tr v-if="!form.roleRows.length"><td colspan="2" class="empty-row">暂无角色</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -104,6 +104,11 @@
|
|||||||
|
|
||||||
<!-- 公告预览 (只读) -->
|
<!-- 公告预览 (只读) -->
|
||||||
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
||||||
|
|
||||||
|
<!-- 底部操作: 返回 (按角色回退) -->
|
||||||
|
<div class="form-actions">
|
||||||
|
<el-button @click="goBack">返回</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -148,7 +153,7 @@ const notices = computed(() => {
|
|||||||
return items.map(m => ({
|
return items.map(m => ({
|
||||||
label: m.label,
|
label: m.label,
|
||||||
url: m.url,
|
url: m.url,
|
||||||
name: m.url ? (m.url.split('/').pop() || '') : ''
|
name: fileNameFromUrl(m.url)
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -176,6 +181,17 @@ function fmtDate(v) {
|
|||||||
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/)
|
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/)
|
||||||
return m ? `${m[1]}.${m[2]}.${m[3]}` : s
|
return m ? `${m[1]}.${m[2]}.${m[3]}` : s
|
||||||
}
|
}
|
||||||
|
// 从 OSS URL 提取并解码文件名 (中文文件名是 URL 编码的, 需 decodeURIComponent 还原)
|
||||||
|
function fileNameFromUrl(url) {
|
||||||
|
if (!url) return ''
|
||||||
|
try {
|
||||||
|
const path = String(url).split('?')[0]
|
||||||
|
return decodeURIComponent(path.split('/').pop() || '')
|
||||||
|
} catch (e) {
|
||||||
|
// 非标准编码导致解码失败 → 退回原始最后一段
|
||||||
|
return String(url).split('?')[0].split('/').pop() || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===================== 加载 =====================
|
// ===================== 加载 =====================
|
||||||
async function loadProject() {
|
async function loadProject() {
|
||||||
@@ -279,9 +295,9 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 1:1 抄列表页 (manager/Projects.vue) 风格 */
|
/* 项目详情 — 统一排版: 正文 14px, 章节标题/表头 600, 金额等宽, 三级灰 strong/regular/muted */
|
||||||
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; max-width: 1200px; }
|
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; max-width: 1200px; }
|
||||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
.breadcrumb { font-size: 13px; color: #909399; margin-bottom: 12px; }
|
||||||
.breadcrumb .current { color: #262626; font-weight: 500; }
|
.breadcrumb .current { color: #262626; font-weight: 500; }
|
||||||
|
|
||||||
/* 章节标题 (与编辑页 ProjectsNew.vue 保持一致) */
|
/* 章节标题 (与编辑页 ProjectsNew.vue 保持一致) */
|
||||||
@@ -292,15 +308,21 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
.new-card-title:first-child { margin-top: 0; }
|
.new-card-title:first-child { margin-top: 0; }
|
||||||
|
|
||||||
/* 项目信息: 11 字段, 一列竖排 (el-form + 文本, 无分隔线) */
|
/* 底部操作按钮 (返回) */
|
||||||
|
.form-actions { margin-top: 24px; display: flex; gap: 8px; }
|
||||||
|
|
||||||
|
/* 项目信息: 只读 el-form, 标签 14px/regular, 值 14px/strong */
|
||||||
.info-form { padding-top: 4px; }
|
.info-form { padding-top: 4px; }
|
||||||
.info-form :deep(.el-form-item) { margin-bottom: 8px; }
|
.info-form :deep(.el-form-item) { margin-bottom: 8px; }
|
||||||
.info-form :deep(.el-form-item__label) {
|
.info-form :deep(.el-form-item__label) {
|
||||||
font-size: 14px; color: #595959; width: 130px; padding-right: 16px;
|
font-size: 14px; color: #606266; width: 130px; padding-right: 16px;
|
||||||
}
|
}
|
||||||
.info-value { font-size: 15px; color: #1a1a1a; font-weight: 500; }
|
.info-value { font-size: 14px; color: #262626; font-weight: 400; }
|
||||||
.info-value.money {
|
|
||||||
font-family: ui-monospace, "Courier New", monospace;
|
/* 金额: 统一等宽字体 + tabular 数字对齐 (表单值 + 两处表格通用) */
|
||||||
|
.money {
|
||||||
|
font-family: ui-monospace, "SF Mono", "JetBrains Mono", Consolas, "Courier New", monospace;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 监督员 */
|
/* 监督员 */
|
||||||
@@ -310,31 +332,33 @@ onMounted(async () => {
|
|||||||
padding: 8px 12px; background: #fafafa; border-radius: 4px;
|
padding: 8px 12px; background: #fafafa; border-radius: 4px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
.monitor-num { width: 24px; color: #8c8c8c; font-size: 13px; }
|
.monitor-num { width: 24px; color: #909399; font-size: 13px; }
|
||||||
.monitor-name { flex: 1; font-size: 14px; color: #1a1a1a; }
|
.monitor-name { flex: 1; font-size: 14px; color: #262626; }
|
||||||
.empty-hint { text-align: center; color: #c0c4cc; padding: 12px 0; font-size: 13px; }
|
.empty-hint { text-align: center; color: #909399; padding: 12px 0; font-size: 13px; }
|
||||||
|
|
||||||
/* 角色劳务表格 */
|
/* 角色劳务表格 + 服务公司表格: 统一 14px, 表头 600/strong, 单元格 strong */
|
||||||
.info-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
.info-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||||
.role-labor-table { width: auto; border: 1px solid #ebeef5; } /* 角色劳务表格按内容紧凑显示, 不撑满卡片 */
|
.role-labor-table { width: auto; border: 1px solid #ebeef5; } /* 角色劳务表格按内容紧凑显示, 不撑满卡片 */
|
||||||
.role-labor-table th,
|
.role-labor-table th,
|
||||||
.role-labor-table td { border-right: 1px solid #ebeef5; }
|
.role-labor-table td { border-right: 1px solid #ebeef5; }
|
||||||
.role-labor-table th:last-child,
|
.role-labor-table th:last-child,
|
||||||
.role-labor-table td:last-child { border-right: none; }
|
.role-labor-table td:last-child { border-right: none; }
|
||||||
/* 服务公司表格 (el-table in form): 紧凑, 不要撑满整张卡片 */
|
/* 服务公司表格 (el-table in form): 紧凑, 不要撑满整张卡片 */
|
||||||
.assign-table-el { width: auto; max-width: 560px; }
|
.assign-table-el { width: auto; max-width: 560px; font-size: 14px; }
|
||||||
.assign-table-el :deep(.el-table__empty-block) { min-height: 48px; }
|
.assign-table-el :deep(.el-table__empty-block) { min-height: 48px; }
|
||||||
.info-table th { background: #fafafa; padding: 10px 12px; text-align: left; color: #1a1a1a; font-weight: 600; border-bottom: 1px solid #f0f0f0; white-space: nowrap; }
|
.assign-table-el :deep(th.el-table__cell) { color: #262626; font-weight: 600; background: #fafafa; }
|
||||||
.info-table td { padding: 10px 12px; border-bottom: 1px solid #f5f5f5; color: #595959; vertical-align: middle; }
|
.assign-table-el :deep(td.el-table__cell) { color: #262626; }
|
||||||
.empty-row { text-align: center; color: #c0c4cc; }
|
.info-table th { background: #fafafa; padding: 10px 12px; text-align: left; color: #262626; font-weight: 600; border-bottom: 1px solid #f0f0f0; white-space: nowrap; }
|
||||||
|
.info-table td { padding: 10px 12px; border-bottom: 1px solid #f5f5f5; color: #262626; vertical-align: middle; }
|
||||||
|
.empty-row { text-align: center; color: #909399; }
|
||||||
|
|
||||||
/* 公告文件 (只读 4 行: 邀请函/支持函/通知/日程) */
|
/* 公告文件 (只读 4 行: 邀请函/支持函/通知/日程) */
|
||||||
.notice-list { display: flex; flex-direction: column; gap: 6px; }
|
.notice-list { display: flex; flex-direction: column; gap: 6px; }
|
||||||
.notice-row { display: flex; align-items: center; gap: 12px; padding: 6px 0; }
|
.notice-row { display: flex; align-items: center; gap: 12px; padding: 6px 0; }
|
||||||
.notice-label { width: 90px; flex-shrink: 0; color: #606266; font-size: 14px; }
|
.notice-label { width: 90px; flex-shrink: 0; color: #606266; font-size: 14px; }
|
||||||
.notice-file { flex: 1; display: flex; align-items: center; gap: 12px; min-width: 0; }
|
.notice-file { flex: 1; display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||||
.file-name { color: #1a1a1a; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.file-name { color: #262626; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.file-empty { color: #c0c4cc; font-size: 13px; }
|
.file-empty { color: #909399; font-size: 13px; }
|
||||||
|
|
||||||
/* 提示 (灰色, 跟 ProjectsNew 一致) */
|
/* 提示 (灰色, 跟 ProjectsNew 一致) */
|
||||||
.hint-text { font-size: 12px; color: #909399; margin: 8px 0; line-height: 1.6; }
|
.hint-text { font-size: 12px; color: #909399; margin: 8px 0; line-height: 1.6; }
|
||||||
|
|||||||
@@ -51,7 +51,8 @@
|
|||||||
<el-select v-model="assignForm.sponsorOrgId" placeholder="请选择支持单位" filterable
|
<el-select v-model="assignForm.sponsorOrgId" placeholder="请选择支持单位" filterable
|
||||||
:filter-method="searchSponsorOrgs" clearable style="width:100%" @change="onSponsorOrgPick">
|
:filter-method="searchSponsorOrgs" clearable style="width:100%" @change="onSponsorOrgPick">
|
||||||
<el-option v-for="u in sponsorOrgOptions" :key="u.orgId"
|
<el-option v-for="u in sponsorOrgOptions" :key="u.orgId"
|
||||||
:label="u.orgName" :value="u.orgId" />
|
:label="u.orgName + (isOrgDisabled(u) ? '(已禁用)' : '')" :value="u.orgId"
|
||||||
|
:disabled="isOrgDisabled(u)" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -84,7 +85,8 @@
|
|||||||
style="width:100%"
|
style="width:100%"
|
||||||
@change="v => onExecUserPick(row, v)">
|
@change="v => onExecUserPick(row, v)">
|
||||||
<el-option v-for="u in execOptions(row)" :key="u.orgId"
|
<el-option v-for="u in execOptions(row)" :key="u.orgId"
|
||||||
:label="u.orgName" :value="u.orgId" />
|
:label="u.orgName + (isOrgDisabled(u) ? '(已禁用)' : '')" :value="u.orgId"
|
||||||
|
:disabled="isOrgDisabled(u)" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -184,6 +186,11 @@ function openPreview(url, title) {
|
|||||||
previewOpen.value = true
|
previewOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 公司是否被禁用 (biz_org.status='1') — 禁用项仍展示但置灰、不可选
|
||||||
|
function isOrgDisabled(u) {
|
||||||
|
return String(u?.status) === '1'
|
||||||
|
}
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
const singleLoading = ref(false)
|
const singleLoading = ref(false)
|
||||||
const batchLoading = ref(false)
|
const batchLoading = ref(false)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="info-row"><span class="info-label">会议开始时间:</span><span class="info-value">{{ fmtDateTime(row.startTime) }}</span></div>
|
<div class="info-row"><span class="info-label">会议开始时间:</span><span class="info-value">{{ fmtDateTime(row.startTime) }}</span></div>
|
||||||
<div class="info-row"><span class="info-label">支持单位:</span><span class="info-value">{{ row.orgName || '-' }}</span></div>
|
<div class="info-row"><span class="info-label">支持单位:</span><span class="info-value">{{ row.orgName || '-' }}</span></div>
|
||||||
<div class="info-row"><span class="info-label">会议结束时间:</span><span class="info-value">{{ fmtDateTime(row.endTime) }}</span></div>
|
<div class="info-row"><span class="info-label">会议结束时间:</span><span class="info-value">{{ fmtDateTime(row.endTime) }}</span></div>
|
||||||
<div class="info-row"><span class="info-label">材料审核状态:</span><span class="info-value">{{ fmtAuditStage(row.materialAuditStage) }}</span></div>
|
<div class="info-row"><span class="info-label">材料审核状态:</span><span class="info-value">劳务 {{ fmtAuditStage(row.laborAuditStage) }} · 会务 {{ fmtAuditStage(row.serviceAuditStage) }}</span></div>
|
||||||
<div class="info-row"><span class="info-label">创建时间:</span><span class="info-value">{{ fmtDateTime(row.createTime) }}</span></div>
|
<div class="info-row"><span class="info-label">创建时间:</span><span class="info-value">{{ fmtDateTime(row.createTime) }}</span></div>
|
||||||
<div class="info-row"><span class="info-label">创建人员:</span><span class="info-value">{{ row.createBy || '-' }}</span></div>
|
<div class="info-row"><span class="info-label">创建人员:</span><span class="info-value">{{ row.createBy || '-' }}</span></div>
|
||||||
<div class="info-row"><span class="info-label">劳务费用:</span><span class="info-value fee-val">¥ {{ fmtMoney(row.laborFee) }}</span></div>
|
<div class="info-row"><span class="info-label">劳务费用:</span><span class="info-value fee-val">¥ {{ fmtMoney(row.laborFee) }}</span></div>
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
|
|
||||||
<div class="file-list" style="margin-top: 16px;">
|
<div class="file-list" style="margin-top: 16px;">
|
||||||
<!-- 劳务协议: 改为"按参会人逐个回填"的收发闭环, 不再作为单文件材料直接上传保存 -->
|
<!-- 劳务协议: 改为"按参会人逐个回填"的收发闭环, 不再作为单文件材料直接上传保存 -->
|
||||||
<div v-if="!isSponsor && !isReadonly" class="file-row">
|
<div v-if="!isSponsor && !isReadonly && laborEditable" class="file-row">
|
||||||
<span class="file-label">劳务协议:</span>
|
<span class="file-label">劳务协议:</span>
|
||||||
<div class="agreement-actions">
|
<div class="agreement-actions">
|
||||||
<el-button size="small" :loading="agreementTplLoading" @click="onDownloadAgreementTemplate">下载目录</el-button>
|
<el-button size="small" :loading="agreementTplLoading" @click="onDownloadAgreementTemplate">下载目录</el-button>
|
||||||
@@ -188,13 +188,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-for="r in laborMaterialRows" :key="r.label" class="file-row">
|
<div v-for="r in laborMaterialRows" :key="r.label" class="file-row">
|
||||||
<span class="file-label">{{ r.label }}:</span>
|
<span class="file-label">{{ r.label }}:</span>
|
||||||
<CameraQrUpload v-if="isCameraSubType(r.subType)" v-model="r.url" :meeting-id="meetingId" :sub-type="r.subType" :label="r.label" :readonly="isSponsor || isReadonly" class="file-uploader" />
|
<CameraQrUpload v-if="isCameraSubType(r.subType)" v-model="r.url" :meeting-id="meetingId" :sub-type="r.subType" :label="r.label" :readonly="isSponsor || isReadonly || !laborEditable" class="file-uploader" />
|
||||||
<OssFileUploader v-else v-model="r.url" v-model:name="r.fileName" :dir="`ry8080/meeting/${meetingId}/labor/`" :placeholder="`点击上传 ${r.label}`" class="file-uploader" :readonly="isSponsor || isReadonly" />
|
<OssFileUploader v-else v-model="r.url" v-model:name="r.fileName" :dir="`ry8080/meeting/${meetingId}/labor/`" :placeholder="`点击上传 ${r.label}`" class="file-uploader" :readonly="isSponsor || isReadonly || !laborEditable" />
|
||||||
<span v-if="materialAmount(r.subType) > 0" class="invoice-amount">¥ {{ materialAmount(r.subType).toFixed(2) }}</span>
|
<span v-if="materialAmount(r.subType) > 0" class="invoice-amount">¥ {{ materialAmount(r.subType).toFixed(2) }}</span>
|
||||||
<div v-if="r.hint" class="file-hint">{{ r.hint }}</div>
|
<div v-if="r.hint" class="file-hint">{{ r.hint }}</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 专家照片: 与劳务协议同构的"下载目录 + 回填"闭环, 不再作为单文件材料直接上传保存 -->
|
<!-- 专家照片: 与劳务协议同构的"下载目录 + 回填"闭环, 不再作为单文件材料直接上传保存 -->
|
||||||
<div v-if="!isSponsor && !isReadonly" class="file-row">
|
<div v-if="!isSponsor && !isReadonly && laborEditable" class="file-row">
|
||||||
<span class="file-label">专家照片:</span>
|
<span class="file-label">专家照片:</span>
|
||||||
<div class="agreement-actions">
|
<div class="agreement-actions">
|
||||||
<el-button size="small" :loading="expertPhotoTplLoading" @click="onDownloadExpertPhotoTemplate">下载目录</el-button>
|
<el-button size="small" :loading="expertPhotoTplLoading" @click="onDownloadExpertPhotoTemplate">下载目录</el-button>
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
<div class="file-list">
|
<div class="file-list">
|
||||||
<div v-for="r in serviceMaterialRows" :key="r.label" class="file-row">
|
<div v-for="r in serviceMaterialRows" :key="r.label" class="file-row">
|
||||||
<span class="file-label">{{ r.label }}:</span>
|
<span class="file-label">{{ r.label }}:</span>
|
||||||
<OssFileUploader v-model="r.url" v-model:name="r.fileName" :dir="`ry8080/meeting/${meetingId}/service/`" :placeholder="`点击上传 ${r.label}`" class="file-uploader" :readonly="isSponsor || isReadonly" />
|
<OssFileUploader v-model="r.url" v-model:name="r.fileName" :dir="`ry8080/meeting/${meetingId}/service/`" :placeholder="`点击上传 ${r.label}`" class="file-uploader" :readonly="isSponsor || isReadonly || !serviceEditable" />
|
||||||
<span v-if="materialAmount(r.subType) > 0" class="invoice-amount">¥ {{ materialAmount(r.subType).toFixed(2) }}</span>
|
<span v-if="materialAmount(r.subType) > 0" class="invoice-amount">¥ {{ materialAmount(r.subType).toFixed(2) }}</span>
|
||||||
<div v-if="r.hint" class="file-hint">{{ r.hint }}</div>
|
<div v-if="r.hint" class="file-hint">{{ r.hint }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
<el-tag v-if="meetingFee.useMain" type="warning" size="small" effect="plain">已上传总发票, 以总发票为准</el-tag>
|
<el-tag v-if="meetingFee.useMain" type="warning" size="small" effect="plain">已上传总发票, 以总发票为准</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<!-- 打包上传: 下载空目录模板 + 上传会务材料 (单目录多文件自动打 zip) -->
|
<!-- 打包上传: 下载空目录模板 + 上传会务材料 (单目录多文件自动打 zip) -->
|
||||||
<div v-if="!isSponsor && !isReadonly" class="file-row">
|
<div v-if="!isSponsor && !isReadonly && serviceEditable" class="file-row">
|
||||||
<span class="file-label">打包上传:</span>
|
<span class="file-label">打包上传:</span>
|
||||||
<div class="agreement-actions">
|
<div class="agreement-actions">
|
||||||
<el-button size="small" :loading="serviceTplLoading" @click="onDownloadServiceTemplate">下载目录</el-button>
|
<el-button size="small" :loading="serviceTplLoading" @click="onDownloadServiceTemplate">下载目录</el-button>
|
||||||
@@ -269,15 +269,22 @@
|
|||||||
</el-tabs>
|
</el-tabs>
|
||||||
<div class="tab-actions">
|
<div class="tab-actions">
|
||||||
<span style="flex:1"></span>
|
<span style="flex:1"></span>
|
||||||
<!-- 执行人员: 提交材料 -->
|
<!-- 执行人员: 提交材料 (三按钮按轨) -->
|
||||||
<el-button v-if="canSubmitMaterial && !isReadonly" type="warning" :loading="busy.submitMaterial" @click="onSubmitMaterial">提交材料</el-button>
|
<el-button v-if="canSubmitLabor && !isReadonly" type="primary" :loading="busy.submitMaterial" @click="onSubmitMaterials(['LABOR'])">提交劳务</el-button>
|
||||||
<!-- 合规 / 监察审核 (材料) -->
|
<el-button v-if="canSubmitService && !isReadonly" type="primary" :loading="busy.submitMaterial" @click="onSubmitMaterials(['SERVICE'])">提交会务</el-button>
|
||||||
<el-button v-if="canComplianceAudit() && !isReadonly" type="success" @click="openAuditDialog('COMPLIANCE')">合规审核 (材料)</el-button>
|
<el-button v-if="canSubmitAll && !isReadonly" type="primary" :loading="busy.submitMaterial" @click="onSubmitMaterials(['LABOR', 'SERVICE'])">提交全部</el-button>
|
||||||
<el-button v-if="canSupervisionAudit() && !isReadonly" type="primary" @click="openAuditDialog('SUPERVISION')">监察审核 (材料)</el-button>
|
<!-- 合规 / 监察审核 (材料, 分轨三按钮) -->
|
||||||
|
<el-button v-if="isManager && !isReadonly && laborC0" type="primary" @click="openAuditDialog('COMPLIANCE', 'LABOR')">审核劳务</el-button>
|
||||||
|
<el-button v-if="isManager && !isReadonly && serviceC0" type="primary" @click="openAuditDialog('COMPLIANCE', 'SERVICE')">审核会务</el-button>
|
||||||
|
<el-button v-if="isManager && !isReadonly && laborC0 && serviceC0" type="primary" @click="openAuditDialog('COMPLIANCE', 'ALL')">审核全部</el-button>
|
||||||
|
<el-button v-if="supervisorAuditor && !isReadonly && laborC1" type="primary" @click="openAuditDialog('SUPERVISION', 'LABOR')">审核劳务</el-button>
|
||||||
|
<el-button v-if="supervisorAuditor && !isReadonly && serviceC1" type="primary" @click="openAuditDialog('SUPERVISION', 'SERVICE')">审核会务</el-button>
|
||||||
|
<el-button v-if="supervisorAuditor && !isReadonly && laborC1 && serviceC1" type="primary" @click="openAuditDialog('SUPERVISION', 'ALL')">审核全部</el-button>
|
||||||
<!-- 结算 / 完结 (合规/管理员 手动点击) -->
|
<!-- 结算 / 完结 (合规/管理员 手动点击) -->
|
||||||
<el-button v-if="canSettle && !isReadonly" type="success" :loading="busy.settle" @click="onSettle">结算</el-button>
|
<el-button v-if="canSettle && !isReadonly" type="success" :loading="busy.settle" @click="onSettle">结算</el-button>
|
||||||
<el-button v-if="canFinish && !isReadonly" type="primary" :loading="busy.finish" @click="onFinish">完结</el-button>
|
<el-button v-if="canFinish && !isReadonly" type="primary" :loading="busy.finish" @click="onFinish">完结</el-button>
|
||||||
<el-button v-if="!isSponsor && !isReadonly && materialsEditable" type="primary" :loading="saving" @click="onSave">保存</el-button>
|
<!-- 保存: 合规方(manager) 永远可保存; 执行方/管理员仅材料可编辑(未提交/已退回)时可保存 -->
|
||||||
|
<el-button v-if="!isSponsor && !isReadonly && (isManager || materialsEditable)" type="primary" :loading="saving" @click="onSave">保存</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -287,50 +294,83 @@
|
|||||||
<div class="card audit-column">
|
<div class="card audit-column">
|
||||||
<div class="section-title">材料审核</div>
|
<div class="section-title">材料审核</div>
|
||||||
<div class="timeline">
|
<div class="timeline">
|
||||||
<!-- 固定节点 1 -->
|
<!-- 节点 1: 会议已执行 -->
|
||||||
<div :class="['timeline-item', fixedNodeStatus('PRE')]">
|
<div :class="['timeline-item', fixedNodeStatus('PRE')]">
|
||||||
<div class="timeline-title">会议已执行</div>
|
<div class="timeline-title">会议已执行</div>
|
||||||
<div class="timeline-desc">{{ nodeDesc('PRE') }}</div>
|
<div class="timeline-desc">{{ nodeDesc('PRE') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 动态轮次 2/3/4 -->
|
<!-- 节点 2: 审核时间轴 (内嵌 提交 → 合规 → 监察 三步, 两轨并列, 保留重交历史) -->
|
||||||
<template v-for="(round, idx) in displayMaterialRounds" :key="`mat-r${idx}`">
|
<div :class="['timeline-item', auditNodeStatus()]">
|
||||||
<div :class="['timeline-item', partStatus(round.submit)]">
|
<div class="timeline-title">审核时间轴</div>
|
||||||
<div class="timeline-title">执行方提交材料</div>
|
<div class="audit-sub-timeline">
|
||||||
<div v-if="round.submit" class="timeline-meta">
|
<div :class="['sub-timeline-item', nodeStatus('submit')]">
|
||||||
<span>{{ round.submit.auditor }}</span>
|
<div class="sub-timeline-title">执行方提交材料</div>
|
||||||
|
<div v-if="!materialTracks.length" class="timeline-desc pending-text">待执行人员提交</div>
|
||||||
|
<div v-for="track in materialTracks" :key="track.key" class="track-block">
|
||||||
|
<div class="track-label-row">
|
||||||
|
<el-tag v-if="track.label" size="small" effect="plain">{{ track.label }}</el-tag>
|
||||||
|
<span v-if="!track.cycles.length" class="pending-text">待提交</span>
|
||||||
|
</div>
|
||||||
|
<div v-for="(c, i) in track.cycles" :key="i" class="timeline-meta">
|
||||||
|
<span v-if="track.cycles.length > 1" class="cycle-no">第{{ i + 1 }}次</span>
|
||||||
|
<span>{{ c.submit.auditor }}</span>
|
||||||
<span class="dot">·</span>
|
<span class="dot">·</span>
|
||||||
<span>{{ fmtDateTime(round.submit.auditTime) }}</span>
|
<span>{{ fmtDateTime(c.submit.auditTime) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="timeline-desc pending-text">待执行人员提交</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div :class="['timeline-item', partStatus(round.compliance)]">
|
</div>
|
||||||
<div class="timeline-title">合规审核</div>
|
<div :class="['sub-timeline-item', nodeStatus('compliance')]">
|
||||||
<div v-if="round.compliance" class="timeline-meta">
|
<div class="sub-timeline-title">合规审核</div>
|
||||||
<span>{{ round.compliance.auditor }}</span>
|
<div v-if="!materialTracks.length" class="timeline-desc pending-text">待合规审核</div>
|
||||||
|
<div v-for="track in materialTracks" :key="track.key" class="track-block">
|
||||||
|
<div class="track-label-row">
|
||||||
|
<el-tag v-if="track.label" size="small" effect="plain">{{ track.label }}</el-tag>
|
||||||
|
<span v-if="!track.cycles.length" class="pending-text">—</span>
|
||||||
|
</div>
|
||||||
|
<template v-for="(c, i) in track.cycles" :key="i">
|
||||||
|
<div class="timeline-meta">
|
||||||
|
<span v-if="track.cycles.length > 1" class="cycle-no">第{{ i + 1 }}次</span>
|
||||||
|
<template v-if="c.compliance">
|
||||||
|
<span>{{ c.compliance.auditor }}</span>
|
||||||
<span class="dot">·</span>
|
<span class="dot">·</span>
|
||||||
<span>{{ fmtDateTime(round.compliance.auditTime) }}</span>
|
<span>{{ fmtDateTime(c.compliance.auditTime) }}</span>
|
||||||
<el-tag size="small" :type="round.compliance.auditResult === 'REJECTED' ? 'danger' : 'success'">
|
<el-tag size="small" :type="c.compliance.auditResult === 'REJECTED' ? 'danger' : 'success'">{{ c.compliance.auditResult === 'REJECTED' ? '拒绝' : '通过' }}</el-tag>
|
||||||
{{ round.compliance.auditResult === 'REJECTED' ? '拒绝' : '通过' }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
<div v-else class="timeline-desc pending-text">待合规审核</div>
|
|
||||||
<div v-if="round.compliance?.opinion" class="timeline-opinion">💬 {{ round.compliance.opinion }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!(round.compliance?.auditResult === 'REJECTED')" :class="['timeline-item', partStatus(round.supervision)]">
|
|
||||||
<div class="timeline-title">监察意见</div>
|
|
||||||
<div v-if="round.supervision" class="timeline-meta">
|
|
||||||
<span>{{ round.supervision.auditor }}</span>
|
|
||||||
<span class="dot">·</span>
|
|
||||||
<span>{{ fmtDateTime(round.supervision.auditTime) }}</span>
|
|
||||||
<el-tag size="small" :type="round.supervision.auditResult === 'REJECTED' ? 'danger' : 'success'">
|
|
||||||
{{ round.supervision.auditResult === 'REJECTED' ? '拒绝' : '通过' }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
<div v-else class="timeline-desc pending-text">待监察审核</div>
|
|
||||||
<div v-if="round.supervision?.opinion" class="timeline-opinion">💬 {{ round.supervision.opinion }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<!-- 固定节点 5/6 -->
|
<span v-else class="pending-text">待审核</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="c.compliance?.opinion" class="timeline-opinion">💬 {{ c.compliance.opinion }}</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="['sub-timeline-item', nodeStatus('supervision')]">
|
||||||
|
<div class="sub-timeline-title">监察意见</div>
|
||||||
|
<div v-if="!materialTracks.length" class="timeline-desc pending-text">待监察审核</div>
|
||||||
|
<div v-for="track in materialTracks" :key="track.key" class="track-block">
|
||||||
|
<div class="track-label-row">
|
||||||
|
<el-tag v-if="track.label" size="small" effect="plain">{{ track.label }}</el-tag>
|
||||||
|
<span v-if="!track.cycles.length" class="pending-text">—</span>
|
||||||
|
</div>
|
||||||
|
<template v-for="(c, i) in track.cycles" :key="i">
|
||||||
|
<div class="timeline-meta">
|
||||||
|
<span v-if="track.cycles.length > 1" class="cycle-no">第{{ i + 1 }}次</span>
|
||||||
|
<template v-if="c.compliance && c.compliance.auditResult === 'REJECTED'">
|
||||||
|
<span class="pending-text">已退回 · 未进入监察</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="c.supervision">
|
||||||
|
<span>{{ c.supervision.auditor }}</span>
|
||||||
|
<span class="dot">·</span>
|
||||||
|
<span>{{ fmtDateTime(c.supervision.auditTime) }}</span>
|
||||||
|
<el-tag size="small" :type="c.supervision.auditResult === 'REJECTED' ? 'danger' : 'success'">{{ c.supervision.auditResult === 'REJECTED' ? '拒绝' : '通过' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
<span v-else class="pending-text">待监察审核</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="c.supervision?.opinion" class="timeline-opinion">💬 {{ c.supervision.opinion }}</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 节点 3/4: 结算 / 完结 -->
|
||||||
<div :class="['timeline-item', fixedNodeStatus('POST')]">
|
<div :class="['timeline-item', fixedNodeStatus('POST')]">
|
||||||
<div class="timeline-title">会议结算</div>
|
<div class="timeline-title">会议结算</div>
|
||||||
<div class="timeline-desc">{{ nodeDesc('POST') }}</div>
|
<div class="timeline-desc">{{ nodeDesc('POST') }}</div>
|
||||||
@@ -355,17 +395,22 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 审核 dialog (合规/监察通用, 材料审核) -->
|
<!-- 审核 dialog (合规/监察通用, 材料审核, 分轨独立通过/拒绝) -->
|
||||||
<el-dialog v-model="auditDialog.show" :title="auditDialog.title" width="500px">
|
<el-dialog v-model="auditDialog.show" :title="auditDialog.title" width="500px">
|
||||||
<el-form label-width="80px">
|
<el-form label-width="80px">
|
||||||
<el-form-item label="意见">
|
<el-form-item label="审核项">
|
||||||
<el-input v-model="auditDialog.opinion" type="textarea" :rows="3" placeholder="请输入审核意见" />
|
<div style="display:flex;flex-direction:column;gap:8px;width:100%">
|
||||||
</el-form-item>
|
<div v-for="t in auditDialog.tracks" :key="t.type" style="display:flex;align-items:center;gap:12px">
|
||||||
<el-form-item label="结果">
|
<span style="width:72px">{{ t.label }}</span>
|
||||||
<el-radio-group v-model="auditDialog.approved">
|
<el-radio-group v-model="t.approved">
|
||||||
<el-radio :label="true">通过</el-radio>
|
<el-radio :label="true">通过</el-radio>
|
||||||
<el-radio :label="false">拒绝</el-radio>
|
<el-radio :label="false">拒绝</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="意见">
|
||||||
|
<el-input v-model="auditDialog.opinion" type="textarea" :rows="3" placeholder="请输入审核意见 (两轨共用)" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -650,58 +695,78 @@ const laborMaterialRows = ref(makeRows(r => r.type === 'LABOR'))
|
|||||||
const laborVoucherRows = ref(makeRows(r => r.type === 'LABOR_VOUCHER'))
|
const laborVoucherRows = ref(makeRows(r => r.type === 'LABOR_VOUCHER'))
|
||||||
const serviceVoucherRows = ref(makeRows(r => r.type === 'SERVICE_VOUCHER'))
|
const serviceVoucherRows = ref(makeRows(r => r.type === 'SERVICE_VOUCHER'))
|
||||||
|
|
||||||
// ===================== 双时间轴: 解析 audit_trail 为轮次 =====================
|
// ===================== 时间轴: 解析 audit_trail 为「轨道 × 轮次」 =====================
|
||||||
/**
|
/**
|
||||||
* 解析 audit_trail 为轮次结构 (纯函数, 不依赖外部状态)
|
* 解析指定材料轨 (materialType) 的审核日志为提交轮次数组 (保留重交历史).
|
||||||
* 规则: 一次 SUBMITTED + APPROVED = 一轮起点 (执行人员提交)
|
* 每次 SUBMITTED 起一轮, 后续 APPROVED/REJECTED 顺序填入 compliance → supervision 槽.
|
||||||
* 之后 1-2 条填入 compliance / supervision 槽
|
* 方案 B: 同轨被驳回后重交, 产生第 2/3 轮, 时间轴逐轮渲染「第 N 次」.
|
||||||
* 拒绝事件 (REJECTED) 不会被识别为新提交, 仍归入当前轮
|
|
||||||
*/
|
*/
|
||||||
function parseRounds(auditType) {
|
function parseTrackCycles(materialType) {
|
||||||
const rows = (auditTrail.value || [])
|
const rows = (auditTrail.value || [])
|
||||||
.filter(r => r.auditType === auditType)
|
.filter(r => r.auditType === 'MATERIAL' && r.materialType === materialType)
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => new Date(a.auditTime).getTime() - new Date(b.auditTime).getTime())
|
.sort((a, b) => new Date(a.auditTime).getTime() - new Date(b.auditTime).getTime())
|
||||||
const rounds = []
|
const cycles = []
|
||||||
let cur = null
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const isSubmit = row.auditResult === 'SUBMITTED'
|
if (row.auditResult === 'SUBMITTED') {
|
||||||
if (isSubmit) {
|
cycles.push({ submit: row, compliance: null, supervision: null })
|
||||||
if (cur) rounds.push(cur)
|
continue
|
||||||
cur = { submit: row, compliance: null, supervision: null }
|
}
|
||||||
} else if (cur) {
|
const cur = cycles[cycles.length - 1]
|
||||||
|
if (!cur) continue
|
||||||
if (!cur.compliance) cur.compliance = row
|
if (!cur.compliance) cur.compliance = row
|
||||||
else if (!cur.supervision) cur.supervision = row
|
else if (!cur.supervision) cur.supervision = row
|
||||||
}
|
}
|
||||||
|
return cycles
|
||||||
}
|
}
|
||||||
if (cur) rounds.push(cur)
|
|
||||||
return rounds
|
|
||||||
}
|
|
||||||
|
|
||||||
const materialRounds = computed(() => parseRounds('MATERIAL'))
|
|
||||||
|
|
||||||
/** 至少保证 1 轮 (空轮 = 三个节点都待提交), 保证 2/3/4 始终渲染 */
|
|
||||||
const displayMaterialRounds = computed(() =>
|
|
||||||
materialRounds.value.length ? materialRounds.value : [{ submit: null, compliance: null, supervision: null }]
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点状态视觉:
|
* 轨道列表: 新数据 (两轨独立) 拆 劳务/会务 两轨; 历史数据 material_type=null
|
||||||
* - done → 绿色 (动作完成 + 通过)
|
* 回退单轨 (无 label), 迁移后两轨同值, 时间轴不再按顺序错配.
|
||||||
* - rejected → 红色 (动作完成但拒绝, 通常触发下一轮)
|
|
||||||
* - pending → 灰色 (还没轮到)
|
|
||||||
*/
|
*/
|
||||||
function partStatus(part) {
|
const materialTracks = computed(() => {
|
||||||
if (!part) return 'pending'
|
const labor = parseTrackCycles('LABOR')
|
||||||
if (part.auditResult === 'REJECTED') return 'rejected'
|
const service = parseTrackCycles('SERVICE')
|
||||||
return 'done'
|
const legacy = parseTrackCycles(null)
|
||||||
|
if (labor.length || service.length) {
|
||||||
|
return [
|
||||||
|
{ key: 'labor', label: '劳务', cycles: labor },
|
||||||
|
{ key: 'service', label: '会务', cycles: service }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (legacy.length) return [{ key: 'legacy', label: '', cycles: legacy }]
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核时间轴内嵌子步骤 (提交/合规/监察) 状态 — 由物理阶段 (两轨取最小) 推导.
|
||||||
|
* - submit: 越过 NOT_STARTED/RUNNING (已提交过) 即 done
|
||||||
|
* - compliance: RECTIFYING=rejected, AWAITING_COMPLIANCE=pending(当前), 之后=done
|
||||||
|
* - supervision: RECTIFYING=rejected, AWAITING_SUPERVISION=pending(当前), 之后=done
|
||||||
|
*/
|
||||||
|
function nodeStatus(slot) {
|
||||||
|
const s = derivePhysicalStage(row.value)
|
||||||
|
if (slot === 'submit') return (s !== 'NOT_STARTED' && s !== 'RUNNING') ? 'done' : 'pending'
|
||||||
|
if (slot === 'compliance') {
|
||||||
|
if (s === 'RECTIFYING') return 'rejected'
|
||||||
|
if (s === 'AWAITING_COMPLIANCE') return 'pending'
|
||||||
|
if (['AWAITING_SUPERVISION', 'AWAITING_SETTLEMENT', 'SETTLED', 'FINISHED'].includes(s)) return 'done'
|
||||||
|
return 'pending'
|
||||||
|
}
|
||||||
|
if (slot === 'supervision') {
|
||||||
|
if (s === 'RECTIFYING') return 'rejected'
|
||||||
|
if (s === 'AWAITING_SUPERVISION') return 'pending'
|
||||||
|
if (['AWAITING_SETTLEMENT', 'SETTLED', 'FINISHED'].includes(s)) return 'done'
|
||||||
|
return 'pending'
|
||||||
|
}
|
||||||
|
return 'pending'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 固定节点 (1/5/6) 状态 — 跟随 current_stage (BizMeetingStageEnum 10 值 code)
|
* 固定节点 (会议已执行/结算/完结) 状态 — 跟随 current_stage (BizMeetingStageEnum 10 值 code)
|
||||||
* slot: PRE = 节点1 (会议已执行) = NOT_STARTED/FROZEN 之外都算已执行
|
* slot: PRE = 会议已执行 = NOT_STARTED/FROZEN 之外都算已执行
|
||||||
* POST = 节点5 (结算) = AWAITING_SETTLEMENT / SETTLED / FINISHED (已结算)
|
* POST = 结算 = AWAITING_SETTLEMENT / SETTLED / FINISHED (已结算)
|
||||||
* DONE = 节点6 (完结) = FINISHED (is_finished=1)
|
* DONE = 完结 = FINISHED (is_finished=1)
|
||||||
*/
|
*/
|
||||||
function fixedNodeStatus(slot) {
|
function fixedNodeStatus(slot) {
|
||||||
const s = derivePhysicalStage(row.value)
|
const s = derivePhysicalStage(row.value)
|
||||||
@@ -718,6 +783,12 @@ function nodeDesc(slot) {
|
|||||||
return '-'
|
return '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 审核时间轴 组节点状态: 两轨均审核通过 (进入待结算) 后 done, 否则 pending (内嵌子步骤各自带色). */
|
||||||
|
function auditNodeStatus() {
|
||||||
|
const s = derivePhysicalStage(row.value)
|
||||||
|
return ['AWAITING_SETTLEMENT', 'SETTLED', 'FINISHED'].includes(s) ? 'done' : 'pending'
|
||||||
|
}
|
||||||
|
|
||||||
// ===================== 按钮显隐 =====================
|
// ===================== 按钮显隐 =====================
|
||||||
// 执行方判定: 用 role (executor) 而非 biz_meeting_executor (会议级执行人员).
|
// 执行方判定: 用 role (executor) 而非 biz_meeting_executor (会议级执行人员).
|
||||||
// 执行单位是项目级分配 (biz_project_assign), 不在 biz_meeting_executor 里, 旧判定会让执行单位在"执行中"看不到提交按钮.
|
// 执行单位是项目级分配 (biz_project_assign), 不在 biz_meeting_executor 里, 旧判定会让执行单位在"执行中"看不到提交按钮.
|
||||||
@@ -729,22 +800,30 @@ const isAssignedSupervisor = computed(() => isSponsor.value)
|
|||||||
const isSponsorMain = computed(() => currentRole.value === 'sponsor' && userStore.isMain)
|
const isSponsorMain = computed(() => currentRole.value === 'sponsor' && userStore.isMain)
|
||||||
const executed = computed(() => isOne(row.value.isExecuted))
|
const executed = computed(() => isOne(row.value.isExecuted))
|
||||||
const frozen = computed(() => isOne(row.value.isFrozen))
|
const frozen = computed(() => isOne(row.value.isFrozen))
|
||||||
// 材料可提交: 已执行 + 未冻结 + material ∈ {NOT_SUBMITTED, REJECTED}
|
/** 单轨是否处于「可提交/可编辑」态 (未提交 或 被驳回) */
|
||||||
const canSubmitMaterial = computed(() => isExecutor.value && executed.value && !frozen.value
|
const laborEditable = computed(() => ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.laborAuditStage))
|
||||||
&& ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.materialAuditStage))
|
const serviceEditable = computed(() => ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.serviceAuditStage))
|
||||||
// 材料可编辑 (保存修改): 未提交 / 被驳回 时; 提交审核后 (SUBMITTED) / 通过后 (APPROVED) 锁定, 隐藏保存按钮
|
/** 单轨状态判定: C0=已提交待合规审 / C1=已提交待支持方审 */
|
||||||
const materialsEditable = computed(() => ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.materialAuditStage))
|
function isC0(stage, compliance) { return stage === 'SUBMITTED' && !isOne(compliance) }
|
||||||
function canComplianceAudit() {
|
function isC1(stage, compliance) { return stage === 'SUBMITTED' && isOne(compliance) }
|
||||||
if (!isManager.value) return false
|
// 材料可提交: 已执行 + 未冻结 + 对应轨可编辑 (三按钮按轨显隐)
|
||||||
return row.value.materialAuditStage === 'SUBMITTED' && !isOne(row.value.materialComplianceApproved)
|
const executorSubmitReady = computed(() => isExecutor.value && executed.value && !frozen.value)
|
||||||
}
|
const canSubmitLabor = computed(() => executorSubmitReady.value && laborEditable.value)
|
||||||
function canSupervisionAudit() {
|
const canSubmitService = computed(() => executorSubmitReady.value && serviceEditable.value)
|
||||||
if (!isAssignedSupervisor.value && !isSponsorMain.value) return false
|
const canSubmitAll = computed(() => executorSubmitReady.value && laborEditable.value && serviceEditable.value)
|
||||||
return row.value.materialAuditStage === 'SUBMITTED' && isOne(row.value.materialComplianceApproved)
|
// 材料可编辑 (保存修改): 任一轨未提交/被驳回 时可保存; 两轨都进入审核后锁定
|
||||||
}
|
const materialsEditable = computed(() => laborEditable.value || serviceEditable.value)
|
||||||
// 结算/完结 (合规/管理员 手动点击, 后端强校验 role)
|
// 合规审核 (合规方/manager): 三按钮按轨 C0 显隐
|
||||||
|
const laborC0 = computed(() => isC0(row.value.laborAuditStage, row.value.laborComplianceApproved))
|
||||||
|
const serviceC0 = computed(() => isC0(row.value.serviceAuditStage, row.value.serviceComplianceApproved))
|
||||||
|
// 监察审核 (支持方/监察员): 三按钮按轨 C1 显隐
|
||||||
|
const supervisorAuditor = computed(() => isAssignedSupervisor.value || isSponsorMain.value)
|
||||||
|
const laborC1 = computed(() => isC1(row.value.laborAuditStage, row.value.laborComplianceApproved))
|
||||||
|
const serviceC1 = computed(() => isC1(row.value.serviceAuditStage, row.value.serviceComplianceApproved))
|
||||||
|
// 结算 (合规/管理员 手动点击): 两轨均审核通过才可结算
|
||||||
const canSettle = computed(() => (isManager.value || isAdmin.value)
|
const canSettle = computed(() => (isManager.value || isAdmin.value)
|
||||||
&& row.value.materialAuditStage === 'APPROVED'
|
&& row.value.laborAuditStage === 'APPROVED'
|
||||||
|
&& row.value.serviceAuditStage === 'APPROVED'
|
||||||
&& !isOne(row.value.isSettled))
|
&& !isOne(row.value.isSettled))
|
||||||
const canFinish = computed(() => (isManager.value || isAdmin.value)
|
const canFinish = computed(() => (isManager.value || isAdmin.value)
|
||||||
&& isOne(row.value.isSettled) && !isOne(row.value.isFinished))
|
&& isOne(row.value.isSettled) && !isOne(row.value.isFinished))
|
||||||
@@ -1397,7 +1476,7 @@ async function loadProjectRoles() {
|
|||||||
/**
|
/**
|
||||||
* 把当前 2 个 tab 已上传的文件 (r.url) 全量保存到 biz_meeting_material (后端 DELETE + INSERT),
|
* 把当前 2 个 tab 已上传的文件 (r.url) 全量保存到 biz_meeting_material (后端 DELETE + INSERT),
|
||||||
* 并触发 OCR. 返回 { saved, ocrCount }; 出错抛出, 由调用方决定 toast.
|
* 并触发 OCR. 返回 { saved, ocrCount }; 出错抛出, 由调用方决定 toast.
|
||||||
* onSave (保存按钮) 与 onSubmitMaterial (提交前自动落库) 共用.
|
* onSave (保存按钮) 与 onSubmitMaterials (提交前自动落库) 共用.
|
||||||
*/
|
*/
|
||||||
async function saveMaterials() {
|
async function saveMaterials() {
|
||||||
const all = [...serviceMaterialRows.value, ...laborMaterialRows.value]
|
const all = [...serviceMaterialRows.value, ...laborMaterialRows.value]
|
||||||
@@ -1599,18 +1678,17 @@ function submitOcrForMaterials(items) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== 提交材料 =====================
|
// ===================== 提交材料 (三按钮: 提交劳务/会务/全部) =====================
|
||||||
// 提交前先 saveMaterials() 落库: 后端 submit-material 校验的是 biz_meeting_material 表,
|
/** 提交前先 saveMaterials() 落库: 后端 submit-material 校验的是 biz_meeting_material 表 */
|
||||||
// 只上传不保存时表为空, 会误报「未上传」.
|
async function onSubmitMaterials(types) {
|
||||||
async function onSubmitMaterial() {
|
if (!types || !types.length) { ElMessage.warning('无可提交的材料轨'); return }
|
||||||
busy.value.submitMaterial = true
|
busy.value.submitMaterial = true
|
||||||
try {
|
try {
|
||||||
await saveMaterials()
|
await saveMaterials()
|
||||||
// __silentError: 拦截器不自动 toast, 错误统一由下方 catch 处理 (避免 axios 拦截器和 catch 双 toast)
|
// __silentError: 拦截器不自动 toast, 错误统一由下方 catch 处理 (避免 axios 拦截器和 catch 双 toast)
|
||||||
const resp = await request.post(`/business/meeting/${meetingId.value}/submit-material`, null, { __silentError: true })
|
await request.post(`/business/meeting/${meetingId.value}/submit-material`, { types }, { __silentError: true })
|
||||||
ElMessage.success('材料已提交, 待合规审核')
|
ElMessage.success('材料已提交, 待合规审核')
|
||||||
row.value.materialAuditStage = (resp && resp.data) || 'SUBMITTED'
|
await refreshMeeting()
|
||||||
await loadTrail()
|
|
||||||
} catch (e) { ElMessage.error(e?.msg || e?.message || '提交失败') }
|
} catch (e) { ElMessage.error(e?.msg || e?.message || '提交失败') }
|
||||||
finally { busy.value.submitMaterial = false }
|
finally { busy.value.submitMaterial = false }
|
||||||
}
|
}
|
||||||
@@ -1654,30 +1732,43 @@ async function confirmAssign() {
|
|||||||
finally { assignDialog.value.saving = false }
|
finally { assignDialog.value.saving = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== 审核 dialog (材料审核) =====================
|
// ===================== 审核 dialog (材料审核, 分轨独立通过/拒绝) =====================
|
||||||
const auditDialog = ref({
|
const auditDialog = ref({
|
||||||
show: false, action: '', title: '',
|
show: false, action: '', title: '',
|
||||||
opinion: '', approved: true, saving: false
|
opinion: '', tracks: [], saving: false
|
||||||
})
|
})
|
||||||
function openAuditDialog(action) {
|
function openAuditDialog(action, scope = 'ALL') {
|
||||||
|
const isCompliance = action === 'COMPLIANCE'
|
||||||
|
const include = type => scope === 'ALL' || scope === type
|
||||||
|
const tracks = []
|
||||||
|
if (isCompliance) {
|
||||||
|
if (include('LABOR') && isC0(row.value.laborAuditStage, row.value.laborComplianceApproved)) tracks.push({ type: 'LABOR', label: '劳务材料', approved: true })
|
||||||
|
if (include('SERVICE') && isC0(row.value.serviceAuditStage, row.value.serviceComplianceApproved)) tracks.push({ type: 'SERVICE', label: '会务材料', approved: true })
|
||||||
|
} else {
|
||||||
|
if (include('LABOR') && isC1(row.value.laborAuditStage, row.value.laborComplianceApproved)) tracks.push({ type: 'LABOR', label: '劳务材料', approved: true })
|
||||||
|
if (include('SERVICE') && isC1(row.value.serviceAuditStage, row.value.serviceComplianceApproved)) tracks.push({ type: 'SERVICE', label: '会务材料', approved: true })
|
||||||
|
}
|
||||||
|
const scopeLabel = scope === 'LABOR' ? '劳务' : scope === 'SERVICE' ? '会务' : '全部'
|
||||||
auditDialog.value = {
|
auditDialog.value = {
|
||||||
show: true,
|
show: true,
|
||||||
action,
|
action,
|
||||||
title: action === 'COMPLIANCE' ? '合规审核 (材料)' : '监察审核 (材料)',
|
title: isCompliance ? `合规审核 (材料 · ${scopeLabel})` : `监察审核 (材料 · ${scopeLabel})`,
|
||||||
opinion: '',
|
opinion: '',
|
||||||
approved: true,
|
tracks,
|
||||||
saving: false
|
saving: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function confirmAudit() {
|
async function confirmAudit() {
|
||||||
const { action, opinion, approved } = auditDialog.value
|
const { action, opinion, tracks } = auditDialog.value
|
||||||
|
const items = tracks.map(t => ({ type: t.type, approved: t.approved }))
|
||||||
|
if (!items.length) { ElMessage.warning('无可审核的材料轨'); return }
|
||||||
const url = action === 'COMPLIANCE'
|
const url = action === 'COMPLIANCE'
|
||||||
? `/business/meeting/${meetingId.value}/audit-compliance`
|
? `/business/meeting/${meetingId.value}/audit-compliance`
|
||||||
: `/business/meeting/${meetingId.value}/audit-supervision`
|
: `/business/meeting/${meetingId.value}/audit-supervision`
|
||||||
auditDialog.value.saving = true
|
auditDialog.value.saving = true
|
||||||
try {
|
try {
|
||||||
await request.post(url, { approved, opinion }, { __silentError: true })
|
await request.post(url, { items, opinion }, { __silentError: true })
|
||||||
ElMessage.success(approved ? '审核通过' : '已拒绝')
|
ElMessage.success('审核完成')
|
||||||
auditDialog.value.show = false
|
auditDialog.value.show = false
|
||||||
await refreshMeeting()
|
await refreshMeeting()
|
||||||
} catch (e) { ElMessage.error(e?.msg || e?.message || '审核失败') }
|
} catch (e) { ElMessage.error(e?.msg || e?.message || '审核失败') }
|
||||||
@@ -1865,8 +1956,21 @@ onBeforeUnmount(stopFeePolling)
|
|||||||
.timeline-desc.pending-text { color: #c0c4cc; font-style: italic; }
|
.timeline-desc.pending-text { color: #c0c4cc; font-style: italic; }
|
||||||
.timeline-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 12px; color: #595959; line-height: 1.6; }
|
.timeline-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 12px; color: #595959; line-height: 1.6; }
|
||||||
.timeline-meta .dot { color: #c0c4cc; }
|
.timeline-meta .dot { color: #c0c4cc; }
|
||||||
|
.track-block { margin-bottom: 6px; }
|
||||||
|
.track-block:last-child { margin-bottom: 0; }
|
||||||
|
.track-label-row { display: flex; align-items: center; gap: 6px; margin-bottom: 2px; }
|
||||||
|
.cycle-no { font-size: 11px; color: #909399; }
|
||||||
.timeline-opinion { margin-top: 4px; font-size: 12px; color: #f56c6c; background: #fef0f0; padding: 4px 8px; border-radius: 3px; line-height: 1.5; word-break: break-all; }
|
.timeline-opinion { margin-top: 4px; font-size: 12px; color: #f56c6c; background: #fef0f0; padding: 4px 8px; border-radius: 3px; line-height: 1.5; word-break: break-all; }
|
||||||
.timeline-item.done .timeline-opinion { color: #909399; background: #f5f7fa; }
|
.timeline-item.done .timeline-opinion { color: #909399; background: #f5f7fa; }
|
||||||
|
/* 审核时间轴内嵌子时间轴 (提交 → 合规 → 监察) */
|
||||||
|
.audit-sub-timeline { position: relative; padding-left: 14px; border-left: 2px solid #e8e8e8; margin: 6px 0 2px; }
|
||||||
|
.sub-timeline-item { padding: 4px 0 10px 12px; position: relative; }
|
||||||
|
.sub-timeline-item:last-child { padding-bottom: 0; }
|
||||||
|
.sub-timeline-item::before { content: ''; position: absolute; left: -20px; top: 7px; width: 8px; height: 8px; border-radius: 50%; background: var(--brand-primary); }
|
||||||
|
.sub-timeline-item.done::before { background: #67c23a; }
|
||||||
|
.sub-timeline-item.pending::before { background: #c0c4cc; }
|
||||||
|
.sub-timeline-item.rejected::before { background: #f56c6c; box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.2); }
|
||||||
|
.sub-timeline-title { font-size: 12px; font-weight: 600; color: #303133; margin-bottom: 4px; line-height: 1.4; }
|
||||||
|
|
||||||
.audit-columns { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; min-width: 0; }
|
.audit-columns { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; min-width: 0; }
|
||||||
.audit-column { margin-bottom: 0; padding: 16px 18px; min-width: 0; }
|
.audit-column { margin-bottom: 0; padding: 16px 18px; min-width: 0; }
|
||||||
|
|||||||
@@ -65,13 +65,14 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="currentStage" label="当前阶段" width="140" align="center">
|
<el-table-column prop="currentStage" label="当前阶段" width="140" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span :class="['stage-tag', 'stage-' + stageClass(row)]">{{ stageLabel(roleSegment, row) }}</span>
|
<span :class="['stage-tag', 'stage-' + stageClass(roleSegment, row)]">{{ stageLabel(roleSegment, row) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" width="500" fixed="right" align="center">
|
<el-table-column label="操作" width="500" fixed="right" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="viewOnly(row)">查看</el-button>
|
<el-button link type="primary" @click="viewOnly(row)">查看</el-button>
|
||||||
|
<el-button v-if="isRole('manager') && isCompliancePending(row)" link type="warning" @click="onAudit(row)">审核</el-button>
|
||||||
<el-button v-if="isRole('admin', 'manager')" link type="primary" @click="viewDetail(row)">编辑材料</el-button>
|
<el-button v-if="isRole('admin', 'manager')" link type="primary" @click="viewDetail(row)">编辑材料</el-button>
|
||||||
<el-button v-if="isRole('admin', 'manager')" link type="primary" @click="onEdit(row)">修改</el-button>
|
<el-button v-if="isRole('admin', 'manager')" link type="primary" @click="onEdit(row)">修改</el-button>
|
||||||
<el-button v-if="isRole('admin', 'manager')" link type="primary" @click="onCopy(row)">复制</el-button>
|
<el-button v-if="isRole('admin', 'manager')" link type="primary" @click="onCopy(row)">复制</el-button>
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
<el-button v-if="isRole('admin', 'manager')" link type="primary" @click="onDownloadService(row)">会务下载</el-button>
|
<el-button v-if="isRole('admin', 'manager')" link type="primary" @click="onDownloadService(row)">会务下载</el-button>
|
||||||
<el-button v-if="isRole('admin', 'manager')" link type="primary" @click="onDownloadLabor(row)">劳务下载</el-button>
|
<el-button v-if="isRole('admin', 'manager')" link type="primary" @click="onDownloadLabor(row)">劳务下载</el-button>
|
||||||
<el-button v-if="isRole('admin', 'manager')" link type="danger" @click="onDelete(row)">删除</el-button>
|
<el-button v-if="isRole('admin', 'manager')" link type="danger" @click="onDelete(row)">删除</el-button>
|
||||||
<el-button v-if="isRole('sponsor') && derivePhysicalStage(row) === 'AWAITING_SUPERVISION'" link type="warning" @click="onApproval(row)">审批</el-button>
|
<el-button v-if="isRole('sponsor') && isSponsorPending(row)" link type="warning" @click="onAudit(row)">审核</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -100,26 +101,6 @@
|
|||||||
<!-- 修改 / 复制: 按角色跳 admin-meetings-new / manager-meetings-new (MeetingNew.vue 公共页) -->
|
<!-- 修改 / 复制: 按角色跳 admin-meetings-new / manager-meetings-new (MeetingNew.vue 公共页) -->
|
||||||
<!-- 查看: 按角色跳 /admin/meetings/detail/:id 或 /manager/meetings/detail/:id -->
|
<!-- 查看: 按角色跳 /admin/meetings/detail/:id 或 /manager/meetings/detail/:id -->
|
||||||
|
|
||||||
<!-- 支持方(监察员) 审批 dialog: 仅 sponsor 且 current_stage=AWAITING_SUPERVISION 时可见 -->
|
|
||||||
<el-dialog v-model="approvalOpen" title="支持方审批" width="560px">
|
|
||||||
<el-form label-width="100px">
|
|
||||||
<el-form-item label="会议名称"><span>{{ approvalRow.meetingName }}</span></el-form-item>
|
|
||||||
<el-form-item label="审批结果">
|
|
||||||
<el-radio-group v-model="approvalForm.approved">
|
|
||||||
<el-radio :label="true">通过</el-radio>
|
|
||||||
<el-radio :label="false">退回</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="审批意见">
|
|
||||||
<el-input v-model="approvalForm.opinion" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="退回时意见必填, 将通知执行方整改" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="approvalOpen = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="approvalSaving" @click="onApprovalConfirm">确定</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 合规人员(manager) 批量审核 dialog: 对选中的「待合规审核」会议批量通过/退回 -->
|
<!-- 合规人员(manager) 批量审核 dialog: 对选中的「待合规审核」会议批量通过/退回 -->
|
||||||
<el-dialog v-model="batchAuditOpen" title="批量合规审核" width="560px">
|
<el-dialog v-model="batchAuditOpen" title="批量合规审核" width="560px">
|
||||||
<el-form label-width="100px">
|
<el-form label-width="100px">
|
||||||
@@ -149,7 +130,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
|
|||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { bizList, bizDelete } from '@/api/public'
|
import { bizList, bizDelete } from '@/api/public'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
import { stageLabel, STAGE_OPTIONS, derivePhysicalStage, stageClass } from '@/utils/meetingStage'
|
import { stageLabel, STAGE_OPTIONS, stageClass } from '@/utils/meetingStage'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
// ========== 角色感知 (admin/manager 共用此页, 按 URL 第 1 段识别) ==========
|
// ========== 角色感知 (admin/manager 共用此页, 按 URL 第 1 段识别) ==========
|
||||||
@@ -304,8 +285,10 @@ function isOneVal(v) { return v === 1 || v === '1' || v === true }
|
|||||||
// 已提交(SUBMITTED/APPROVED): 曾被冻结过 → 超时提交, 否则 → 按时提交
|
// 已提交(SUBMITTED/APPROVED): 曾被冻结过 → 超时提交, 否则 → 按时提交
|
||||||
// 未提交/退回(NOT_SUBMITTED/REJECTED): 已冻结 → 红色 0小时; 否则 submitDeadline - now 倒计时
|
// 未提交/退回(NOT_SUBMITTED/REJECTED): 已冻结 → 红色 0小时; 否则 submitDeadline - now 倒计时
|
||||||
function submitRemain(row) {
|
function submitRemain(row) {
|
||||||
const stage = row.materialAuditStage
|
// 两轨都进入审核 (SUBMITTED/APPROVED) 才算已提交; 否则任一条未提交/被驳回 → 仍在提交窗口
|
||||||
if (stage === 'SUBMITTED' || stage === 'APPROVED') {
|
const laborDone = row.laborAuditStage === 'SUBMITTED' || row.laborAuditStage === 'APPROVED'
|
||||||
|
const serviceDone = row.serviceAuditStage === 'SUBMITTED' || row.serviceAuditStage === 'APPROVED'
|
||||||
|
if (laborDone && serviceDone) {
|
||||||
return row.freezeTime ? { text: '超时提交', red: false } : { text: '按时提交', red: false }
|
return row.freezeTime ? { text: '超时提交', red: false } : { text: '按时提交', red: false }
|
||||||
}
|
}
|
||||||
if (isOneVal(row.isFrozen)) return { text: '0小时', red: true }
|
if (isOneVal(row.isFrozen)) return { text: '0小时', red: true }
|
||||||
@@ -317,9 +300,9 @@ function submitRemain(row) {
|
|||||||
const hours = totalHours % 24
|
const hours = totalHours % 24
|
||||||
return { text: `${days}天${hours}小时`, red: false }
|
return { text: `${days}天${hours}小时`, red: false }
|
||||||
}
|
}
|
||||||
// 列表结算按钮显隐: 与 MeetingDetail.canSettle 一致 (材料 APPROVED 且未结算)
|
// 列表结算按钮显隐: 与 MeetingDetail.canSettle 一致 (两轨均 APPROVED 且未结算)
|
||||||
function canSettleRow(row) {
|
function canSettleRow(row) {
|
||||||
return row.materialAuditStage === 'APPROVED' && !isOneVal(row.isSettled)
|
return row.laborAuditStage === 'APPROVED' && row.serviceAuditStage === 'APPROVED' && !isOneVal(row.isSettled)
|
||||||
}
|
}
|
||||||
function onSettle(row) {
|
function onSettle(row) {
|
||||||
// 结算需要上传付款凭证 (详情页结算 dialog 二合一), 跳详情并自动打开结算 dialog
|
// 结算需要上传付款凭证 (详情页结算 dialog 二合一), 跳详情并自动打开结算 dialog
|
||||||
@@ -352,38 +335,15 @@ async function onUnfreeze(row) {
|
|||||||
} catch (e) { ElMessage.error(e?.msg || e?.message || '解冻失败') }
|
} catch (e) { ElMessage.error(e?.msg || e?.message || '解冻失败') }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 支持方(监察员) 审批 ==========
|
// ========== 支持方(监察员) 审核: 跳详情页 (详情页内按 C1 轨给 审核劳务/会务/全部) ==========
|
||||||
const approvalOpen = ref(false)
|
// 有任一轨处于 C1 (已提交待支持方审) 即显示「审核」; 会务被退回时物理阶段=待整改, 但劳务 C1 仍需审, 不能用物理阶段判
|
||||||
const approvalSaving = ref(false)
|
function isSponsorPending(row) {
|
||||||
const approvalRow = ref({})
|
return (row.laborAuditStage === 'SUBMITTED' && isOneVal(row.laborComplianceApproved))
|
||||||
const approvalForm = reactive({ approved: true, opinion: '' })
|
|| (row.serviceAuditStage === 'SUBMITTED' && isOneVal(row.serviceComplianceApproved))
|
||||||
|
|
||||||
function onApproval(row) {
|
|
||||||
approvalRow.value = row
|
|
||||||
approvalForm.approved = true
|
|
||||||
approvalForm.opinion = ''
|
|
||||||
approvalOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onApprovalConfirm() {
|
|
||||||
if (!approvalForm.approved && !approvalForm.opinion.trim()) {
|
|
||||||
ElMessage.warning('退回时意见不能为空')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
approvalSaving.value = true
|
|
||||||
try {
|
|
||||||
await request.post(`/business/meeting/${approvalRow.value.meetingId}/audit-supervision`, {
|
|
||||||
approved: approvalForm.approved,
|
|
||||||
opinion: approvalForm.opinion
|
|
||||||
})
|
|
||||||
ElMessage.success(approvalForm.approved ? '已通过 → 审核通过' : '已退回 → 待整改')
|
|
||||||
approvalOpen.value = false
|
|
||||||
load()
|
|
||||||
} catch (e) {
|
|
||||||
ElMessage.error(e?.msg || e?.message || '审批失败')
|
|
||||||
} finally {
|
|
||||||
approvalSaving.value = false
|
|
||||||
}
|
}
|
||||||
|
// 审核: 跳详情页 (非 readonly), 详情页按角色控制显示三按钮
|
||||||
|
function onAudit(row) {
|
||||||
|
router.push(`${detailBase.value}/detail/${row.meetingId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 合规人员(manager) 批量审核 ==========
|
// ========== 合规人员(manager) 批量审核 ==========
|
||||||
@@ -392,9 +352,11 @@ const batchAuditSaving = ref(false)
|
|||||||
const batchAuditForm = reactive({ approved: true, opinion: '' })
|
const batchAuditForm = reactive({ approved: true, opinion: '' })
|
||||||
const batchEligibleIds = ref([])
|
const batchEligibleIds = ref([])
|
||||||
|
|
||||||
// 待合规审核判据: material_audit_stage=SUBMITTED 且 compliance_approved≠1 (与详情页 canComplianceAudit 一致)
|
// 待合规审核判据: 任一轨 SUBMITTED 且 compliance_approved≠1 (C0, 与详情页 canComplianceAudit 一致)
|
||||||
function isCompliancePending(row) {
|
function isCompliancePending(row) {
|
||||||
return row.materialAuditStage === 'SUBMITTED' && !isOneVal(row.materialComplianceApproved)
|
const laborC0 = row.laborAuditStage === 'SUBMITTED' && !isOneVal(row.laborComplianceApproved)
|
||||||
|
const serviceC0 = row.serviceAuditStage === 'SUBMITTED' && !isOneVal(row.serviceComplianceApproved)
|
||||||
|
return laborC0 || serviceC0
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBatchAudit() {
|
function onBatchAudit() {
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
<!-- 右: 操作按钮 - 悬浮固定到页面最右 (按选中 tab 动态显示) -->
|
<!-- 右: 操作按钮 - 悬浮固定到页面最右 (按选中 tab 动态显示) -->
|
||||||
<aside class="detail-actions">
|
<aside class="detail-actions">
|
||||||
<!-- 邀请函: 立即报名 -->
|
<!-- 邀请函: 立即报名 -->
|
||||||
<button v-if="activeTab === 'invitation'" class="action-btn primary signup-btn" :disabled="signing || signed" @click="onSignup">
|
<button v-if="activeTab === 'invitation' && canShowSignupBtn" class="action-btn primary signup-btn" :disabled="signing || signed" @click="onSignup">
|
||||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="currentColor" width="14" height="14">
|
<svg class="btn-icon" viewBox="0 0 24 24" fill="currentColor" width="14" height="14">
|
||||||
<path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 7V3.5L18.5 9H13z"/>
|
<path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 7V3.5L18.5 9H13z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -217,6 +217,12 @@ const canShowExecutionBtn = computed(() => {
|
|||||||
if (!r) return true
|
if (!r) return true
|
||||||
return r === 'executor'
|
return r === 'executor'
|
||||||
})
|
})
|
||||||
|
// 立即报名 (专家参与邀请函): 仅专家 + 匿名可见; sponsor/executor/manager/admin 隐藏
|
||||||
|
const canShowSignupBtn = computed(() => {
|
||||||
|
const r = userStore.user?.role || ''
|
||||||
|
if (!r) return true
|
||||||
|
return r === 'doctor'
|
||||||
|
})
|
||||||
|
|
||||||
const showQr = ref(false)
|
const showQr = ref(false)
|
||||||
const qrUrl = ref('')
|
const qrUrl = ref('')
|
||||||
@@ -380,6 +386,7 @@ async function checkSigned() {
|
|||||||
}
|
}
|
||||||
const signing = ref(false)
|
const signing = ref(false)
|
||||||
async function onSignup() {
|
async function onSignup() {
|
||||||
|
if (!canShowSignupBtn.value) return // 防御: 防止 v-if 被绕过
|
||||||
if (!loggedIn.value) {
|
if (!loggedIn.value) {
|
||||||
ElMessage.warning('请先登录系统')
|
ElMessage.warning('请先登录系统')
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<el-row :gutter="12">
|
<el-row :gutter="12">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="所属公司" prop="orgName">
|
<el-form-item label="所属公司" prop="orgName">
|
||||||
<el-input v-model="form.orgName" :readonly="!!myOrg" placeholder="请输入所属公司" maxlength="200">
|
<el-input v-model="form.orgName" :readonly="!!myOrg || isEdit" placeholder="请输入所属公司" maxlength="200">
|
||||||
<template #append v-if="myOrg">
|
<template #append v-if="myOrg">
|
||||||
<el-tooltip content="主账号公司, 注册时已自动关联" placement="top">
|
<el-tooltip content="主账号公司, 注册时已自动关联" placement="top">
|
||||||
<el-icon><Lock /></el-icon>
|
<el-icon><Lock /></el-icon>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
筛选栏: <el-form inline :model="q" class="filter-form">
|
筛选栏: <el-form inline :model="q" class="filter-form">
|
||||||
工具栏: <div class="toolbar">
|
工具栏: <div class="toolbar">
|
||||||
列表列 (按原型 sponsor-people.html left 排序):
|
列表列 (按原型 sponsor-people.html left 排序):
|
||||||
复选框 / 姓名 / 手机号 / 工作单位 / 部门 / 职务 / 角色(角色+账号类型已合并) / 状态 / 操作
|
姓名 / 手机号 / 工作单位 / 部门 / 职务 / 角色(角色+账号类型已合并) / 状态 / 操作
|
||||||
-->
|
-->
|
||||||
<div class="page-card">
|
<div class="page-card">
|
||||||
<div class="breadcrumb">首页 / 人员管理</div>
|
<div class="breadcrumb">首页 / 人员管理</div>
|
||||||
@@ -39,11 +39,9 @@
|
|||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<el-button type="primary" @click="onCreate">新建人员</el-button>
|
<el-button type="primary" @click="onCreate">新建人员</el-button>
|
||||||
<el-button @click="onImport">批量导入</el-button>
|
<el-button @click="onImport">批量导入</el-button>
|
||||||
<el-button :disabled="!selected.length || hasSelfInSelection" @click="onBatchRemove">批量删除</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelectionChange">
|
<el-table :data="rows" v-loading="loading" stripe border>
|
||||||
<el-table-column type="selection" width="48" :selectable="(row) => row.userId !== store.user?.userId" />
|
|
||||||
<el-table-column prop="account" label="账号" width="120" show-overflow-tooltip />
|
<el-table-column prop="account" label="账号" width="120" show-overflow-tooltip />
|
||||||
<el-table-column prop="name" label="姓名" width="100" />
|
<el-table-column prop="name" label="姓名" width="100" />
|
||||||
<el-table-column prop="phone" label="手机号" width="130" />
|
<el-table-column prop="phone" label="手机号" width="130" />
|
||||||
@@ -136,7 +134,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, computed } from 'vue'
|
import { reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { bizUpdate, bizDelete, resetPersonPassword } from '@/api/public'
|
import { bizUpdate, bizDelete, resetPersonPassword } from '@/api/public'
|
||||||
@@ -148,9 +146,6 @@ import request from '@/utils/request'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useUserStore()
|
const store = useUserStore()
|
||||||
|
|
||||||
// 批量操作禁用判断: 选了主账号自己 (跟"操作列禁用/删除隐藏"是同一条规则, 防止误删)
|
|
||||||
const hasSelfInSelection = computed(() => selected.value.some(r => r.userId === store.user?.userId))
|
|
||||||
|
|
||||||
// sponsor 端只看自己团队 (主账号+子账号) 创建的人, 走专属接口 /business/person/sponsorList (后端强制隔离)
|
// sponsor 端只看自己团队 (主账号+子账号) 创建的人, 走专属接口 /business/person/sponsorList (后端强制隔离)
|
||||||
const q = reactive({
|
const q = reactive({
|
||||||
name: '', phone: '', orgName: '', department: '', status: ''
|
name: '', phone: '', orgName: '', department: '', status: ''
|
||||||
@@ -158,7 +153,6 @@ const q = reactive({
|
|||||||
const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
|
const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const selected = ref([])
|
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -183,8 +177,6 @@ function reset() {
|
|||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSelectionChange(arr) { selected.value = arr }
|
|
||||||
|
|
||||||
function onCreate() {
|
function onCreate() {
|
||||||
router.push({ path: '/sponsor/people/new' })
|
router.push({ path: '/sponsor/people/new' })
|
||||||
}
|
}
|
||||||
@@ -241,21 +233,6 @@ async function onRemoveOne(row) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onBatchRemove() {
|
|
||||||
if (!selected.value.length) { ElMessage.warning('请先勾选要删除的人员'); return }
|
|
||||||
if (hasSelfInSelection.value) { ElMessage.warning('选中的包含您本人, 不能删除'); return }
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(`确定删除选中的 ${selected.value.length} 条? 该操作不可恢复`, '批量删除', { type: 'warning' })
|
|
||||||
} catch { return }
|
|
||||||
let ok = 0
|
|
||||||
for (const r of selected.value) {
|
|
||||||
try { await bizDelete('person', r.personId); ok++ } catch {}
|
|
||||||
}
|
|
||||||
ElMessage.success(`已删除 ${ok} 条`)
|
|
||||||
selected.value = []
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量导入 (Excel)
|
// 批量导入 (Excel)
|
||||||
const importVisible = ref(false)
|
const importVisible = ref(false)
|
||||||
const uploadRef = ref()
|
const uploadRef = ref()
|
||||||
@@ -338,4 +315,6 @@ load()
|
|||||||
:deep(.el-table .el-button.is-link) { color: var(--brand-primary); background: transparent; border: none; }
|
:deep(.el-table .el-button.is-link) { color: var(--brand-primary); background: transparent; border: none; }
|
||||||
:deep(.el-table .el-button.is-link:hover) { color: var(--brand-primary-deep); background: transparent; }
|
:deep(.el-table .el-button.is-link:hover) { color: var(--brand-primary-deep); background: transparent; }
|
||||||
:deep(.el-table .el-button.is-link.is-disabled) { color: #c0c4cc; background: transparent; }
|
:deep(.el-table .el-button.is-link.is-disabled) { color: #c0c4cc; background: transparent; }
|
||||||
|
:deep(.el-table .el-button--danger.is-link) { color: var(--el-color-danger); }
|
||||||
|
:deep(.el-table .el-button--danger.is-link:hover) { color: var(--el-color-danger); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user