feat(publicity): 公示页支持/执行意向 (匿名提交 + 管理审核)
- 新增 biz_publicity_support_intent / biz_publicity_execution_intent 两张独立表 (与已登录视角的 biz_support_intent / biz_execution_intent 解耦, 避免匿名数据污染强绑表) - 后端 BizPublicityIntentController: 公开提交 + 公开查重 (按 project_id+phone+source) + 管理 CRUD 已登录直接取 user_id, 未登录但 phone 命中 sys_user 仍自动关联 user_id (不回强制登录) - /business/publicity/** 加 Spring Security permitAll - 前端 PublicityDetail.vue: 右侧 2 个意向按钮 (已登录/匿名都弹 dialog 填 5 字段), 已提交状态用 hasPublicityIntent 按 phonenumber 持久化查回 - 重写 manager/SupportIntent.vue + manager/ExecIntent.vue: 列表/筛选/CSV导出/状态变更/批量删除
This commit is contained in:
@@ -98,6 +98,12 @@
|
||||
</svg>
|
||||
<span>{{ signed ? '已报名' : (signing ? '报名中…' : '立即报名') }}</span>
|
||||
</button>
|
||||
<button class="action-btn primary" :disabled="supportSubmitting || supportSubmitted" @click="onSupportIntent">
|
||||
<span>{{ supportSubmitted ? '已支持' : (supportSubmitting ? '提交中…' : '表达支持意向') }}</span>
|
||||
</button>
|
||||
<button class="action-btn primary" :disabled="executionSubmitting || executionSubmitted" @click="onExecutionIntent">
|
||||
<span>{{ executionSubmitted ? '已表达意向' : (executionSubmitting ? '提交中…' : '表达执行意向') }}</span>
|
||||
</button>
|
||||
<button class="action-btn primary share-btn" @click="showQr = true">
|
||||
<svg class="btn-icon" viewBox="0 0 24 24" fill="currentColor" width="14" height="14">
|
||||
<path d="M3 11h8V3H3v8zm2-6h4v4H5V5zm8-2v8h8V3h-8zm6 6h-4V5h4v4zM3 21h8v-8H3v8zm2-6h4v4H5v-4z"/>
|
||||
@@ -177,15 +183,51 @@
|
||||
<el-button type="primary" :disabled="!qrUrl" @click="onDownloadQr">下载二维码</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ========== 匿名意向收集 dialog (未登录时弹出, 已登录时直接提交) ========== -->
|
||||
<el-dialog
|
||||
v-model="guestDialog.open"
|
||||
:title="guestDialog.title"
|
||||
width="480px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form :model="guestDialog.form" label-width="100px">
|
||||
<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-input v-model="guestDialog.form.phone" placeholder="请输入手机号" maxlength="11" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="工作单位 *" required>
|
||||
<el-input v-model="guestDialog.form.workUnit" placeholder="请输入工作单位" maxlength="200" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门">
|
||||
<el-input v-model="guestDialog.form.department" placeholder="选填" maxlength="100" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="职务">
|
||||
<el-input v-model="guestDialog.form.position" placeholder="选填" maxlength="100" clearable />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="guestDialog.open = false">取消</el-button>
|
||||
<el-button type="primary" :loading="guestDialog.submitting" @click="onGuestDialogConfirm">提交意向</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ref, computed, watch, reactive, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { logout as logoutApi } from '@/api/auth'
|
||||
import {
|
||||
submitPublicitySupportIntent,
|
||||
submitPublicityExecutionIntent,
|
||||
hasPublicityIntent
|
||||
} from '@/api/public'
|
||||
import request from '@/utils/request'
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
@@ -318,6 +360,7 @@ async function load() {
|
||||
if (firstKey) activeTab.value = firstKey
|
||||
// 项目加载完, 同步查"是否已报名"
|
||||
checkSigned()
|
||||
refreshIntentStatus()
|
||||
} catch (e) {
|
||||
console.error('[publicity-detail] load failed', e)
|
||||
ann.value = null
|
||||
@@ -384,6 +427,119 @@ async function onSignup() {
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 公示页-支持/执行意向 (匿名 + 已登录均可) ============
|
||||
const supportSubmitting = ref(false)
|
||||
const supportSubmitted = ref(false)
|
||||
const executionSubmitting = ref(false)
|
||||
const executionSubmitted = ref(false)
|
||||
|
||||
// 持久化"已提交"标记 - 已登录按 sys_user.phonenumber, 未登录无身份只能本次会话判定
|
||||
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 {
|
||||
const res = await hasPublicityIntent(projectId, phone, type)
|
||||
return res?.data === true
|
||||
} catch (e) { return false }
|
||||
}
|
||||
|
||||
async function refreshIntentStatus() {
|
||||
supportSubmitted.value = await checkIntentSubmitted('support')
|
||||
executionSubmitted.value = await checkIntentSubmitted('execution')
|
||||
}
|
||||
|
||||
// 匿名 dialog (未登录时弹, 已登录直接提交)
|
||||
const guestDialog = reactive({
|
||||
open: false,
|
||||
title: '',
|
||||
type: '', // 'support' | 'execution'
|
||||
submitting: false,
|
||||
form: { name: '', phone: '', workUnit: '', department: '', position: '' }
|
||||
})
|
||||
|
||||
function resetGuestForm() {
|
||||
guestDialog.form = { name: '', phone: '', workUnit: '', department: '', position: '' }
|
||||
}
|
||||
|
||||
function onSupportIntent() {
|
||||
if (supportSubmitting.value || supportSubmitted.value) return
|
||||
if (loggedIn.value) {
|
||||
doSubmitIntent('support')
|
||||
} else {
|
||||
resetGuestForm()
|
||||
guestDialog.type = 'support'
|
||||
guestDialog.title = '表达支持意向'
|
||||
guestDialog.open = true
|
||||
}
|
||||
}
|
||||
|
||||
function onExecutionIntent() {
|
||||
if (executionSubmitting.value || executionSubmitted.value) return
|
||||
if (loggedIn.value) {
|
||||
doSubmitIntent('execution')
|
||||
} else {
|
||||
resetGuestForm()
|
||||
guestDialog.type = 'execution'
|
||||
guestDialog.title = '表达执行意向'
|
||||
guestDialog.open = true
|
||||
}
|
||||
}
|
||||
|
||||
async function onGuestDialogConfirm() {
|
||||
const f = guestDialog.form
|
||||
if (!f.name?.trim()) return ElMessage.warning('请输入姓名')
|
||||
if (!f.phone?.trim()) return ElMessage.warning('请输入手机号')
|
||||
if (!f.workUnit?.trim()) return ElMessage.warning('请输入工作单位名称')
|
||||
guestDialog.submitting = true
|
||||
try {
|
||||
await doSubmitIntent(guestDialog.type, { name: f.name, phone: f.phone, workUnit: f.workUnit, department: f.department, position: f.position })
|
||||
guestDialog.open = false
|
||||
} catch { /* toast 由 doSubmitIntent 处理 */ }
|
||||
finally { guestDialog.submitting = false }
|
||||
}
|
||||
|
||||
// 实际提交: 已登录不带 5 字段 (后端从 sys_user 取 user_id, 但姓名/手机号/单位仍需 dialog 收集 — 故未登录分支专门处理;
|
||||
// 已登录分支: 仍然弹 dialog 让用户填 5 字段, 仅 user_id 自动回填, 不复用登录信息是因为匿名流程设计的字段是访客视角的"姓名/手机号/单位/部门/职务",
|
||||
// 与登录专家视角的"姓名/手机号/工作单位/科室/职称" 不完全一致; 但支持/执行意向两表字段一致, 所以走 dialog)
|
||||
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
|
||||
}
|
||||
const setter = type === 'support' ? s => { supportSubmitting.value = s } : s => { executionSubmitting.value = s }
|
||||
const markDone = type === 'support' ? () => { supportSubmitted.value = true } : () => { executionSubmitted.value = true }
|
||||
setter(true)
|
||||
try {
|
||||
const payload = { projectId, ...fields }
|
||||
const fn = type === 'support' ? submitPublicitySupportIntent : submitPublicityExecutionIntent
|
||||
const res = await fn(payload)
|
||||
if (res?.code === 200) {
|
||||
markDone()
|
||||
ElMessage.success(type === 'support' ? '已记录您的支持意向' : '已记录您的执行意向')
|
||||
} else {
|
||||
const msg = res?.msg || '提交失败,请稍后再试'
|
||||
if (typeof msg === 'string' && msg.includes('已提交')) markDone()
|
||||
ElMessage.error(msg)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[publicity-detail] submit intent failed', e)
|
||||
ElMessage.error('提交失败,请稍后再试')
|
||||
} finally {
|
||||
setter(false)
|
||||
}
|
||||
}
|
||||
|
||||
function onKeydown(e) { if (e.key === 'Escape') showQr.value = false }
|
||||
function handleScroll() { isScrolled.value = window.scrollY > 10 }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user