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:
@@ -0,0 +1,10 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 当前登录用户的专家信息 (按 token 拿 userId 查 biz_expert)
|
||||
* 返回 biz_expert 完整记录, 含 name / phone / department / title / workUnit 等
|
||||
* 若当前用户不是专家 / 没注册过, 返回 null
|
||||
*/
|
||||
export function getMyExpertProfile() {
|
||||
return request({ url: '/business/expert/profile', method: 'get' })
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 劳务协议模板 API
|
||||
* 全局共享, admin 在 /admin/labor-protocol 管理
|
||||
*/
|
||||
export function listLaborProtocolTemplates(params) {
|
||||
return request({ url: '/business/laborProtocolTemplate/list', method: 'get', params })
|
||||
}
|
||||
export function getLaborProtocolTemplate(id) {
|
||||
return request({ url: `/business/laborProtocolTemplate/${id}`, method: 'get' })
|
||||
}
|
||||
export function getDefaultLaborProtocolTemplate() {
|
||||
return request({ url: '/business/laborProtocolTemplate/default', method: 'get' })
|
||||
}
|
||||
export function listEnabledLaborProtocolTemplates() {
|
||||
return request({ url: '/business/laborProtocolTemplate/allEnabled', method: 'get' })
|
||||
}
|
||||
export function createLaborProtocolTemplate(data) {
|
||||
return request({ url: '/business/laborProtocolTemplate', method: 'post', data })
|
||||
}
|
||||
export function updateLaborProtocolTemplate(data) {
|
||||
return request({ url: '/business/laborProtocolTemplate', method: 'put', data })
|
||||
}
|
||||
export function deleteLaborProtocolTemplate(id) {
|
||||
return request({ url: `/business/laborProtocolTemplate/${id}`, method: 'delete' })
|
||||
}
|
||||
export function setDefaultLaborProtocolTemplate(id) {
|
||||
return request({ url: `/business/laborProtocolTemplate/${id}/default`, method: 'put' })
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 当前登录用户的"待签署"会议列表 (handsign 或 labor_protocol 任一为空)
|
||||
* 返回 [{id, meetingId, meetingName, startTime, ...}, ...]
|
||||
*/
|
||||
export function listUnsignedMeetingProtocols() {
|
||||
return request({ url: '/business/meetingAttendee/unsigned', method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新手写签名 (Base64 字符串, 直接存 DB longtext)
|
||||
* @param {number|string} id 中间表主键
|
||||
* @param {string} handsign Base64 编码的签名图片
|
||||
*/
|
||||
export function updateHandsign(id, handsign) {
|
||||
return request({ url: `/business/meetingAttendee/${id}/handsign`, method: 'put', data: { handsign } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新劳务协议 URL (OSS 文件 URL, 由前端 OssImageUploader 上传后传入)
|
||||
*/
|
||||
export function updateLaborProtocol(id, laborProtocol) {
|
||||
return request({ url: `/business/meetingAttendee/${id}/laborProtocol`, method: 'put', data: { laborProtocol } })
|
||||
}
|
||||
@@ -82,7 +82,8 @@ const MENU = {
|
||||
]},
|
||||
{ path: '/admin/manage', title: '网站管理', icon: Setting, children: [
|
||||
{ path: '/admin/article', title: '协议管理', icon: Files },
|
||||
{ path: '/admin/special-plan', title: '专项计划管理', icon: Compass }
|
||||
{ path: '/admin/special-plan', title: '专项计划管理', icon: Compass },
|
||||
{ path: '/admin/labor-protocol', title: '劳务协议配置', icon: Document }
|
||||
]},
|
||||
{ path: '/admin/account', title: '账号信息', icon: User }
|
||||
],
|
||||
|
||||
@@ -47,6 +47,7 @@ const routes = [
|
||||
{ path: 'special-plan', name: 'admin-special-plan', component: () => import('@/views/admin/BizSpecialPlanAdmin.vue'), meta: { title: '专项计划管理' } },
|
||||
{ path: 'special-plan/edit/:id', name: 'admin-special-plan-edit', component: () => import('@/views/admin/BizSpecialPlanEdit.vue'), meta: { title: '编辑专项计划' } },
|
||||
{ path: 'project-category', name: 'admin-project-category', component: () => import('@/views/admin/ProjectCategory.vue'), meta: { title: '项目类别管理' } },
|
||||
{ path: 'labor-protocol', name: 'admin-labor-protocol', component: () => import('@/views/admin/LaborProtocol.vue'), meta: { title: '劳务协议配置' } },
|
||||
{ path: 'account', name: 'admin-account', component: () => import('@/views/admin/Account.vue'), meta: { title: '账号信息' } }
|
||||
]
|
||||
},
|
||||
|
||||
@@ -260,7 +260,7 @@ function fmtTime(d) {
|
||||
// ===== 审核 =====
|
||||
const auditOpen = ref(false)
|
||||
// result: '2'=通过, '3'=拒绝 (BizAuditStatusEnum: 0未提交 1待审核 2通过 3拒绝)
|
||||
const auditForm = reactive({ expertId: '', name: '', phone: '', result: '2', opinion: '' })
|
||||
const auditForm = reactive({ expertId: null, name: '', phone: '', result: '2', opinion: '' })
|
||||
function onAudit(row) {
|
||||
auditForm.expertId = row.expertId
|
||||
auditForm.name = row.name
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<div class="page-card admin-labor-protocol">
|
||||
<div class="breadcrumb">首页 / 网站管理 / 劳务协议配置</div>
|
||||
|
||||
<!-- 筛选 -->
|
||||
<el-form inline :model="q" class="filter-form">
|
||||
<el-form-item label="模板名">
|
||||
<el-input v-model="q.templateName" placeholder="输入模板名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="q.status" placeholder="全部" clearable style="width:120px">
|
||||
<el-option label="启用" value="Y" />
|
||||
<el-option label="禁用" value="N" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
<el-button type="success" @click="openCreate">新建模板</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 列表 -->
|
||||
<el-table :data="rows" border stripe v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="templateName" label="模板名" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="sortOrder" label="排序" width="80" align="center" />
|
||||
<el-table-column label="默认" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.defaultFlag === 'Y'" type="success">默认</el-tag>
|
||||
<span v-else style="color:#c0c4cc">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'Y' ? 'success' : 'info'">{{ row.status === 'Y' ? '启用' : '禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="updateTime" label="更新时间" width="170" />
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" link type="warning" :disabled="row.defaultFlag === 'Y'" @click="setDefault(row)">设为默认</el-button>
|
||||
<el-button size="small" link type="danger" @click="removeRow(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pager">
|
||||
<el-pagination
|
||||
v-model:current-page="q.pageNum"
|
||||
v-model:page-size="q.pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="load"
|
||||
@size-change="load"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑 dialog -->
|
||||
<el-dialog v-model="editOpen" :title="form.id ? '编辑模板' : '新建模板'" width="780px" destroy-on-close>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="模板名" prop="templateName">
|
||||
<el-input v-model="form.templateName" placeholder="如: 默认劳务协议" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sortOrder">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="默认模板">
|
||||
<el-switch v-model="form.defaultFlag" active-value="Y" inactive-value="N" />
|
||||
<span style="margin-left:12px;color:#909399;font-size:12px">开启后其他模板自动取消默认</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio value="Y">启用</el-radio>
|
||||
<el-radio value="N">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板内容" prop="templateContent">
|
||||
<el-input v-model="form.templateContent" type="textarea" :rows="14"
|
||||
placeholder="HTML 模板, 支持占位符: {name} {phone} {会议名称} {会议时间} {会议地址} {劳务形式} {费用总额} ..." />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" placeholder="选填" maxlength="500" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editOpen = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const q = reactive({ templateName: '', status: '', pageNum: 1, pageSize: 20 })
|
||||
const rows = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const r = await request({ url: '/business/laborProtocolTemplate/list', method: 'get', params: { ...q } })
|
||||
rows.value = (r.data && r.data.rows) || r.rows || []
|
||||
total.value = (r.data && r.data.total) || r.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function reset() {
|
||||
q.templateName = ''
|
||||
q.status = ''
|
||||
q.pageNum = 1
|
||||
load()
|
||||
}
|
||||
|
||||
const editOpen = ref(false)
|
||||
const submitting = ref(false)
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
id: null,
|
||||
templateName: '',
|
||||
templateContent: '',
|
||||
defaultFlag: 'N',
|
||||
sortOrder: 0,
|
||||
status: 'Y',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
templateName: [{ required: true, message: '请输入模板名', trigger: 'blur' }],
|
||||
templateContent: [{ required: true, message: '请输入模板内容', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
Object.assign(form, {
|
||||
id: null,
|
||||
templateName: '',
|
||||
templateContent: '',
|
||||
defaultFlag: 'N',
|
||||
sortOrder: 0,
|
||||
status: 'Y',
|
||||
remark: ''
|
||||
})
|
||||
editOpen.value = true
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
templateName: row.templateName,
|
||||
templateContent: row.templateContent,
|
||||
defaultFlag: row.defaultFlag || 'N',
|
||||
sortOrder: row.sortOrder || 0,
|
||||
status: row.status || 'Y',
|
||||
remark: row.remark || ''
|
||||
})
|
||||
editOpen.value = true
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
await formRef.value.validate()
|
||||
submitting.value = true
|
||||
try {
|
||||
if (form.id) {
|
||||
await request({ url: '/business/laborProtocolTemplate', method: 'put', data: form })
|
||||
ElMessage.success('已更新')
|
||||
} else {
|
||||
await request({ url: '/business/laborProtocolTemplate', method: 'post', data: form })
|
||||
ElMessage.success('已新建')
|
||||
}
|
||||
editOpen.value = false
|
||||
load()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.msg || '保存失败')
|
||||
} finally { submitting.value = false }
|
||||
}
|
||||
|
||||
async function setDefault(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认将"${row.templateName}"设为默认模板? 其他默认模板将自动取消`, '提示', { type: 'warning' })
|
||||
} catch { return }
|
||||
try {
|
||||
await request({ url: `/business/laborProtocolTemplate/${row.id}/default`, method: 'put' })
|
||||
ElMessage.success('已设为默认')
|
||||
load()
|
||||
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
|
||||
}
|
||||
|
||||
async function removeRow(row) {
|
||||
if (row.defaultFlag === 'Y') {
|
||||
return ElMessage.warning('默认模板不能删除, 请先取消默认')
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除"${row.templateName}"?`, '提示', { type: 'warning' })
|
||||
} catch { return }
|
||||
try {
|
||||
await request({ url: `/business/laborProtocolTemplate/${row.id}`, method: 'delete' })
|
||||
ElMessage.success('已删除')
|
||||
load()
|
||||
} catch (e) { ElMessage.error(e?.msg || '删除失败') }
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-labor-protocol { padding: 16px; max-width: 1400px; }
|
||||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||
.filter-form { margin-bottom: 12px; }
|
||||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
:deep(.el-textarea__inner) { font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; }
|
||||
</style>
|
||||
@@ -267,6 +267,7 @@ async function afterLogin(token, displayName, fallbackRole) {
|
||||
userId: u.userId,
|
||||
userName: u.userName || displayName,
|
||||
nickName: u.nickName || displayName,
|
||||
phonenumber: u.phonenumber || '',
|
||||
accountType: u.accountType || 'MAIN',
|
||||
parentUserId: u.parentUserId || null,
|
||||
role
|
||||
@@ -277,6 +278,7 @@ async function afterLogin(token, displayName, fallbackRole) {
|
||||
userId: null,
|
||||
userName: displayName,
|
||||
nickName: displayName,
|
||||
phonenumber: '',
|
||||
accountType: 'MAIN',
|
||||
parentUserId: null,
|
||||
role: fallbackRole
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
<el-input v-model="form.workUnit" placeholder="请输入工作单位(医院全称)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="科室" prop="department">
|
||||
<DoctorDeptSelect v-model="form.department" placeholder="请输入所在科室" />
|
||||
<DoctorDeptSelect v-model="form.department" value-field="label" placeholder="请输入所在科室" />
|
||||
</el-form-item>
|
||||
<el-form-item label="职称" prop="doctorTitle">
|
||||
<DoctorTitleSelect v-model="form.doctorTitle" placeholder="请输入职称" />
|
||||
<DoctorTitleSelect v-model="form.doctorTitle" value-field="label" placeholder="请输入职称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号码" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入手机号码" maxlength="11" />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<!-- 欢迎栏 -->
|
||||
<div class="welcome-bar">
|
||||
<div class="welcome-text">
|
||||
<h2>下午好,{{ store.user?.userName || '专家' }}专家</h2>
|
||||
<h2>下午好,{{ displayName }}专家</h2>
|
||||
<p>欢迎使用项目管理系统</p>
|
||||
</div>
|
||||
<div class="welcome-time">
|
||||
@@ -37,13 +37,11 @@
|
||||
<a class="more" @click.prevent="$router.push('/doctor/submissions')">更多 →</a>
|
||||
</h2>
|
||||
<ul class="simple-list">
|
||||
<li class="simple-item" v-for="s in pendingAgreements" :key="s.planId">
|
||||
<li class="simple-item" v-for="s in pendingAgreements" :key="s.id">
|
||||
<div class="item-main">
|
||||
<span class="item-title">{{ s.planName }}</span>
|
||||
<span class="item-title">{{ s.meetingName || ('会议 #' + s.meetingId) }}</span>
|
||||
</div>
|
||||
<span class="item-status" :class="{ done: s.status === '2' }">
|
||||
{{ s.status === '2' ? '已通过' : (s.status === '3' ? '已退回' : (s.status === '1' ? '审核中' : '待提交')) }}
|
||||
</span>
|
||||
<span class="item-status">待签署</span>
|
||||
</li>
|
||||
<li v-if="!pendingAgreements.length" class="empty">暂无待签协议</li>
|
||||
</ul>
|
||||
@@ -72,15 +70,23 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { bizList, listMyMessages } from '@/api/public'
|
||||
import { getMyExpertProfile } from '@/api/business/expert'
|
||||
import { listUnsignedMeetingProtocols } from '@/api/business/meetingAttendee'
|
||||
|
||||
const store = useUserStore()
|
||||
|
||||
const upcomingMeetings = ref([])
|
||||
const pendingAgreements = ref([])
|
||||
const notices = ref([])
|
||||
// 专家真实姓名 (从 biz_expert.name 拿, 不显示 sys_user.userName (登录账号/手机号))
|
||||
const expertName = ref('')
|
||||
// 兜底显示名: 优先专家真实姓名 > nickName > userName > '专家'
|
||||
const displayName = computed(() =>
|
||||
expertName.value || store.user?.nickName || store.user?.userName || '专家'
|
||||
)
|
||||
|
||||
const nowTime = ref('')
|
||||
const nowDate = ref('')
|
||||
@@ -109,13 +115,20 @@ async function load() {
|
||||
upcomingMeetings.value = (data?.rows || []).slice(0, 5)
|
||||
} catch (e) { upcomingMeetings.value = [] }
|
||||
|
||||
// 待签署协议 (改走 biz_project_plan, 后端 doctor 角色已自动按当前用户过滤)
|
||||
// 当前用户的专家真实姓名 (用于欢迎栏)
|
||||
try {
|
||||
const { data } = await bizList('projectPlan', { pageNum: 1, pageSize: 5 })
|
||||
pendingAgreements.value = (data?.rows || []).slice(0, 5).map(s => ({
|
||||
planId: s.planId,
|
||||
planName: s.planName,
|
||||
status: s.status
|
||||
const { data } = await getMyExpertProfile()
|
||||
expertName.value = data?.name || ''
|
||||
} catch (e) { expertName.value = '' }
|
||||
|
||||
// 待签署协议 (v3: 改走 biz_meeting_attendee, 任一未签即显示)
|
||||
try {
|
||||
const { data } = await listUnsignedMeetingProtocols()
|
||||
pendingAgreements.value = (data || []).slice(0, 5).map(s => ({
|
||||
id: s.id, // biz_meeting_attendee.id (用于签署接口)
|
||||
meetingId: s.meetingId,
|
||||
meetingName: s.meetingName,
|
||||
startTime: s.startTime
|
||||
}))
|
||||
} catch (e) { pendingAgreements.value = [] }
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<el-form :model="form" label-width="120px" :rules="rules" ref="formRef" class="account-form">
|
||||
<el-form-item label="姓名" prop="nickName"><el-input v-model="form.nickName" placeholder="请输入姓名" /></el-form-item>
|
||||
<el-form-item label="手机号" prop="phonenumber"><el-input v-model="form.phonenumber" placeholder="请输入手机号" /></el-form-item>
|
||||
<el-form-item label="手机号" prop="phonenumber"><el-input v-model="form.phonenumber" placeholder="请输入手机号" maxlength="11" /></el-form-item>
|
||||
|
||||
<el-form-item label="原密码" prop="oldPassword"><el-input v-model="form.oldPassword" type="password" show-password placeholder="请输入原密码" /></el-form-item>
|
||||
<el-form-item label="新密码" prop="newPassword"><el-input v-model="form.newPassword" type="password" show-password placeholder="请输入新密码" /></el-form-item>
|
||||
|
||||
@@ -278,7 +278,7 @@ function fmtTime(d) {
|
||||
const editOpen = ref(false)
|
||||
const editFormRef = ref(null)
|
||||
const editForm = reactive({
|
||||
expertId: '', name: '', phone: '', workUnit: '', department: '', title: '',
|
||||
expertId: null, name: '', phone: '', workUnit: '', department: '', title: '',
|
||||
practiceCertUrl: '', titleCertUrl: ''
|
||||
})
|
||||
const editRules = {
|
||||
@@ -289,7 +289,7 @@ const editRules = {
|
||||
title: [{ required: true, message: '请选择职称', trigger: 'change' }]
|
||||
}
|
||||
function openAdd() {
|
||||
Object.assign(editForm, { expertId: '', name: '', phone: '', workUnit: '', department: '', title: '', practiceCertUrl: '', titleCertUrl: '' })
|
||||
Object.assign(editForm, { expertId: null, name: '', phone: '', workUnit: '', department: '', title: '', practiceCertUrl: '', titleCertUrl: '' })
|
||||
editOpen.value = true
|
||||
}
|
||||
// 修改按钮: 跳转独立页 /manager/experts/edit/:id (参考 /admin/experts/new)
|
||||
@@ -316,7 +316,7 @@ async function submitEdit() {
|
||||
// ========== 审核 dialog ==========
|
||||
const auditOpen = ref(false)
|
||||
// result: '2'=通过, '3'=拒绝 (BizAuditStatusEnum: 0未提交 1待审核 2通过 3拒绝)
|
||||
const auditForm = reactive({ expertId: '', name: '', phone: '', result: '2', opinion: '' })
|
||||
const auditForm = reactive({ expertId: null, name: '', phone: '', result: '2', opinion: '' })
|
||||
function onAudit(row) {
|
||||
auditForm.expertId = row.expertId
|
||||
auditForm.name = row.name
|
||||
|
||||
@@ -410,8 +410,6 @@ function confirmDeleteAnn() {
|
||||
publishUrl: '',
|
||||
invitationUrl: '',
|
||||
supportLetterUrl: '',
|
||||
noticeUrl: '',
|
||||
scheduleUrl: '',
|
||||
isPublished: '0'
|
||||
})
|
||||
.then(() => { ElMessage.success(`已删除项目 ${delAnnTargetRow.value.projectNo} 的公告`); delAnnModalOpen.value = false; load() })
|
||||
|
||||
@@ -239,12 +239,10 @@ async function loadProject() {
|
||||
if (m) { n.url = m.url || ''; n.name = m.name || '' }
|
||||
}
|
||||
} else {
|
||||
// 兼容老数据
|
||||
// 兼容老数据 (DB 实测只剩 invitationUrl/supportLetterUrl/publishUrl 3 个独立列)
|
||||
const noticeMap = {
|
||||
invitation: p.invitationUrl,
|
||||
supportLetter: p.supportLetterUrl,
|
||||
notice: p.noticeUrl,
|
||||
schedule: p.scheduleUrl,
|
||||
publish: p.publishUrl
|
||||
}
|
||||
for (const n of form.notices) {
|
||||
@@ -307,7 +305,7 @@ async function submit(mode = 'save') {
|
||||
leadUserId: form.leadUserId,
|
||||
isBidProject: form.isBidProject
|
||||
}
|
||||
// 公告文件以 publicityFiles JSON 数组存储 (替代旧 invitationUrl/supportLetterUrl/noticeUrl/scheduleUrl)
|
||||
// 公告文件以 publicityFiles JSON 数组存储 (替代旧 invitationUrl/supportLetterUrl/publishUrl 独立列)
|
||||
payload.publicityFiles = JSON.stringify(uploaded.map(n => ({
|
||||
type: n.key,
|
||||
label: n.label,
|
||||
@@ -318,8 +316,6 @@ async function submit(mode = 'save') {
|
||||
for (const n of uploaded) {
|
||||
if (n.key === 'invitation') payload.invitationUrl = n.url
|
||||
else if (n.key === 'supportLetter') payload.supportLetterUrl = n.url
|
||||
else if (n.key === 'notice') payload.noticeUrl = n.url
|
||||
else if (n.key === 'schedule') payload.scheduleUrl = n.url
|
||||
}
|
||||
// 发布时设置 is_published='1' + publish_time (同步后端字段)
|
||||
if (mode === 'publish') {
|
||||
|
||||
@@ -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