feat: 会议提交剩余时间+解冻 + 会务/劳务材料批量下载 + 多角色人员/账号模块完善

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
郭庆泰
2026-08-25 06:53:18 +08:00
co-authored by Claude
parent 0c882ae846
commit 591a43fe61
126 changed files with 3789 additions and 1334 deletions
+47 -49
View File
@@ -5,7 +5,7 @@
<el-form inline :model="q" class="filter-form">
<el-form-item label="支持单位:">
<el-select
v-model="q.sponsorAdminUserIds"
v-model="q.sponsorOrgIds"
multiple
filterable
remote
@@ -17,12 +17,12 @@
placeholder="输入支持单位名称搜索"
style="width:260px"
>
<el-option v-for="o in sponsorOrgOptions" :key="o.userId" :label="o.orgName" :value="o.userId" />
<el-option v-for="o in sponsorOrgOptions" :key="o.orgId" :label="o.orgName" :value="o.orgId" />
</el-select>
</el-form-item>
<el-form-item label="服务机构:">
<el-select
v-model="q.execAdminUserIds"
v-model="q.execOrgIds"
multiple
filterable
remote
@@ -34,7 +34,7 @@
placeholder="输入服务机构名称搜索"
style="width:260px"
>
<el-option v-for="o in executorOrgOptions" :key="o.userId" :label="o.orgName" :value="o.userId" />
<el-option v-for="o in executorOrgOptions" :key="o.orgId" :label="o.orgName" :value="o.orgId" />
</el-select>
</el-form-item>
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
@@ -60,7 +60,6 @@
<el-option label="已结算" value="Y" /><el-option label="未结算" value="N" />
</el-select>
</el-form-item>
<el-form-item label="评价得分:"><el-input v-model="q.managerScore" placeholder="输入评分" clearable /></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>
@@ -119,22 +118,22 @@
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<div class="op-cell">
<el-button link type="primary" @click="viewDetail(row)">查看</el-button>
<el-button link type="primary" @click="doEdit(row)">编辑</el-button>
<el-button link type="primary" @click="doCreateMeeting(row)">建会</el-button>
<el-button link type="primary" @click="doAssign(row)">分配</el-button>
<el-dropdown trigger="click" @command="(cmd) => onAction(cmd, row)">
<el-button link type="primary" class="op-dropdown">
更多<el-icon class="op-caret"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="createMeeting">建会</el-dropdown-item>
<el-dropdown-item v-if="row.isFinished !== '1' && row.isFinished !== 1" command="close">结题</el-dropdown-item>
<el-dropdown-item command="assign">项目分配</el-dropdown-item>
<el-dropdown-item command="view">查看</el-dropdown-item>
<el-dropdown-item command="edit">编辑</el-dropdown-item>
<el-dropdown-item command="activate">开通</el-dropdown-item>
<el-dropdown-item command="delAnn">删除公告</el-dropdown-item>
<el-dropdown-item command="rate">执行单位评分</el-dropdown-item>
<el-dropdown-item command="exportExperts" divided>导出报名专家</el-dropdown-item>
<el-dropdown-item v-if="row.isPublished === '1' || row.publishUrl" command="activate" divided>开通</el-dropdown-item>
<el-dropdown-item v-if="row.isPublished === '1' || row.publishUrl" command="delAnn" divided>删除公告</el-dropdown-item>
<el-dropdown-item v-if="canDelete" command="delete" divided>删除项目</el-dropdown-item>
<el-dropdown-item command="exportExperts">导出报名专家</el-dropdown-item>
<el-dropdown-item v-if="canDelete" command="delete">删除项目</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -180,7 +179,7 @@
<!-- 开通专用字段 -->
<template v-else-if="batchKind==='activate'">
<p>已选择 <b>{{ selection.length }}</b> 个项目</p>
<el-form-item label="开通截止时间"><el-date-picker v-model="activateDeadline" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" /></el-form-item>
<el-form-item label="开通截止时间"><el-date-picker v-model="activateDeadline" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
</template>
<template #footer>
<el-button @click="batchOpen=false">取消</el-button>
@@ -196,8 +195,11 @@
</template>
</el-dialog>
<!-- ============= 单行开通 modal ============= -->
<el-dialog v-model="activateModalOpen" title="您确定要开通吗?" width="420px" :show-close="false">
<!-- ============= 单行开通 modal ( open_status=Y + 开通截止时间 open_deadline) ============= -->
<el-dialog v-model="activateModalOpen" title="开通项目" width="420px" :show-close="false">
<el-form-item label="开通截止时间">
<el-date-picker v-model="activateDeadlineSingle" type="date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
<template #footer>
<el-button type="primary" @click="confirmActivate">确定</el-button>
<el-button @click="activateModalOpen=false">取消</el-button>
@@ -248,7 +250,7 @@ const userStore = useUserStore()
const router = useRouter()
const route = useRoute()
const q = ref({ sponsorAdminUserIds: [], execAdminUserIds: [], projectNo: '', projectName: '', projectForm: '', startTime: '', endTime: '', isFinished: '', isSettled: '', managerScore: '' })
const q = ref({ sponsorOrgIds: [], execOrgIds: [], projectNo: '', projectName: '', projectForm: '', startTime: '', endTime: '', isFinished: '', isSettled: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
@@ -266,19 +268,19 @@ async function load() {
}
// 操作列按钮 handlers
function onAction(cmd, row) {
if (cmd === 'createMeeting') doCreateMeeting(row)
else if (cmd === 'close') openClose(row)
else if (cmd === 'assign') doAssign(row)
else if (cmd === 'rate') openSingleScore(row)
else if (cmd === 'exportExperts') exportExperts(row)
if (cmd === 'close') openClose(row)
else if (cmd === 'view') viewDetail(row)
else if (cmd === 'edit') doEdit(row)
else if (cmd === 'activate') openActivate(row)
else if (cmd === 'delAnn') openDeleteAnnouncement(row)
else if (cmd === 'rate') openSingleScore(row)
else if (cmd === 'exportExperts') exportExperts(row)
else if (cmd === 'delete') doDelete(row)
}
// 仅 admin / manager 可见删除按钮 (前端 v-if, 后端 controller 再兜底 role 校验)
// 仅 admin 可见删除按钮 (前端 v-if, 后端 controller 再兜底 role 校验)
const canDelete = computed(() => {
const r = userStore.role
return r === 'admin' || r === 'manager'
return r === 'admin'
})
function doCreateMeeting(row) { router.push(`/manager/meetings/new?projectId=${row.projectId}`) }
function doAssign(row) {
@@ -288,7 +290,7 @@ function doAssign(row) {
function doEdit(row) { router.push(`/manager/projects/edit/${row.projectId}`) }
// 删除项目 (软删除 + 级联置 5 张项目表 + 项目下所有会议 is_deleted=1)
// 后端 BizProjectController.remove 已强制 role in (admin, manager), 前端只做可见性 v-if
// 后端 BizProjectController.remove 已强制仅 admin, 前端只做可见性 v-if
async function doDelete(row) {
try {
await ElMessageBox.confirm(
@@ -335,7 +337,7 @@ async function onBatchDelete() {
}
}
function reset() { q.value = { sponsorAdminUserIds:[], execAdminUserIds:[], projectNo:'', projectName:'', projectForm:'', startTime:'', endTime:'', isFinished:'', isSettled:'', managerScore:'' }; page.pageNum = 1; load() }
function reset() { q.value = { sponsorOrgIds:[], execOrgIds:[], projectNo:'', projectName:'', projectForm:'', startTime:'', endTime:'', isFinished:'', isSettled:'' }; page.pageNum = 1; load() }
// 读 URL query 写入 q (Workbench KPI 卡跳转时带 ?isFinished=1 / ?isSettled=Y 等, 让列表页自动应用筛选)
// 注意: 只读不改 URL — 列表页内 reset()/search 不反向写 URL, Workbench 是 source of truth
@@ -393,11 +395,13 @@ function confirmClose() {
function openActivate(row) {
activateTargetRow.value = row
activateDeadlineSingle.value = row.openDeadline || ''
activateModalOpen.value = true
}
function confirmActivate() {
if (!activateTargetRow.value) return
bizUpdate('project', { projectId: activateTargetRow.value.projectId, isFinished: '0' })
if (!activateDeadlineSingle.value) return ElMessage.warning('请选择开通截止时间')
bizUpdate('project', { projectId: activateTargetRow.value.projectId, openStatus: 'Y', openDeadline: activateDeadlineSingle.value })
.then(() => { ElMessage.success('已开通'); activateModalOpen.value = false; load() })
.catch(() => ElMessage.error('开通失败'))
}
@@ -408,15 +412,8 @@ function openDeleteAnnouncement(row) {
}
function confirmDeleteAnn() {
if (!delAnnTargetRow.value) return
// 清空全部公示 URL + 取消发布状态
// (mapper.updateByPrimaryKey 的 trim prefix="SET" 必须有字段, 不能只传空字符串)
bizUpdate('project', {
projectId: delAnnTargetRow.value.projectId,
publishUrl: '',
invitationUrl: '',
supportLetterUrl: '',
isPublished: '0'
})
// 删除公告: 后端专用端点, 将 3 个公示 URL 置 NULL (未发布由 URL 为空判定, 无独立 is_published 列)
request.delete(`/business/project/${delAnnTargetRow.value.projectId}/announcement`)
.then(() => { ElMessage.success(`已删除项目 ${delAnnTargetRow.value.projectNo} 的公告`); delAnnModalOpen.value = false; load() })
.catch(() => ElMessage.error('删除失败'))
}
@@ -500,6 +497,7 @@ const batchSubmitting = ref(false)
const batchMap = { score: '批量评分', close: '批量结题', activate: '批量开通' }
const batchScoreForm = reactive({ qualityScore: 5, responseScore: 5, cooperationScore: 5, complianceScore: 5 }) // 批量评分: 共享一份 4 维度
const activateDeadline = ref('')
const activateDeadlineSingle = ref('')
// 5 个单行 modal 状态
const closeModalOpen = ref(false)
@@ -564,7 +562,7 @@ async function confirmBatch() {
if (!activateDeadline.value) return ElMessage.warning('请选择开通截止时间')
try {
batchSubmitting.value = true
await Promise.all(selection.value.map(r => bizUpdate('project', { projectId: r.projectId, endTime: activateDeadline.value })))
await Promise.all(selection.value.map(r => bizUpdate('project', { projectId: r.projectId, openStatus: 'Y', openDeadline: activateDeadline.value })))
ElMessage.success(`已批量开通 ${selection.value.length} 个项目`)
batchOpen.value = false
selection.value = []
@@ -632,12 +630,12 @@ function searchSponsorOrgsForFilter(query) {
if (query) params.orgName = query
const r = await listSponsorOrgs(params)
sponsorOrgOptions.value = r.data || []
// 已选项不在结果里时按 userId 单独拉回 (multi-select 必须保证每个已选 tag 都有 label)
const ids = q.value.sponsorAdminUserIds || []
for (const uid of ids) {
if (!sponsorOrgOptions.value.find(u => u.userId === uid)) {
const r2 = await listSponsorOrgs({ userId: uid })
const sel = (r2.data || []).find(u => u.userId === uid)
// 已选项不在结果里时按 orgId 单独拉回 (multi-select 必须保证每个已选 tag 都有 label)
const ids = q.value.sponsorOrgIds || []
for (const id of ids) {
if (!sponsorOrgOptions.value.find(u => u.orgId === id)) {
const r2 = await listSponsorOrgs({ orgId: id })
const sel = (r2.data || []).find(u => u.orgId === id)
if (sel) sponsorOrgOptions.value = [sel, ...sponsorOrgOptions.value]
}
}
@@ -659,12 +657,12 @@ function searchExecutorOrgsForFilter(query) {
if (query) params.orgName = query
const r = await listExecutorOrgs(params)
executorOrgOptions.value = r.data || []
// 已选项不在结果里时按 userId 单独拉回 (multi-select 必须保证每个已选 tag 都有 label)
const ids = q.value.execAdminUserIds || []
for (const uid of ids) {
if (!executorOrgOptions.value.find(u => u.userId === uid)) {
const r2 = await listExecutorOrgs({ userId: uid })
const sel = (r2.data || []).find(u => u.userId === uid)
// 已选项不在结果里时按 orgId 单独拉回 (multi-select 必须保证每个已选 tag 都有 label)
const ids = q.value.execOrgIds || []
for (const id of ids) {
if (!executorOrgOptions.value.find(u => u.orgId === id)) {
const r2 = await listExecutorOrgs({ orgId: id })
const sel = (r2.data || []).find(u => u.orgId === id)
if (sel) executorOrgOptions.value = [sel, ...executorOrgOptions.value]
}
}