提交所有代码
This commit is contained in:
@@ -79,7 +79,7 @@
|
||||
让内容超出时容器内出现横向滚动条, 不影响外层 grid 布局.
|
||||
-->
|
||||
<div class="attendee-table-wrap">
|
||||
<el-table :data="attendeeRows" v-loading="attendeeLoading" border size="small" style="width: 100%;" show-summary :summary-method="attendeeSummary" @selection-change="onAttendeeSelectionChange" class="attendee-table" :row-class-name="attendeeRowClass">
|
||||
<el-table :data="attendeeRows" v-loading="attendeeLoading" border size="small" style="width: 100%;" @selection-change="onAttendeeSelectionChange" class="attendee-table" :row-class-name="attendeeRowClass">
|
||||
<el-table-column v-if="!isSponsor && !isReadonly" type="selection" width="42" />
|
||||
<el-table-column type="index" label="序号" width="50" align="center" />
|
||||
<el-table-column label="医生" min-width="90">
|
||||
@@ -87,29 +87,17 @@
|
||||
<span :style="(row.hasIntent === 1 || row.hasIntent === '1') ? {} : { color: '#f56c6c' }">{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="联系电话" width="120">
|
||||
<template #default="{ row }">{{ isSponsor ? maskPhone(row.phone) : row.phone }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="phone" label="联系电话" width="120" />
|
||||
<el-table-column prop="workUnit" label="医院" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="department" label="科室" min-width="90" show-overflow-tooltip />
|
||||
<el-table-column prop="title" label="职称" min-width="90" show-overflow-tooltip />
|
||||
<el-table-column prop="bankName" label="银行" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="银行卡号码" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ isSponsor ? maskBankCard(row.bankCard) : row.bankCard }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="bankCard" label="银行卡号码" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="bankBranch" label="开户行" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="accountName" label="账户名称" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="bankRegion" label="开户行地址" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="bankAddress" label="银行详细地址" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="身份证号" min-width="170" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ isSponsor ? maskIdCard(row.idCard) : row.idCard }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!isSponsor" label="身份证附件" min-width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link :underline="false" v-if="idCardFiles(row).length" type="primary" @click="openPreview(idCardFiles(row), '身份证')">查看</el-link>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="idCard" label="身份证号" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column label="应发金额" width="110" align="right">
|
||||
<template #default="{ row }">{{ row.feePreTax != null ? '¥ ' + Number(row.feePreTax).toFixed(2) : '-' }}</template>
|
||||
</el-table-column>
|
||||
@@ -124,6 +112,12 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="summary" label="摘要" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="laborForm" label="角色" min-width="90" show-overflow-tooltip />
|
||||
<el-table-column v-if="!isSponsor" label="身份证附件" min-width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link :underline="false" v-if="idCardFiles(row).length" type="primary" @click="openPreview(idCardFiles(row), '身份证')">查看</el-link>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="劳务协议" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link :underline="false" v-if="protocolUrl(row)" type="primary" @click="openPreview([protocolUrl(row)], '劳务协议')">查看</el-link>
|
||||
@@ -136,12 +130,6 @@
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="签字" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.handsign" type="success" size="small">已签</el-tag>
|
||||
<el-tag v-else type="info" size="small">未签</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否邀请" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.isInvited === 1 || row.isInvited === '1'" type="success" size="small">已邀请</el-tag>
|
||||
@@ -178,6 +166,10 @@
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="labor-fee-summary">
|
||||
<span class="fee-label">劳务费合计:</span>
|
||||
<span class="fee-value">¥ {{ laborFeeTotal.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="file-list" style="margin-top: 16px;">
|
||||
@@ -230,9 +222,33 @@
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="会务材料" name="service">
|
||||
<div class="file-list">
|
||||
<!-- 发票表: 会议全部会务发票明细 (物料制作上方) -->
|
||||
<div v-if="invoiceRows.length" class="invoice-table-wrap">
|
||||
<div class="invoice-table-title">发票表</div>
|
||||
<el-table :data="invoiceRows" size="small" border class="invoice-table">
|
||||
<el-table-column label="材料类型" width="100">
|
||||
<template #default="{ row }">{{ serviceLabel(row.materialSubType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件名" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ invoiceFileName(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发票号" width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.invoiceNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开票时间" width="110">
|
||||
<template #default="{ row }">{{ row.invoiceDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.recognizeStatus === 'RECOGNIZED'" class="invoice-table-amount">¥ {{ Number(row.amount || 0).toFixed(2) }}</span>
|
||||
<span v-else class="invoice-table-pending">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<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 || !serviceEditable" />
|
||||
<OssFileUploader v-model="r.url" v-model:name="r.fileName" :dir="`ry8080/meeting/${meetingId}/service/`" accept=".pdf,.png,.jpg,.jpeg,.zip" :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>
|
||||
@@ -282,9 +298,9 @@
|
||||
<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="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>
|
||||
@@ -385,65 +401,67 @@
|
||||
|
||||
<!-- 参会人 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 ref="attendeeFormRef" :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-form-item label="联系方式" prop="phone">
|
||||
<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-form-item label="姓名" prop="name">
|
||||
<el-input v-model="attendeeDialog.form.name" placeholder="请输入姓名(未注册手机号将以此作为昵称建号)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="医院">
|
||||
<el-form-item label="医院" prop="workUnit">
|
||||
<el-input v-model="attendeeDialog.form.workUnit" placeholder="请输入医院名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="身份证号">
|
||||
<el-form-item label="身份证号" prop="idCard">
|
||||
<el-input v-model="attendeeDialog.form.idCard" placeholder="18位身份证号" maxlength="18" />
|
||||
</el-form-item>
|
||||
<el-form-item label="身份证附件" style="grid-column: 1 / -1">
|
||||
<el-form-item label="身份证附件" prop="idCardAttachments" style="grid-column: 1 / -1">
|
||||
<IdCardUploader v-model="attendeeDialog.form.idCardAttachments" :dir="`ry8080/meeting/${meetingId}/idcard/`" />
|
||||
</el-form-item>
|
||||
<el-form-item label="科室">
|
||||
<el-form-item label="科室" prop="department">
|
||||
<doctor-dept-select v-model="attendeeDialog.form.department" value-field="label" placeholder="输入关键词搜索" filterable style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="职称">
|
||||
<el-form-item label="职称" prop="title">
|
||||
<doctor-title-select v-model="attendeeDialog.form.title" value-field="label" placeholder="输入关键词搜索" filterable style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="银行">
|
||||
<el-form-item label="银行" prop="bankName">
|
||||
<el-input v-model="attendeeDialog.form.bankName" placeholder="如: 中国工商银行" />
|
||||
</el-form-item>
|
||||
<el-form-item label="银行卡号码">
|
||||
<el-form-item label="银行卡号码" prop="bankCard">
|
||||
<el-input v-model="attendeeDialog.form.bankCard" placeholder="持卡人银行卡号" maxlength="25" />
|
||||
</el-form-item>
|
||||
<!-- Col 2: 财务 + 备注 (8 字段) -->
|
||||
<el-form-item label="开户行">
|
||||
<el-form-item label="开户行" prop="bankBranch">
|
||||
<el-input v-model="attendeeDialog.form.bankBranch" placeholder="如: 北京东城支行" />
|
||||
</el-form-item>
|
||||
<el-form-item label="账户名称">
|
||||
<el-form-item label="账户名称" prop="accountName">
|
||||
<el-input v-model="attendeeDialog.form.accountName" placeholder="持卡人姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开户行地址">
|
||||
<el-form-item label="开户行地址" prop="bankRegion">
|
||||
<el-input v-model="attendeeDialog.form.bankRegion" placeholder="如: 北京市东城区" />
|
||||
</el-form-item>
|
||||
<el-form-item label="银行详细地址">
|
||||
<el-form-item label="银行详细地址" prop="bankAddress">
|
||||
<el-input v-model="attendeeDialog.form.bankAddress" placeholder="银行详细地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" style="grid-column: 1 / -1">
|
||||
<project-role-multi-select v-model="attendeeDialog.form.laborForm" :roles="projectRoles" />
|
||||
<el-form-item label="角色" prop="laborForm" style="grid-column: 1 / -1">
|
||||
<el-radio-group v-model="attendeeDialog.form.laborForm">
|
||||
<el-radio v-for="o in projectRoleOptions" :key="o.value" :value="o.value">{{ o.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="应发金额">
|
||||
<el-form-item label="应发金额" prop="feePreTax">
|
||||
<el-input-number v-model="attendeeDialog.form.feePreTax" :precision="2" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="个税税金">
|
||||
<el-form-item label="个税税金" prop="tax">
|
||||
<el-input-number v-model="attendeeDialog.form.tax" :precision="2" :min="0" controls-position="right" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="增值税附加">
|
||||
<el-form-item label="增值税附加" prop="vatAndSurcharge">
|
||||
<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">
|
||||
<div :class="{ 'fee-warn': feeDeviates }" style="width:100%">
|
||||
<el-input-number v-model="attendeeDialog.form.fee" :precision="2" :min="0" controls-position="right" style="width:100%" />
|
||||
</div>
|
||||
@@ -538,7 +556,6 @@ import ImportResultDialog from '@/components/ImportResultDialog.vue'
|
||||
import Preview from '@/components/Preview.vue'
|
||||
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, Search } from '@element-plus/icons-vue'
|
||||
import { derivePhysicalStage } from '@/utils/meetingStage'
|
||||
@@ -616,23 +633,6 @@ function fmtMoney(v) {
|
||||
return (isNaN(n) ? 0 : n).toFixed(2)
|
||||
}
|
||||
function isOne(v) { return v === 1 || v === '1' || v === true }
|
||||
// sponsor 脱敏展示 (对齐后端 desensitizeHtml 规则: 手机前3+后4 / 银行卡前4+后4 / 身份证前6+后4)
|
||||
function maskPhone(v) {
|
||||
const s = String(v ?? '')
|
||||
if (!s) return ''
|
||||
if (s.length === 11) return s.slice(0, 3) + '****' + s.slice(7)
|
||||
return s.replace(/^(\d{3})\d+(\d{4})$/, '$1****$2')
|
||||
}
|
||||
function maskBankCard(v) {
|
||||
const s = String(v ?? '')
|
||||
if (!s) return ''
|
||||
return s.length >= 8 ? s.slice(0, 4) + '****' + s.slice(-4) : s
|
||||
}
|
||||
function maskIdCard(v) {
|
||||
const s = String(v ?? '')
|
||||
if (!s) return ''
|
||||
return s.length === 18 ? s.slice(0, 6) + '********' + s.slice(14) : s
|
||||
}
|
||||
/** sponsor 看脱敏 PDF, 其他角色看完整 PDF */
|
||||
function protocolUrl(row) {
|
||||
if (isSponsor.value) return row.laborProtocolMasked || ''
|
||||
@@ -930,16 +930,6 @@ const meetingFee = computed(() => {
|
||||
return { total, useMain: false }
|
||||
})
|
||||
|
||||
/** 参会人表格底部合计行: "医生"列显示"合计", "应发金额"列显示劳务费合计 */
|
||||
function attendeeSummary({ columns }) {
|
||||
const sums = columns.map(() => '')
|
||||
const nameIdx = columns.findIndex(c => c.label === '医生')
|
||||
const feeIdx = columns.findIndex(c => c.label === '应发金额')
|
||||
if (nameIdx >= 0) sums[nameIdx] = '合计'
|
||||
if (feeIdx >= 0) sums[feeIdx] = '¥ ' + laborFeeTotal.value.toFixed(2)
|
||||
return sums
|
||||
}
|
||||
|
||||
/** 已勾选的参会人 (批量邀请/推送共用) */
|
||||
const selectedAttendees = ref([])
|
||||
/** 推送电子签中 (批量工具栏按钮 loading) */
|
||||
@@ -954,6 +944,13 @@ const inviteLoadingId = ref(null)
|
||||
/** 项目级角色 (来自 biz_project.role_labor JSON 解析), 用于参会人 dialog 角色下拉 */
|
||||
const projectRoles = ref([])
|
||||
|
||||
/** 参会人角色单选选项 (来自 projectRoles, 去掉"其他"逃生口) */
|
||||
const projectRoleOptions = computed(() => {
|
||||
return (projectRoles.value || [])
|
||||
.filter(r => r && r.role && r.role !== '其他')
|
||||
.map(r => ({ value: r.role, label: r.role }))
|
||||
})
|
||||
|
||||
/** 参会人 dialog 状态 (新增/编辑共用) */
|
||||
const attendeeDialog = ref({
|
||||
show: false,
|
||||
@@ -988,6 +985,28 @@ function emptyAttendeeForm() {
|
||||
onSitePhotos: ''
|
||||
}
|
||||
}
|
||||
/** 参会人表单必填规则 (除 摘要/现场照片 外全必填) */
|
||||
const attendeeRules = {
|
||||
phone: [{ required: true, message: '请填写联系方式', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请填写姓名', trigger: 'blur' }],
|
||||
workUnit: [{ required: true, message: '请填写医院', trigger: 'blur' }],
|
||||
idCard: [{ required: true, message: '请填写身份证号', trigger: 'blur' }],
|
||||
idCardAttachments: [{ required: true, message: '请上传身份证附件', trigger: 'change' }],
|
||||
department: [{ required: true, message: '请选择科室', trigger: 'change' }],
|
||||
title: [{ required: true, message: '请选择职称', trigger: 'change' }],
|
||||
bankName: [{ required: true, message: '请填写银行', trigger: 'blur' }],
|
||||
bankCard: [{ required: true, message: '请填写银行卡号码', trigger: 'blur' }],
|
||||
bankBranch: [{ required: true, message: '请填写开户行', trigger: 'blur' }],
|
||||
accountName: [{ required: true, message: '请填写账户名称', trigger: 'blur' }],
|
||||
bankRegion: [{ required: true, message: '请填写开户行地址', trigger: 'blur' }],
|
||||
bankAddress: [{ required: true, message: '请填写银行详细地址', trigger: 'blur' }],
|
||||
laborForm: [{ required: true, message: '请选择角色', trigger: 'change' }],
|
||||
feePreTax: [{ required: true, message: '请填写应发金额', trigger: 'change' }],
|
||||
tax: [{ required: true, message: '请填写个税税金', trigger: 'change' }],
|
||||
vatAndSurcharge: [{ required: true, message: '请填写增值税附加', trigger: 'change' }],
|
||||
fee: [{ required: true, message: '请填写实发金额', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function resetAttendeeForm() {
|
||||
attendeeDialog.value = {
|
||||
show: false, title: '', editing: false,
|
||||
@@ -1059,7 +1078,13 @@ watch(
|
||||
else if (f > 3360 && f <= 21000) tax = (0.16 * f) / 0.84
|
||||
else if (f > 21000 && f <= 49500) tax = (0.24 * f - 2000) / 0.76
|
||||
else if (f > 49500) tax = (0.32 * f - 7000) / 0.68
|
||||
attendeeDialog.value.form.tax = Number(tax.toFixed(2))
|
||||
tax = Number(tax.toFixed(2))
|
||||
const vat = Number(((tax + f) * 0.0151).toFixed(2))
|
||||
// 直接补全整链 (tax→vat→feePreTax), 不依赖下游 watch:
|
||||
// 小金额 (fee<=800) 个税为 0, tax 赋值前后不变, 下游 watch 不触发会漏算 vat/feePreTax
|
||||
attendeeDialog.value.form.tax = tax
|
||||
attendeeDialog.value.form.vatAndSurcharge = vat
|
||||
attendeeDialog.value.form.feePreTax = Number((f + tax + vat).toFixed(2))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1571,7 +1596,7 @@ async function load() {
|
||||
row.value = (resp && (resp.data || resp)) || {}
|
||||
scheduleUrl.value = row.value.scheduleUrl || ''
|
||||
invitationUrl.value = row.value.invitationUrl || ''
|
||||
await Promise.all([loadMaterials(), loadStaff(), loadTrail(), loadAttendees(), loadProjectRoles()])
|
||||
await Promise.all([loadMaterials(), loadStaff(), loadTrail(), loadAttendees(), loadProjectRoles(), loadInvoices()])
|
||||
if (row.value.feeCalcStatus === 0) scheduleFeePoll()
|
||||
// 从列表「结算」按钮跳入 (带 ?settle=1): 自动弹出结算确认 (结算前需已上传付款凭证)
|
||||
if (route.query.settle === '1' && canSettle.value) {
|
||||
@@ -1643,15 +1668,14 @@ async function saveMaterials() {
|
||||
if (!m || !m.id || !m.ossUrl) continue
|
||||
if (!isRecognizable(m.ossUrl)) continue
|
||||
if (NON_OCR_SUBTYPES.has(m.subType)) continue
|
||||
const isZip = isZipUrl(m.ossUrl)
|
||||
const old = preSaveState.get(m.subType)
|
||||
if (!old) {
|
||||
// 新增
|
||||
toOcr.push({ materialId: m.id, ossUrl: m.ossUrl, isZip, oldMaterialId: null, subType: m.subType })
|
||||
toOcr.push({ materialId: m.id, ossUrl: m.ossUrl, oldMaterialId: null, subType: m.subType })
|
||||
} else if (old.ossUrl !== m.ossUrl) {
|
||||
// 替换 (URL 变了 → 后端需先清旧 invoice + amount=0 再 OCR)
|
||||
toOcr.push({
|
||||
materialId: m.id, ossUrl: m.ossUrl, isZip,
|
||||
materialId: m.id, ossUrl: m.ossUrl,
|
||||
oldMaterialId: old.id, subType: m.subType
|
||||
})
|
||||
}
|
||||
@@ -1662,9 +1686,13 @@ async function saveMaterials() {
|
||||
submitOcrForMaterials(toOcr)
|
||||
}
|
||||
|
||||
// 5. 1.5s 后重拉 materials, 让 amount 字段更新可见
|
||||
// 5. 保存即刷新发票表: 后端 replaceByMeetingId 已同步删孤儿发票/迁移 material_id,
|
||||
// 移除材料后立即可见减少 (doSave 走 refreshFee, onSubmitMaterials 走这里)
|
||||
loadInvoices()
|
||||
|
||||
// 6. 1.5s 后重拉 materials + 发票表, 让 OCR 回写的 amount/发票号 更新可见
|
||||
if (toOcr.length) {
|
||||
setTimeout(() => loadMaterials(), 1500)
|
||||
setTimeout(() => { loadMaterials(); loadInvoices() }, 1500)
|
||||
}
|
||||
|
||||
return { saved, ocrCount: toOcr.length }
|
||||
@@ -1770,12 +1798,7 @@ function isImageUrl(url) {
|
||||
|
||||
/** 判断是否可识别: 图片或 PDF */
|
||||
function isRecognizable(url) {
|
||||
return /\.(jpe?g|png|pdf)$/i.test(url || '')
|
||||
}
|
||||
|
||||
/** 判断是否 zip (走 zip 路径: 解压 + 重传 OSS) */
|
||||
function isZipUrl(url) {
|
||||
return /\.zip(\?|$)/i.test(url || '')
|
||||
return /\.(jpe?g|png|pdf|zip)$/i.test(url || '')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1788,13 +1811,39 @@ function materialAmount(subType) {
|
||||
return Number(m.amount)
|
||||
}
|
||||
|
||||
// ===================== 发票表 (会务材料 tab 顶部展示会议全部会务发票明细) =====================
|
||||
/** 会议下所有会务发票明细 (biz_meeting_invoice join material 带出 subType/文件名) */
|
||||
const invoiceRows = ref([])
|
||||
|
||||
/** 会务材料 subType → 中文名 (ROW_CONFIG label) */
|
||||
function serviceLabel(subType) {
|
||||
const r = ROW_CONFIG.find(x => x.subType === subType)
|
||||
return r ? r.label : (subType || '—')
|
||||
}
|
||||
|
||||
/** 发票文件名: sourceFilename(zip) → materialFileName(单文件) → ossUrl 末段 → '—' */
|
||||
function invoiceFileName(row) {
|
||||
if (row.sourceFilename) return row.sourceFilename
|
||||
if (row.materialFileName) return row.materialFileName
|
||||
const seg = (((row.ossUrl || '').split('?')[0]).split('/').pop()) || ''
|
||||
return seg || '—'
|
||||
}
|
||||
|
||||
/** 拉取发票明细 (静默, 发票表非关键) */
|
||||
async function loadInvoices() {
|
||||
try {
|
||||
const resp = await request.get(`/business/meeting/invoice/list`, { params: { meetingId: meetingId.value }, __silentError: true })
|
||||
invoiceRows.value = Array.isArray(resp && resp.data) ? resp.data : []
|
||||
} catch (e) { /* 忽略 */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存成功后, 对所有需要识别的 material 提交 OCR 任务 (并行 fire-and-forget).
|
||||
* 后端 InvoiceOcrService.submitRecognition:
|
||||
* - 单文件 (isZip=false): 后台 OCR → 是发票 → update invoice + 回写 material.amount
|
||||
* 不是发票 → 删占位 invoice 行 + amount=0
|
||||
* - zip (isZip=true): 下载解压 → 遍历 → 是发票 → 重传 OSS (新 URL) → 写 invoice 行
|
||||
* 不是发票 → 跳过 (不入库)
|
||||
* - 单文件: 后台 OCR → 是发票 → update invoice + 回写 material.amount
|
||||
* 不是发票 → 删占位 invoice 行 + amount=0
|
||||
* - zip (后端按 ossUrl 后缀判断): 下载解压 → 遍历 → 是发票 → 重传 OSS (新 URL) → 写 invoice 行
|
||||
* 不是发票 → 跳过 (不入库)
|
||||
* - 替换 (oldMaterialId != null): 先 DELETE invoice WHERE material_id=old + amount=0
|
||||
*
|
||||
* 前端仅做"提交", 不等结果; 识别状态由 InvoiceOcrScheduler 兜底
|
||||
@@ -1806,7 +1855,6 @@ function submitOcrForMaterials(items) {
|
||||
materialId: m.materialId,
|
||||
meetingId: Number(meetingId.value),
|
||||
ossUrl: m.ossUrl,
|
||||
isZip: m.isZip, // v3 新增
|
||||
oldMaterialId: m.oldMaterialId // v3 新增 (替换场景)
|
||||
}, { __silentError: true }).then(r => {
|
||||
const data = (r && r.data) || {}
|
||||
@@ -1837,7 +1885,8 @@ async function onSubmitMaterials(types) {
|
||||
}
|
||||
busy.value.submitMaterial = true
|
||||
try {
|
||||
await saveMaterials()
|
||||
// 保存全部未落库内容 (材料 + 付款凭证 + 海报/邀请函), 与普通「保存」按钮同款 doSave
|
||||
await doSave()
|
||||
// __silentError: 拦截器不自动 toast, 错误统一由下方 catch 处理 (避免 axios 拦截器和 catch 双 toast)
|
||||
await request.post(`/business/meeting/${meetingId.value}/submit-material`, { types }, { __silentError: true })
|
||||
ElMessage.success('材料已提交, 待合规审核')
|
||||
@@ -1905,7 +1954,7 @@ function openAuditDialog(action, scope = 'ALL') {
|
||||
auditDialog.value = {
|
||||
show: true,
|
||||
action,
|
||||
title: isCompliance ? `合规审核 (材料 · ${scopeLabel})` : `监察审核 (材料 · ${scopeLabel})`,
|
||||
title: isCompliance ? `合规审核 (材料 · ${scopeLabel})` : `监察意见 (材料 · ${scopeLabel})`,
|
||||
opinion: '',
|
||||
tracks,
|
||||
saving: false
|
||||
@@ -1995,6 +2044,7 @@ async function refreshFee() {
|
||||
const m = (resp && (resp.data || resp)) || {}
|
||||
row.value = { ...row.value, laborFee: m.laborFee, meetingFee: m.meetingFee, totalFee: m.totalFee, feeCalcStatus: m.feeCalcStatus }
|
||||
} catch (e) { /* 拉取失败忽略, 下轮再试 */ }
|
||||
loadInvoices()
|
||||
if (row.value.feeCalcStatus === 0) scheduleFeePoll()
|
||||
else stopFeePolling()
|
||||
}
|
||||
@@ -2050,6 +2100,7 @@ async function onServiceZipChange(ev) {
|
||||
const n = (resp && resp.data != null) ? resp.data : 0
|
||||
ElMessage.success(`已回填 ${n} 类会务材料`)
|
||||
loadMaterials()
|
||||
loadInvoices()
|
||||
refreshFee()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || e?.message || '上传失败')
|
||||
@@ -2106,12 +2157,18 @@ onBeforeUnmount(stopFeePolling)
|
||||
.file-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.file-row { display: grid; grid-template-columns: 110px 1fr; gap: 6px 12px; align-items: center; padding: 4px 0; }
|
||||
.file-label { font-size: 14px; color: #595959; text-align: right; line-height: 28px; }
|
||||
.fee-summary { display: flex; align-items: center; gap: 10px; padding: 10px 0 0 110px; border-top: 1px dashed #e8e8e8; margin-top: 4px; }
|
||||
.fee-summary { display: flex; justify-content: flex-end; align-items: center; gap: 10px; padding: 10px 0 0; border-top: 1px dashed #e8e8e8; margin-top: 4px; }
|
||||
.labor-fee-summary { display: flex; justify-content: flex-end; align-items: center; gap: 10px; padding: 10px 0 0; border-top: 1px dashed #e8e8e8; margin-top: 8px; }
|
||||
.fee-label { font-size: 14px; color: #595959; }
|
||||
.fee-value { font-size: 16px; font-weight: 600; color: #f5222d; }.file-uploader { width: 100%; min-width: 0; }
|
||||
.file-hint { grid-column: 2; font-size: 11px; color: #8c8c8c; line-height: 1.5; }
|
||||
.agreement-actions { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.invoice-amount { font-size: 13px; font-weight: 600; color: #f56c6c; padding-left: 8px; white-space: nowrap; }
|
||||
.invoice-amount { grid-column: 2; justify-self: end; font-size: 13px; font-weight: 600; color: #f56c6c; white-space: nowrap; }
|
||||
.invoice-table-wrap { margin: 2px 0 8px; overflow-x: auto; }
|
||||
.invoice-table-title { font-size: 14px; font-weight: 600; color: #1a1a1a; margin-bottom: 8px; }
|
||||
.invoice-table { width: 100%; min-width: 640px; }
|
||||
.invoice-table-amount { font-weight: 600; color: #f56c6c; }
|
||||
.invoice-table-pending { color: #c0c4cc; }
|
||||
.file-link { color: var(--brand-primary); text-decoration: none; font-size: 14px; }
|
||||
.file-link:hover { text-decoration: underline; }
|
||||
.file-link.inline-link { font-size: 13px; }
|
||||
@@ -2321,6 +2378,8 @@ onBeforeUnmount(stopFeePolling)
|
||||
color: var(--el-text-color-secondary) !important;
|
||||
}
|
||||
.file-hint { grid-column: 1 !important; padding-left: 0 !important; }
|
||||
.invoice-amount { grid-column: 1 !important; }
|
||||
.invoice-table-wrap { overflow-x: auto; }
|
||||
|
||||
/* fee-summary: 去掉 110px 左 padding */
|
||||
.fee-summary {
|
||||
|
||||
Reference in New Issue
Block a user