278 lines
12 KiB
Vue
278 lines
12 KiB
Vue
<template>
|
||
<div class="page-card">
|
||
<div class="breadcrumb">首页 / 我参与的会议</div>
|
||
|
||
<el-form inline :model="q" class="filter-form">
|
||
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
|
||
<el-form-item label="会议名称"><el-input v-model="q.meetingName" placeholder="输入会议名称" clearable /></el-form-item>
|
||
<el-form-item label="当前阶段">
|
||
<el-select v-model="q.currentStage" placeholder="请选择" clearable>
|
||
<el-option v-for="o in STAGE_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item><el-button type="primary" @click="load">查找</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||
</el-form>
|
||
|
||
<el-table :data="rows" v-loading="loading" stripe border>
|
||
<el-table-column prop="projectNo" label="项目编号" width="160" />
|
||
<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>
|
||
</template>
|
||
</el-table-column>
|
||
<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 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>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-pagination
|
||
v-model:current-page="page.pageNum"
|
||
v-model:page-size="page.pageSize"
|
||
:total="page.total"
|
||
:page-sizes="[10, 20, 50]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@current-change="load"
|
||
@size-change="load"
|
||
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">
|
||
<div v-if="signQrLoading" v-loading="true" class="sign-qr-loading"></div>
|
||
<img v-else-if="signQrUrl" :src="signQrUrl" class="sign-qr-img" />
|
||
<div class="sign-link-tip">会议:{{ signForm.meetingName }}</div>
|
||
<el-link type="primary" :underline="false" class="sign-copy" @click="copySignLink">复制链接</el-link>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="signOpen=false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 查看劳务 dialog: 已签状态显示, 含 PDF iframe 预览 -->
|
||
<el-dialog v-model="laborOpen" title="查看劳务" width="720px">
|
||
<div class="pdf-preview">
|
||
<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" type="primary" @click="downloadPdf(detail.attendeeLaborProtocol, detail.meetingName)">下载</el-button>
|
||
<el-button @click="laborOpen=false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { reactive, ref } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import QRCode from 'qrcode'
|
||
import { bizList } from '@/api/public'
|
||
import { stageLabel, stageTag, STAGE_OPTIONS } from '@/utils/meetingStage'
|
||
|
||
const q = reactive({ projectNo: '', meetingName: '', currentStage: '' })
|
||
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
|
||
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('')
|
||
const signQrUrl = ref('')
|
||
const signQrLoading = ref(false)
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
const { data } = await bizList('meeting', { ...q, pageNum: page.pageNum, pageSize: page.pageSize })
|
||
rows.value = data?.rows || []
|
||
page.total = data?.total || 0
|
||
} catch (e) {
|
||
rows.value = []
|
||
page.total = 0
|
||
} finally { loading.value = false }
|
||
}
|
||
|
||
function reset() {
|
||
q.projectNo = ''
|
||
q.meetingName = ''
|
||
q.currentStage = ''
|
||
page.pageNum = 1
|
||
load()
|
||
}
|
||
|
||
function onViewLabor(row) {
|
||
detail.value = row
|
||
laborOpen.value = true
|
||
}
|
||
|
||
// ===== 日程 / 邀请函 预览 (与 publicity 同技术: OSS 代理重写 inline + iframe/img) =====
|
||
function isPdf(url) {
|
||
if (!url) return false
|
||
const path = url.split('?')[0].toLowerCase()
|
||
return /\.pdf$/.test(path)
|
||
}
|
||
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 = 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 || ''
|
||
const attendeeId = row.attendeeId
|
||
if (!attendeeId) { ElMessage.warning('缺少参会记录, 无法生成签署链接'); return }
|
||
const token = localStorage.getItem('ry_token') || ''
|
||
const url = window.location.origin + import.meta.env.BASE_URL + '#/doctor/sign-fill?attendeeId=' + attendeeId
|
||
+ (row.meetingId != null ? '&meetingId=' + row.meetingId : '')
|
||
+ '&token=' + encodeURIComponent(token)
|
||
signLink.value = url
|
||
signOpen.value = true
|
||
signQrUrl.value = ''
|
||
signQrLoading.value = true
|
||
try {
|
||
signQrUrl.value = await QRCode.toDataURL(url, {
|
||
width: 240,
|
||
margin: 2,
|
||
color: { dark: '#1a1a1a', light: '#ffffff' }
|
||
})
|
||
} catch (e) {
|
||
ElMessage.error('生成二维码失败')
|
||
} finally {
|
||
signQrLoading.value = false
|
||
}
|
||
}
|
||
|
||
function copySignLink() {
|
||
if (!signLink.value) return
|
||
try {
|
||
if (navigator.clipboard && window.isSecureContext) {
|
||
navigator.clipboard.writeText(signLink.value)
|
||
} else {
|
||
const ta = document.createElement('textarea')
|
||
ta.value = signLink.value
|
||
ta.style.position = 'fixed'
|
||
ta.style.opacity = '0'
|
||
document.body.appendChild(ta)
|
||
ta.select()
|
||
document.execCommand('copy')
|
||
document.body.removeChild(ta)
|
||
}
|
||
ElMessage.success('已复制链接')
|
||
} catch (e) {
|
||
ElMessage.error('复制失败, 请手动复制')
|
||
}
|
||
}
|
||
|
||
function downloadPdf(url, name) {
|
||
if (!url) return
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = `${name || '劳务'}_${Date.now()}.pdf`
|
||
a.target = '_blank'
|
||
document.body.appendChild(a)
|
||
a.click()
|
||
document.body.removeChild(a)
|
||
}
|
||
|
||
load()
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
||
.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; }
|
||
.sign-qr-loading { width: 240px; height: 240px; margin: 0 auto; }
|
||
.sign-qr-img { width: 240px; height: 240px; }
|
||
.sign-qr-wrap .sign-link-tip { margin-top: 16px; }
|
||
.sign-copy { display: inline-block; margin-top: 16px; font-size: 13px; }
|
||
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
|
||
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep); border-color: var(--brand-primary-deep); }
|
||
:deep(.el-table .el-button) { border-radius: 4px; }
|
||
: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; }
|
||
|
||
</style> |