涵盖改动范围: - 后端 BizOrg / BizProject / BizProjectRating / BizSpecialPlan controller + domain + mapper + service - 前端 admin (BizSpecialPlanAdmin, ProjectCategory) / executor / leader / manager / sponsor 多页调整 - api/dict.js + AdminLayout.vue 同步
948 lines
48 KiB
Vue
948 lines
48 KiB
Vue
<template>
|
||
<div class="page-card">
|
||
<div class="breadcrumb">首页 / 项目管理</div>
|
||
|
||
<el-form inline :model="q" class="filter-form">
|
||
<el-form-item label="支持单位:"><el-input v-model="q.sponsorOrgName" placeholder="输入支持单位名称" clearable /></el-form-item>
|
||
<el-form-item label="服务机构:"><el-input v-model="q.execOrgName" placeholder="输入服务机构" clearable /></el-form-item>
|
||
<el-form-item label="项目编号:"><el-input v-model="q.projectNo" placeholder="输入项目编号" clearable /></el-form-item>
|
||
<el-form-item label="项目名称:"><el-input v-model="q.projectName" placeholder="输入项目名称" clearable /></el-form-item>
|
||
<el-form-item label="项目形式:">
|
||
<el-select v-model="q.projectForm" placeholder="请选择" clearable style="width:140px">
|
||
<el-option label="线上" value="线上" /><el-option label="线下" value="线下" />
|
||
<el-option label="线上+线下" value="线上+线下" /><el-option label="其他" value="其他" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="项目时间:">
|
||
<el-date-picker v-model="q.startTime" type="date" placeholder="开始日期" value-format="YYYY-MM-DD HH:mm:ss" />
|
||
<span style="margin:0 4px;">至</span>
|
||
<el-date-picker v-model="q.endTime" type="date" placeholder="结束日期" value-format="YYYY-MM-DD HH:mm:ss" />
|
||
</el-form-item>
|
||
<el-form-item label="是否结题:">
|
||
<el-select v-model="q.isFinished" placeholder="请选择" clearable style="width:120px">
|
||
<el-option label="已结题" value="1" /><el-option label="未结题" value="0" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="是否结算:">
|
||
<el-select v-model="q.isSettled" placeholder="请选择" clearable style="width:120px">
|
||
<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>
|
||
|
||
<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>
|
||
</div>
|
||
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSel">
|
||
<el-table-column type="selection" width="44" />
|
||
<el-table-column prop="projectNo" label="项目编号" width="140" 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="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">
|
||
<template #default="{ row }">
|
||
<span v-if="row.managerScore != null">{{ row.managerScore }}</span>
|
||
<span v-else style="color:#c0c4cc">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="sponsorScore" label="执行单位得分(支持)" width="140" align="center">
|
||
<template #default="{ row }">
|
||
<span v-if="row.sponsorScore != null">{{ row.sponsorScore }}</span>
|
||
<span v-else style="color:#c0c4cc">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="sponsorOrgName" label="支持单位" width="160" show-overflow-tooltip />
|
||
<el-table-column prop="execOrgNames" label="服务机构" min-width="180" show-overflow-tooltip />
|
||
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
|
||
<el-table-column prop="isFinished" label="是否结题" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.isFinished === '1' || row.isFinished === 1" type="success">已结题</el-tag>
|
||
<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 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-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="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-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<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>
|
||
|
||
<!-- 项目详情已迁到独立页面 /manager/projects/detail/:projectId -->
|
||
|
||
<Preview v-model="previewOpen" :url="previewUrl" :title="previewTitle" />
|
||
|
||
<!-- ============= 新建项目 dialog(已迁移到独立页面 /manager/projects/new) ============= -->
|
||
|
||
<!-- ============= 项目分配 dialog(按原型 project-assign.html 1:1) ============= -->
|
||
<el-dialog v-model="assignOpen" :title="assignBatchMode ? `批量项目分配 (${assignBatchProjects.length} 个项目)` : '项目分配'" width="780px" @closed="resetAssignForm">
|
||
<el-form :model="assignForm" label-width="120px">
|
||
<!-- 单选模式: 选项目 -->
|
||
<el-form-item v-if="!assignBatchMode" label="项目">
|
||
<el-select v-model="assignForm.projectId" placeholder="请选择项目" filterable style="width:100%" @change="onAssignProjectChange">
|
||
<el-option v-for="r in rows" :key="r.projectId" :label="`${r.projectNo} - ${r.projectName}`" :value="r.projectId" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<!-- 批量模式: 显示已选项目数 -->
|
||
<el-form-item v-else label="已选项目">
|
||
<div style="color:#606266;font-size:13px">共 <b>{{ assignBatchProjects.length }}</b> 个项目</div>
|
||
</el-form-item>
|
||
<el-form-item label="支持方">
|
||
<el-select v-model="assignForm.sponsorAdminUserId" placeholder="请选择支持单位" filterable :filter-method="searchSponsorOrgs" clearable style="width:100%" @change="onSponsorOrgPick">
|
||
<el-option v-for="u in sponsorOrgOptions" :key="u.userId" :label="u.orgName" :value="u.userId" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-divider>执行方分配</el-divider>
|
||
<!-- 单选模式: 显示项目总场次 / 总金额(元) vs 已分配 (实时) -->
|
||
<div v-if="!assignBatchMode" class="assign-sessions-bar">
|
||
<span>项目总场次: <b>{{ assignForm.totalSessions || 0 }}</b></span>
|
||
<span :class="{ 'is-over': singleSessionOver }">已分配场次: <b>{{ assignedSessions }}</b></span>
|
||
<span v-if="singleSessionOver" class="over-warn">⚠ 已超出 {{ assignedSessions - assignForm.totalSessions }} 场</span>
|
||
<span class="sep">|</span>
|
||
<span>总金额(元): <b>{{ assignForm.totalAmount || 0 }}</b></span>
|
||
<span :class="{ 'is-over': singleAmountOver }">已分配金额: <b>¥ {{ assignedAmount }}</b></span>
|
||
<span v-if="singleAmountOver" class="over-warn">⚠ 已超出 ¥ {{ Math.round((assignedAmount - assignForm.totalAmount) * 100) / 100 }}</span>
|
||
</div>
|
||
<!-- 批量模式: 提示保存时按各项目自身总场次 / 总金额逐项校验 -->
|
||
<div v-else class="assign-sessions-bar">
|
||
<span>本次分配: <b>{{ assignedSessions }}</b> 场 / <b>¥ {{ assignedAmount }}</b></span>
|
||
<span class="hint">(保存时按各项目自身的总场次和总金额逐项校验)</span>
|
||
</div>
|
||
<el-table :data="assignForm.execRows" border>
|
||
<el-table-column type="index" label="#" width="50" />
|
||
<el-table-column label="执行方名称" min-width="220">
|
||
<template #default="{ row }">
|
||
<el-select v-model="row.execUserId" filterable
|
||
:filter-method="q => searchExecutors(q, row)"
|
||
:loading="row._loading"
|
||
placeholder="搜索执行方用户"
|
||
style="width:100%"
|
||
@change="v => onExecUserPick(row, v)">
|
||
<el-option v-for="u in (row._options || [])" :key="u.userId"
|
||
:label="u.orgName" :value="u.userId" />
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="场次" width="110">
|
||
<template #default="{ row }">
|
||
<el-input-number v-model="row.sessions" :min="0" controls-position="right" style="width:100%" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="金额" width="140">
|
||
<template #default="{ row }">
|
||
<el-input-number v-model="row.amount" :min="0" :precision="2" controls-position="right" style="width:100%" />
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80">
|
||
<template #default="{ $index }">
|
||
<el-button link type="danger" @click="assignForm.execRows.splice($index, 1)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div style="margin-top:8px">
|
||
<el-button type="primary" link @click="addExecRow">+ 增加执行方</el-button>
|
||
</div>
|
||
<el-form-item label="提交截止(天)" style="margin-top:12px">
|
||
<el-input-number v-model="assignForm.deadlineDays" :min="0" controls-position="right" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="assignOpen=false">取消</el-button>
|
||
<el-button type="primary" :loading="assignSubmitting" @click="submitAssign">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ============= 批量评分 dialog(按原型 4 维度评分) ============= -->
|
||
<el-dialog v-model="batchOpen" :title="batchMap[batchKind]" width="626px" :show-close="false">
|
||
<!-- 评分专用字段: 共享一份评分 (与 singleScoreForm 同样的三列 table 布局) -->
|
||
<template v-if="batchKind==='score'">
|
||
<p style="color:#606266;margin-bottom:8px">将对以下 <b>{{ selection.length }}</b> 个项目共用同一份评分:</p>
|
||
<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:130px;padding:10px 12px;text-align:center">评分标准(1~5 分)</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"><el-input-number v-model="batchScoreForm.qualityScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></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"><el-input-number v-model="batchScoreForm.responseScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></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"><el-input-number v-model="batchScoreForm.cooperationScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></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"><el-input-number v-model="batchScoreForm.complianceScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
|
||
</tbody>
|
||
</table>
|
||
</template>
|
||
<!-- 结题专用字段 -->
|
||
<template v-else-if="batchKind==='close'">
|
||
<p>已选择 <b>{{ selection.length }}</b> 个项目。</p>
|
||
<p style="color:#606266;margin-top:8px">确定要将这些项目标记为结题吗?</p>
|
||
</template>
|
||
<!-- 开通专用字段 -->
|
||
<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>
|
||
</template>
|
||
<template #footer>
|
||
<el-button @click="batchOpen=false">取消</el-button>
|
||
<el-button type="primary" :loading="batchSubmitting" @click="confirmBatch">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ============= 单行结题 modal ============= -->
|
||
<el-dialog v-model="closeModalOpen" title="您确定要结题吗?" width="420px" :show-close="false">
|
||
<template #footer>
|
||
<el-button type="primary" @click="confirmClose">确定</el-button>
|
||
<el-button @click="closeModalOpen=false">取消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ============= 单行开通 modal ============= -->
|
||
<el-dialog v-model="activateModalOpen" title="您确定要开通吗?" width="420px" :show-close="false">
|
||
<template #footer>
|
||
<el-button type="primary" @click="confirmActivate">确定</el-button>
|
||
<el-button @click="activateModalOpen=false">取消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ============= 单行删除公告 modal ============= -->
|
||
<el-dialog v-model="delAnnModalOpen" title="您确定要删除公告吗?删除后无法恢复" width="420px" :show-close="false">
|
||
<template #footer>
|
||
<el-button type="primary" @click="confirmDeleteAnn">确定</el-button>
|
||
<el-button @click="delAnnModalOpen=false">取消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ============= 单行执行单位评分 modal(4 维度) ============= -->
|
||
<el-dialog v-model="singleScoreModalOpen" title="执行单位评分" width="626px" :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:130px;padding:10px 12px;text-align:center">评分标准(1~5 分)</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"><el-input-number v-model="singleScoreForm.qualityScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></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"><el-input-number v-model="singleScoreForm.responseScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></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"><el-input-number v-model="singleScoreForm.cooperationScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></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"><el-input-number v-model="singleScoreForm.complianceScore" :min="0" :max="5" :step="1" controls-position="right" placeholder="1~5" /></td></tr>
|
||
</tbody>
|
||
</table>
|
||
<template #footer>
|
||
<el-button type="primary" @click="confirmSingleScore">确定</el-button>
|
||
<el-button @click="singleScoreModalOpen=false">取消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<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 { listExecutorOrgs, listSponsorOrgs } from '@/api/system'
|
||
import request from '@/utils/request'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { ArrowDown } from '@element-plus/icons-vue'
|
||
import Preview from '@/components/Preview.vue'
|
||
import { useUserStore } from '@/store/user'
|
||
const userStore = useUserStore()
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
|
||
const q = ref({ sponsorOrgName: '', execOrgName: '', projectNo: '', projectName: '', projectForm: '', startTime: '', endTime: '', isFinished: '', isSettled: '', managerScore: '' })
|
||
const rows = ref([])
|
||
const loading = ref(false)
|
||
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
console.info('[manager-projects.load] START, q=', JSON.stringify(q.value), 'page=', JSON.stringify(page))
|
||
// axios 拦截器已将 TableDataInfo 归一化为 {code, msg, data: {total, rows}}
|
||
const { data } = await bizList('project', { ...q.value, pageNum: page.pageNum, pageSize: page.pageSize })
|
||
console.info('[manager-projects.load] OK, total=', data?.total, 'rows.length=', data?.rows?.length, 'firstRow=', JSON.stringify(data?.rows?.[0] || null))
|
||
rows.value = data?.rows || []
|
||
page.total = data?.total || 0
|
||
} catch (e) {
|
||
console.error('[manager-projects.load] FAILED', e)
|
||
rows.value = []; page.total = 0
|
||
} finally { loading.value = false }
|
||
}
|
||
// 操作列按钮 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)
|
||
else if (cmd === 'activate') openActivate(row)
|
||
else if (cmd === 'delAnn') openDeleteAnnouncement(row)
|
||
}
|
||
function doCreateMeeting(row) { router.push(`/manager/meetings/new?projectId=${row.projectId}`) }
|
||
function doAssign(row) {
|
||
resetAssignForm()
|
||
assignForm.projectId = row.projectId
|
||
// 先打开 dialog, 再异步加载 (loadAssigns 是 async)
|
||
assignOpen.value = true
|
||
onAssignProjectChange(row.projectId)
|
||
// 预加载 sponsor 候选
|
||
setTimeout(() => loadSponsorOrgs(''), 100)
|
||
}
|
||
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删除后无法恢复!`, '删除确认', { type: 'error', confirmButtonText: '确认删除', cancelButtonText: '取消' })
|
||
await bizDelete('project', row.projectId)
|
||
ElMessage.success('已删除')
|
||
load()
|
||
} catch (e) {
|
||
if (e !== 'cancel' && e !== 'close') ElMessage.error(e?.msg || '删除失败')
|
||
}
|
||
}
|
||
|
||
function reset() { q.value = { sponsorOrgName:'', execOrgName:'', projectNo:'', projectName:'', projectForm:'', startTime:'', endTime:'', isFinished:'', isSettled:'', managerScore:'' }; page.pageNum = 1; load() }
|
||
|
||
// 导出
|
||
function exportProjects() { ElMessage.info('导出项目功能开发中') }
|
||
function exportEval() { ElMessage.info('项目评价导出功能开发中') }
|
||
// 导出某项目的报名专家 (biz_execution_intent)
|
||
// 字段: 专家姓名 科室 医院 职称 报名时间 手机号
|
||
async function exportExperts(row) {
|
||
if (!row || !row.projectNo) { ElMessage.warning('缺少项目编号'); return }
|
||
try {
|
||
// 后端 BizExecutionIntent 参数没 @RequestBody, 必须走 URL query string
|
||
const res = await request({
|
||
url: '/business/executionIntent/export',
|
||
method: 'post',
|
||
params: { projectNo: row.projectNo },
|
||
responseType: 'blob'
|
||
})
|
||
const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||
const url = window.URL.createObjectURL(blob)
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = `报名专家_${row.projectNo}.xlsx`
|
||
document.body.appendChild(a)
|
||
a.click()
|
||
document.body.removeChild(a)
|
||
window.URL.revokeObjectURL(url)
|
||
ElMessage.success('导出成功')
|
||
} catch (e) {
|
||
const msg = e?.msg || e?.message || '导出失败'
|
||
ElMessage.error(msg)
|
||
}
|
||
}
|
||
|
||
// ========== 单行操作按钮 handlers(弹 5 个独立 dialog) ==========
|
||
function openClose(row) {
|
||
if (row.isFinished === '1' || row.isFinished === 1) {
|
||
return ElMessage.warning('该项目已结题,无需重复操作')
|
||
}
|
||
closeTargetRow.value = row
|
||
closeModalOpen.value = true
|
||
}
|
||
function confirmClose() {
|
||
if (!closeTargetRow.value) return
|
||
bizUpdate('project', { projectId: closeTargetRow.value.projectId, isFinished: '1' })
|
||
.then(() => { ElMessage.success('结题成功'); closeModalOpen.value = false; load() })
|
||
.catch(() => ElMessage.error('结题失败'))
|
||
}
|
||
|
||
function openActivate(row) {
|
||
activateTargetRow.value = row
|
||
activateModalOpen.value = true
|
||
}
|
||
function confirmActivate() {
|
||
if (!activateTargetRow.value) return
|
||
bizUpdate('project', { projectId: activateTargetRow.value.projectId, isFinished: '0' })
|
||
.then(() => { ElMessage.success('已开通'); activateModalOpen.value = false; load() })
|
||
.catch(() => ElMessage.error('开通失败'))
|
||
}
|
||
|
||
function openDeleteAnnouncement(row) {
|
||
delAnnTargetRow.value = row
|
||
delAnnModalOpen.value = true
|
||
}
|
||
function confirmDeleteAnn() {
|
||
if (!delAnnTargetRow.value) return
|
||
// 清空全部公示 URL + 取消发布状态
|
||
// (mapper.updateByPrimaryKey 的 trim prefix="SET" 必须有字段, 不能只传空字符串)
|
||
bizUpdate('project', {
|
||
projectId: delAnnTargetRow.value.projectId,
|
||
publishUrl: '',
|
||
invitationUrl: '',
|
||
supportLetterUrl: '',
|
||
noticeUrl: '',
|
||
scheduleUrl: '',
|
||
isPublished: '0'
|
||
})
|
||
.then(() => { ElMessage.success(`已删除项目 ${delAnnTargetRow.value.projectNo} 的公告`); delAnnModalOpen.value = false; load() })
|
||
.catch(() => ElMessage.error('删除失败'))
|
||
}
|
||
|
||
async function openSingleScore(row) {
|
||
singleScoreTargetRow.value = row
|
||
singleScoreForm.qualityScore = 0
|
||
singleScoreForm.responseScore = 0
|
||
singleScoreForm.cooperationScore = 0
|
||
singleScoreForm.complianceScore = 0
|
||
singleScoreModalOpen.value = true
|
||
// 反显当前登录管理员的历史明细
|
||
const uid = userStore.user?.userId
|
||
if (!uid) return
|
||
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 mine = list.find(x =>
|
||
String(x.projectId) === String(row.projectId) &&
|
||
String(x.raterId) === String(uid) &&
|
||
x.raterRole === 'manager'
|
||
)
|
||
if (mine) {
|
||
singleScoreForm.qualityScore = Number(mine.qualityScore) || 0
|
||
singleScoreForm.responseScore = Number(mine.responseScore) || 0
|
||
singleScoreForm.cooperationScore = Number(mine.cooperationScore) || 0
|
||
singleScoreForm.complianceScore = Number(mine.complianceScore) || 0
|
||
}
|
||
} catch (e) { /* 拉取失败不影响打开, 维持默认 0 */ }
|
||
}
|
||
async function confirmSingleScore() {
|
||
if (!singleScoreTargetRow.value) return
|
||
const f = singleScoreForm
|
||
if (!f.qualityScore || f.qualityScore < 1 || f.qualityScore > 5) return ElMessage.error('履约质量必须是 1~5 的数字')
|
||
if (!f.responseScore || f.responseScore < 1 || f.responseScore > 5) return ElMessage.error('时效响应必须是 1~5 的数字')
|
||
if (!f.cooperationScore || f.cooperationScore < 1 || f.cooperationScore > 5) return ElMessage.error('配合度必须是 1~5 的数字')
|
||
if (!f.complianceScore || f.complianceScore < 1 || f.complianceScore > 5) return ElMessage.error('合规安全必须是 1~5 的数字')
|
||
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)
|
||
await request.post('/business/project/rate', {
|
||
projectId,
|
||
raterRole: 'manager',
|
||
qualityScore: f.qualityScore,
|
||
responseScore: f.responseScore,
|
||
cooperationScore: f.cooperationScore,
|
||
complianceScore: f.complianceScore
|
||
})
|
||
ElMessage.success(`评分完成,平均分:${avg}`)
|
||
singleScoreModalOpen.value = false
|
||
load()
|
||
} catch (e) {
|
||
ElMessage.error(e?.msg || '评分失败')
|
||
}
|
||
}
|
||
function viewDetail(row) { router.push(`/manager/projects/detail/${row.projectId}`) }
|
||
|
||
// 金额格式化: 1234567.5 -> "1,234,567.50"
|
||
function fmtMoney(row, col, v) {
|
||
if (v === null || v === undefined || v === '') return ''
|
||
const n = Number(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) {
|
||
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
|
||
}
|
||
|
||
const selection = ref([])
|
||
function onSel(rs) { selection.value = rs }
|
||
const batchOpen = ref(false)
|
||
const batchKind = ref('')
|
||
const batchSubmitting = ref(false)
|
||
const batchMap = { score: '批量评分', close: '批量结题', activate: '批量开通' }
|
||
const batchScoreRows = ref([]) // 保留兼容旧引用 (实际不再使用)
|
||
const batchScoreForm = reactive({ qualityScore: 5, responseScore: 5, cooperationScore: 5, complianceScore: 5 }) // 批量评分: 共享一份 4 维度
|
||
const activateDeadline = ref('')
|
||
|
||
// 5 个单行 modal 状态
|
||
const closeModalOpen = ref(false)
|
||
const closeTargetRow = ref(null)
|
||
const activateModalOpen = ref(false)
|
||
const activateTargetRow = ref(null)
|
||
const delAnnModalOpen = ref(false)
|
||
const delAnnTargetRow = ref(null)
|
||
const singleScoreModalOpen = ref(false)
|
||
const singleScoreTargetRow = ref(null)
|
||
const singleScoreForm = reactive({ qualityScore: 0, responseScore: 0, cooperationScore: 0, complianceScore: 0 })
|
||
|
||
// 新建项目已迁到独立页面 /manager/projects/new (ManagerProjectsNew.vue)
|
||
function openNewProject() {
|
||
router.push('/manager/projects/new')
|
||
}
|
||
function openAssign() {
|
||
if (!selection.value.length) {
|
||
ElMessage.warning('请先勾选项目')
|
||
return
|
||
}
|
||
resetAssignForm()
|
||
if (selection.value.length >= 2) {
|
||
assignBatchMode.value = true
|
||
assignBatchProjects.value = selection.value.map(r => ({ projectId: r.projectId, projectNo: r.projectNo, projectName: r.projectName }))
|
||
} else {
|
||
assignBatchMode.value = false
|
||
assignBatchProjects.value = []
|
||
assignForm.projectId = selection.value[0].projectId
|
||
onAssignProjectChange(assignForm.projectId)
|
||
}
|
||
assignOpen.value = true
|
||
setTimeout(() => {
|
||
assignForm.execRows.forEach(r => searchExecutors('', r))
|
||
loadSponsorOrgs('')
|
||
}, 100)
|
||
}
|
||
function openBatch(kind) {
|
||
if (!selection.value.length) return ElMessage.warning('请先勾选项目')
|
||
batchKind.value = kind
|
||
if (kind === 'score') {
|
||
// 共享评分: 重置 form 为默认值 (5 分)
|
||
Object.assign(batchScoreForm, { qualityScore: 5, responseScore: 5, cooperationScore: 5, complianceScore: 5 })
|
||
}
|
||
if (kind === 'activate') {
|
||
activateDeadline.value = ''
|
||
}
|
||
batchOpen.value = true
|
||
}
|
||
|
||
async function confirmBatch() {
|
||
if (batchKind.value === 'close') {
|
||
// 过滤掉已结题项目, 避免无效请求
|
||
const targets = selection.value.filter(r => r.isFinished !== '1' && r.isFinished !== 1)
|
||
const skipCount = selection.value.length - targets.length
|
||
try {
|
||
batchSubmitting.value = true
|
||
await Promise.all(targets.map(r => bizUpdate('project', { projectId: r.projectId, isFinished: '1' })))
|
||
ElMessage.success(skipCount > 0
|
||
? `已批量结题 ${targets.length} 个项目 (跳过 ${skipCount} 个已结题)`
|
||
: `已批量结题 ${targets.length} 个项目`)
|
||
batchOpen.value = false
|
||
selection.value = []
|
||
load()
|
||
} catch { ElMessage.error('批量结题失败') }
|
||
finally { batchSubmitting.value = false }
|
||
} else if (batchKind.value === 'activate') {
|
||
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 })))
|
||
ElMessage.success(`已批量开通 ${selection.value.length} 个项目`)
|
||
batchOpen.value = false
|
||
selection.value = []
|
||
load()
|
||
} catch { ElMessage.error('批量开通失败') }
|
||
finally { batchSubmitting.value = false }
|
||
} else if (batchKind.value === 'score') {
|
||
const f = batchScoreForm
|
||
if (!f.qualityScore || f.qualityScore < 1 || f.qualityScore > 5) return ElMessage.error('履约质量必须是 1~5 的数字')
|
||
if (!f.responseScore || f.responseScore < 1 || f.responseScore > 5) return ElMessage.error('时效响应必须是 1~5 的数字')
|
||
if (!f.cooperationScore || f.cooperationScore < 1 || f.cooperationScore > 5) return ElMessage.error('配合度必须是 1~5 的数字')
|
||
if (!f.complianceScore || f.complianceScore < 1 || f.complianceScore > 5) return ElMessage.error('合规安全必须是 1~5 的数字')
|
||
const total = ((f.qualityScore + f.responseScore + f.cooperationScore + f.complianceScore) / 4).toFixed(1)
|
||
try {
|
||
batchSubmitting.value = true
|
||
let ok = 0
|
||
// 共享评分: 所有项目用同一组 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',
|
||
qualityScore: f.qualityScore,
|
||
responseScore: f.responseScore,
|
||
cooperationScore: f.cooperationScore,
|
||
complianceScore: f.complianceScore
|
||
})
|
||
ok++
|
||
} catch {}
|
||
}
|
||
ElMessage.success(`已批量评分 ${ok}/${selection.value.length} 个项目 (共用评分 ${total})`)
|
||
batchOpen.value = false
|
||
selection.value = []
|
||
load()
|
||
} finally { batchSubmitting.value = false }
|
||
}
|
||
}
|
||
|
||
// ========== 新建项目 ==========
|
||
const newOpen = ref(false)
|
||
// 新建项目独立页面: /manager/projects/new (ManagerProjectsNew.vue)
|
||
|
||
// ========== 项目分配 ==========
|
||
const assignOpen = ref(false)
|
||
const assignBatchMode = ref(false) // 项目分配批量模式开关
|
||
const assignBatchProjects = ref([]) // 批量模式下的项目列表 [{projectId, projectNo, projectName}]
|
||
const assignSubmitting = ref(false)
|
||
const assignForm = reactive({
|
||
projectId: '',
|
||
sponsorAdminUserId: null,
|
||
sponsorAdminUserName: '',
|
||
totalSessions: 0, // 项目总场次 (来自项目本身, 不可被分配覆盖)
|
||
totalAmount: 0, // 项目总金额(元) (来自项目本身, 不可被分配覆盖)
|
||
execRows: [], // 动态加
|
||
deadlineDays: 30
|
||
})
|
||
const sponsorOrgOptions = ref([])
|
||
let _supporterTimer = null
|
||
|
||
// 已分配场次合计 (实时)
|
||
const assignedSessions = computed(() =>
|
||
assignForm.execRows.reduce((s, r) => s + Number(r.sessions || 0), 0)
|
||
)
|
||
const singleSessionOver = computed(() =>
|
||
!assignBatchMode.value && Number(assignForm.totalSessions || 0) > 0 && assignedSessions.value > Number(assignForm.totalSessions)
|
||
)
|
||
// 已分配金额合计 (实时, 保留 2 位小数避免浮点漂移)
|
||
const assignedAmount = computed(() => {
|
||
const sum = assignForm.execRows.reduce((s, r) => s + Number(r.amount || 0), 0)
|
||
return Math.round(sum * 100) / 100
|
||
})
|
||
const singleAmountOver = computed(() =>
|
||
!assignBatchMode.value && Number(assignForm.totalAmount || 0) > 0 && assignedAmount.value > Number(assignForm.totalAmount)
|
||
)
|
||
|
||
function makeEmptyExecRow() {
|
||
return { _loading: false, _timer: null, _options: [], execUserId: null, sessions: 0, amount: 0, remark: '' }
|
||
}
|
||
|
||
function resetAssignForm() {
|
||
Object.assign(assignForm, { projectId:'', sponsorAdminUserId:null, sponsorAdminUserName:'', totalSessions:0, totalAmount:0, execRows:[makeEmptyExecRow()], deadlineDays:30 })
|
||
sponsorOrgOptions.value = []
|
||
assignBatchMode.value = false
|
||
assignBatchProjects.value = []
|
||
}
|
||
|
||
function addExecRow() {
|
||
const row = makeEmptyExecRow()
|
||
assignForm.execRows.push(row)
|
||
// 新行立即加载默认列表
|
||
searchExecutors('', row)
|
||
}
|
||
|
||
function onAssignProjectChange(v) {
|
||
const proj = rows.value.find(r => r.projectId === v)
|
||
if (proj) {
|
||
assignForm.sponsorAdminUserId = proj.sponsorAdminUserId || null
|
||
assignForm.sponsorAdminUserName = proj.sponsorAdminUserName || ''
|
||
assignForm.totalSessions = Number(proj.totalSessions || 0)
|
||
assignForm.totalAmount = Number(proj.totalAmount || 0)
|
||
}
|
||
// 切换项目时重新拉已分配记录 (回显)
|
||
loadAssigns(v)
|
||
// 重新拉支持方候选
|
||
loadSponsorOrgs('')
|
||
}
|
||
|
||
// 加载支持方 (sponsor 角色) - 按公司名查, JOIN biz_org + sys_user
|
||
async function loadSponsorOrgs(query) {
|
||
if (_supporterTimer) clearTimeout(_supporterTimer)
|
||
_supporterTimer = setTimeout(async () => {
|
||
try {
|
||
// 主搜索: 按 org_name 模糊匹配
|
||
const params = {}
|
||
if (query) params.orgName = query
|
||
const r = await listSponsorOrgs(params)
|
||
sponsorOrgOptions.value = r.data || []
|
||
// 已选项不在结果里时按 userId 单独拉取并加首位
|
||
if (assignForm.sponsorAdminUserId && !sponsorOrgOptions.value.find(u => u.userId === assignForm.sponsorAdminUserId)) {
|
||
const r2 = await listSponsorOrgs({ userId: assignForm.sponsorAdminUserId })
|
||
const sel = (r2.data || []).find(u => u.userId === assignForm.sponsorAdminUserId)
|
||
if (sel) sponsorOrgOptions.value = [sel, ...sponsorOrgOptions.value]
|
||
}
|
||
} catch (e) {
|
||
sponsorOrgOptions.value = []
|
||
}
|
||
}, 200)
|
||
}
|
||
|
||
function searchSponsorOrgs(q) { loadSponsorOrgs(q) }
|
||
|
||
// 选中支持方后回填 user_name (缓存到 biz_project.sponsor_admin_user_name)
|
||
function onSponsorOrgPick(userId) {
|
||
const u = sponsorOrgOptions.value.find(x => x.userId === userId)
|
||
if (u) assignForm.sponsorAdminUserName = u.userName
|
||
}
|
||
|
||
// 拉取项目已分配的执行方
|
||
async function loadAssigns(projectId) {
|
||
if (!projectId) { assignForm.execRows = [makeEmptyExecRow()]; return }
|
||
try {
|
||
const r = await request({ url: `/business/project/${projectId}/assigns`, method: 'get' })
|
||
const list = r.data || []
|
||
if (list.length) {
|
||
assignForm.execRows = list.map(a => ({
|
||
...makeEmptyExecRow(),
|
||
execUserId: a.execUserId,
|
||
sessions: a.sessions || 0,
|
||
amount: Number(a.amount || 0),
|
||
remark: a.remark || ''
|
||
}))
|
||
} else {
|
||
assignForm.execRows = [makeEmptyExecRow()]
|
||
}
|
||
// 每一行立即触发远程搜索, 让 el-select 拿到 _options 才能正确显示已选 label
|
||
assignForm.execRows.forEach(r => searchExecutors('', r))
|
||
} catch (e) {
|
||
console.error('加载已分配执行方失败', e)
|
||
assignForm.execRows = [makeEmptyExecRow()]
|
||
assignForm.execRows.forEach(r => searchExecutors('', r))
|
||
}
|
||
}
|
||
|
||
// 加载执行方 (按执行单位名 org_name 模糊匹配)
|
||
// 关键: 数据源是 biz_org (org_type='executor') JOIN sys_user (MAIN 账号 only)
|
||
// label=orgName (执行单位名称), value=MAIN user_id (直接写 biz_project_assign.exec_user_id)
|
||
// 走 /business/org/executorOptions (不在走 sys_user.role_type='executor' 查询, 避免子账号混入)
|
||
async function searchExecutors(query, row) {
|
||
if (row._timer) clearTimeout(row._timer)
|
||
row._loading = true
|
||
row._timer = setTimeout(async () => {
|
||
try {
|
||
const params = {}
|
||
if (query) params.orgName = query
|
||
const r = await listExecutorOrgs(params)
|
||
row._options = r.data || []
|
||
// 选中的项如果不在结果里, 按 userId 单独拉取并加到首位 (保证已选项在 el-option 中能找到 label)
|
||
if (row.execUserId && !row._options.find(u => u.userId === row.execUserId)) {
|
||
const r2 = await listExecutorOrgs({ userId: row.execUserId })
|
||
const sel = (r2.data || []).find(u => u.userId === row.execUserId)
|
||
if (sel) row._options = [sel, ...row._options]
|
||
}
|
||
} catch (e) {
|
||
row._options = []
|
||
} finally {
|
||
row._loading = false
|
||
}
|
||
}, 300)
|
||
}
|
||
|
||
// 选中执行方后只保留 userId (后端 service 层会反查 biz_org 写 execution_unit_id;
|
||
// 名称/nickName/deptName 不再缓存, 由前端从 _options 里实时取)
|
||
function onExecUserPick(row, userId) {
|
||
const u = (row._options || []).find(x => x.userId === userId)
|
||
if (u) {
|
||
// 仅清掉旧的非持久化字段 (兼容旧 _options 残留), 业务字段不写
|
||
}
|
||
}
|
||
|
||
async function submitAssign() {
|
||
// 批量模式不需要选项目, 单选模式必选
|
||
if (!assignBatchMode.value && !assignForm.projectId) return ElMessage.warning('请选择项目')
|
||
const valid = assignForm.execRows.filter(r => r.execUserId && Number(r.sessions) > 0)
|
||
if (!valid.length) return ElMessage.warning('请至少选择 1 个执行方并填写场次')
|
||
const amount = Math.round(valid.reduce((s, r) => s + Number(r.amount || 0), 0) * 100) / 100
|
||
const sessions = valid.reduce((s, r) => s + Number(r.sessions || 0), 0)
|
||
|
||
// 校验: 各执行方分配场次 / 金额总和 <= 项目总场次 / 总金额
|
||
// (项目自身的 total_sessions / total_amount 是创建时定的, 不再被分配覆盖)
|
||
if (assignBatchMode.value) {
|
||
for (const p of assignBatchProjects.value) {
|
||
const proj = rows.value.find(r => r.projectId === p.projectId)
|
||
const capS = proj ? Number(proj.totalSessions || 0) : 0
|
||
const capA = proj ? Number(proj.totalAmount || 0) : 0
|
||
if (capS > 0 && sessions > capS) {
|
||
return ElMessage.warning(`项目 ${p.projectNo} 已分配 ${sessions} 场, 超过总场次 ${capS}, 请调整`)
|
||
}
|
||
if (capA > 0 && amount > capA) {
|
||
return ElMessage.warning(`项目 ${p.projectNo} 已分配 ¥${amount}, 超过总金额 ¥${capA}, 请调整`)
|
||
}
|
||
}
|
||
} else {
|
||
const capS = Number(assignForm.totalSessions || 0)
|
||
const capA = Number(assignForm.totalAmount || 0)
|
||
if (capS > 0 && sessions > capS) {
|
||
return ElMessage.warning(`已分配 ${sessions} 场, 超过项目总场次 ${capS}, 请调整`)
|
||
}
|
||
if (capA > 0 && amount > capA) {
|
||
return ElMessage.warning(`已分配 ¥${amount}, 超过项目总金额 ¥${capA}, 请调整`)
|
||
}
|
||
}
|
||
|
||
const assignBody = valid.map(r => ({
|
||
execUserId: r.execUserId,
|
||
sessions: r.sessions,
|
||
amount: r.amount,
|
||
remark: r.remark
|
||
}))
|
||
assignSubmitting.value = true
|
||
try {
|
||
if (assignBatchMode.value) {
|
||
// 批量: 每个项目单独更新 + 单独写分配 (后端先删后插策略)
|
||
let ok = 0, errs = []
|
||
for (const p of assignBatchProjects.value) {
|
||
try {
|
||
await bizUpdate('project', {
|
||
projectId: p.projectId,
|
||
sponsorAdminUserId: assignForm.sponsorAdminUserId,
|
||
sponsorAdminUserName: assignForm.sponsorAdminUserName,
|
||
submitDeadlineDays: assignForm.deadlineDays,
|
||
})
|
||
await request({
|
||
url: `/business/project/${p.projectId}/assigns`,
|
||
method: 'post',
|
||
data: assignBody
|
||
})
|
||
ok++
|
||
} catch (e) {
|
||
errs.push(`${p.projectNo}: ${e?.msg || e.message}`)
|
||
}
|
||
}
|
||
if (errs.length) ElMessageBox.alert(`批量分配: 成功 ${ok}/${assignBatchProjects.value.length}\n\n失败:\n${errs.join('\n')}`, '分配结果', { type: 'warning' })
|
||
else ElMessage.success(`批量分配成功 (${ok}/${assignBatchProjects.value.length})`)
|
||
} else {
|
||
// 单条模式
|
||
await bizUpdate('project', {
|
||
projectId: assignForm.projectId,
|
||
sponsorAdminUserId: assignForm.sponsorAdminUserId,
|
||
sponsorAdminUserName: assignForm.sponsorAdminUserName,
|
||
submitDeadlineDays: assignForm.deadlineDays,
|
||
})
|
||
await request({
|
||
url: `/business/project/${assignForm.projectId}/assigns`,
|
||
method: 'post',
|
||
data: assignBody
|
||
})
|
||
ElMessage.success('分配成功')
|
||
}
|
||
assignOpen.value = false
|
||
load()
|
||
} catch (e) {
|
||
console.error(e)
|
||
ElMessage.error(e?.msg || '分配失败')
|
||
}
|
||
finally { assignSubmitting.value = false }
|
||
}
|
||
|
||
const previewOpen = ref(false)
|
||
const previewUrl = ref('')
|
||
const previewTitle = ref('附件预览')
|
||
function openPreview(url, title) { previewUrl.value = url; previewTitle.value = title || '附件预览'; previewOpen.value = true }
|
||
|
||
onMounted(load)
|
||
// 监听路由变化 (从 /manager/projects/new 跳回时强制重载)
|
||
watch(() => route.fullPath, (newPath, oldPath) => {
|
||
if (newPath === '/manager/projects') {
|
||
console.info('[manager-projects] route watch reload, from=', oldPath, 'to=', newPath)
|
||
load()
|
||
}
|
||
})
|
||
// 兜底: 即便路由完全相同,从列表跳到详情再回来也触发
|
||
watch(() => route.path + (route.query._t || ''), () => load())
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 1:1 抄 sponsor/SponsorProjects.vue */
|
||
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
||
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||
.filter-form { display: flex; flex-wrap: wrap; gap: 0 16px; padding-bottom: 12px; border-bottom: 1px solid #f0f0f0; margin-bottom: 12px; }
|
||
.toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
|
||
|
||
/* 操作列下拉 */
|
||
.op-cell { display: inline-flex; align-items: center; gap: 12px; }
|
||
.op-cell :deep(.el-button) { margin-left: 0 !important; padding: 0 4px !important; }
|
||
.op-dropdown { padding-right: 0; }
|
||
.op-caret { margin-left: 2px; font-size: 10px; }
|
||
|
||
/* 工具按钮 (与 sponsor 一致) */
|
||
.toolbar :deep(.action-btn) {
|
||
height: 32px; padding: 0 16px; font-size: 13px; border-radius: 4px;
|
||
background: var(--brand-primary); color: #fff; border: 1px solid var(--brand-primary);
|
||
display: inline-flex; align-items: center; justify-content: center; white-space: nowrap;
|
||
transition: opacity .15s;
|
||
}
|
||
.toolbar :deep(.action-btn:hover) { opacity: .85; }
|
||
.toolbar :deep(.action-btn.secondary) {
|
||
background: #fff; color: var(--brand-primary); border: 1px solid var(--brand-primary);
|
||
}
|
||
.toolbar :deep(.action-btn:disabled) {
|
||
background: #f5f7fa; color: #c0c4cc; border-color: #e4e7ed; cursor: not-allowed;
|
||
}
|
||
.toolbar :deep(.badge) {
|
||
margin-left: 4px; padding: 0 6px; background: var(--brand-primary); color: #fff;
|
||
border-radius: 8px; font-size: 11px; line-height: 16px;
|
||
}
|
||
.toolbar :deep(.action-btn.secondary .badge) { background: var(--brand-primary); color: #fff; }
|
||
.page-toolbar :deep(.action-btn.secondary:hover) { background: rgba(30,58,138,.06); }
|
||
.page-toolbar :deep(.action-btn.is-disabled),
|
||
.page-toolbar :deep(.action-btn:disabled) { background: #f5f7fa; color: #c0c4cc; border-color: #e4e7ed; opacity: 1; cursor: not-allowed; }
|
||
.page-toolbar :deep(.action-btn.secondary.is-disabled),
|
||
.page-toolbar :deep(.action-btn.secondary:disabled) { background: #fff; color: #c0c4cc; border-color: #e4e7ed; }
|
||
.page-toolbar :deep(.action-btn .badge) {
|
||
display: inline-block; margin-left: 6px; padding: 0 6px; min-width: 18px; height: 18px;
|
||
line-height: 18px; font-size: 11px; text-align: center;
|
||
background: #fff; color: var(--brand-primary); border-radius: 9px;
|
||
}
|
||
.page-toolbar :deep(.action-btn.secondary .badge) { background: var(--brand-primary); color: #fff; }
|
||
.score-row { padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
|
||
.score-row:last-child { border-bottom: none; }
|
||
.score-title { font-weight: 600; color: #303133; margin-bottom: 6px; }
|
||
.score-no { color: #909399; font-weight: normal; font-size: 12px; }
|
||
.score-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||
.score-item { display: flex; align-items: center; gap: 8px; }
|
||
.score-item span { width: 70px; color: #606266; font-size: 13px; }
|
||
|
||
/* 分配 dialog 总场次条 */
|
||
.assign-sessions-bar {
|
||
display: flex; align-items: center; gap: 18px;
|
||
margin: -4px 0 12px; padding: 10px 14px;
|
||
background: #f5f7fa; border: 1px solid #ebeef5; border-radius: 4px;
|
||
font-size: 13px; color: #606266;
|
||
}
|
||
.assign-sessions-bar b { color: #303133; font-size: 14px; margin: 0 2px; }
|
||
.assign-sessions-bar .is-over b { color: #f56c6c; }
|
||
.assign-sessions-bar .over-warn { color: #f56c6c; font-weight: 600; }
|
||
.assign-sessions-bar .hint { color: #909399; font-size: 12px; }
|
||
.assign-sessions-bar .sep { color: #dcdfe6; margin: 0 4px; }
|
||
|
||
|
||
</style> |