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:
@@ -1,9 +1,16 @@
|
||||
<template>
|
||||
<div class="sign-fill">
|
||||
<div v-loading="loading">
|
||||
<div v-if="notInvited" class="not-invited">
|
||||
<div class="not-invited-text">您暂未被邀请,请联系执会人员</div>
|
||||
<el-button type="primary" @click="goBack">返回</el-button>
|
||||
</div>
|
||||
<div v-else v-loading="loading">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-position="top" class="sign-fill-form">
|
||||
<!-- 参会人信息 -->
|
||||
<div class="section-title">参会人信息</div>
|
||||
<!-- 大标题: 会议名称 + 期数 -->
|
||||
<div class="sign-header">
|
||||
<div class="sign-header-title">{{ meetingName || '劳务协议签署' }}</div>
|
||||
<div class="sign-header-period">期数:{{ periodDisplay || '-' }}</div>
|
||||
</div>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
|
||||
</el-form-item>
|
||||
@@ -16,9 +23,15 @@
|
||||
<el-form-item label="科室" prop="department">
|
||||
<el-input v-model="form.department" placeholder="请输入科室" maxlength="100" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 银行账户 -->
|
||||
<div class="section-title">银行账户</div>
|
||||
<el-form-item label="医务职称">
|
||||
<el-select v-model="form.title" placeholder="请选择" clearable filterable style="width: 100%">
|
||||
<el-option v-for="t in titleOptions" :key="t" :value="t" :label="t" />
|
||||
<template #footer>
|
||||
<el-input v-if="showTitleOther" v-model="titleOther" placeholder="其他职称" maxlength="50" @blur="form.title = titleOther" />
|
||||
<el-button v-else link type="primary" @click="showTitleOther = true">+ 其他</el-button>
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="账户名称(持卡人姓名)" prop="accountName">
|
||||
<el-input v-model="form.accountName" placeholder="请输入持卡人姓名" maxlength="100" />
|
||||
</el-form-item>
|
||||
@@ -53,8 +66,6 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 劳务信息 (只读, admin 预填) -->
|
||||
<div class="section-title">本次劳务</div>
|
||||
<el-form-item label="劳务形式">
|
||||
<div class="labor-form-cell">
|
||||
<el-checkbox-group v-model="laborFormSelected">
|
||||
@@ -65,15 +76,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="医务职称">
|
||||
<el-select v-model="form.title" placeholder="请选择" clearable filterable style="width: 240px">
|
||||
<el-option v-for="t in titleOptions" :key="t" :value="t" :label="t" />
|
||||
<template #footer>
|
||||
<el-input v-if="showTitleOther" v-model="titleOther" placeholder="其他职称" maxlength="50" @blur="form.title = titleOther" />
|
||||
<el-button v-else link type="primary" @click="showTitleOther = true">+ 其他</el-button>
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitting" @click="onSubmit">下一步</el-button>
|
||||
@@ -88,13 +90,18 @@
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getSignInfo, saveSignProfile } from '@/api/business/sign'
|
||||
import { getSignInfo, saveSignProfile, resolveSign } from '@/api/business/sign'
|
||||
import { getInfo } from '@/api/auth'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import IdCardUploader from '@/components/IdCardUploader.vue'
|
||||
import AreaCascader from '@/components/AreaCascader.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const attendeeId = computed(() => Number(route.query.attendeeId) || null)
|
||||
const userStore = useUserStore()
|
||||
const attendeeId = ref(null)
|
||||
const meetingId = computed(() => route.query.meetingId ? Number(route.query.meetingId) : null)
|
||||
const notInvited = ref(false)
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
@@ -120,6 +127,19 @@ const form = reactive({
|
||||
})
|
||||
const feeDisplay = reactive({ preTax: '—', tax: '—', fee: '—' })
|
||||
|
||||
// 会议大标题 (会议名称 + 期数), 由后端 getSignInfo 返回
|
||||
const meetingName = ref('')
|
||||
const periodNo = ref(null)
|
||||
const totalPeriods = ref(null)
|
||||
const periodDisplay = computed(() => {
|
||||
const p = periodNo.value
|
||||
const t = totalPeriods.value
|
||||
if (p == null && t == null) return ''
|
||||
if (p == null) return `${t}`
|
||||
if (t == null) return `${p}`
|
||||
return `${p}/${t}`
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
phone: [
|
||||
@@ -135,6 +155,73 @@ const rules = {
|
||||
bankBranch: [{ required: true, message: '请输入开户行支行', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
/** 扫码带 token 直登 / 无 token 校验登录; 返回 true 表示已就绪 */
|
||||
async function ensureLogin() {
|
||||
const token = route.query.token
|
||||
if (token) {
|
||||
// 扫码带 token: 用新 token 重新登录 (覆盖本地已登录态)
|
||||
userStore.setToken(token)
|
||||
try {
|
||||
const info = await getInfo()
|
||||
const u = info.user || {}
|
||||
const role = u.roleType
|
||||
if (!role) {
|
||||
ElMessage.error('账号角色未配置, 请联系管理员')
|
||||
router.replace({ name: 'login' })
|
||||
return false
|
||||
}
|
||||
userStore.setUser({
|
||||
userId: u.userId,
|
||||
userName: u.userName || u.nickName || '',
|
||||
nickName: u.nickName || u.userName || '',
|
||||
phonenumber: u.phonenumber || '',
|
||||
accountType: u.accountType || 'MAIN',
|
||||
parentUserId: u.parentUserId || null,
|
||||
role
|
||||
})
|
||||
return true
|
||||
} catch (e) {
|
||||
ElMessage.error('登录已失效, 请重新登录')
|
||||
router.replace({ name: 'login' })
|
||||
return false
|
||||
}
|
||||
}
|
||||
// 无 token: 必须已登录
|
||||
if (userStore.user) return true
|
||||
ElMessage.error('请先登录')
|
||||
router.replace({ name: 'login', query: { redirect: route.fullPath } })
|
||||
return false
|
||||
}
|
||||
|
||||
async function init() {
|
||||
loading.value = true
|
||||
const ok = await ensureLogin()
|
||||
if (!ok) return
|
||||
// 解析 attendeeId: 优先 URL 里的 attendeeId; 没有则按 meetingId 查
|
||||
let aid = route.query.attendeeId ? Number(route.query.attendeeId) : null
|
||||
if (!aid && meetingId.value) {
|
||||
try {
|
||||
const { data } = await resolveSign(meetingId.value)
|
||||
if (!data || !data.attendeeId) {
|
||||
notInvited.value = true
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
aid = data.attendeeId
|
||||
// 标题: 有 meetingId → 按 meetingId 查 (resolve 已返回会议名/期数)
|
||||
meetingName.value = data.meetingName || ''
|
||||
periodNo.value = data.periodNo ?? null
|
||||
totalPeriods.value = data.totalPeriods ?? null
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '加载失败')
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
attendeeId.value = aid
|
||||
await load()
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!attendeeId.value) {
|
||||
ElMessage.error('参数缺失, 请从工作台进入')
|
||||
@@ -162,25 +249,43 @@ async function load() {
|
||||
idCardAttachments: cur.idCardAttachments || '',
|
||||
title: cur.title || def.title || ''
|
||||
})
|
||||
// 先设置选项 (项目角色 + "其他"), 解析劳务形式需要用它区分"项目角色"和"其他自定义"
|
||||
laborFormOptions.value = (data.laborFormOptions || []).map(o => ({ value: o.value, label: o.label }))
|
||||
titleOptions.value = data.titleOptions || []
|
||||
|
||||
// 劳务形式回显: 兼容旧 JSON 数组 (["主席","__other__:xx"]) + 新逗号分隔 ("主席,主持,xx")
|
||||
laborFormSelected.value = []
|
||||
laborFormOther.value = ''
|
||||
if (cur.laborForm) {
|
||||
try {
|
||||
const parsed = JSON.parse(cur.laborForm)
|
||||
if (Array.isArray(parsed)) {
|
||||
laborFormSelected.value = parsed.filter(x => x !== '__other__' && !x.startsWith('__other__:'))
|
||||
const otherItem = parsed.find(x => x === '__other__' || x.startsWith('__other__:'))
|
||||
if (otherItem) {
|
||||
laborFormSelected.value.push('__other__')
|
||||
laborFormOther.value = otherItem.startsWith('__other__:') ? otherItem.substring('__other__:'.length) : ''
|
||||
}
|
||||
let items = []
|
||||
if (typeof cur.laborForm === 'string' && cur.laborForm.trim().startsWith('[')) {
|
||||
try { const p = JSON.parse(cur.laborForm); if (Array.isArray(p)) items = p } catch (e) { items = [cur.laborForm] }
|
||||
} else {
|
||||
items = String(cur.laborForm).split(',').map(s => s.trim()).filter(Boolean)
|
||||
}
|
||||
const known = new Set(laborFormOptions.value.map(o => o.value))
|
||||
items.forEach(it => {
|
||||
const s = String(it)
|
||||
if (s === '__other__' || s.startsWith('__other__:')) {
|
||||
if (!laborFormSelected.value.includes('__other__')) laborFormSelected.value.push('__other__')
|
||||
if (s.startsWith('__other__:')) laborFormOther.value = s.substring('__other__:'.length)
|
||||
} else if (known.has(s)) {
|
||||
if (!laborFormSelected.value.includes(s)) laborFormSelected.value.push(s)
|
||||
} else {
|
||||
// 不在项目角色里 → 当作"其他"自定义
|
||||
if (!laborFormSelected.value.includes('__other__')) laborFormSelected.value.push('__other__')
|
||||
laborFormOther.value = s
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
})
|
||||
}
|
||||
|
||||
feeDisplay.preTax = cur.feePreTax || '—'
|
||||
feeDisplay.tax = cur.tax || '—'
|
||||
feeDisplay.fee = cur.fee || '—'
|
||||
|
||||
laborFormOptions.value = (data.laborFormOptions || []).map(o => ({ value: o.value, label: o.label }))
|
||||
titleOptions.value = data.titleOptions || []
|
||||
meetingName.value = data.meetingName || ''
|
||||
periodNo.value = data.periodNo ?? null
|
||||
totalPeriods.value = data.totalPeriods ?? null
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '加载失败')
|
||||
} finally {
|
||||
@@ -195,16 +300,13 @@ async function onSubmit() {
|
||||
ElMessage.warning('请上传身份证正反面')
|
||||
return
|
||||
}
|
||||
// 劳务形式序列化 (JSON 数组字符串, 含 __other__ + 自定义内容)
|
||||
// 劳务形式序列化: 逗号分隔字符串, "其他"自定义内容作为普通项
|
||||
let laborForm = ''
|
||||
if (laborFormSelected.value.length) {
|
||||
const arr = laborFormSelected.value.map(x => {
|
||||
if (x === '__other__' && laborFormOther.value) {
|
||||
return '__other__:' + laborFormOther.value
|
||||
}
|
||||
return x
|
||||
})
|
||||
laborForm = JSON.stringify(arr)
|
||||
laborForm = laborFormSelected.value
|
||||
.map(x => (x === '__other__' ? (laborFormOther.value || '').trim() : x))
|
||||
.filter(x => x !== '' && x !== '__other__')
|
||||
.join(',')
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
@@ -228,23 +330,21 @@ function goBack() {
|
||||
router.push('/doctor/home')
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
onMounted(init)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 手机端全屏表单 (无 navbar / 无 page-card) */
|
||||
.sign-fill { padding: 16px; }
|
||||
|
||||
/* 章节标题: 左边 3px 蓝竖线 */
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin: 16px 0 12px;
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid var(--brand-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
/* 大标题: 会议名称 + 期数 */
|
||||
.sign-header { margin: 4px 0 20px; padding-bottom: 14px; border-bottom: 1px solid #f0f0f0; }
|
||||
.sign-header-title { font-size: 20px; font-weight: 600; color: #1a1a1a; line-height: 1.4; }
|
||||
.sign-header-period { margin-top: 6px; font-size: 14px; color: #595959; }
|
||||
|
||||
/* 未受邀提示 */
|
||||
.not-invited { padding: 60px 20px; text-align: center; }
|
||||
.not-invited-text { font-size: 16px; color: #606266; margin-bottom: 20px; }
|
||||
|
||||
/* 开户地址 3 列布局 */
|
||||
.region-row {
|
||||
|
||||
Reference in New Issue
Block a user