feat: 合规员(manager) 自建会议可自传自审, 现场照片支持直接上传
- 后端 submitMaterial: manager 跳过执行方/已执行/已冻结三道门槛 - 前端提交按钮门槛同步放宽 (manager 不受已执行/已冻结限制) - 现场照片(签到表/前全景/后全景) 新增直接上传入口, 与扫码拍照并存 - 劳务材料 tab 增加"劳务明细表"小标题 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+9
-5
@@ -351,8 +351,8 @@ public class BizMeetingController extends BaseController {
|
|||||||
/**
|
/**
|
||||||
* 执行人员提交材料 (分轨: 劳务 LABOR / 会务 SERVICE 可多选, 每轨独立提交).
|
* 执行人员提交材料 (分轨: 劳务 LABOR / 会务 SERVICE 可多选, 每轨独立提交).
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>校验 1: 当前用户是该会议执行方 (项目级归属, 强校验)</li>
|
* <li>校验 1: 当前用户是该会议执行方 (项目级归属, 强校验) — 合规员(manager) 自建会议自传自审, 跳过此校验</li>
|
||||||
* <li>校验 2: 已执行 (is_executed=1) 且未冻结</li>
|
* <li>校验 2: 已执行 (is_executed=1) 且未冻结 — 合规员(manager) 跳过"已执行"与"已冻结"两道时间门槛 (不依赖 scheduler)</li>
|
||||||
* <li>校验 3: 所选轨 audit_stage ∈ {NOT_SUBMITTED, REJECTED}</li>
|
* <li>校验 3: 所选轨 audit_stage ∈ {NOT_SUBMITTED, REJECTED}</li>
|
||||||
* <li>校验 4: 会务轨在 biz_meeting_material 至少 1 条 (M_*); 劳务轨 = 参会人员列表至少 1 人 (非上传文件)</li>
|
* <li>校验 4: 会务轨在 biz_meeting_material 至少 1 条 (M_*); 劳务轨 = 参会人员列表至少 1 人 (非上传文件)</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
@@ -366,11 +366,15 @@ public class BizMeetingController extends BaseController {
|
|||||||
if (m == null) throw new ServiceException("会议不存在");
|
if (m == null) throw new ServiceException("会议不存在");
|
||||||
|
|
||||||
Long userId = SecurityUtils.getUserId();
|
Long userId = SecurityUtils.getUserId();
|
||||||
if (!bizProjectService.isExecutorOfProject(m.getProjectId(), userId)) {
|
String roleType = SecurityUtils.getLoginUser().getUser().getRoleType();
|
||||||
|
// 合规员(manager) 自建会议可自传自审: 跳过"必须是项目执行方"的强校验
|
||||||
|
if (!"manager".equals(roleType) && !bizProjectService.isExecutorOfProject(m.getProjectId(), userId)) {
|
||||||
throw new ServiceException("您不是该项目的执行方, 无法提交材料");
|
throw new ServiceException("您不是该项目的执行方, 无法提交材料");
|
||||||
}
|
}
|
||||||
if (!isExecuted(m)) throw new ServiceException("会议尚未执行, 不能提交材料");
|
// 合规员(manager) 自建会议自传自审: 不依赖 scheduler 翻 is_executed, 跳过"已执行"时间门槛
|
||||||
if (isFrozen(m)) throw new ServiceException("会议已冻结, 不能提交材料");
|
if (!"manager".equals(roleType) && !isExecuted(m)) throw new ServiceException("会议尚未执行, 不能提交材料");
|
||||||
|
// 合规员(manager) 自建会议自传自审: 同样跳过"已冻结"门槛 (冻结=提交截止已过, 但自审流程不应对合规员本人设卡)
|
||||||
|
if (!"manager".equals(roleType) && isFrozen(m)) throw new ServiceException("会议已冻结, 不能提交材料");
|
||||||
|
|
||||||
List<String> types = body == null ? null : body.getTypes();
|
List<String> types = body == null ? null : body.getTypes();
|
||||||
if (types == null || types.isEmpty()) throw new ServiceException("请选择要提交的材料类型");
|
if (types == null || types.isEmpty()) throw new ServiceException("请选择要提交的材料类型");
|
||||||
|
|||||||
@@ -63,8 +63,9 @@
|
|||||||
<div class="section-title">材料管理</div>
|
<div class="section-title">材料管理</div>
|
||||||
<el-tabs v-model="activeTab" class="material-tabs">
|
<el-tabs v-model="activeTab" class="material-tabs">
|
||||||
<el-tab-pane label="劳务材料" name="labor">
|
<el-tab-pane label="劳务材料" name="labor">
|
||||||
<!-- 参会人管理 (放在 劳务明细 + 劳务协议 上面, 无表题直接出表) -->
|
<!-- 参会人管理 (放在 劳务明细 + 劳务协议 上面) -->
|
||||||
<div class="attendee-section">
|
<div class="attendee-section">
|
||||||
|
<div class="attendee-subtitle">劳务明细表</div>
|
||||||
<div v-if="!isSponsor && !isReadonly" class="attendee-toolbar">
|
<div v-if="!isSponsor && !isReadonly" class="attendee-toolbar">
|
||||||
<el-button type="primary" size="small" :loading="attendeeLoading" :disabled="!attendeeEditable" @click="openAttendeeDialog()">+ 新增参会人</el-button>
|
<el-button type="primary" size="small" :loading="attendeeLoading" :disabled="!attendeeEditable" @click="openAttendeeDialog()">+ 新增参会人</el-button>
|
||||||
<el-button size="small" :disabled="!attendeeEditable" @click="openAttendeeImport">批量导入</el-button>
|
<el-button size="small" :disabled="!attendeeEditable" @click="openAttendeeImport">批量导入</el-button>
|
||||||
@@ -195,7 +196,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<CameraQrUpload v-if="isCameraSubType(r.subType)" v-model="r.url" :meeting-id="meetingId" :sub-type="r.subType" :label="r.label" :readonly="isSponsor || isReadonly || !laborEditable || laborLocked" class="file-uploader" />
|
<div v-if="isCameraSubType(r.subType)" class="camera-upload-row">
|
||||||
|
<CameraQrUpload v-model="r.url" :meeting-id="meetingId" :sub-type="r.subType" :label="r.label" :readonly="isSponsor || isReadonly || !laborEditable || laborLocked" class="file-uploader" />
|
||||||
|
<OssFileUploader v-model="r.url" v-model:name="r.fileName" :dir="`ry8080/meeting/${meetingId}/labor/`" accept=".png,.jpg,.jpeg,.gif,.webp,.bmp" placeholder="直接上传(本地图片)" :readonly="isSponsor || isReadonly || !laborEditable || laborLocked" class="file-uploader" />
|
||||||
|
</div>
|
||||||
<template v-else-if="r.subType === 'L_ESIGN_IN'">
|
<template v-else-if="r.subType === 'L_ESIGN_IN'">
|
||||||
<div class="esign-actions">
|
<div class="esign-actions">
|
||||||
<OssFileUploader v-model="r.url" :dir="`ry8080/meeting/${meetingId}/labor/`" :accept="r.accept" multiple block :max-size="20" placeholder="点击上传多个 Excel (xls/xlsx/csv)" :readonly="isSponsor || isReadonly || !laborEditable || laborLocked" />
|
<OssFileUploader v-model="r.url" :dir="`ry8080/meeting/${meetingId}/labor/`" :accept="r.accept" multiple block :max-size="20" placeholder="点击上传多个 Excel (xls/xlsx/csv)" :readonly="isSponsor || isReadonly || !laborEditable || laborLocked" />
|
||||||
@@ -898,10 +902,10 @@ const frozen = computed(() => isOne(row.value.isFrozen))
|
|||||||
/** 单轨是否处于「可提交/可编辑」态: 执行方仅未提交/被驳回可编辑; 合规(manager)/管理员 永远可编辑 */
|
/** 单轨是否处于「可提交/可编辑」态: 执行方仅未提交/被驳回可编辑; 合规(manager)/管理员 永远可编辑 */
|
||||||
const laborEditable = computed(() => isManager.value || isAdmin.value || ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.laborAuditStage))
|
const laborEditable = computed(() => isManager.value || isAdmin.value || ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.laborAuditStage))
|
||||||
const serviceEditable = computed(() => isManager.value || isAdmin.value || ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.serviceAuditStage))
|
const serviceEditable = computed(() => isManager.value || isAdmin.value || ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.serviceAuditStage))
|
||||||
// 提交审核后该轨锁定 (manager/admin 也跟着锁), 让 OssFileUploader 移除按钮消失
|
// 提交审核后该轨锁定执行方 (executor), 让 OssFileUploader 移除按钮消失; 合规/管理员 (manager/admin) 任何时候都可编辑保存, 故不锁
|
||||||
// (仅控制 OssFileUploader 的 readonly; 不影响 laborEditable (下载模板/参会人列表的判定))
|
// (仅控制 OssFileUploader 的 readonly; 不影响 laborEditable (下载模板/参会人列表的判定))
|
||||||
const laborLocked = computed(() => ['SUBMITTED', 'APPROVED'].includes(row.value.laborAuditStage))
|
const laborLocked = computed(() => !isManager.value && !isAdmin.value && ['SUBMITTED', 'APPROVED'].includes(row.value.laborAuditStage))
|
||||||
const serviceLocked = computed(() => ['SUBMITTED', 'APPROVED'].includes(row.value.serviceAuditStage))
|
const serviceLocked = computed(() => !isManager.value && !isAdmin.value && ['SUBMITTED', 'APPROVED'].includes(row.value.serviceAuditStage))
|
||||||
// 参会人名单是否可改 (新增/导入/编辑/删除): sponsor/只读不可; executor 劳务提交后(SUBMITTED/APPROVED)锁定, 未提交/退回可改; manager/admin 恒可改
|
// 参会人名单是否可改 (新增/导入/编辑/删除): sponsor/只读不可; executor 劳务提交后(SUBMITTED/APPROVED)锁定, 未提交/退回可改; manager/admin 恒可改
|
||||||
const attendeeEditable = computed(() => {
|
const attendeeEditable = computed(() => {
|
||||||
if (isSponsor.value || isReadonly.value) return false
|
if (isSponsor.value || isReadonly.value) return false
|
||||||
@@ -911,11 +915,17 @@ const attendeeEditable = computed(() => {
|
|||||||
/** 单轨状态判定: C0=已提交待合规审 / C1=已提交待支持方审 */
|
/** 单轨状态判定: C0=已提交待合规审 / C1=已提交待支持方审 */
|
||||||
function isC0(stage, compliance) { return stage === 'SUBMITTED' && !isOne(compliance) }
|
function isC0(stage, compliance) { return stage === 'SUBMITTED' && !isOne(compliance) }
|
||||||
function isC1(stage, compliance) { return stage === 'SUBMITTED' && isOne(compliance) }
|
function isC1(stage, compliance) { return stage === 'SUBMITTED' && isOne(compliance) }
|
||||||
// 材料可提交: 已执行 + 未冻结 + 对应轨可编辑 (三按钮按轨显隐)
|
// 材料可提交: 执行方需"已执行"+未冻结; 合规员(manager 自建会议自传自审)不受"已执行"与"已冻结"两道时间门槛限制
|
||||||
const executorSubmitReady = computed(() => isExecutor.value && executed.value && !frozen.value)
|
const executorSubmitReady = computed(() => {
|
||||||
const canSubmitLabor = computed(() => executorSubmitReady.value && laborEditable.value)
|
if (isManager.value) return true
|
||||||
const canSubmitService = computed(() => executorSubmitReady.value && serviceEditable.value)
|
return isExecutor.value && executed.value && !frozen.value
|
||||||
const canSubmitAll = computed(() => executorSubmitReady.value && laborEditable.value && serviceEditable.value)
|
})
|
||||||
|
// 单轨"可提交"门槛 (区别于 laborEditable 的"可编辑"): 仅未提交/被驳回可提交, 提交后按钮隐藏
|
||||||
|
const laborSubmittable = computed(() => ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.laborAuditStage))
|
||||||
|
const serviceSubmittable = computed(() => ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.serviceAuditStage))
|
||||||
|
const canSubmitLabor = computed(() => executorSubmitReady.value && laborSubmittable.value)
|
||||||
|
const canSubmitService = computed(() => executorSubmitReady.value && serviceSubmittable.value)
|
||||||
|
const canSubmitAll = computed(() => executorSubmitReady.value && laborSubmittable.value && serviceSubmittable.value)
|
||||||
// 材料可编辑 (保存修改): 任一轨未提交/被驳回 时可保存; 两轨都进入审核后锁定
|
// 材料可编辑 (保存修改): 任一轨未提交/被驳回 时可保存; 两轨都进入审核后锁定
|
||||||
const materialsEditable = computed(() => laborEditable.value || serviceEditable.value)
|
const materialsEditable = computed(() => laborEditable.value || serviceEditable.value)
|
||||||
// 合规审核 (合规方/manager): 三按钮按轨 C0 显隐
|
// 合规审核 (合规方/manager): 三按钮按轨 C0 显隐
|
||||||
@@ -2590,6 +2600,10 @@ onBeforeUnmount(() => {
|
|||||||
.agreement-actions { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
.agreement-actions { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||||
/* 电子签到表 多文件上传 + zip 按钮 纵向排列 */
|
/* 电子签到表 多文件上传 + zip 按钮 纵向排列 */
|
||||||
.esign-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; min-width: 0; }
|
.esign-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; min-width: 0; }
|
||||||
|
/* 现场照片 (签到表/前全景/后全景): 扫码拍照 + 直接上传 左右并存 */
|
||||||
|
.camera-upload-row { display: flex; align-items: flex-start; gap: 8px; min-width: 0; }
|
||||||
|
.camera-upload-row :deep(.camera-qr-upload) { width: auto; flex: 0 0 auto; }
|
||||||
|
.camera-upload-row :deep(.ht-file-upload) { flex: 1 1 0; min-width: 0; }
|
||||||
.upload-progress { grid-column: 2; display: flex; align-items: center; gap: 8px; max-width: 400px; }
|
.upload-progress { grid-column: 2; display: flex; align-items: center; gap: 8px; max-width: 400px; }
|
||||||
.upload-progress .el-progress { flex: 1; }
|
.upload-progress .el-progress { flex: 1; }
|
||||||
.upload-progress-status { font-size: 12px; color: #8c8c8c; white-space: nowrap; }
|
.upload-progress-status { font-size: 12px; color: #8c8c8c; white-space: nowrap; }
|
||||||
@@ -2638,6 +2652,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
/* 参会人管理 (放在 劳务材料 tab 顶部, 在 劳务明细 + 劳务协议 上面) */
|
/* 参会人管理 (放在 劳务材料 tab 顶部, 在 劳务明细 + 劳务协议 上面) */
|
||||||
.attendee-section { padding-bottom: 16px; border-bottom: 1px dashed #e8e8e8; }
|
.attendee-section { padding-bottom: 16px; border-bottom: 1px dashed #e8e8e8; }
|
||||||
|
.attendee-subtitle { font-size: 14px; font-weight: 600; color: #1a1a1a; margin-bottom: 12px; }
|
||||||
.attendee-toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; flex-wrap: wrap; }
|
.attendee-toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; flex-wrap: wrap; }
|
||||||
.form-hint { font-size: 12px; color: #909399; margin-top: -8px; margin-bottom: 8px; line-height: 1.5; grid-column: 1 / -1; }
|
.form-hint { font-size: 12px; color: #909399; margin-top: -8px; margin-bottom: 8px; line-height: 1.5; grid-column: 1 / -1; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user