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:
郭庆泰
2026-08-17 01:15:16 +08:00
parent a1c54842e2
commit b41cf77cac
16 changed files with 1168 additions and 42 deletions
+37
View File
@@ -107,3 +107,40 @@ export const downloadImportTemplate = (unitType) =>
// 专家导入模板下载
export const downloadExpertTemplate = () =>
request.get('/business/expert/importTemplate', { responseType: 'blob' })
// ========== 公示页意向 (公开匿名提交, 已在登录或未登录时均可调) ==========
// 提交支持意向: body { projectId, name, phone, workUnit, department?, position? }
export function submitPublicitySupportIntent(data) {
return request.post('/business/publicity/supportIntent', data)
}
// 提交执行意向
export function submitPublicityExecutionIntent(data) {
return request.post('/business/publicity/executionIntent', data)
}
// 持久化查重: GET /business/publicity/hasIntent?projectId=&phone=&type=support|execution
export function hasPublicityIntent(projectId, phone, type) {
return request.get('/business/publicity/hasIntent', { params: { projectId, phone, type } })
}
// ========== 公示页意向 管理端 (manager / admin 后台) ==========
// 列表分页 (后端 startPage 模式)
export function listPublicitySupportIntent(params) {
return request.get('/business/publicitySupportIntent/list', { params })
}
export function listPublicityExecutionIntent(params) {
return request.get('/business/publicityExecutionIntent/list', { params })
}
// 编辑 (改 intentStatus / remark)
export function updatePublicitySupportIntent(data, opts) {
return request.put('/business/publicitySupportIntent', data, opts)
}
export function updatePublicityExecutionIntent(data, opts) {
return request.put('/business/publicityExecutionIntent', data, opts)
}
// 删除 (单/多)
export function deletePublicitySupportIntent(ids) {
return request.delete(`/business/publicitySupportIntent/${Array.isArray(ids) ? ids.join(',') : ids}`)
}
export function deletePublicityExecutionIntent(ids) {
return request.delete(`/business/publicityExecutionIntent/${Array.isArray(ids) ? ids.join(',') : ids}`)
}
+121 -20
View File
@@ -2,24 +2,30 @@
<div class="page-card manager-exec-intent">
<div class="breadcrumb">首页 / 执行意向</div>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<!-- ========== 筛选区 ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="q.projectNo" clearable /></el-form-item>
<el-form-item label="项目名称"><el-input v-model="q.projectName" clearable /></el-form-item>
<el-form-item label="姓名"><el-input v-model="q.name" clearable /></el-form-item>
<el-form-item label="工作单位"><el-input v-model="q.workUnit" clearable /></el-form-item>
<el-form-item label="手机号"><el-input v-model="q.phone" clearable /></el-form-item>
<el-form-item label="签约状态">
<el-select v-model="q.onboardStatus" clearable>
<el-option label="待签约" value="待签约" /><el-option label="已签约" value="已签约" />
<el-option label="已拒绝" value="已拒绝" /><el-option label="已退出" value="已退出" />
<el-form-item label="审核状态">
<el-select v-model="q.intentStatus" clearable style="width: 130px">
<el-option label="待审核" value="待审核" />
<el-option label="已通过" value="已通过" />
<el-option label="已拒绝" 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>
<el-form-item>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
<el-button type="success" @click="exportCsv">导出CSV</el-button>
</el-form-item>
</el-form>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border>
<!-- ========== 表格 ========== -->
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelChange">
<el-table-column type="selection" width="46" />
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="projectNo" label="意向项目编号" width="140" />
<el-table-column prop="projectName" label="意向项目名称" min-width="180" show-overflow-tooltip />
@@ -28,14 +34,30 @@
<el-table-column prop="department" label="部门" width="120" />
<el-table-column prop="position" label="职务" width="100" />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column label="签约状态" width="100">
<el-table-column label="账号状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.onboardStatus==='已签约' ? 'success' : row.onboardStatus==='已拒绝' ? 'danger' : row.onboardStatus==='已退出' ? 'info' : 'warning'">{{ row.onboardStatus }}</el-tag>
<el-tag :type="row.userId ? 'success' : 'warning'">{{ row.userId ? '存在' : '不存在' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="审核状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.intentStatus)">{{ row.intentStatus || '待审核' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="{ row }">
<el-button v-if="row.intentStatus !== '已通过'" type="primary" link size="small" @click="changeStatus(row, '已通过')">通过</el-button>
<el-button v-if="row.intentStatus !== '已拒绝'" type="danger" link size="small" @click="changeStatus(row, '已拒绝')">拒绝</el-button>
<el-button v-if="row.intentStatus !== '待审核'" type="warning" link size="small" @click="changeStatus(row, '待审核')">重置</el-button>
<el-button type="danger" link size="small" @click="removeOne(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<div class="pager-left">
<el-button type="danger" :disabled="!selected.length" @click="removeBatch">批量删除 ({{ selected.length }})</el-button>
</div>
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
</div>
@@ -43,30 +65,109 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { bizList } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
listPublicityExecutionIntent,
updatePublicityExecutionIntent,
deletePublicityExecutionIntent
} from '@/api/public'
const q = ref({ projectNo: '', projectName: '', name: '', workUnit: '', phone: '', onboardStatus: '' })
const q = ref({ projectNo: '', projectName: '', name: '', workUnit: '', phone: '', intentStatus: '' })
const rows = ref([])
const selected = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
async function load() {
loading.value = true
try {
const { data } = await bizList('executionIntent', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []; page.total = data?.total || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
const { data } = await listPublicityExecutionIntent({ ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []
page.total = data?.total || 0
} catch {
rows.value = []; page.total = 0
} finally {
loading.value = false
}
}
function reset() {
q.value = { projectNo: '', projectName: '', name: '', workUnit: '', phone: '', intentStatus: '' }
page.pageNum = 1
load()
}
function onSelChange(arr) { selected.value = arr }
function statusTagType(s) {
if (s === '已通过') return 'success'
if (s === '已拒绝') return 'danger'
return 'warning'
}
async function changeStatus(row, status) {
try {
await updatePublicityExecutionIntent({ intentId: row.intentId, intentStatus: status }, { __silentError: true })
ElMessage.success(`${status === '已通过' ? '通过' : status === '已拒绝' ? '拒绝' : '重置'}`)
load()
} catch (e) {
ElMessage.error(e?.msg || '状态变更失败')
}
}
async function removeOne(row) {
try {
await ElMessageBox.confirm(`确认删除 ${row.name} 的执行意向?`, '提示', { type: 'warning' })
} catch { return }
try {
await deletePublicityExecutionIntent([row.intentId])
ElMessage.success('已删除')
load()
} catch (e) {
ElMessage.error(e?.msg || '删除失败')
}
}
async function removeBatch() {
if (!selected.value.length) return
try {
await ElMessageBox.confirm(`确认删除 ${selected.value.length} 条执行意向?`, '提示', { type: 'warning' })
} catch { return }
try {
const ids = selected.value.map(r => r.intentId)
await deletePublicityExecutionIntent(ids)
ElMessage.success('已删除')
load()
} catch (e) {
ElMessage.error(e?.msg || '删除失败')
}
}
function exportCsv() {
if (!rows.value.length) return ElMessage.warning('当前列表无数据')
const headers = ['项目编号', '项目名称', '姓名', '工作单位', '部门', '职务', '手机号', '账号状态', '审核状态', '创建时间']
const csvRows = rows.value.map(r => [
r.projectNo, r.projectName, r.name, r.workUnit, r.department, r.position,
r.phone, r.userId ? '存在' : '不存在', r.intentStatus || '待审核', r.createTime
].map(v => `"` + String(v ?? '').replace(/"/g, '""') + `"`).join(','))
const csv = '' + [headers.join(','), ...csvRows].join('\r\n')
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `执行意向_${new Date().toISOString().slice(0, 10)}.csv`
document.body.appendChild(a); a.click(); document.body.removeChild(a)
URL.revokeObjectURL(url)
ElMessage.success(`已导出 ${rows.value.length}`)
}
function reset() { q.value = { projectNo:'', projectName:'', name:'', workUnit:'', phone:'', onboardStatus:'' }; page.pageNum = 1; load() }
onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-exec-intent { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
</style>
.pager { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
.pager-left { display: flex; gap: 8px; }
</style>
+126 -20
View File
@@ -2,42 +2,69 @@
<div class="page-card manager-support-intent">
<div class="breadcrumb">首页 / 支持意向</div>
<!-- 提示语 (按原型 top=217-243) -->
<!-- 提示语 -->
<div class="hint-list">
<p>*项目公示中点击支持意向后显示在此</p>
<p>*支持意向不需要注册但是如果已经注册账号则显示账号状态</p>
<p>*项目公示中点击"表达支持意向"显示在此</p>
<p>*支持意向不需要注册注册账号自动回填用户状态</p>
<p>*根据手机号与系统账号进行匹配</p>
</div>
<!-- ========== 筛选区 ( People.vue 风格一致) ========== -->
<!-- ========== 筛选区 ========== -->
<el-form inline :model="q" class="filter-form">
<el-form-item label="项目编号"><el-input v-model="q.projectNo" placeholder="如 ZH-2026-658" clearable /></el-form-item>
<el-form-item label="意向项目名称"><el-input v-model="q.projectName" placeholder="输入意向项目名称" clearable /></el-form-item>
<el-form-item label="姓名"><el-input v-model="q.name" placeholder="输入姓名" clearable /></el-form-item>
<el-form-item label="工作单位"><el-input v-model="q.workUnit" placeholder="输入工作单位" clearable /></el-form-item>
<el-form-item label="手机号"><el-input v-model="q.phone" placeholder="输入手机号" clearable /></el-form-item>
<el-form-item label="审核状态">
<el-select v-model="q.intentStatus" clearable style="width: 130px">
<el-option label="待审核" value="待审核" />
<el-option label="已通过" value="已通过" />
<el-option label="已拒绝" value="已拒绝" />
</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="exportCsv">导出CSV</el-button>
</el-form-item>
</el-form>
<!-- ========== 表格 ( People.vue 风格一致, page-card 包裹) ========== -->
<el-table :data="rows" v-loading="loading" stripe border>
<!-- ========== 表格 ========== -->
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelChange">
<el-table-column type="selection" width="46" />
<el-table-column type="index" label="#" width="50" />
<el-table-column prop="projectNo" label="支持意向项目编号" width="140" align="center" />
<el-table-column prop="projectName" label="支持意向项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="projectNo" label="项目编号" width="140" align="center" />
<el-table-column prop="projectName" label="意向项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="name" label="姓名" width="80" align="center" />
<el-table-column prop="workUnit" label="工作单位名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="workUnit" label="工作单位" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" width="120" align="center" />
<el-table-column prop="position" label="职务" width="120" align="center" />
<el-table-column prop="phone" label="手机号" width="130" align="center" />
<el-table-column label="账号状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.accountStatus==='存在' ? 'success' : 'warning'">{{ row.accountStatus || '不存在' }}</el-tag>
<el-tag :type="row.userId ? 'success' : 'warning'">{{ row.userId ? '存在' : '不存在' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="审核状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.intentStatus)">{{ row.intentStatus || '待审核' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" align="center" />
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="{ row }">
<el-button v-if="row.intentStatus !== '已通过'" type="primary" link size="small" @click="changeStatus(row, '已通过')">通过</el-button>
<el-button v-if="row.intentStatus !== '已拒绝'" type="danger" link size="small" @click="changeStatus(row, '已拒绝')">拒绝</el-button>
<el-button v-if="row.intentStatus !== '待审核'" type="warning" link size="small" @click="changeStatus(row, '待审核')">重置</el-button>
<el-button type="danger" link size="small" @click="removeOne(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<div class="pager-left">
<el-button type="danger" :disabled="!selected.length" @click="removeBatch">批量删除 ({{ selected.length }})</el-button>
</div>
<el-pagination v-model:current-page="page.pageNum" v-model:page-size="page.pageSize" :total="page.total" :page-sizes="[10,20,50]" layout="total, sizes, prev, pager, next, jumper" @current-change="load" @size-change="load" />
</div>
</div>
@@ -45,34 +72,113 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { bizList } from '@/api/public'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
listPublicitySupportIntent,
updatePublicitySupportIntent,
deletePublicitySupportIntent
} from '@/api/public'
const q = ref({ projectNo: '', projectName: '', name: '', workUnit: '' })
const q = ref({ projectNo: '', projectName: '', name: '', workUnit: '', phone: '', intentStatus: '' })
const rows = ref([])
const selected = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
async function load() {
loading.value = true
try {
const { data } = await bizList('supportIntent', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []; page.total = data?.total || 0
} catch { rows.value = []; page.total = 0 }
finally { loading.value = false }
const { data } = await listPublicitySupportIntent({ ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = data?.rows || []
page.total = data?.total || 0
} catch {
rows.value = []; page.total = 0
} finally {
loading.value = false
}
}
function reset() {
q.value = { projectNo: '', projectName: '', name: '', workUnit: '', phone: '', intentStatus: '' }
page.pageNum = 1
load()
}
function onSelChange(arr) { selected.value = arr }
function statusTagType(s) {
if (s === '已通过') return 'success'
if (s === '已拒绝') return 'danger'
return 'warning'
}
async function changeStatus(row, status) {
try {
await updatePublicitySupportIntent({ intentId: row.intentId, intentStatus: status }, { __silentError: true })
ElMessage.success(`${status === '已通过' ? '通过' : status === '已拒绝' ? '拒绝' : '重置'}`)
load()
} catch (e) {
ElMessage.error(e?.msg || '状态变更失败')
}
}
async function removeOne(row) {
try {
await ElMessageBox.confirm(`确认删除 ${row.name} 的支持意向?`, '提示', { type: 'warning' })
} catch { return }
try {
await deletePublicitySupportIntent([row.intentId])
ElMessage.success('已删除')
load()
} catch (e) {
ElMessage.error(e?.msg || '删除失败')
}
}
async function removeBatch() {
if (!selected.value.length) return
try {
await ElMessageBox.confirm(`确认删除 ${selected.value.length} 条支持意向?`, '提示', { type: 'warning' })
} catch { return }
try {
const ids = selected.value.map(r => r.intentId)
await deletePublicitySupportIntent(ids)
ElMessage.success('已删除')
load()
} catch (e) {
ElMessage.error(e?.msg || '删除失败')
}
}
// 客户端导出 CSV (UTF-8 BOM 避免 Excel 乱码)
function exportCsv() {
if (!rows.value.length) return ElMessage.warning('当前列表无数据')
const headers = ['项目编号', '项目名称', '姓名', '工作单位', '部门', '职务', '手机号', '账号状态', '审核状态', '创建时间']
const csvRows = rows.value.map(r => [
r.projectNo, r.projectName, r.name, r.workUnit, r.department, r.position,
r.phone, r.userId ? '存在' : '不存在', r.intentStatus || '待审核', r.createTime
].map(v => `"` + String(v ?? '').replace(/"/g, '""') + `"`).join(','))
const csv = '' + [headers.join(','), ...csvRows].join('\r\n')
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `支持意向_${new Date().toISOString().slice(0, 10)}.csv`
document.body.appendChild(a); a.click(); document.body.removeChild(a)
URL.revokeObjectURL(url)
ElMessage.success(`已导出 ${rows.value.length}`)
}
function reset() { q.value = { projectNo:'', projectName:'', name:'', workUnit:'' }; page.pageNum = 1; load() }
onMounted(load)
</script>
<style scoped>
/* 整页面板 (与 People.vue 风格一致) */
.manager-support-intent { padding: 16px; }
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
.pager { display: flex; justify-content: space-between; align-items: center; margin-top: 12px; }
.pager-left { display: flex; gap: 8px; }
/* 红字提示 (与 People.vue 一致) */
.hint-list {
background: #fff;
border: 1px solid #f0f0f0;
+157 -1
View File
@@ -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 }