批量推送代码
This commit is contained in:
@@ -299,75 +299,70 @@
|
||||
<div class="timeline-title">会议已执行</div>
|
||||
<div class="timeline-desc">{{ nodeDesc('PRE') }}</div>
|
||||
</div>
|
||||
<!-- 节点 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>
|
||||
<!-- 材料审核三步 (提交 → 合规 → 监察) 并入主时间轴, 两轨并列, 保留重交历史 -->
|
||||
<div :class="['timeline-item', nodeStatus('submit')]">
|
||||
<div class="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 :class="['timeline-item', nodeStatus('compliance')]">
|
||||
<div class="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.submit.auditTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<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 :class="['timeline-item', nodeStatus('supervision')]">
|
||||
<div class="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 :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 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>
|
||||
<div v-if="c.supervision?.opinion" class="timeline-opinion">💬 {{ c.supervision.opinion }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 节点 3/4: 结算 / 完结 -->
|
||||
@@ -421,14 +416,18 @@
|
||||
|
||||
<!-- 参会人 dialog (新增/编辑通用, 2 列布局) -->
|
||||
<el-dialog v-model="attendeeDialog.show" :title="attendeeDialog.title" width="760px" append-to-body :close-on-click-modal="false" @closed="resetAttendeeForm">
|
||||
<el-form :model="attendeeDialog.form" label-width="100px" class="attendee-form-2col">
|
||||
<el-form ref="attendeeFormRef" :model="attendeeDialog.form" :rules="attendeeRules" label-width="100px" class="attendee-form-2col">
|
||||
<!-- Col 1: 基础档案 (8 字段) -->
|
||||
<el-form-item label="联系方式" required>
|
||||
<el-input v-model="attendeeDialog.form.phone" :disabled="attendeeDialog.editing" placeholder="11位手机号" maxlength="11">
|
||||
<template v-if="!attendeeDialog.editing" #suffix>
|
||||
<el-icon class="phone-lookup" title="按手机号查专家并回填" @click="lookupExpertByPhone"><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="attendeeDialog.form.name" placeholder="请输入姓名(未注册手机号将以此作为昵称建号)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系方式" required>
|
||||
<el-input v-model="attendeeDialog.form.phone" :disabled="attendeeDialog.editing" placeholder="11位手机号" maxlength="11" />
|
||||
</el-form-item>
|
||||
<el-form-item label="医院">
|
||||
<el-input v-model="attendeeDialog.form.workUnit" placeholder="请输入医院名称" />
|
||||
</el-form-item>
|
||||
@@ -475,7 +474,7 @@
|
||||
<el-form-item label="增值税附加">
|
||||
<el-input-number v-model="attendeeDialog.form.vatAndSurcharge" :precision="2" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="实发金额">
|
||||
<el-form-item label="实发金额" prop="fee">
|
||||
<el-input-number v-model="attendeeDialog.form.fee" :precision="2" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="摘要">
|
||||
@@ -570,7 +569,7 @@ import DoctorTitleSelect from '@/components/DoctorTitleSelect.vue'
|
||||
import DoctorDeptSelect from '@/components/DoctorDeptSelect.vue'
|
||||
import ProjectRoleMultiSelect from '@/components/ProjectRoleMultiSelect.vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { Upload, ArrowDown } from '@element-plus/icons-vue'
|
||||
import { Upload, ArrowDown, Search } from '@element-plus/icons-vue'
|
||||
import { derivePhysicalStage } from '@/utils/meetingStage'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -783,12 +782,6 @@ 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 里, 旧判定会让执行单位在"执行中"看不到提交按钮.
|
||||
@@ -937,6 +930,50 @@ function resetAttendeeForm() {
|
||||
show: false, title: '', editing: false,
|
||||
form: emptyAttendeeForm(), saving: false
|
||||
}
|
||||
// 清掉上次的校验错误 (防止关掉后重开还残留"实发金额不能超过..."红字)
|
||||
attendeeFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
/** 参会人表单 ref (el-form validate 用) */
|
||||
const attendeeFormRef = ref()
|
||||
|
||||
/** 计算所选角色(可多个, 逗号分隔)的项目劳务金额合计; 返回 { sum, matchedAny } */
|
||||
function roleAmountSum(laborForm) {
|
||||
const result = { sum: 0, matchedAny: false }
|
||||
if (!laborForm) return result
|
||||
const items = String(laborForm).split(',').map(s => s.trim()).filter(Boolean)
|
||||
items.forEach(item => {
|
||||
const matched = (projectRoles.value || []).find(r => {
|
||||
if (!r) return false
|
||||
const label = r.role === '其他' ? (r.customName || '').trim() : r.role
|
||||
return label === item
|
||||
})
|
||||
if (matched && matched.amount != null) {
|
||||
result.sum += Number(matched.amount) || 0
|
||||
result.matchedAny = true
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/** 实发金额校验: 不超所选角色的劳务金额合计 (未选角色时跳过) */
|
||||
function validateFee(rule, value, callback) {
|
||||
const laborForm = attendeeDialog.value.form.laborForm
|
||||
if (!laborForm || !String(laborForm).trim()) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const { sum } = roleAmountSum(laborForm)
|
||||
const fee = Number(value) || 0
|
||||
if (fee > sum) {
|
||||
callback(new Error(`实发金额不能超过角色金额 ${sum.toFixed(2)} 元`))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const attendeeRules = {
|
||||
fee: [{ validator: validateFee, trigger: ['blur', 'change'] }]
|
||||
}
|
||||
|
||||
// ===================== 金额联动 (照搬 hwt guest.vue 单向链) =====================
|
||||
@@ -994,20 +1031,7 @@ watch(
|
||||
if (!val) return
|
||||
// 多选: 逗号分隔, 按每个角色名匹配项目劳务金额求和, 填进 fee (实发).
|
||||
// "其他"自定义角色不在项目列表里 → 不计入; 全"其他"时 matchedAny=false → 不动 fee.
|
||||
const items = String(val).split(',').map(s => s.trim()).filter(Boolean)
|
||||
let sum = 0
|
||||
let matchedAny = false
|
||||
items.forEach(item => {
|
||||
const matched = (projectRoles.value || []).find(r => {
|
||||
if (!r) return false
|
||||
const label = r.role === '其他' ? (r.customName || '').trim() : r.role
|
||||
return label === item
|
||||
})
|
||||
if (matched && matched.amount != null) {
|
||||
sum += Number(matched.amount) || 0
|
||||
matchedAny = true
|
||||
}
|
||||
})
|
||||
const { sum, matchedAny } = roleAmountSum(val)
|
||||
if (matchedAny) {
|
||||
attendeeDialog.value.form.fee = Number(sum.toFixed(2))
|
||||
}
|
||||
@@ -1165,6 +1189,44 @@ function openAttendeeDialog(row) {
|
||||
nextTick(() => { suppressFeeLink = false })
|
||||
}
|
||||
|
||||
/** 手机号放大镜: 按手机号查专家, 命中则回填参会人表单 (身份证/银行/科室/职称等) */
|
||||
async function lookupExpertByPhone() {
|
||||
const phone = (attendeeDialog.value.form.phone || '').trim()
|
||||
if (!phone) {
|
||||
ElMessage.warning('请先输入手机号')
|
||||
return
|
||||
}
|
||||
if (!phone.match(/^1\d{10}$/)) {
|
||||
ElMessage.warning('手机号格式不正确')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const resp = await request.get(`/business/expert/byPhone/${phone}`, { __silentError: true })
|
||||
const expert = (resp && resp.data) || null
|
||||
if (!expert) {
|
||||
ElMessage.info('未找到该手机号对应的专家')
|
||||
return
|
||||
}
|
||||
const f = attendeeDialog.value.form
|
||||
f.name = expert.name || f.name
|
||||
f.workUnit = expert.workUnit || f.workUnit
|
||||
f.department = expert.department || f.department
|
||||
f.title = expert.title || f.title
|
||||
f.idCard = expert.idCard || f.idCard
|
||||
f.bankName = expert.bankName || f.bankName
|
||||
f.bankCard = expert.bankCard || f.bankCard
|
||||
f.bankRegion = expert.bankRegion || f.bankRegion
|
||||
f.bankAddress = expert.bankAddress || f.bankAddress
|
||||
f.idCardAttachments = expert.idCardAttachments || f.idCardAttachments
|
||||
// 账户名称 = 持卡人姓名, 默认取专家姓名
|
||||
f.accountName = expert.name || f.accountName
|
||||
ElMessage.success('已回填专家信息')
|
||||
} catch (e) {
|
||||
console.error('[meeting-detail] lookupExpertByPhone failed', e)
|
||||
ElMessage.error(e?.msg || e?.message || '查询失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交 dialog (新增 → POST; 编辑 → PUT) */
|
||||
async function confirmAttendee() {
|
||||
const { editing, form } = attendeeDialog.value
|
||||
@@ -1176,6 +1238,12 @@ async function confirmAttendee() {
|
||||
ElMessage.warning('手机号格式不正确')
|
||||
return
|
||||
}
|
||||
// 校验实发金额不超所选角色劳务金额合计 (未选角色时跳过)
|
||||
try {
|
||||
await attendeeFormRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
attendeeDialog.value.saving = true
|
||||
try {
|
||||
if (editing) {
|
||||
@@ -1962,15 +2030,6 @@ onBeforeUnmount(stopFeePolling)
|
||||
.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; }
|
||||
@@ -1987,6 +2046,9 @@ onBeforeUnmount(stopFeePolling)
|
||||
.attendee-form-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 16px; }
|
||||
.attendee-form-2col :deep(.el-form-item) { margin-bottom: 12px; }
|
||||
.attendee-form-2col :deep(.el-form-item__content) { min-width: 0; }
|
||||
/* 手机号放大镜 suffix: 可点 + hover 高亮 */
|
||||
.phone-lookup { cursor: pointer; color: var(--el-text-color-secondary); transition: color 0.2s; }
|
||||
.phone-lookup:hover { color: var(--brand-primary, #409eff); }
|
||||
/* 表格外层横向滚动容器: 内容总宽 ~1680px 超出 left-col 宽度时, 容器内出现横向滚动条, 不撑爆外层 grid */
|
||||
.attendee-table-wrap { overflow-x: auto; max-width: 100%; min-width: 0; }
|
||||
.row-actions { display: inline-flex; align-items: center; gap: 4px; }
|
||||
@@ -2161,8 +2223,6 @@ onBeforeUnmount(stopFeePolling)
|
||||
/* 时间轴缩窄 */
|
||||
.timeline { padding-left: 18px !important; }
|
||||
.timeline-item::before { left: -23px !important; }
|
||||
.audit-sub-timeline { padding-left: 12px !important; }
|
||||
.sub-timeline-item::before { left: -18px !important; }
|
||||
|
||||
/* 顶部"返回/操作"按钮组横滚 */
|
||||
.page-title { flex-wrap: wrap !important; gap: 8px !important; }
|
||||
|
||||
Reference in New Issue
Block a user