feat: 项目导出 + 会议跳转 + 机构软删除 + 供应商账号同步

- 项目/项目评价/报名专家 3 类导出, 勾选透传 projectIds
- 项目列表 总场次/已执行/未执行 三列点击跳对应会议列表 (executor 复用后端 role 隔离)
- 机构软删除 + 级联软删机构下账号
- 供应商开放接口账号同步

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
郭庆泰
2026-08-30 10:42:35 +08:00
co-authored by Claude
parent 1876601ba7
commit f4d8bc1463
25 changed files with 880 additions and 171 deletions
+97 -20
View File
@@ -60,23 +60,21 @@
<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-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-button @click="doExport">导出</el-button>
<el-button @click="doExportRating">项目评价导出</el-button>
<el-button v-if="isManager" @click="doExportSignupExpert">已报名专家导出</el-button>
</el-form-item>
</el-form>
<div class="toolbar">
<el-button class="action-btn" @click="openNewProject">新建项目</el-button>
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openAssign">
项目分配<span v-if="selection.length" class="badge">{{ selection.length }}</span>
</el-button>
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openBatch('score')">
批量评分<span v-if="selection.length" class="badge">{{ selection.length }}</span>
</el-button>
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openBatch('close')">
批量结题<span v-if="selection.length" class="badge">{{ selection.length }}</span>
</el-button>
<el-button class="action-btn secondary" :disabled="!selection.length" @click="openBatch('activate')">
批量开通<span v-if="selection.length" class="badge">{{ selection.length }}</span>
</el-button>
<el-button @click="openAssign">项目分配</el-button>
<el-button @click="openBatch('score')">批量评分</el-button>
<el-button @click="openBatch('close')">批量结题</el-button>
<el-button @click="openBatch('activate')">批量开通</el-button>
<el-button v-if="canDelete" class="action-btn secondary" type="danger" :disabled="!selection.length" @click="onBatchDelete">
批量删除<span v-if="selection.length" class="badge">{{ selection.length }}</span>
</el-button>
@@ -96,9 +94,24 @@
<el-link :underline="false" type="primary" @click="viewDetail(row)">{{ row.projectName }}</el-link>
</template>
</el-table-column>
<el-table-column prop="totalSessions" label="总场次/总期数" width="130" align="center" />
<el-table-column prop="doneSessions" label="已执行" width="80" align="center" />
<el-table-column prop="todoSessions" label="未执行" width="80" align="center" />
<el-table-column prop="totalSessions" label="总场次/总期数" width="130" align="center">
<template #default="{ row }">
<el-link v-if="row.totalSessions" :underline="false" type="primary" @click="goMeetings(row, 'all')">{{ row.totalSessions }}</el-link>
<span v-else style="color:#c0c4cc">0</span>
</template>
</el-table-column>
<el-table-column prop="doneSessions" label="已执行" width="80" align="center">
<template #default="{ row }">
<el-link v-if="row.doneSessions" :underline="false" type="primary" @click="goMeetings(row, 'done')">{{ row.doneSessions }}</el-link>
<span v-else style="color:#c0c4cc">0</span>
</template>
</el-table-column>
<el-table-column prop="todoSessions" label="未执行" width="80" align="center">
<template #default="{ row }">
<el-link v-if="row.todoSessions" :underline="false" type="primary" @click="goMeetings(row, 'todo')">{{ row.todoSessions }}</el-link>
<span v-else style="color:#c0c4cc">0</span>
</template>
</el-table-column>
<el-table-column prop="totalAmount" label="总金额" width="130" align="right" :formatter="fmtMoney" />
<el-table-column prop="availableAmount" label="可用金额" width="130" align="right" :formatter="fmtMoney" />
<el-table-column prop="paidLaborAmount" label="已支付劳务费" width="140" align="right" :formatter="fmtMoney" />
@@ -273,7 +286,7 @@
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { bizList, bizAdd, bizUpdate, bizDelete } from '@/api/public'
import { bizList, bizAdd, bizUpdate, bizDelete, exportProjectList, exportProjectRating, exportSignupExpert } from '@/api/public'
import { listExecutorOrgs, listSponsorOrgs } from '@/api/system'
import request from '@/utils/request'
import { ElMessage, ElMessageBox } from 'element-plus'
@@ -379,9 +392,63 @@ function readQueryFromRoute() {
if (q2.isSettled != null && q2.isSettled !== '') q.value.isSettled = String(q2.isSettled)
}
// 导出
function exportProjects() { ElMessage.info('导出项目功能开发中') }
function exportEval() { ElMessage.info('项目评价导出功能开发中') }
// 仅 manager 可见「已报名专家导出」 (前端 v-if, 后端 controller 再兜底 role 校验)
const isManager = computed(() => userStore.role === 'manager')
// ========== 导出 (跟随当前筛选, 与 load() 同一口径) ==========
function buildExportParams() {
// 勾选了项目 → 只导出勾选项; 否则按当前筛选导出全部
if (selection.value.length > 0) {
return { projectIds: selection.value.map(r => r.projectId) }
}
const params = { ...q.value }
if (params.startTime) params.startTime = params.startTime + ' 00:00:00'
if (params.endTime) params.endTime = params.endTime + ' 23:59:59'
return params
}
function downloadBlob(res, filename) {
const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
function dateStamp() {
const d = new Date()
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}`
}
async function doExport() {
try {
const res = await exportProjectList(buildExportParams())
downloadBlob(res, `项目列表_${dateStamp()}.xlsx`)
ElMessage.success('导出成功')
} catch (e) {
ElMessage.error(e?.msg || '导出失败')
}
}
async function doExportRating() {
try {
const res = await exportProjectRating(buildExportParams())
downloadBlob(res, `项目评价_${dateStamp()}.xlsx`)
ElMessage.success('导出成功')
} catch (e) {
ElMessage.error(e?.msg || '导出失败')
}
}
async function doExportSignupExpert() {
try {
const res = await exportSignupExpert(buildExportParams())
downloadBlob(res, `报名专家_${dateStamp()}.xlsx`)
ElMessage.success('导出成功')
} catch (e) {
ElMessage.error(e?.msg || '导出失败')
}
}
// ========== 单行操作按钮 handlers(弹 5 个独立 dialog ==========
function openClose(row) {
@@ -481,6 +548,16 @@ function viewDetail(row) {
router.push(`${base}/projects/detail/${row.projectId}`)
}
// 点击「总场次/已执行/未执行」列 → 跳对应角色的会议列表, 按项目 + 阶段筛选 (会议列表后端按 role 隔离数据权限)
// scope: 'all'(全部) | 'done'(已执行) | 'todo'(未执行)
function goMeetings(row, scope) {
const base = userStore.role === 'admin' ? '/admin' : '/manager'
const q = { projectId: row.projectId }
if (scope === 'done') q.currentStageNotIn = 'NOT_STARTED,IN_PROGRESS'
if (scope === 'todo') q.currentStage = 'NOT_STARTED'
router.push({ path: `${base}/meetings`, query: q })
}
// 点击列表「执行单位评分(合规/支持)」列 → 只读显示该角色 4 维度评分明细
async function openScoreDetail(row, role) {
scoreDetailTitle.value = role === 'sponsor' ? '执行单位评分详情(支持方)' : '执行单位评分详情(合规)'