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:
@@ -65,7 +65,8 @@ const rawOptions = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 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 ?? '')
|
||||
|
||||
@@ -65,7 +65,8 @@ const rawOptions = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 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 ?? '')
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* 会议阶段显示 (事实驱动, 与后端 StageDeriver 镜像).
|
||||
*
|
||||
* biz_meeting 现在存「事实」: is_executed / is_frozen / is_settled / is_finished
|
||||
* + material_audit_stage (4 值: NOT_SUBMITTED/SUBMITTED/APPROVED/REJECTED)
|
||||
* + material_compliance_approved (区分两级审核) + 审核时间.
|
||||
* + 劳务/会务两轨各自 audit_stage (4 值: NOT_SUBMITTED/SUBMITTED/APPROVED/REJECTED)
|
||||
* + 各自 compliance_approved (区分两级审核) + 审核时间.
|
||||
*
|
||||
* 各角色看到的「阶段名称」由这些事实实时推导, 不再是单一 current_stage 枚举投影.
|
||||
* current_stage 仍是物理阶段缓存 (10 值), 仅供列表筛选精确匹配.
|
||||
@@ -13,21 +13,87 @@ function isTrue(v) {
|
||||
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), 用于颜色/筛选.
|
||||
* 两轨取最小进度 (流程最靠前的一轨决定会议物理态).
|
||||
*/
|
||||
export function derivePhysicalStage(row) {
|
||||
if (!row) return 'NOT_STARTED'
|
||||
if (isTrue(row.isFrozen)) return 'FROZEN'
|
||||
if (isTrue(row.isFinished)) return 'FINISHED'
|
||||
if (isTrue(row.isSettled)) return 'SETTLED'
|
||||
const material = row.materialAuditStage
|
||||
if (material === 'REJECTED') return 'RECTIFYING'
|
||||
if (material === 'APPROVED') return 'AWAITING_SETTLEMENT'
|
||||
if (material === 'SUBMITTED') return isTrue(row.materialComplianceApproved) ? 'AWAITING_SUPERVISION' : 'AWAITING_COMPLIANCE'
|
||||
const s = Math.min(laborState(row), serviceState(row))
|
||||
if (s === 0) return 'RECTIFYING'
|
||||
if (s === 2) return 'AWAITING_COMPLIANCE'
|
||||
if (s === 3) return 'AWAITING_SUPERVISION'
|
||||
if (s === 4) return 'AWAITING_SETTLEMENT'
|
||||
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).
|
||||
* 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.isFinished)) return '已完结'
|
||||
if (isTrue(row.isSettled)) return '已结算'
|
||||
|
||||
const material = row.materialAuditStage
|
||||
|
||||
// 退回: 执行方看「已退回」, 其他方看「待整改」
|
||||
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 '未执行'
|
||||
const chosen = chooseState(role, laborState(row), serviceState(row))
|
||||
return render(role, chosen, isTrue(row.isExecuted))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,31 +114,38 @@ export function stageLabel(role, row) {
|
||||
return deriveStage(role, row)
|
||||
}
|
||||
|
||||
/** 颜色 class (业务专用 tag), 基于物理阶段 */
|
||||
export function stageClass(row) {
|
||||
const s = derivePhysicalStage(row)
|
||||
if (s === 'NOT_STARTED') return 'pending'
|
||||
if (s === 'RUNNING') return 'running'
|
||||
if (s === 'AWAITING_COMPLIANCE' || s === 'AWAITING_SUPERVISION') return 'reviewing'
|
||||
if (s === 'SUPERVISION_APPROVED') return 'done'
|
||||
if (s === 'RECTIFYING') return 'waiting'
|
||||
if (s === 'AWAITING_SETTLEMENT') return 'waiting'
|
||||
if (s === 'SETTLED') return 'done'
|
||||
if (s === 'FINISHED') return 'done'
|
||||
if (s === 'FROZEN') return 'frozen'
|
||||
return 'default'
|
||||
/**
|
||||
* 展示阶段名 → 颜色映射 (class + el-tag type), 与 render() 措辞一一对应.
|
||||
* 颜色跟随「各角色看到的展示阶段」而非物理阶段, 避免文案与颜色错位
|
||||
* (如 sponsor 看「待审核」却因物理阶段 RECTIFYING 显示红色).
|
||||
* 规则: 待整改/已退回=红, 未执行=灰, 执行中/已执行未传材料=蓝, 待审核=橙, 通过/待结算/完结=绿.
|
||||
*/
|
||||
const STAGE_STYLE = {
|
||||
'冻结中': { cls: 'frozen', tag: 'info' },
|
||||
'已完结': { cls: 'done', tag: 'success' },
|
||||
'已结算': { cls: 'done', tag: 'success' },
|
||||
'待整改': { cls: 'waiting', tag: 'danger' },
|
||||
'已退回': { cls: 'waiting', tag: 'danger' },
|
||||
'未执行': { 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 列表用), 基于物理阶段 */
|
||||
export function stageTag(row) {
|
||||
const s = derivePhysicalStage(row)
|
||||
if (s === 'NOT_STARTED') return 'info'
|
||||
if (s === 'RUNNING' || s === 'RECTIFYING') return 'primary'
|
||||
if (s === 'AWAITING_COMPLIANCE' || s === 'AWAITING_SUPERVISION') return 'warning'
|
||||
if (s === 'SUPERVISION_APPROVED' || s === 'SETTLED' || s === 'FINISHED') return 'success'
|
||||
if (s === 'AWAITING_SETTLEMENT') return 'warning'
|
||||
if (s === 'FROZEN') return 'info'
|
||||
return 'info'
|
||||
function stageStyle(role, row) {
|
||||
return STAGE_STYLE[deriveStage(role, row)] || { cls: 'default', tag: 'info' }
|
||||
}
|
||||
|
||||
/** 颜色 class (业务专用 tag), 基于角色展示阶段 (与 stageLabel 文案一致). */
|
||||
export function stageClass(role, row) {
|
||||
return stageStyle(role, row).cls
|
||||
}
|
||||
|
||||
/** 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-card">
|
||||
|
||||
<h2 class="page-title">项目执行单位注册</h2>
|
||||
|
||||
<el-steps :active="step" finish-status="success" simple class="steps">
|
||||
<el-step title="基本信息" />
|
||||
<el-step title="注册成功" />
|
||||
@@ -196,6 +198,7 @@ onUnmounted(() => { if (smsTimer) clearInterval(smsTimer) })
|
||||
|
||||
<style scoped>
|
||||
.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; }
|
||||
.step-body { max-width: 640px; margin: 32px auto 0; }
|
||||
.sms-row { display: flex; gap: 12px; width: 100%; }
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
<div class="page-wrap">
|
||||
<div class="page-card">
|
||||
|
||||
<h2 class="page-title">项目参与专家注册</h2>
|
||||
|
||||
<el-steps :active="step" finish-status="success" simple class="steps">
|
||||
<el-step title="基本信息" />
|
||||
<el-step title="注册成功" />
|
||||
@@ -185,6 +187,7 @@ onUnmounted(() => { if (smsTimer) clearInterval(smsTimer) })
|
||||
|
||||
<style scoped>
|
||||
.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; }
|
||||
.step-body { max-width: 640px; margin: 32px auto 0; }
|
||||
.sms-row { display: flex; gap: 12px; width: 100%; }
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
<div class="page-wrap">
|
||||
<div class="page-card">
|
||||
|
||||
<h2 class="page-title">项目支持单位注册</h2>
|
||||
|
||||
<el-steps :active="step" finish-status="success" simple class="steps">
|
||||
<el-step title="基本信息" />
|
||||
<el-step title="注册成功" />
|
||||
@@ -210,6 +212,7 @@ onUnmounted(() => { if (smsTimer) clearInterval(smsTimer) })
|
||||
|
||||
<style scoped>
|
||||
.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; }
|
||||
.step-body { max-width: 640px; margin: 32px auto 0; }
|
||||
.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="currentStage" label="当前阶段" width="140">
|
||||
<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>
|
||||
</el-table-column>
|
||||
<el-table-column label="日程" width="130" align="center">
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="currentStage" label="当前阶段" width="140" align="center">
|
||||
<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>
|
||||
</el-table-column>
|
||||
<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="sessions" label="场次" width="90" 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>
|
||||
<template #empty>
|
||||
<span style="color:#c0c4cc">暂无分配 (到 项目管理 / 项目分配 添加)</span>
|
||||
<span style="color:#909399">暂无分配 (到 项目管理 / 项目分配 添加)</span>
|
||||
</template>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
@@ -71,7 +71,7 @@
|
||||
<tbody>
|
||||
<tr v-for="(r, idx) in form.roleRows" :key="idx">
|
||||
<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 v-if="!form.roleRows.length"><td colspan="2" class="empty-row">暂无角色</td></tr>
|
||||
</tbody>
|
||||
@@ -104,6 +104,11 @@
|
||||
|
||||
<!-- 公告预览 (只读) -->
|
||||
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
||||
|
||||
<!-- 底部操作: 返回 (按角色回退) -->
|
||||
<div class="form-actions">
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -148,7 +153,7 @@ const notices = computed(() => {
|
||||
return items.map(m => ({
|
||||
label: m.label,
|
||||
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})/)
|
||||
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() {
|
||||
@@ -279,9 +295,9 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<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; }
|
||||
.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; }
|
||||
|
||||
/* 章节标题 (与编辑页 ProjectsNew.vue 保持一致) */
|
||||
@@ -292,15 +308,21 @@ onMounted(async () => {
|
||||
}
|
||||
.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 :deep(.el-form-item) { margin-bottom: 8px; }
|
||||
.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.money {
|
||||
font-family: ui-monospace, "Courier New", monospace;
|
||||
.info-value { font-size: 14px; color: #262626; font-weight: 400; }
|
||||
|
||||
/* 金额: 统一等宽字体 + 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;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.monitor-num { width: 24px; color: #8c8c8c; font-size: 13px; }
|
||||
.monitor-name { flex: 1; font-size: 14px; color: #1a1a1a; }
|
||||
.empty-hint { text-align: center; color: #c0c4cc; padding: 12px 0; font-size: 13px; }
|
||||
.monitor-num { width: 24px; color: #909399; font-size: 13px; }
|
||||
.monitor-name { flex: 1; font-size: 14px; color: #262626; }
|
||||
.empty-hint { text-align: center; color: #909399; padding: 12px 0; font-size: 13px; }
|
||||
|
||||
/* 角色劳务表格 */
|
||||
.info-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
/* 角色劳务表格 + 服务公司表格: 统一 14px, 表头 600/strong, 单元格 strong */
|
||||
.info-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||
.role-labor-table { width: auto; border: 1px solid #ebeef5; } /* 角色劳务表格按内容紧凑显示, 不撑满卡片 */
|
||||
.role-labor-table th,
|
||||
.role-labor-table td { border-right: 1px solid #ebeef5; }
|
||||
.role-labor-table th:last-child,
|
||||
.role-labor-table td:last-child { border-right: none; }
|
||||
/* 服务公司表格 (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; }
|
||||
.info-table th { background: #fafafa; padding: 10px 12px; text-align: left; color: #1a1a1a; font-weight: 600; border-bottom: 1px solid #f0f0f0; white-space: nowrap; }
|
||||
.info-table td { padding: 10px 12px; border-bottom: 1px solid #f5f5f5; color: #595959; vertical-align: middle; }
|
||||
.empty-row { text-align: center; color: #c0c4cc; }
|
||||
.assign-table-el :deep(th.el-table__cell) { color: #262626; font-weight: 600; background: #fafafa; }
|
||||
.assign-table-el :deep(td.el-table__cell) { color: #262626; }
|
||||
.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 行: 邀请函/支持函/通知/日程) */
|
||||
.notice-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.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-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-empty { color: #c0c4cc; font-size: 13px; }
|
||||
.file-name { color: #262626; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.file-empty { color: #909399; font-size: 13px; }
|
||||
|
||||
/* 提示 (灰色, 跟 ProjectsNew 一致) */
|
||||
.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
|
||||
:filter-method="searchSponsorOrgs" clearable style="width:100%" @change="onSponsorOrgPick">
|
||||
<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-form-item>
|
||||
|
||||
@@ -84,7 +85,8 @@
|
||||
style="width:100%"
|
||||
@change="v => onExecUserPick(row, v)">
|
||||
<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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -184,6 +186,11 @@ function openPreview(url, title) {
|
||||
previewOpen.value = true
|
||||
}
|
||||
|
||||
// 公司是否被禁用 (biz_org.status='1') — 禁用项仍展示但置灰、不可选
|
||||
function isOrgDisabled(u) {
|
||||
return String(u?.status) === '1'
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
const singleLoading = 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">{{ 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">{{ 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">{{ row.createBy || '-' }}</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 v-if="!isSponsor && !isReadonly" class="file-row">
|
||||
<div v-if="!isSponsor && !isReadonly && laborEditable" class="file-row">
|
||||
<span class="file-label">劳务协议:</span>
|
||||
<div class="agreement-actions">
|
||||
<el-button size="small" :loading="agreementTplLoading" @click="onDownloadAgreementTemplate">下载目录</el-button>
|
||||
@@ -188,13 +188,13 @@
|
||||
</div>
|
||||
<div v-for="r in laborMaterialRows" :key="r.label" class="file-row">
|
||||
<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" />
|
||||
<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" />
|
||||
<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 || !laborEditable" />
|
||||
<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>
|
||||
<!-- 专家照片: 与劳务协议同构的"下载目录 + 回填"闭环, 不再作为单文件材料直接上传保存 -->
|
||||
<div v-if="!isSponsor && !isReadonly" class="file-row">
|
||||
<div v-if="!isSponsor && !isReadonly && laborEditable" class="file-row">
|
||||
<span class="file-label">专家照片:</span>
|
||||
<div class="agreement-actions">
|
||||
<el-button size="small" :loading="expertPhotoTplLoading" @click="onDownloadExpertPhotoTemplate">下载目录</el-button>
|
||||
@@ -227,7 +227,7 @@
|
||||
<div class="file-list">
|
||||
<div v-for="r in serviceMaterialRows" :key="r.label" class="file-row">
|
||||
<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>
|
||||
<div v-if="r.hint" class="file-hint">{{ r.hint }}</div>
|
||||
</div>
|
||||
@@ -237,7 +237,7 @@
|
||||
<el-tag v-if="meetingFee.useMain" type="warning" size="small" effect="plain">已上传总发票, 以总发票为准</el-tag>
|
||||
</div>
|
||||
<!-- 打包上传: 下载空目录模板 + 上传会务材料 (单目录多文件自动打 zip) -->
|
||||
<div v-if="!isSponsor && !isReadonly" class="file-row">
|
||||
<div v-if="!isSponsor && !isReadonly && serviceEditable" class="file-row">
|
||||
<span class="file-label">打包上传:</span>
|
||||
<div class="agreement-actions">
|
||||
<el-button size="small" :loading="serviceTplLoading" @click="onDownloadServiceTemplate">下载目录</el-button>
|
||||
@@ -269,15 +269,22 @@
|
||||
</el-tabs>
|
||||
<div class="tab-actions">
|
||||
<span style="flex:1"></span>
|
||||
<!-- 执行人员: 提交材料 -->
|
||||
<el-button v-if="canSubmitMaterial && !isReadonly" type="warning" :loading="busy.submitMaterial" @click="onSubmitMaterial">提交材料</el-button>
|
||||
<!-- 合规 / 监察审核 (材料) -->
|
||||
<el-button v-if="canComplianceAudit() && !isReadonly" type="success" @click="openAuditDialog('COMPLIANCE')">合规审核 (材料)</el-button>
|
||||
<el-button v-if="canSupervisionAudit() && !isReadonly" type="primary" @click="openAuditDialog('SUPERVISION')">监察审核 (材料)</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="canSubmitAll && !isReadonly" type="primary" :loading="busy.submitMaterial" @click="onSubmitMaterials(['LABOR', 'SERVICE'])">提交全部</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="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>
|
||||
@@ -287,50 +294,83 @@
|
||||
<div class="card audit-column">
|
||||
<div class="section-title">材料审核</div>
|
||||
<div class="timeline">
|
||||
<!-- 固定节点 1 -->
|
||||
<!-- 节点 1: 会议已执行 -->
|
||||
<div :class="['timeline-item', fixedNodeStatus('PRE')]">
|
||||
<div class="timeline-title">会议已执行</div>
|
||||
<div class="timeline-desc">{{ nodeDesc('PRE') }}</div>
|
||||
</div>
|
||||
<!-- 动态轮次 2/3/4 -->
|
||||
<template v-for="(round, idx) in displayMaterialRounds" :key="`mat-r${idx}`">
|
||||
<div :class="['timeline-item', partStatus(round.submit)]">
|
||||
<div class="timeline-title">执行方提交材料</div>
|
||||
<div v-if="round.submit" class="timeline-meta">
|
||||
<span>{{ round.submit.auditor }}</span>
|
||||
<span class="dot">·</span>
|
||||
<span>{{ fmtDateTime(round.submit.auditTime) }}</span>
|
||||
<!-- 节点 2: 审核时间轴 (内嵌 提交 → 合规 → 监察 三步, 两轨并列, 保留重交历史) -->
|
||||
<div :class="['timeline-item', auditNodeStatus()]">
|
||||
<div class="timeline-title">审核时间轴</div>
|
||||
<div class="audit-sub-timeline">
|
||||
<div :class="['sub-timeline-item', nodeStatus('submit')]">
|
||||
<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>{{ fmtDateTime(c.submit.auditTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="timeline-desc pending-text">待执行人员提交</div>
|
||||
</div>
|
||||
<div :class="['timeline-item', partStatus(round.compliance)]">
|
||||
<div class="timeline-title">合规审核</div>
|
||||
<div v-if="round.compliance" class="timeline-meta">
|
||||
<span>{{ round.compliance.auditor }}</span>
|
||||
<span class="dot">·</span>
|
||||
<span>{{ fmtDateTime(round.compliance.auditTime) }}</span>
|
||||
<el-tag size="small" :type="round.compliance.auditResult === 'REJECTED' ? 'danger' : 'success'">
|
||||
{{ round.compliance.auditResult === 'REJECTED' ? '拒绝' : '通过' }}
|
||||
</el-tag>
|
||||
<div :class="['sub-timeline-item', nodeStatus('compliance')]">
|
||||
<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">
|
||||
<span>{{ c.compliance.auditor }}</span>
|
||||
<span class="dot">·</span>
|
||||
<span>{{ fmtDateTime(c.compliance.auditTime) }}</span>
|
||||
<el-tag size="small" :type="c.compliance.auditResult === 'REJECTED' ? 'danger' : 'success'">{{ c.compliance.auditResult === 'REJECTED' ? '拒绝' : '通过' }}</el-tag>
|
||||
</template>
|
||||
<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 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 :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 v-else class="timeline-desc pending-text">待监察审核</div>
|
||||
<div v-if="round.supervision?.opinion" class="timeline-opinion">💬 {{ round.supervision.opinion }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 固定节点 5/6 -->
|
||||
</div>
|
||||
<!-- 节点 3/4: 结算 / 完结 -->
|
||||
<div :class="['timeline-item', fixedNodeStatus('POST')]">
|
||||
<div class="timeline-title">会议结算</div>
|
||||
<div class="timeline-desc">{{ nodeDesc('POST') }}</div>
|
||||
@@ -355,17 +395,22 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 审核 dialog (合规/监察通用, 材料审核) -->
|
||||
<!-- 审核 dialog (合规/监察通用, 材料审核, 分轨独立通过/拒绝) -->
|
||||
<el-dialog v-model="auditDialog.show" :title="auditDialog.title" width="500px">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="意见">
|
||||
<el-input v-model="auditDialog.opinion" type="textarea" :rows="3" placeholder="请输入审核意见" />
|
||||
<el-form-item label="审核项">
|
||||
<div style="display:flex;flex-direction:column;gap:8px;width:100%">
|
||||
<div v-for="t in auditDialog.tracks" :key="t.type" style="display:flex;align-items:center;gap:12px">
|
||||
<span style="width:72px">{{ t.label }}</span>
|
||||
<el-radio-group v-model="t.approved">
|
||||
<el-radio :label="true">通过</el-radio>
|
||||
<el-radio :label="false">拒绝</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="结果">
|
||||
<el-radio-group v-model="auditDialog.approved">
|
||||
<el-radio :label="true">通过</el-radio>
|
||||
<el-radio :label="false">拒绝</el-radio>
|
||||
</el-radio-group>
|
||||
<el-form-item label="意见">
|
||||
<el-input v-model="auditDialog.opinion" type="textarea" :rows="3" placeholder="请输入审核意见 (两轨共用)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -650,58 +695,78 @@ const laborMaterialRows = ref(makeRows(r => r.type === 'LABOR'))
|
||||
const laborVoucherRows = ref(makeRows(r => r.type === 'LABOR_VOUCHER'))
|
||||
const serviceVoucherRows = ref(makeRows(r => r.type === 'SERVICE_VOUCHER'))
|
||||
|
||||
// ===================== 双时间轴: 解析 audit_trail 为轮次 =====================
|
||||
// ===================== 时间轴: 解析 audit_trail 为「轨道 × 轮次」 =====================
|
||||
/**
|
||||
* 解析 audit_trail 为轮次结构 (纯函数, 不依赖外部状态)
|
||||
* 规则: 一次 SUBMITTED + APPROVED = 一轮起点 (执行人员提交)
|
||||
* 之后 1-2 条填入 compliance / supervision 槽
|
||||
* 拒绝事件 (REJECTED) 不会被识别为新提交, 仍归入当前轮
|
||||
* 解析指定材料轨 (materialType) 的审核日志为提交轮次数组 (保留重交历史).
|
||||
* 每次 SUBMITTED 起一轮, 后续 APPROVED/REJECTED 顺序填入 compliance → supervision 槽.
|
||||
* 方案 B: 同轨被驳回后重交, 产生第 2/3 轮, 时间轴逐轮渲染「第 N 次」.
|
||||
*/
|
||||
function parseRounds(auditType) {
|
||||
function parseTrackCycles(materialType) {
|
||||
const rows = (auditTrail.value || [])
|
||||
.filter(r => r.auditType === auditType)
|
||||
.filter(r => r.auditType === 'MATERIAL' && r.materialType === materialType)
|
||||
.slice()
|
||||
.sort((a, b) => new Date(a.auditTime).getTime() - new Date(b.auditTime).getTime())
|
||||
const rounds = []
|
||||
let cur = null
|
||||
const cycles = []
|
||||
for (const row of rows) {
|
||||
const isSubmit = row.auditResult === 'SUBMITTED'
|
||||
if (isSubmit) {
|
||||
if (cur) rounds.push(cur)
|
||||
cur = { submit: row, compliance: null, supervision: null }
|
||||
} else if (cur) {
|
||||
if (!cur.compliance) cur.compliance = row
|
||||
else if (!cur.supervision) cur.supervision = row
|
||||
if (row.auditResult === 'SUBMITTED') {
|
||||
cycles.push({ submit: row, compliance: null, supervision: null })
|
||||
continue
|
||||
}
|
||||
const cur = cycles[cycles.length - 1]
|
||||
if (!cur) continue
|
||||
if (!cur.compliance) cur.compliance = row
|
||||
else if (!cur.supervision) cur.supervision = row
|
||||
}
|
||||
if (cur) rounds.push(cur)
|
||||
return rounds
|
||||
return cycles
|
||||
}
|
||||
|
||||
const materialRounds = computed(() => parseRounds('MATERIAL'))
|
||||
|
||||
/** 至少保证 1 轮 (空轮 = 三个节点都待提交), 保证 2/3/4 始终渲染 */
|
||||
const displayMaterialRounds = computed(() =>
|
||||
materialRounds.value.length ? materialRounds.value : [{ submit: null, compliance: null, supervision: null }]
|
||||
)
|
||||
|
||||
/**
|
||||
* 节点状态视觉:
|
||||
* - done → 绿色 (动作完成 + 通过)
|
||||
* - rejected → 红色 (动作完成但拒绝, 通常触发下一轮)
|
||||
* - pending → 灰色 (还没轮到)
|
||||
* 轨道列表: 新数据 (两轨独立) 拆 劳务/会务 两轨; 历史数据 material_type=null
|
||||
* 回退单轨 (无 label), 迁移后两轨同值, 时间轴不再按顺序错配.
|
||||
*/
|
||||
function partStatus(part) {
|
||||
if (!part) return 'pending'
|
||||
if (part.auditResult === 'REJECTED') return 'rejected'
|
||||
return 'done'
|
||||
const materialTracks = computed(() => {
|
||||
const labor = parseTrackCycles('LABOR')
|
||||
const service = parseTrackCycles('SERVICE')
|
||||
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)
|
||||
* slot: PRE = 节点1 (会议已执行) = NOT_STARTED/FROZEN 之外都算已执行
|
||||
* POST = 节点5 (结算) = AWAITING_SETTLEMENT / SETTLED / FINISHED (已结算)
|
||||
* DONE = 节点6 (完结) = FINISHED (is_finished=1)
|
||||
* 固定节点 (会议已执行/结算/完结) 状态 — 跟随 current_stage (BizMeetingStageEnum 10 值 code)
|
||||
* slot: PRE = 会议已执行 = NOT_STARTED/FROZEN 之外都算已执行
|
||||
* POST = 结算 = AWAITING_SETTLEMENT / SETTLED / FINISHED (已结算)
|
||||
* DONE = 完结 = FINISHED (is_finished=1)
|
||||
*/
|
||||
function fixedNodeStatus(slot) {
|
||||
const s = derivePhysicalStage(row.value)
|
||||
@@ -718,6 +783,12 @@ function nodeDesc(slot) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
/** 审核时间轴 组节点状态: 两轨均审核通过 (进入待结算) 后 done, 否则 pending (内嵌子步骤各自带色). */
|
||||
function auditNodeStatus() {
|
||||
const s = derivePhysicalStage(row.value)
|
||||
return ['AWAITING_SETTLEMENT', 'SETTLED', 'FINISHED'].includes(s) ? 'done' : 'pending'
|
||||
}
|
||||
|
||||
// ===================== 按钮显隐 =====================
|
||||
// 执行方判定: 用 role (executor) 而非 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 executed = computed(() => isOne(row.value.isExecuted))
|
||||
const frozen = computed(() => isOne(row.value.isFrozen))
|
||||
// 材料可提交: 已执行 + 未冻结 + material ∈ {NOT_SUBMITTED, REJECTED}
|
||||
const canSubmitMaterial = computed(() => isExecutor.value && executed.value && !frozen.value
|
||||
&& ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.materialAuditStage))
|
||||
// 材料可编辑 (保存修改): 未提交 / 被驳回 时; 提交审核后 (SUBMITTED) / 通过后 (APPROVED) 锁定, 隐藏保存按钮
|
||||
const materialsEditable = computed(() => ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.materialAuditStage))
|
||||
function canComplianceAudit() {
|
||||
if (!isManager.value) return false
|
||||
return row.value.materialAuditStage === 'SUBMITTED' && !isOne(row.value.materialComplianceApproved)
|
||||
}
|
||||
function canSupervisionAudit() {
|
||||
if (!isAssignedSupervisor.value && !isSponsorMain.value) return false
|
||||
return row.value.materialAuditStage === 'SUBMITTED' && isOne(row.value.materialComplianceApproved)
|
||||
}
|
||||
// 结算/完结 (合规/管理员 手动点击, 后端强校验 role)
|
||||
/** 单轨是否处于「可提交/可编辑」态 (未提交 或 被驳回) */
|
||||
const laborEditable = computed(() => ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.laborAuditStage))
|
||||
const serviceEditable = computed(() => ['NOT_SUBMITTED', 'REJECTED'].includes(row.value.serviceAuditStage))
|
||||
/** 单轨状态判定: C0=已提交待合规审 / C1=已提交待支持方审 */
|
||||
function isC0(stage, compliance) { return stage === 'SUBMITTED' && !isOne(compliance) }
|
||||
function isC1(stage, compliance) { return stage === 'SUBMITTED' && isOne(compliance) }
|
||||
// 材料可提交: 已执行 + 未冻结 + 对应轨可编辑 (三按钮按轨显隐)
|
||||
const executorSubmitReady = computed(() => isExecutor.value && executed.value && !frozen.value)
|
||||
const canSubmitLabor = computed(() => executorSubmitReady.value && laborEditable.value)
|
||||
const canSubmitService = computed(() => executorSubmitReady.value && serviceEditable.value)
|
||||
const canSubmitAll = computed(() => executorSubmitReady.value && laborEditable.value && serviceEditable.value)
|
||||
// 材料可编辑 (保存修改): 任一轨未提交/被驳回 时可保存; 两轨都进入审核后锁定
|
||||
const materialsEditable = computed(() => laborEditable.value || serviceEditable.value)
|
||||
// 合规审核 (合规方/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)
|
||||
&& row.value.materialAuditStage === 'APPROVED'
|
||||
&& row.value.laborAuditStage === 'APPROVED'
|
||||
&& row.value.serviceAuditStage === 'APPROVED'
|
||||
&& !isOne(row.value.isSettled))
|
||||
const canFinish = computed(() => (isManager.value || isAdmin.value)
|
||||
&& isOne(row.value.isSettled) && !isOne(row.value.isFinished))
|
||||
@@ -1397,7 +1476,7 @@ async function loadProjectRoles() {
|
||||
/**
|
||||
* 把当前 2 个 tab 已上传的文件 (r.url) 全量保存到 biz_meeting_material (后端 DELETE + INSERT),
|
||||
* 并触发 OCR. 返回 { saved, ocrCount }; 出错抛出, 由调用方决定 toast.
|
||||
* onSave (保存按钮) 与 onSubmitMaterial (提交前自动落库) 共用.
|
||||
* onSave (保存按钮) 与 onSubmitMaterials (提交前自动落库) 共用.
|
||||
*/
|
||||
async function saveMaterials() {
|
||||
const all = [...serviceMaterialRows.value, ...laborMaterialRows.value]
|
||||
@@ -1599,18 +1678,17 @@ function submitOcrForMaterials(items) {
|
||||
})
|
||||
}
|
||||
|
||||
// ===================== 提交材料 =====================
|
||||
// 提交前先 saveMaterials() 落库: 后端 submit-material 校验的是 biz_meeting_material 表,
|
||||
// 只上传不保存时表为空, 会误报「未上传」.
|
||||
async function onSubmitMaterial() {
|
||||
// ===================== 提交材料 (三按钮: 提交劳务/会务/全部) =====================
|
||||
/** 提交前先 saveMaterials() 落库: 后端 submit-material 校验的是 biz_meeting_material 表 */
|
||||
async function onSubmitMaterials(types) {
|
||||
if (!types || !types.length) { ElMessage.warning('无可提交的材料轨'); return }
|
||||
busy.value.submitMaterial = true
|
||||
try {
|
||||
await saveMaterials()
|
||||
// __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('材料已提交, 待合规审核')
|
||||
row.value.materialAuditStage = (resp && resp.data) || 'SUBMITTED'
|
||||
await loadTrail()
|
||||
await refreshMeeting()
|
||||
} catch (e) { ElMessage.error(e?.msg || e?.message || '提交失败') }
|
||||
finally { busy.value.submitMaterial = false }
|
||||
}
|
||||
@@ -1654,30 +1732,43 @@ async function confirmAssign() {
|
||||
finally { assignDialog.value.saving = false }
|
||||
}
|
||||
|
||||
// ===================== 审核 dialog (材料审核) =====================
|
||||
// ===================== 审核 dialog (材料审核, 分轨独立通过/拒绝) =====================
|
||||
const auditDialog = ref({
|
||||
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 = {
|
||||
show: true,
|
||||
action,
|
||||
title: action === 'COMPLIANCE' ? '合规审核 (材料)' : '监察审核 (材料)',
|
||||
title: isCompliance ? `合规审核 (材料 · ${scopeLabel})` : `监察审核 (材料 · ${scopeLabel})`,
|
||||
opinion: '',
|
||||
approved: true,
|
||||
tracks,
|
||||
saving: false
|
||||
}
|
||||
}
|
||||
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'
|
||||
? `/business/meeting/${meetingId.value}/audit-compliance`
|
||||
: `/business/meeting/${meetingId.value}/audit-supervision`
|
||||
auditDialog.value.saving = true
|
||||
try {
|
||||
await request.post(url, { approved, opinion }, { __silentError: true })
|
||||
ElMessage.success(approved ? '审核通过' : '已拒绝')
|
||||
await request.post(url, { items, opinion }, { __silentError: true })
|
||||
ElMessage.success('审核完成')
|
||||
auditDialog.value.show = false
|
||||
await refreshMeeting()
|
||||
} catch (e) { ElMessage.error(e?.msg || e?.message || '审核失败') }
|
||||
@@ -1865,8 +1956,21 @@ onBeforeUnmount(stopFeePolling)
|
||||
.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 .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-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-column { margin-bottom: 0; padding: 16px 18px; min-width: 0; }
|
||||
|
||||
@@ -65,13 +65,14 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="currentStage" label="当前阶段" width="140" align="center">
|
||||
<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>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="500" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<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="onEdit(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="onDownloadLabor(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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -100,26 +101,6 @@
|
||||
<!-- 修改 / 复制: 按角色跳 admin-meetings-new / manager-meetings-new (MeetingNew.vue 公共页) -->
|
||||
<!-- 查看: 按角色跳 /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: 对选中的「待合规审核」会议批量通过/退回 -->
|
||||
<el-dialog v-model="batchAuditOpen" title="批量合规审核" width="560px">
|
||||
<el-form label-width="100px">
|
||||
@@ -149,7 +130,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { bizList, bizDelete } from '@/api/public'
|
||||
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'
|
||||
|
||||
// ========== 角色感知 (admin/manager 共用此页, 按 URL 第 1 段识别) ==========
|
||||
@@ -304,8 +285,10 @@ function isOneVal(v) { return v === 1 || v === '1' || v === true }
|
||||
// 已提交(SUBMITTED/APPROVED): 曾被冻结过 → 超时提交, 否则 → 按时提交
|
||||
// 未提交/退回(NOT_SUBMITTED/REJECTED): 已冻结 → 红色 0小时; 否则 submitDeadline - now 倒计时
|
||||
function submitRemain(row) {
|
||||
const stage = row.materialAuditStage
|
||||
if (stage === 'SUBMITTED' || stage === 'APPROVED') {
|
||||
// 两轨都进入审核 (SUBMITTED/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 }
|
||||
}
|
||||
if (isOneVal(row.isFrozen)) return { text: '0小时', red: true }
|
||||
@@ -317,9 +300,9 @@ function submitRemain(row) {
|
||||
const hours = totalHours % 24
|
||||
return { text: `${days}天${hours}小时`, red: false }
|
||||
}
|
||||
// 列表结算按钮显隐: 与 MeetingDetail.canSettle 一致 (材料 APPROVED 且未结算)
|
||||
// 列表结算按钮显隐: 与 MeetingDetail.canSettle 一致 (两轨均 APPROVED 且未结算)
|
||||
function canSettleRow(row) {
|
||||
return row.materialAuditStage === 'APPROVED' && !isOneVal(row.isSettled)
|
||||
return row.laborAuditStage === 'APPROVED' && row.serviceAuditStage === 'APPROVED' && !isOneVal(row.isSettled)
|
||||
}
|
||||
function onSettle(row) {
|
||||
// 结算需要上传付款凭证 (详情页结算 dialog 二合一), 跳详情并自动打开结算 dialog
|
||||
@@ -352,38 +335,15 @@ async function onUnfreeze(row) {
|
||||
} catch (e) { ElMessage.error(e?.msg || e?.message || '解冻失败') }
|
||||
}
|
||||
|
||||
// ========== 支持方(监察员) 审批 ==========
|
||||
const approvalOpen = ref(false)
|
||||
const approvalSaving = ref(false)
|
||||
const approvalRow = ref({})
|
||||
const approvalForm = reactive({ approved: true, opinion: '' })
|
||||
|
||||
function onApproval(row) {
|
||||
approvalRow.value = row
|
||||
approvalForm.approved = true
|
||||
approvalForm.opinion = ''
|
||||
approvalOpen.value = true
|
||||
// ========== 支持方(监察员) 审核: 跳详情页 (详情页内按 C1 轨给 审核劳务/会务/全部) ==========
|
||||
// 有任一轨处于 C1 (已提交待支持方审) 即显示「审核」; 会务被退回时物理阶段=待整改, 但劳务 C1 仍需审, 不能用物理阶段判
|
||||
function isSponsorPending(row) {
|
||||
return (row.laborAuditStage === 'SUBMITTED' && isOneVal(row.laborComplianceApproved))
|
||||
|| (row.serviceAuditStage === 'SUBMITTED' && isOneVal(row.serviceComplianceApproved))
|
||||
}
|
||||
|
||||
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) 批量审核 ==========
|
||||
@@ -392,9 +352,11 @@ const batchAuditSaving = ref(false)
|
||||
const batchAuditForm = reactive({ approved: true, opinion: '' })
|
||||
const batchEligibleIds = ref([])
|
||||
|
||||
// 待合规审核判据: material_audit_stage=SUBMITTED 且 compliance_approved≠1 (与详情页 canComplianceAudit 一致)
|
||||
// 待合规审核判据: 任一轨 SUBMITTED 且 compliance_approved≠1 (C0, 与详情页 canComplianceAudit 一致)
|
||||
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() {
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<!-- 右: 操作按钮 - 悬浮固定到页面最右 (按选中 tab 动态显示) -->
|
||||
<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">
|
||||
<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>
|
||||
@@ -217,6 +217,12 @@ const canShowExecutionBtn = computed(() => {
|
||||
if (!r) return true
|
||||
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 qrUrl = ref('')
|
||||
@@ -380,6 +386,7 @@ async function checkSigned() {
|
||||
}
|
||||
const signing = ref(false)
|
||||
async function onSignup() {
|
||||
if (!canShowSignupBtn.value) return // 防御: 防止 v-if 被绕过
|
||||
if (!loggedIn.value) {
|
||||
ElMessage.warning('请先登录系统')
|
||||
router.push('/login')
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12">
|
||||
<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">
|
||||
<el-tooltip content="主账号公司, 注册时已自动关联" placement="top">
|
||||
<el-icon><Lock /></el-icon>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
筛选栏: <el-form inline :model="q" class="filter-form">
|
||||
工具栏: <div class="toolbar">
|
||||
列表列 (按原型 sponsor-people.html left 排序):
|
||||
复选框 / 姓名 / 手机号 / 工作单位 / 部门 / 职务 / 角色(角色+账号类型已合并) / 状态 / 操作
|
||||
姓名 / 手机号 / 工作单位 / 部门 / 职务 / 角色(角色+账号类型已合并) / 状态 / 操作
|
||||
-->
|
||||
<div class="page-card">
|
||||
<div class="breadcrumb">首页 / 人员管理</div>
|
||||
@@ -39,11 +39,9 @@
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="onCreate">新建人员</el-button>
|
||||
<el-button @click="onImport">批量导入</el-button>
|
||||
<el-button :disabled="!selected.length || hasSelfInSelection" @click="onBatchRemove">批量删除</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelectionChange">
|
||||
<el-table-column type="selection" width="48" :selectable="(row) => row.userId !== store.user?.userId" />
|
||||
<el-table :data="rows" v-loading="loading" stripe border>
|
||||
<el-table-column prop="account" label="账号" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="name" label="姓名" width="100" />
|
||||
<el-table-column prop="phone" label="手机号" width="130" />
|
||||
@@ -136,7 +134,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, computed } from 'vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { bizUpdate, bizDelete, resetPersonPassword } from '@/api/public'
|
||||
@@ -148,9 +146,6 @@ import request from '@/utils/request'
|
||||
const router = useRouter()
|
||||
const store = useUserStore()
|
||||
|
||||
// 批量操作禁用判断: 选了主账号自己 (跟"操作列禁用/删除隐藏"是同一条规则, 防止误删)
|
||||
const hasSelfInSelection = computed(() => selected.value.some(r => r.userId === store.user?.userId))
|
||||
|
||||
// sponsor 端只看自己团队 (主账号+子账号) 创建的人, 走专属接口 /business/person/sponsorList (后端强制隔离)
|
||||
const q = reactive({
|
||||
name: '', phone: '', orgName: '', department: '', status: ''
|
||||
@@ -158,7 +153,6 @@ const q = reactive({
|
||||
const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
|
||||
const rows = ref([])
|
||||
const loading = ref(false)
|
||||
const selected = ref([])
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
@@ -183,8 +177,6 @@ function reset() {
|
||||
load()
|
||||
}
|
||||
|
||||
function onSelectionChange(arr) { selected.value = arr }
|
||||
|
||||
function onCreate() {
|
||||
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)
|
||||
const importVisible = ref(false)
|
||||
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: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--danger.is-link) { color: var(--el-color-danger); }
|
||||
:deep(.el-table .el-button--danger.is-link:hover) { color: var(--el-color-danger); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user