批量推送代码

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
郭庆泰
2026-08-29 17:52:05 +08:00
co-authored by Claude
parent 36528b6e0c
commit e47f76478b
130 changed files with 3972 additions and 2546 deletions
+84 -37
View File
@@ -60,7 +60,7 @@
<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-form-item>
</el-form>
<div class="toolbar">
@@ -91,23 +91,27 @@
>
<el-table-column type="selection" width="44" />
<el-table-column prop="projectNo" label="项目编号" width="170" fixed="left" />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="totalSessions" label="总场次/总期数" width="100" align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<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="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" />
<el-table-column prop="paidMeetingAmount" label="已支付会务费" width="140" align="right" :formatter="fmtMoney" />
<el-table-column prop="managerScore" label="执行单位评分(合规)" width="140" align="center">
<el-table-column prop="managerScore" label="执行单位评分(合规)" width="170" align="center">
<template #default="{ row }">
<span v-if="row.managerScore != null">{{ row.managerScore }}</span>
<el-link v-if="row.managerScore != null" :underline="false" type="primary" @click="openScoreDetail(row, 'manager')">{{ row.managerScore }}</el-link>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column prop="sponsorScore" label="执行单位评分(支持)" width="140" align="center">
<el-table-column prop="sponsorScore" label="执行单位评分(支持)" width="170" align="center">
<template #default="{ row }">
<span v-if="row.sponsorScore != null">{{ row.sponsorScore }}</span>
<el-link v-if="row.sponsorScore != null" :underline="false" type="primary" @click="openScoreDetail(row, 'sponsor')">{{ row.sponsorScore }}</el-link>
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
@@ -120,12 +124,12 @@
<el-tag v-else type="warning">未结题</el-tag>
</template>
</el-table-column>
<el-table-column prop="startTime" label="项目开始时间" width="120" align="center" :formatter="fmtDate" />
<el-table-column prop="endTime" label="项目结束时间" width="120" align="center" :formatter="fmtDate" />
<el-table-column prop="startTime" label="项目开始时间" width="170" align="center" :formatter="fmtDateTime" />
<el-table-column prop="endTime" label="项目结束时间" width="170" align="center" :formatter="fmtDateTime" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }"><div class="table-actions">
<div class="op-cell">
<el-link :underline="false" type="primary" @click="doCreateMeeting(row)">建会</el-link>
<el-link v-if="!isFinishedRow(row)" :underline="false" type="primary" @click="doCreateMeeting(row)">建会</el-link>
<el-link :underline="false" type="primary" :disabled="isFinishedRow(row)" @click="doAssign(row)">分配</el-link>
<el-dropdown trigger="hover" @command="(cmd) => onAction(cmd, row)">
<el-link :underline="false" type="primary" class="op-dropdown">
@@ -194,7 +198,8 @@
</el-dialog>
<!-- ============= 单行结题 modal ============= -->
<el-dialog v-model="closeModalOpen" title="您确定要结题吗?" width="420px" :show-close="false">
<el-dialog v-model="closeModalOpen" title="结题确认" width="420px" :show-close="false">
<p style="color:#606266">你确定要结题吗</p>
<template #footer>
<el-button type="primary" @click="confirmClose">确定</el-button>
<el-button @click="closeModalOpen=false">取消</el-button>
@@ -240,6 +245,29 @@
<el-button @click="singleScoreModalOpen=false">取消</el-button>
</template>
</el-dialog>
<!-- ============= 评分详情 modal只读 4 维度明细, 点列表列查看 ============= -->
<el-dialog v-model="scoreDetailOpen" :title="scoreDetailTitle" width="520px" :show-close="false">
<table class="score-table" style="width:100%;border-collapse:collapse;font-size:13px">
<thead><tr style="background:#fafafa">
<th style="width:110px;padding:10px 12px;text-align:left">评价维度</th>
<th style="padding:10px 12px;text-align:left">评价内容(简洁版)</th>
<th style="width:110px;padding:10px 12px;text-align:center">平均得分</th>
</tr></thead>
<tbody>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">履约质量</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">服务/活动效果达标度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center">{{ scoreDetail.qualityScore ?? '-' }}</td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">时效响应</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">执行 & 售后响应速度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center">{{ scoreDetail.responseScore ?? '-' }}</td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">配合度</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">沟通配合 & 问题处理</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center">{{ scoreDetail.cooperationScore ?? '-' }}</td></tr>
<tr><td style="padding:10px 12px;border-top:1px solid #f5f5f5">合规安全</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5">流程合规 & 无安全事故</td><td style="padding:10px 12px;border-top:1px solid #f5f5f5;text-align:center">{{ scoreDetail.complianceScore ?? '-' }}</td></tr>
</tbody>
</table>
<div style="margin-top:12px;text-align:right;font-size:13px;color:#303133">
平均分: <b>{{ scoreDetailTotal }}</b>
</div>
<template #footer>
<el-button @click="scoreDetailOpen=false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup>
@@ -265,8 +293,12 @@ const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
async function load() {
loading.value = true
try {
const params = { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize }
// 时间范围含两端: 开始日 00:00:00 ~ 结束日 23:59:59
if (params.startTime) params.startTime = params.startTime + ' 00:00:00'
if (params.endTime) params.endTime = params.endTime + ' 23:59:59'
// axios 拦截器已将 TableDataInfo 归一化为 {code, msg, data: {total, rows}}
const { data } = await bizList('project', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
const { data } = await bizList('project', params)
rows.value = data?.rows || []
page.total = data?.total || 0
} catch (e) {
@@ -302,13 +334,9 @@ function doEdit(row) { router.push(`/manager/projects/edit/${row.projectId}`) }
async function doDelete(row) {
try {
await ElMessageBox.confirm(
`确认删除项目"${row.projectName}"\n项目编号: ${row.projectNo}\n\n` +
`此操作将级联软删除:\n` +
`· 项目主表 + 方案 + 执行方分配 + 支持方分配 + 评分 (5 张表)\n` +
`· 项目下所有会议 + 会议附件 + 参会人 + 监督员 + 执行人 + 审计日志 (6 张表)\n\n` +
`数据保留在库, 不再展示给前台用户, 审计追溯可查。`,
`确认删除项目"${row.projectName}"`,
'删除确认',
{ type: 'error', confirmButtonText: '确认删除', cancelButtonText: '取消' }
{ type: 'error', confirmButtonText: '确认删除', cancelButtonText: '取消' }
)
await bizDelete('project', row.projectId)
ElMessage.success('已删除')
@@ -325,11 +353,7 @@ async function onBatchDelete() {
if (!selection.value.length) return ElMessage.warning('请先勾选项目')
try {
await ElMessageBox.confirm(
`确认批量软删除选中的 ${selection.value.length} 个项目?\n\n` +
`此操作将级联软删除每个项目下的:\n` +
`· 5 张项目表 (主表/方案/分配/支持方/评分)\n` +
`· 6 张会议表 (会议本身 + 附件 + 参会人 + 监督员 + 执行人 + 审计日志)\n\n` +
`数据保留, 不再展示给前台用户。`,
`确认删除选中的 ${selection.value.length} 个项目?`,
'批量删除确认',
{ type: 'error', confirmButtonText: `确认删除 ${selection.value.length}`, cancelButtonText: '取消' }
)
@@ -337,7 +361,7 @@ async function onBatchDelete() {
const ids = selection.value.map(r => r.projectId)
try {
await bizDelete('project', ids)
ElMessage.success(`批量软删除 ${ids.length} 个项目`)
ElMessage.success(`已删除 ${ids.length} 个项目`)
selection.value = []
load()
} catch (e) {
@@ -435,9 +459,7 @@ async function confirmSingleScore() {
const avg = ((f.qualityScore + f.responseScore + f.cooperationScore + f.complianceScore) / 4).toFixed(1)
const projectId = singleScoreTargetRow.value.projectId
try {
// 1. 写聚合分到 biz_project.manager_score
await bizUpdate('project', { projectId, managerScore: Number(avg) })
// 2. 写 4 维度明细到 biz_project_rating (rater_role='manager', 后端自动填 raterId)
// 写 4 维度明细到 biz_project_rating (rater_role='manager', 后端自动填 raterId + 重算聚合分)
await request.post('/business/project/rate', {
projectId,
raterRole: 'manager',
@@ -453,7 +475,32 @@ async function confirmSingleScore() {
ElMessage.error(e?.msg || '评分失败')
}
}
function viewDetail(row) { router.push(`/manager/projects/detail/${row.projectId}`) }
function viewDetail(row) {
// admin 复用本列表页, 详情须跳到 /admin/projects/detail (否则被 beforeEach 角色守卫拦截)
const base = userStore.role === 'admin' ? '/admin' : '/manager'
router.push(`${base}/projects/detail/${row.projectId}`)
}
// 点击列表「执行单位评分(合规/支持)」列 → 只读显示该角色 4 维度评分明细
async function openScoreDetail(row, role) {
scoreDetailTitle.value = role === 'sponsor' ? '执行单位评分详情(支持方)' : '执行单位评分详情(合规)'
Object.assign(scoreDetail, { qualityScore: null, responseScore: null, cooperationScore: null, complianceScore: null })
scoreDetailTotal.value = role === 'sponsor' ? (row.sponsorScore ?? '—') : (row.managerScore ?? '—')
scoreDetailOpen.value = true
try {
const r = await request({ url: '/business/project/ratings', method: 'get', params: { projectId: row.projectId } })
const list = (r.data && (Array.isArray(r.data) ? r.data : r.data.rows)) || r.rows || []
const items = list.filter(x => String(x.projectId) === String(row.projectId) && x.raterRole === role)
if (items.length) {
const n = items.length
const dimAvg = (key) => (items.reduce((s, x) => s + (Number(x[key]) || 0), 0) / n).toFixed(1)
scoreDetail.qualityScore = dimAvg('qualityScore')
scoreDetail.responseScore = dimAvg('responseScore')
scoreDetail.cooperationScore = dimAvg('cooperationScore')
scoreDetail.complianceScore = dimAvg('complianceScore')
}
} catch (e) { /* 拉取失败保持空, 弹窗显示 - */ }
}
// 金额格式化: 1234567.5 -> "1,234,567.50"
function fmtMoney(row, col, v) {
@@ -462,12 +509,10 @@ function fmtMoney(row, col, v) {
if (isNaN(n)) return String(v)
return n.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 2 })
}
// 日期格式化: "2026-08-09" 或 "2026-08-09 10:30:00" -> "2026.08.09"
function fmtDate(row, col, v) {
// 日期时间格式化: 展示完整 yyyy-MM-dd HH:mm:ss (后端 @JsonFormat 已序列化, 直接透传)
function fmtDateTime(row, col, v) {
if (!v) return ''
const s = String(v)
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/)
return m ? `${m[1]}.${m[2]}.${m[3]}` : s
return String(v)
}
const selection = ref([])
@@ -491,6 +536,12 @@ const singleScoreModalOpen = ref(false)
const singleScoreTargetRow = ref(null)
const singleScoreForm = reactive({ qualityScore: 0, responseScore: 0, cooperationScore: 0, complianceScore: 0 })
// ========== 评分详情 (只读 4 维度明细, 点列表 score 列查看) ==========
const scoreDetailOpen = ref(false)
const scoreDetailTitle = ref('评分详情')
const scoreDetail = reactive({ qualityScore: null, responseScore: null, cooperationScore: null, complianceScore: null })
const scoreDetailTotal = ref('—')
// 新建项目已迁到独立页面 /manager/projects/new (ManagerProjectsNew.vue)
function openNewProject() {
router.push('/manager/projects/new')
@@ -568,10 +619,6 @@ async function confirmBatch() {
// 共享评分: 所有项目用同一组 4 维度
for (const r of selection.value) {
try {
await bizUpdate('project', {
projectId: r.projectId,
managerScore: Number(total)
})
await request.post('/business/project/rate', {
projectId: r.projectId,
raterRole: 'manager',