批量推送

This commit is contained in:
郭庆泰
2026-08-16 21:24:53 +08:00
parent 0c093076a4
commit ccfd03a6eb
55 changed files with 3772 additions and 577 deletions
+99 -41
View File
@@ -3,18 +3,13 @@
<div class="breadcrumb">首页 / 项目管理</div>
<el-form inline :model="q" class="filter-form">
<el-form-item label="赞助公司:"><el-input v-model="q.orgName" placeholder="输入赞助公司" clearable /></el-form-item>
<el-form-item label="公司类型:">
<el-select v-model="q.orgType" placeholder="全部" clearable style="width:120px">
<el-option label="赞助方" value="sponsor" />
<el-option label="执行方" value="executor" />
</el-select>
</el-form-item>
<el-form-item label="赞助方负责人:"><el-input v-model="q.sponsorAdminUserName" 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>
<el-option label="线上" value="线上" /><el-option label="线下" value="线" />
<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>
@@ -24,12 +19,12 @@
<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>
<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>
<el-select v-model="q.isSettled" placeholder="请选择" clearable style="width:120px">
<el-option label="已结算" value="1" /><el-option label="未结算" value="0" />
</el-select>
</el-form-item>
@@ -69,14 +64,7 @@
<span v-else style="color:#c0c4cc">-</span>
</template>
</el-table-column>
<el-table-column prop="orgName" label="赞助公司" width="160" show-overflow-tooltip />
<el-table-column prop="orgType" label="公司类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.orgType === 'sponsor' ? 'success' : 'primary'" disable-transitions>
{{ row.orgType === 'sponsor' ? '赞助方' : '执行方' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sponsorAdminUserName" label="赞助方负责人" width="160" 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 }">
@@ -133,11 +121,26 @@
<div style="color:#606266;font-size:13px"> <b>{{ assignBatchProjects.length }}</b> 个项目</div>
</el-form-item>
<el-form-item label="支持方">
<el-select v-model="assignForm.orgId" placeholder="请选择赞助公司" filterable :filter-method="searchSupporters" clearable style="width:100%" @change="onSupporterPick">
<el-select v-model="assignForm.sponsorAdminUserId" placeholder="请选择赞助方负责人" filterable :filter-method="searchSupporters" clearable style="width:100%" @change="onSupporterPick">
<el-option v-for="u in supporterOptions" :key="u.userId" :label="`${u.userName} (${u.nickName || ''})`" :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">
@@ -264,7 +267,7 @@
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { bizList, bizAdd, bizUpdate, bizDelete } from '@/api/public'
import { listExecutor, listSupporters } from '@/api/system'
@@ -275,7 +278,7 @@ import Preview from '@/components/Preview.vue'
const router = useRouter()
const route = useRoute()
const q = ref({ orgName: '', orgType: '', projectNo: '', projectName: '', projectForm: '', startTime: '', endTime: '', isFinished: '', isSettled: '', ratingScore: '' })
const q = ref({ sponsorAdminUserName: '', execOrgName: '', projectNo: '', projectName: '', projectForm: '', startTime: '', endTime: '', isFinished: '', isSettled: '', ratingScore: '' })
const rows = ref([])
const loading = ref(false)
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
@@ -327,7 +330,7 @@ async function doDelete(row) {
}
}
function reset() { q.value = { orgName:'', orgType:'', projectNo:'', projectName:'', projectForm:'', startTime:'', endTime:'', isFinished:'', isSettled:'', ratingScore:'' }; page.pageNum = 1; load() }
function reset() { q.value = { sponsorAdminUserName:'', execOrgName:'', projectNo:'', projectName:'', projectForm:'', startTime:'', endTime:'', isFinished:'', isSettled:'', ratingScore:'' }; page.pageNum = 1; load() }
// 导出
function exportProjects() { ElMessage.info('导出项目功能开发中') }
@@ -563,20 +566,38 @@ const assignBatchProjects = ref([]) // 批量模式下的项目列表 [
const assignSubmitting = ref(false)
const assignForm = reactive({
projectId: '',
orgId: null,
orgName: '',
sponsorAdminUserId: null,
sponsorAdminUserName: '',
totalSessions: 0, // 项目总场次 (来自项目本身, 不可被分配覆盖)
totalAmount: 0, // 项目总金额(元) (来自项目本身, 不可被分配覆盖)
execRows: [], // 动态加
deadlineDays: 30
})
const supporterOptions = 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, execUserName: '', execNickName: '', execOrg: '', sessions: 0, amount: 0, remark: '' }
}
function resetAssignForm() {
Object.assign(assignForm, { projectId:'', orgId:null, orgName:'', execRows:[makeEmptyExecRow()], deadlineDays:30 })
Object.assign(assignForm, { projectId:'', sponsorAdminUserId:null, sponsorAdminUserName:'', totalSessions:0, totalAmount:0, execRows:[makeEmptyExecRow()], deadlineDays:30 })
supporterOptions.value = []
assignBatchMode.value = false
assignBatchProjects.value = []
@@ -592,8 +613,10 @@ function addExecRow() {
function onAssignProjectChange(v) {
const proj = rows.value.find(r => r.projectId === v)
if (proj) {
assignForm.orgId = proj.orgId || null
assignForm.orgName = proj.orgName || ''
assignForm.sponsorAdminUserId = proj.sponsorAdminUserId || null
assignForm.sponsorAdminUserName = proj.sponsorAdminUserName || ''
assignForm.totalSessions = Number(proj.totalSessions || 0)
assignForm.totalAmount = Number(proj.totalAmount || 0)
}
// 切换项目时重新拉已分配记录 (回显)
loadAssigns(v)
@@ -611,9 +634,9 @@ async function loadSupporters(query) {
const r = await listSupporters(params)
supporterOptions.value = r.data || []
// 已选项不在结果里时单独拉取并加首位
if (assignForm.orgId && !supporterOptions.value.find(u => u.userId === assignForm.orgId)) {
const r2 = await listSupporters({ userId: assignForm.orgId })
const sel = (r2.data || []).find(u => u.userId === assignForm.orgId)
if (assignForm.sponsorAdminUserId && !supporterOptions.value.find(u => u.userId === assignForm.sponsorAdminUserId)) {
const r2 = await listSupporters({ userId: assignForm.sponsorAdminUserId })
const sel = (r2.data || []).find(u => u.userId === assignForm.sponsorAdminUserId)
if (sel) supporterOptions.value = [sel, ...supporterOptions.value]
}
} catch (e) {
@@ -627,7 +650,7 @@ function searchSupporters(q) { loadSupporters(q) }
// 选中支持方后回填名称
function onSupporterPick(userId) {
const u = supporterOptions.value.find(x => x.userId === userId)
if (u) assignForm.orgName = u.nickName || u.userName
if (u) assignForm.sponsorAdminUserName = u.nickName || u.userName
}
// 拉取项目已分配的执行方
@@ -698,8 +721,34 @@ 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 = valid.reduce((s, r) => s + Number(r.amount || 0), 0)
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,
execUserName: r.execUserName,
@@ -718,10 +767,8 @@ async function submitAssign() {
try {
await bizUpdate('project', {
projectId: p.projectId,
orgId: assignForm.orgId,
orgName: assignForm.orgName,
totalAmount: amount,
totalSessions: sessions,
sponsorAdminUserId: assignForm.sponsorAdminUserId,
sponsorAdminUserName: assignForm.sponsorAdminUserName,
submitDeadlineDays: assignForm.deadlineDays,
})
await request({
@@ -740,10 +787,8 @@ async function submitAssign() {
// 单条模式
await bizUpdate('project', {
projectId: assignForm.projectId,
orgId: assignForm.orgId,
orgName: assignForm.orgName,
totalAmount: amount,
totalSessions: sessions,
sponsorAdminUserId: assignForm.sponsorAdminUserId,
sponsorAdminUserName: assignForm.sponsorAdminUserName,
submitDeadlineDays: assignForm.deadlineDays,
})
await request({
@@ -830,5 +875,18 @@ watch(() => route.path + (route.query._t || ''), () => load())
.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>