Files
guoju0808/ry-vue3/src/views/doctor/Meetings.vue
T

278 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>