feat: 生产部署 + 会议全链路 (执行方分配/费用结算/签到脱敏/H5相机)
- 生产 nginx 三路径: ry-vue3 /hg/, ry-h5 /camera/, API /hg-api, .env 分环境 - 签约链接/摄像头 base-url 走 yml, 不再硬编码 /hg 与 localhost - 新增 biz_project_executor_assign (镜像 sponsor_assign) 执行方项目级分配 - 会议费用后台汇总 FeeCalcScheduler + 结算回写项目金额 + 状态流转调度 - 签到表拍照高斯模糊脱敏 extra_oss_url, 新增 PosterService/StageDeriver - 清理 biz_support_intent 旧表 (6 Java/XML 删除) - ry-h5 相机黑屏修复: 显式 video.play() + 动态 apiBase 上传 - 资源: simhei 字体 / logo.png / qrcode_1.png / favicon.ico Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,7 @@
|
||||
<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 label="未开始" value="未开始" />
|
||||
<el-option label="执行中" value="执行中" />
|
||||
<el-option label="已结束" value="已结束" />
|
||||
<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>
|
||||
@@ -18,16 +16,16 @@
|
||||
<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="120">
|
||||
<el-table-column prop="currentStage" label="当前阶段" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="stageTag(row.currentStage)" size="small">{{ row.currentStage || '-' }}</el-tag>
|
||||
<el-tag :type="stageTag(row)" size="small">{{ stageLabel('doctor', row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" 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.laborSigned !== '1'" link type="primary" @click="onSign(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>
|
||||
</el-table-column>
|
||||
@@ -44,30 +42,16 @@
|
||||
style="margin-top: 12px; text-align: right;"
|
||||
/>
|
||||
|
||||
<!-- 签署劳务 dialog: 未签状态显示, 含 PDF 上传 -->
|
||||
<el-dialog v-model="signOpen" title="签署劳务" width="560px">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="项目编号">{{ signForm.projectNo }}</el-form-item>
|
||||
<el-form-item label="会议名称">{{ signForm.meetingName }}</el-form-item>
|
||||
<el-form-item label="签字 PDF">
|
||||
<el-upload
|
||||
drag
|
||||
action="#"
|
||||
:before-upload="beforeSignUpload"
|
||||
:http-request="uploadSignPdf"
|
||||
:file-list="signFileList"
|
||||
:on-remove="onSignRemove"
|
||||
accept=".pdf"
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">将签字 PDF 拖到此处,或<em>点击上传</em></div>
|
||||
<div class="el-upload__tip" slot="tip">仅支持 PDF, ≤ 20MB</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 签署劳务 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>
|
||||
<el-button type="primary" :loading="submitting" :disabled="!signForm.signedPdfUrl" @click="submitSign">签署劳务</el-button>
|
||||
<el-button @click="signOpen=false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -80,11 +64,11 @@
|
||||
<el-descriptions-item label="签署时间">{{ detail.updateTime || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="pdf-preview">
|
||||
<iframe v-if="detail.signedPdfUrl" :src="detail.signedPdfUrl" style="width:100%;height:420px;border:1px solid #ebeef5"></iframe>
|
||||
<iframe v-if="detail.attendeeLaborProtocol" :src="detail.attendeeLaborProtocol" style="width:100%;height:420px;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.signedPdfUrl" link type="primary" @click="downloadPdf(detail.signedPdfUrl, detail.meetingName)">下载</el-button>
|
||||
<el-button v-if="detail.attendeeLaborProtocol" link type="primary" @click="downloadPdf(detail.attendeeLaborProtocol, detail.meetingName)">下载</el-button>
|
||||
<el-button @click="laborOpen=false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -94,8 +78,9 @@
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { bizList, bizUpdate } from '@/api/public'
|
||||
import { uploadToOss } from '@/utils/oss'
|
||||
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 })
|
||||
@@ -104,9 +89,10 @@ const loading = ref(false)
|
||||
const detail = ref({})
|
||||
const laborOpen = ref(false)
|
||||
const signOpen = ref(false)
|
||||
const signForm = reactive({ meetingId: '', projectNo: '', meetingName: '', signedPdfUrl: '' })
|
||||
const signFileList = ref([])
|
||||
const submitting = ref(false)
|
||||
const signForm = reactive({ meetingName: '' })
|
||||
const signLink = ref('')
|
||||
const signQrUrl = ref('')
|
||||
const signQrLoading = ref(false)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
@@ -128,12 +114,6 @@ function reset() {
|
||||
load()
|
||||
}
|
||||
|
||||
function stageTag(s) {
|
||||
if (s === '已结束') return 'info'
|
||||
if (s === '执行中') return 'success'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
function onViewLabor(row) {
|
||||
detail.value = row
|
||||
laborOpen.value = true
|
||||
@@ -144,7 +124,7 @@ function onView(row) {
|
||||
// 复用 detail dialog: 复用 meetingInfo 字段直接展示
|
||||
// 这里直接展示一个只读 dialog, 没有劳务签署按钮
|
||||
ElMessageBox.alert(
|
||||
`项目编号: ${row.projectNo || '-'}\n会议名称: ${row.meetingName || '-'}\n当前阶段: ${row.currentStage || '-'}`,
|
||||
`项目编号: ${row.projectNo || '-'}\n会议名称: ${row.meetingName || '-'}\n当前阶段: ${stageLabel('doctor', row)}`,
|
||||
'会议详情',
|
||||
{ confirmButtonText: '关闭' }
|
||||
)
|
||||
@@ -161,54 +141,49 @@ function onDownloadFile(row) {
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
|
||||
function onSign(row) {
|
||||
signForm.meetingId = row.meetingId
|
||||
signForm.projectNo = row.projectNo || ''
|
||||
async function onSign(row) {
|
||||
signForm.meetingName = row.meetingName || ''
|
||||
signForm.signedPdfUrl = ''
|
||||
signFileList.value = []
|
||||
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
|
||||
}
|
||||
|
||||
function beforeSignUpload(file) {
|
||||
const max = 20 * 1024 * 1024
|
||||
if (file.size > max) { ElMessage.error('文件不能超过 20MB'); return false }
|
||||
if (!file.name.toLowerCase().endsWith('.pdf')) { ElMessage.error('仅支持 PDF 格式'); return false }
|
||||
return true
|
||||
}
|
||||
|
||||
async function uploadSignPdf(opts) {
|
||||
signQrUrl.value = ''
|
||||
signQrLoading.value = true
|
||||
try {
|
||||
const url = await uploadToOss(opts.file, 'ry8080/labor/signed/')
|
||||
signForm.signedPdfUrl = url
|
||||
ElMessage.success('签字 PDF 上传成功')
|
||||
signQrUrl.value = await QRCode.toDataURL(url, {
|
||||
width: 240,
|
||||
margin: 2,
|
||||
color: { dark: '#1a1a1a', light: '#ffffff' }
|
||||
})
|
||||
} catch (e) {
|
||||
ElMessage.error('上传失败: ' + (e?.message || e))
|
||||
ElMessage.error('生成二维码失败')
|
||||
} finally {
|
||||
signQrLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSignRemove() {
|
||||
signForm.signedPdfUrl = ''
|
||||
signFileList.value = []
|
||||
}
|
||||
|
||||
async function submitSign() {
|
||||
if (!signForm.signedPdfUrl) return ElMessage.warning('请先上传签字 PDF')
|
||||
submitting.value = true
|
||||
function copySignLink() {
|
||||
if (!signLink.value) return
|
||||
try {
|
||||
await bizUpdate('meeting', {
|
||||
meetingId: signForm.meetingId,
|
||||
laborSigned: '1',
|
||||
signedPdfUrl: signForm.signedPdfUrl,
|
||||
signedTime: new Date().toISOString().slice(0, 19).replace('T', ' ')
|
||||
})
|
||||
ElMessage.success('签署成功')
|
||||
signOpen.value = false
|
||||
load()
|
||||
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(e?.msg || '签署失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
ElMessage.error('复制失败, 请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +206,13 @@ 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; }
|
||||
.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; }
|
||||
|
||||
Reference in New Issue
Block a user