feat: 多处页面改造 + 新增劳务协议模板配置
主要改动: - biz_meeting_attendee 中间表 + doctor/expert 角色数据隔离 - biz_meeting_attendee 加 handsign/labor_protocol 字段 (劳务协议改造) - biz_labor_protocol_template 配置表 + admin 页面 (从 HeguiConstants 迁移) - biz_expert.expertId 改 Long + IdGenerator 生成 (去自增) - Login.vue / PublicityDetail.vue / Home.vue 等多处 bug 修复 + UI 改进 - 新增 page-tech-review 报告 3 篇 (_self/*.md) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -164,8 +164,6 @@ async function load() {
|
||||
const urlMap = {
|
||||
invitation: r.invitationUrl,
|
||||
support: r.supportLetterUrl,
|
||||
notice: r.noticeUrl,
|
||||
schedule: r.scheduleUrl,
|
||||
publish: r.publishUrl
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -98,10 +98,10 @@
|
||||
</svg>
|
||||
<span>{{ signed ? '已报名' : (signing ? '报名中…' : '立即报名') }}</span>
|
||||
</button>
|
||||
<button class="action-btn primary" :disabled="supportSubmitting || supportSubmitted" @click="onSupportIntent">
|
||||
<button v-if="canShowSupportBtn" class="action-btn primary" :disabled="supportSubmitting || supportSubmitted" @click="onSupportIntent">
|
||||
<span>{{ supportSubmitted ? '已支持' : (supportSubmitting ? '提交中…' : '表达支持意向') }}</span>
|
||||
</button>
|
||||
<button class="action-btn primary" :disabled="executionSubmitting || executionSubmitted" @click="onExecutionIntent">
|
||||
<button v-if="canShowExecutionBtn" class="action-btn primary" :disabled="executionSubmitting || executionSubmitted" @click="onExecutionIntent">
|
||||
<span>{{ executionSubmitted ? '已表达意向' : (executionSubmitting ? '提交中…' : '表达执行意向') }}</span>
|
||||
</button>
|
||||
<button class="action-btn primary share-btn" @click="showQr = true">
|
||||
@@ -193,13 +193,13 @@
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form :model="guestDialog.form" label-width="100px">
|
||||
<el-form-item label="姓名 *" required>
|
||||
<el-form-item label="姓名" required>
|
||||
<el-input v-model="guestDialog.form.name" placeholder="请输入姓名" maxlength="50" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号 *" required>
|
||||
<el-form-item label="手机号" required>
|
||||
<el-input v-model="guestDialog.form.phone" placeholder="请输入手机号" maxlength="11" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="工作单位 *" required>
|
||||
<el-form-item label="工作单位" required>
|
||||
<el-input v-model="guestDialog.form.workUnit" placeholder="请输入工作单位" maxlength="200" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门">
|
||||
@@ -223,6 +223,8 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { logout as logoutApi } from '@/api/auth'
|
||||
import { listBizPerson } from '@/api/business/person'
|
||||
import { bizGet } from '@/api/public'
|
||||
import {
|
||||
submitPublicitySupportIntent,
|
||||
submitPublicityExecutionIntent,
|
||||
@@ -239,6 +241,20 @@ const isScrolled = ref(false)
|
||||
const topNavClass = computed(() => isScrolled.value ? 'is-scrolled' : '')
|
||||
const loggedIn = computed(() => !!userStore.token)
|
||||
const userName = computed(() => userStore.user?.userName || '用户')
|
||||
// 意向按钮按角色显隐:
|
||||
// sponsor (含 MAIN/SUB) → 仅支持意向; executor (含 MAIN/SUB) → 仅执行意向
|
||||
// admin / leader / manager / doctor → 两个都隐藏
|
||||
// 匿名 → 两个都显示 (走 guestDialog)
|
||||
const canShowSupportBtn = computed(() => {
|
||||
const r = userStore.user?.role || ''
|
||||
if (!r) return true
|
||||
return r === 'sponsor'
|
||||
})
|
||||
const canShowExecutionBtn = computed(() => {
|
||||
const r = userStore.user?.role || ''
|
||||
if (!r) return true
|
||||
return r === 'executor'
|
||||
})
|
||||
|
||||
const showQr = ref(false)
|
||||
const qrUrl = ref('')
|
||||
@@ -266,12 +282,10 @@ watch(showQr, async (v) => {
|
||||
const ann = ref(null)
|
||||
const activeTab = ref('invitation')
|
||||
|
||||
// 按 URL 字段定义 4 个固定 tab (key 必须与 biz_project 字段名后缀一致)
|
||||
// 按 URL 字段定义固定 tab (key 必须与 biz_project 字段名后缀一致)
|
||||
const TAB_DEFS = [
|
||||
{ key: 'invitation', label: '邀请函', urlKey: 'invitationUrl' },
|
||||
{ key: 'supportLetter', label: '支持函', urlKey: 'supportLetterUrl' },
|
||||
{ key: 'notice', label: '通知', urlKey: 'noticeUrl' },
|
||||
{ key: 'schedule', label: '日程', urlKey: 'scheduleUrl' }
|
||||
{ key: 'supportLetter', label: '支持函', urlKey: 'supportLetterUrl' }
|
||||
]
|
||||
|
||||
// 只显示项目里"实际有文件 URL"的 tab
|
||||
@@ -433,12 +447,11 @@ const supportSubmitted = ref(false)
|
||||
const executionSubmitting = ref(false)
|
||||
const executionSubmitted = ref(false)
|
||||
|
||||
// 持久化"已提交"标记 - 已登录按 sys_user.phonenumber, 未登录无身份只能本次会话判定
|
||||
// 持久化"已提交"标记 - 已登录按 sys_user.phone 查后端 hasIntent
|
||||
async function checkIntentSubmitted(type) {
|
||||
const proj = ann.value || {}
|
||||
const projectId = proj.projectId || proj.id
|
||||
if (!projectId) return false
|
||||
// 已登录: 用登录用户的手机号去查
|
||||
const phone = userStore.user?.phonenumber || userStore.user?.phoneNumber
|
||||
if (!phone) return false
|
||||
try {
|
||||
@@ -465,10 +478,49 @@ function resetGuestForm() {
|
||||
guestDialog.form = { name: '', phone: '', workUnit: '', department: '', position: '' }
|
||||
}
|
||||
|
||||
// 已登录用户 → 从 userStore + biz_person + biz_org 回填全部 5 个字段
|
||||
// 流程: userStore 拿 name/phone → /business/person/list?userId= 拿 department/position/orgId → /business/org/{orgId} 拿 orgName(作 workUnit)
|
||||
// 任何一步失败都优雅降级, 不阻塞 dialog 打开
|
||||
async function prefillGuestFormFromUser() {
|
||||
const u = userStore.user || {}
|
||||
guestDialog.form = {
|
||||
name: u.nickName || u.userName || '',
|
||||
phone: u.phonenumber || u.phoneNumber || '',
|
||||
workUnit: '',
|
||||
department: '',
|
||||
position: ''
|
||||
}
|
||||
const userId = u.userId
|
||||
if (!userId) return // 没 userId (兜底), 不查 biz_person
|
||||
// 1) 拉 biz_person
|
||||
let person = null
|
||||
try {
|
||||
const { data } = await listBizPerson({ userId, pageNum: 1, pageSize: 1 })
|
||||
const rows = data?.rows || []
|
||||
person = rows[0] || null
|
||||
} catch { /* 静默失败: dialog 仍打开, 用户手填 */ }
|
||||
if (!person) return
|
||||
guestDialog.form.department = person.department || ''
|
||||
guestDialog.form.position = person.position || ''
|
||||
// 2) 拉 biz_org → workUnit (优先用 JOIN 出来的 orgName; 没有再单查)
|
||||
if (person.orgName) {
|
||||
guestDialog.form.workUnit = person.orgName
|
||||
} else if (person.orgId) {
|
||||
try {
|
||||
const { data: org } = await bizGet('org', person.orgId)
|
||||
guestDialog.form.workUnit = org?.orgName || ''
|
||||
} catch { /* 静默 */ }
|
||||
}
|
||||
}
|
||||
|
||||
function onSupportIntent() {
|
||||
if (!canShowSupportBtn.value) return // 防御: 防止 v-if 被绕过
|
||||
if (supportSubmitting.value || supportSubmitted.value) return
|
||||
if (loggedIn.value) {
|
||||
doSubmitIntent('support')
|
||||
guestDialog.type = 'support'
|
||||
guestDialog.title = '表达支持意向'
|
||||
guestDialog.open = true
|
||||
prefillGuestFormFromUser() // 异步, 不 await; dialog 已先打开, 数据慢慢填进去
|
||||
} else {
|
||||
resetGuestForm()
|
||||
guestDialog.type = 'support'
|
||||
@@ -478,9 +530,13 @@ function onSupportIntent() {
|
||||
}
|
||||
|
||||
function onExecutionIntent() {
|
||||
if (!canShowExecutionBtn.value) return // 防御
|
||||
if (executionSubmitting.value || executionSubmitted.value) return
|
||||
if (loggedIn.value) {
|
||||
doSubmitIntent('execution')
|
||||
guestDialog.type = 'execution'
|
||||
guestDialog.title = '表达执行意向'
|
||||
guestDialog.open = true
|
||||
prefillGuestFormFromUser() // 异步, 不 await
|
||||
} else {
|
||||
resetGuestForm()
|
||||
guestDialog.type = 'execution'
|
||||
@@ -502,21 +558,14 @@ async function onGuestDialogConfirm() {
|
||||
finally { guestDialog.submitting = false }
|
||||
}
|
||||
|
||||
// 实际提交: 已登录不带 5 字段 (后端从 sys_user 取 user_id, 但姓名/手机号/单位仍需 dialog 收集 — 故未登录分支专门处理;
|
||||
// 已登录分支: 仍然弹 dialog 让用户填 5 字段, 仅 user_id 自动回填, 不复用登录信息是因为匿名流程设计的字段是访客视角的"姓名/手机号/单位/部门/职务",
|
||||
// 与登录专家视角的"姓名/手机号/工作单位/科室/职称" 不完全一致; 但支持/执行意向两表字段一致, 所以走 dialog)
|
||||
// 实际提交: 已登录用户由 onSupportIntent/onExecutionIntent 直接打开 dialog,
|
||||
// dialog 提交时调 onGuestDialogConfirm → 传 fields → 走 doSubmitIntent(type, fields)
|
||||
async function doSubmitIntent(type, fields) {
|
||||
const proj = ann.value || {}
|
||||
const projectId = proj.projectId || proj.id
|
||||
if (!projectId) return ElMessage.warning('项目ID缺失,无法提交')
|
||||
// fields 可能为空 (已登录快速通道) → 触发 dialog
|
||||
if (!fields) {
|
||||
resetGuestForm()
|
||||
guestDialog.type = type
|
||||
guestDialog.title = type === 'support' ? '表达支持意向' : '表达执行意向'
|
||||
guestDialog.open = true
|
||||
return
|
||||
}
|
||||
// 防御: fields 缺失时不开 dialog (dialog 已在 onSupportIntent/onExecutionIntent 里打开)
|
||||
if (!fields) return
|
||||
const setter = type === 'support' ? s => { supportSubmitting.value = s } : s => { executionSubmitting.value = s }
|
||||
const markDone = type === 'support' ? () => { supportSubmitted.value = true } : () => { executionSubmitted.value = true }
|
||||
setter(true)
|
||||
@@ -743,7 +792,40 @@ onBeforeUnmount(() => {
|
||||
|
||||
|
||||
|
||||
* {
|
||||
.logo,
|
||||
.nav-list,
|
||||
.nav-item,
|
||||
.nav-link,
|
||||
.logo-text,
|
||||
.logo-title,
|
||||
.logo-subtitle,
|
||||
.top-tools,
|
||||
.footer-main,
|
||||
.footer-brand,
|
||||
.footer-col,
|
||||
.footer-col h4,
|
||||
.brand-row,
|
||||
.footer-desc,
|
||||
.footer-col a,
|
||||
.footer-col p,
|
||||
.footer-qr,
|
||||
.footer-logo,
|
||||
.qr-image,
|
||||
.qr-label,
|
||||
.footer-bottom,
|
||||
.footer-brand-name,
|
||||
.footer-brand-en,
|
||||
.action-bar,
|
||||
.modal,
|
||||
.modal-qr,
|
||||
.modal-qr-wrap,
|
||||
.modal-title,
|
||||
.modal-tip,
|
||||
.modal-close,
|
||||
.qr-corner,
|
||||
.qr-loading,
|
||||
.qr-dot,
|
||||
.modal-tip {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
@@ -870,7 +952,18 @@ a { color: inherit; text-decoration: none; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
.top-tools .user-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
.top-tools .user-link:hover { opacity: .85; }
|
||||
|
||||
.login-btn {
|
||||
padding: 7px 20px;
|
||||
|
||||
Reference in New Issue
Block a user