feat: 会议提交剩余时间+解冻 + 会务/劳务材料批量下载 + 多角色人员/账号模块完善

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
郭庆泰
2026-08-25 06:53:18 +08:00
co-authored by Claude
parent 0c882ae846
commit 591a43fe61
126 changed files with 3789 additions and 1334 deletions
+81 -26
View File
@@ -21,10 +21,25 @@
<el-tag :type="stageTag(row)" size="small">{{ stageLabel('doctor', row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right">
<el-table-column label="日程" width="130" align="center">
<template #default="{ row }">
<el-button link type="primary" :disabled="!row.scheduleUrl" @click="onPreview(row.scheduleUrl, '日程海报')">查看</el-button>
<el-button link :disabled="!row.scheduleUrl" @click="onDownload(row.scheduleUrl, `${row.meetingName || '会议'}_日程海报`)">下载</el-button>
</template>
</el-table-column>
<el-table-column label="邀请函" width="130" align="center">
<template #default="{ row }">
<el-button link type="primary" :disabled="!row.projectInvitationUrl" @click="onPreview(row.projectInvitationUrl, '邀请函')">查看</el-button>
<el-button link :disabled="!row.projectInvitationUrl" @click="onDownload(row.projectInvitationUrl, `${row.meetingName || '会议'}_邀请函`)">下载</el-button>
</template>
</el-table-column>
<el-table-column label="签署状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.attendeeLaborProtocol ? 'success' : 'info'" size="small">{{ row.attendeeLaborProtocol ? '已签署' : '未签署' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="primary" :disabled="!row.invitationUrl" @click="onDownloadFile(row)">下载</el-button>
<el-button v-if="!row.attendeeLaborProtocol" link type="primary" @click="onSign(row)">签署劳务</el-button>
<el-button v-else link type="primary" @click="onViewLabor(row)">查看劳务</el-button>
</template>
@@ -42,6 +57,23 @@
style="margin-top: 12px; text-align: right;"
/>
<!-- 日程 / 邀请函 预览 dialog (PDF iframe / 图片 img, 复用 publicity OSS 代理预览技术) -->
<el-dialog v-model="previewOpen" :title="previewTitle" width="780px" top="6vh" :close-on-click-modal="false" destroy-on-close>
<div v-if="previewUrl" class="file-preview">
<iframe v-if="isPdf(previewUrl)" :src="proxyUrl(previewUrl)" class="file-preview-iframe"></iframe>
<el-image v-else-if="isImg(previewUrl)" :src="previewUrl" :preview-src-list="[previewUrl]" fit="contain" class="file-preview-img" />
<div v-else class="file-preview-fallback">
<p>该文件类型暂不支持页内预览, 请在新窗口打开查看</p>
<el-button type="primary" @click="openPreviewNew">在新窗口打开</el-button>
</div>
</div>
<el-empty v-else description="暂无附件" />
<template #footer>
<el-button @click="previewOpen = false">关闭</el-button>
<el-button v-if="previewUrl" type="primary" @click="onDownload(previewUrl, previewTitle)">下载</el-button>
</template>
</el-dialog>
<!-- 签署劳务 dialog: 二维码 ( token, 手机扫码可直接登录填写) -->
<el-dialog v-model="signOpen" title="签署劳务" width="380px" align-center destroy-on-close>
<div class="sign-qr-wrap">
@@ -57,18 +89,12 @@
<!-- 查看劳务 dialog: 已签状态显示, PDF iframe 预览 -->
<el-dialog v-model="laborOpen" title="查看劳务" width="720px">
<el-descriptions :column="1" border>
<el-descriptions-item label="项目编号">{{ detail.projectNo }}</el-descriptions-item>
<el-descriptions-item label="会议名称">{{ detail.meetingName }}</el-descriptions-item>
<el-descriptions-item label="签署状态">已签署</el-descriptions-item>
<el-descriptions-item label="签署时间">{{ detail.updateTime || '-' }}</el-descriptions-item>
</el-descriptions>
<div class="pdf-preview">
<iframe v-if="detail.attendeeLaborProtocol" :src="detail.attendeeLaborProtocol" style="width:100%;height:420px;border:1px solid #ebeef5"></iframe>
<iframe v-if="detail.attendeeLaborProtocol" :src="proxyUrl(detail.attendeeLaborProtocol)" style="width:100%;height:70vh;border:1px solid #ebeef5"></iframe>
<div v-else style="padding:24px;color:#909399;text-align:center">暂无签字 PDF</div>
</div>
<template #footer>
<el-button v-if="detail.attendeeLaborProtocol" link type="primary" @click="downloadPdf(detail.attendeeLaborProtocol, detail.meetingName)">下载</el-button>
<el-button v-if="detail.attendeeLaborProtocol" type="primary" @click="downloadPdf(detail.attendeeLaborProtocol, detail.meetingName)">下载</el-button>
<el-button @click="laborOpen=false">关闭</el-button>
</template>
</el-dialog>
@@ -77,7 +103,7 @@
<script setup>
import { reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ElMessage } from 'element-plus'
import QRCode from 'qrcode'
import { bizList } from '@/api/public'
import { stageLabel, stageTag, STAGE_OPTIONS } from '@/utils/meetingStage'
@@ -88,6 +114,9 @@ const rows = ref([])
const loading = ref(false)
const detail = ref({})
const laborOpen = ref(false)
const previewOpen = ref(false)
const previewUrl = ref('')
const previewTitle = ref('')
const signOpen = ref(false)
const signForm = reactive({ meetingName: '' })
const signLink = ref('')
@@ -119,27 +148,47 @@ function onViewLabor(row) {
laborOpen.value = true
}
function onView(row) {
detail.value = row
// 复用 detail dialog: 复用 meetingInfo 字段直接展示
// 这里直接展示一个只读 dialog, 没有劳务签署按钮
ElMessageBox.alert(
`项目编号: ${row.projectNo || '-'}\n会议名称: ${row.meetingName || '-'}\n当前阶段: ${stageLabel('doctor', row)}`,
'会议详情',
{ confirmButtonText: '关闭' }
)
// ===== 日程 / 邀请函 预览 (与 publicity 同技术: OSS 代理重写 inline + iframe/img) =====
function isPdf(url) {
if (!url) return false
const path = url.split('?')[0].toLowerCase()
return /\.pdf$/.test(path)
}
function onDownloadFile(row) {
if (!row.invitationUrl) { ElMessage.warning('该会议暂无邀请函附件'); return }
function isImg(url) {
if (!url) return false
const path = url.split('?')[0].toLowerCase()
return /\.(png|jpg|jpeg|gif|webp)$/.test(path)
}
// OSS bucket 设置 Content-Disposition: attachment, iframe 直接访问会被强制下载,
// 走 /common/oss/proxy 后端代理重写为 inline (#toolbar=0 隐藏工具栏, #zoom=page-width 撑满宽度)
function proxyUrl(url) {
if (!url) return url
if (url.includes('hwtossbamlorgcn.oss-cn-beijing.aliyuncs.com')) {
return import.meta.env.VITE_APP_BASE_API + '/common/oss/proxy?url=' + encodeURIComponent(url) + '#toolbar=0&zoom=page-width'
}
return url
}
function onPreview(url, title) {
if (!url) { ElMessage.warning('暂无附件'); return }
previewUrl.value = url
previewTitle.value = title
previewOpen.value = true
}
function onDownload(url, name) {
if (!url) { ElMessage.warning('暂无附件'); return }
const a = document.createElement('a')
a.href = row.invitationUrl
a.download = `${row.meetingName || '会议'}_邀请函.pdf`
a.href = url
a.download = name
a.target = '_blank'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
function openPreviewNew() {
const url = proxyUrl(previewUrl.value)
if (!url) return
window.open(url, '_blank')
}
async function onSign(row) {
signForm.meetingName = row.meetingName || ''
@@ -206,6 +255,12 @@ load()
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { display: flex; flex-wrap: wrap; gap: 0 16px; padding-bottom: 12px; border-bottom: 1px solid #f0f0f0; margin-bottom: 12px; }
.pdf-preview { margin-top: 12px; }
.file-preview { display: flex; justify-content: center; align-items: flex-start; min-height: 480px; }
.file-preview-iframe { width: 100%; height: 70vh; border: 1px solid #ebeef5; border-radius: 4px; }
.file-preview-img { width: 100%; }
.file-preview-img :deep(.el-image__inner) { width: 100%; height: auto; max-height: 70vh; object-fit: contain; }
.file-preview-fallback { text-align: center; padding: 48px 24px; color: #606266; }
.file-preview-fallback p { margin-bottom: 16px; }
.sign-link-tip { font-size: 13px; color: #606266; margin-bottom: 10px; }
.sign-link-hint { font-size: 12px; color: #909399; margin-top: 10px; }
.sign-qr-wrap { text-align: center; padding: 12px 0; }