refactor(projects): 项目分配 dialog 拆为独立子页面
- 新增 ManagerProjectsAssign.vue (单条/批量共用, 通过 route.query.projectId/projectIds 区分) - router 加 2 个路由 /manager/projects/assign 和 /assign-batch - Projects.vue 移除项目分配 dialog (template + 9 个 state/computed/function) - Projects.vue 两个入口 (toolbar 项目分配 + 行内项目分配) 改为 router.push - 顶部筛选区的 searchSponsorOrgsForFilter / searchExecutorOrgsForFilter 保留 (跟子页面解耦) - 功能 1:1 不变: 支持方 / 实时校验 / 执行方分配 / 总场次超量校验 / 提交截止天数 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,8 @@ const routes = [
|
|||||||
{ path: 'projects/new', name: 'manager-projects-new', component: () => import('@/views/manager/ProjectsNew.vue'), meta: { title: '新建项目' } },
|
{ path: 'projects/new', name: 'manager-projects-new', component: () => import('@/views/manager/ProjectsNew.vue'), meta: { title: '新建项目' } },
|
||||||
{ path: 'projects/edit/:projectId', name: 'manager-projects-edit', component: () => import('@/views/manager/ProjectsNew.vue'), meta: { title: '编辑项目' } },
|
{ path: 'projects/edit/:projectId', name: 'manager-projects-edit', component: () => import('@/views/manager/ProjectsNew.vue'), meta: { title: '编辑项目' } },
|
||||||
{ path: 'projects/detail/:projectId', name: 'manager-projects-detail', component: () => import('@/views/manager/ManagerProjectDetail.vue'), meta: { title: '项目详情' } },
|
{ path: 'projects/detail/:projectId', name: 'manager-projects-detail', component: () => import('@/views/manager/ManagerProjectDetail.vue'), meta: { title: '项目详情' } },
|
||||||
|
{ path: 'projects/assign', name: 'manager-projects-assign', component: () => import('@/views/manager/ManagerProjectsAssign.vue'), meta: { title: '项目分配' } },
|
||||||
|
{ path: 'projects/assign-batch', name: 'manager-projects-assign-batch', component: () => import('@/views/manager/ManagerProjectsAssign.vue'), meta: { title: '批量项目分配' } },
|
||||||
{ path: 'meetings', name: 'manager-meetings', component: () => import('@/views/manager/Meetings.vue'), meta: { title: '会议管理' } },
|
{ path: 'meetings', name: 'manager-meetings', component: () => import('@/views/manager/Meetings.vue'), meta: { title: '会议管理' } },
|
||||||
{ path: 'meetings/new', name: 'manager-meetings-new', component: () => import('@/views/manager/MeetingNew.vue'), meta: { title: '新建会议' } },
|
{ path: 'meetings/new', name: 'manager-meetings-new', component: () => import('@/views/manager/MeetingNew.vue'), meta: { title: '新建会议' } },
|
||||||
{ path: 'experts', name: 'manager-experts', component: () => import('@/views/expert/Experts.vue'), meta: { title: '专家审核' } },
|
{ path: 'experts', name: 'manager-experts', component: () => import('@/views/expert/Experts.vue'), meta: { title: '专家审核' } },
|
||||||
|
|||||||
@@ -0,0 +1,424 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-card">
|
||||||
|
<div class="breadcrumb">
|
||||||
|
首页 / 项目管理 / <span class="current">项目分配</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已选项目 (单条: 显示编号+名称 / 批量: 显示项目数) -->
|
||||||
|
<div v-if="batchMode" class="info-bar">
|
||||||
|
已选 <b>{{ assignBatchProjects.length }}</b> 个项目 (本次分配将按各项目自身的总场次 / 总金额逐项校验)
|
||||||
|
</div>
|
||||||
|
<div v-else class="info-bar">
|
||||||
|
当前项目: <b>{{ currentProject?.projectNo || '-' }}</b> - {{ currentProject?.projectName || '-' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form :model="assignForm" label-width="120px">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 单选模式: 实时校验栏 -->
|
||||||
|
<div v-if="!batchMode" 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>
|
||||||
|
|
||||||
|
<!-- 底部按钮 -->
|
||||||
|
<div class="form-actions">
|
||||||
|
<el-button @click="confirmCancel">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="assignSubmitting" @click="submitAssign">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { bizUpdate, bizGet } from '@/api/public'
|
||||||
|
import { listSponsorOrgs, listExecutorOrgs } from '@/api/system'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// 模式判断: 单条 ?projectId=xxx / 批量 ?projectIds=1,2,3
|
||||||
|
const batchMode = !!route.query.projectIds
|
||||||
|
const singleProjectId = route.query.projectId ? Number(route.query.projectId) : null
|
||||||
|
const batchProjectIds = batchMode ? String(route.query.projectIds).split(',').map(Number) : []
|
||||||
|
|
||||||
|
const currentProject = ref(null) // 单条模式下的项目 (用于顶部 info-bar)
|
||||||
|
const assignBatchProjects = ref([]) // 批量模式下的 [{projectId, projectNo, projectName, totalSessions, totalAmount}]
|
||||||
|
|
||||||
|
const assignForm = reactive({
|
||||||
|
sponsorAdminUserId: null,
|
||||||
|
sponsorAdminUserName: '',
|
||||||
|
totalSessions: 0, // 单条模式的项目总场次 (来自项目本身)
|
||||||
|
totalAmount: 0, // 单条模式的项目总金额
|
||||||
|
execRows: [],
|
||||||
|
deadlineDays: 30
|
||||||
|
})
|
||||||
|
const assignSubmitting = ref(false)
|
||||||
|
|
||||||
|
const sponsorOrgOptions = ref([])
|
||||||
|
let _supporterTimer = null
|
||||||
|
|
||||||
|
// 已分配场次合计 (实时)
|
||||||
|
const assignedSessions = computed(() =>
|
||||||
|
assignForm.execRows.reduce((s, r) => s + Number(r.sessions || 0), 0)
|
||||||
|
)
|
||||||
|
const singleSessionOver = computed(() =>
|
||||||
|
!batchMode && 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(() =>
|
||||||
|
!batchMode && 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, {
|
||||||
|
sponsorAdminUserId: null,
|
||||||
|
sponsorAdminUserName: '',
|
||||||
|
totalSessions: 0,
|
||||||
|
totalAmount: 0,
|
||||||
|
execRows: [makeEmptyExecRow()],
|
||||||
|
deadlineDays: 30
|
||||||
|
})
|
||||||
|
sponsorOrgOptions.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function addExecRow() {
|
||||||
|
const row = makeEmptyExecRow()
|
||||||
|
assignForm.execRows.push(row)
|
||||||
|
searchExecutors('', row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载支持方 (sponsor 角色) - 按公司名查, JOIN biz_org + sys_user
|
||||||
|
async function loadSponsorOrgs(query) {
|
||||||
|
if (_supporterTimer) clearTimeout(_supporterTimer)
|
||||||
|
_supporterTimer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
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) {
|
||||||
|
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)
|
||||||
|
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 单独拉取并加到首位
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExecUserPick(row, userId) {
|
||||||
|
// 仅清掉旧的非持久化字段, 业务字段由前端从 _options 实时取
|
||||||
|
const u = (row._options || []).find(x => x.userId === userId)
|
||||||
|
if (u) { /* 兼容旧 _options 残留, 不写业务字段 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 初始化 ==========
|
||||||
|
async function loadSingleProject() {
|
||||||
|
if (!singleProjectId) return
|
||||||
|
try {
|
||||||
|
const res = await bizGet('project', singleProjectId)
|
||||||
|
const p = res?.data?.data || res?.data || res
|
||||||
|
if (!p) return
|
||||||
|
currentProject.value = p
|
||||||
|
assignForm.sponsorAdminUserId = p.sponsorAdminUserId || null
|
||||||
|
assignForm.sponsorAdminUserName = p.sponsorAdminUserName || ''
|
||||||
|
assignForm.totalSessions = Number(p.totalSessions || 0)
|
||||||
|
assignForm.totalAmount = Number(p.totalAmount || 0)
|
||||||
|
// 拉已分配记录
|
||||||
|
await loadAssigns(singleProjectId)
|
||||||
|
// 拉支持方候选 (含已选项回拉)
|
||||||
|
loadSponsorOrgs('')
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('加载项目数据失败: ' + (e?.msg || e?.message || e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBatchProjects() {
|
||||||
|
if (!batchProjectIds.length) return
|
||||||
|
try {
|
||||||
|
const list = []
|
||||||
|
for (const pid of batchProjectIds) {
|
||||||
|
try {
|
||||||
|
const res = await bizGet('project', pid)
|
||||||
|
const p = res?.data?.data || res?.data || res
|
||||||
|
if (p) {
|
||||||
|
list.push({
|
||||||
|
projectId: p.projectId,
|
||||||
|
projectNo: p.projectNo,
|
||||||
|
projectName: p.projectName,
|
||||||
|
totalSessions: Number(p.totalSessions || 0),
|
||||||
|
totalAmount: Number(p.totalAmount || 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) { /* skip failed project */ }
|
||||||
|
}
|
||||||
|
assignBatchProjects.value = list
|
||||||
|
// 批量模式: 取第一个项目的支持方作为默认 (用户可改)
|
||||||
|
if (list.length) {
|
||||||
|
assignForm.sponsorAdminUserId = list[0].sponsorAdminUserId || null
|
||||||
|
assignForm.sponsorAdminUserName = list[0].sponsorAdminUserName || ''
|
||||||
|
}
|
||||||
|
loadSponsorOrgs('')
|
||||||
|
// 批量模式: 行默认空 1 行 (各项目共用, 保存时按各项目循环)
|
||||||
|
assignForm.execRows = [makeEmptyExecRow()]
|
||||||
|
assignForm.execRows.forEach(r => searchExecutors('', r))
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('加载批量项目失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
resetAssignForm()
|
||||||
|
if (batchMode) loadBatchProjects()
|
||||||
|
else loadSingleProject()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ========== 返回列表 ==========
|
||||||
|
function goBack() {
|
||||||
|
router.push({ path: '/manager/projects', query: { _t: Date.now() } })
|
||||||
|
}
|
||||||
|
function confirmCancel() {
|
||||||
|
ElMessageBox.confirm('确定取消项目分配?未保存的内容将丢失', '提示', { type: 'warning' })
|
||||||
|
.then(() => goBack())
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 保存 ==========
|
||||||
|
async function submitAssign() {
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 校验: 各执行方分配场次 / 金额总和 <= 项目总场次 / 总金额
|
||||||
|
if (batchMode) {
|
||||||
|
for (const p of assignBatchProjects.value) {
|
||||||
|
const capS = Number(p.totalSessions || 0)
|
||||||
|
const capA = Number(p.totalAmount || 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 (batchMode) {
|
||||||
|
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: singleProjectId,
|
||||||
|
sponsorAdminUserId: assignForm.sponsorAdminUserId,
|
||||||
|
sponsorAdminUserName: assignForm.sponsorAdminUserName,
|
||||||
|
submitDeadlineDays: assignForm.deadlineDays,
|
||||||
|
})
|
||||||
|
await request({
|
||||||
|
url: `/business/project/${singleProjectId}/assigns`,
|
||||||
|
method: 'post',
|
||||||
|
data: assignBody
|
||||||
|
})
|
||||||
|
ElMessage.success('分配成功')
|
||||||
|
}
|
||||||
|
goBack()
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.msg || '分配失败')
|
||||||
|
}
|
||||||
|
finally { assignSubmitting.value = false }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-card { background: #fff; padding: 16px 20px; border-radius: 6px; border: 1px solid #f0f0f0; }
|
||||||
|
.breadcrumb { font-size: 13px; color: #8c8c8c; margin-bottom: 12px; }
|
||||||
|
.breadcrumb .current { color: #262626; font-weight: 500; }
|
||||||
|
|
||||||
|
.info-bar {
|
||||||
|
margin-bottom: 12px; padding: 8px 14px;
|
||||||
|
background: #f5f7fa; border: 1px solid #ebeef5; border-radius: 4px;
|
||||||
|
font-size: 13px; color: #606266;
|
||||||
|
}
|
||||||
|
.info-bar b { color: #303133; margin: 0 2px; }
|
||||||
|
|
||||||
|
/* 分配 dialog 总场次条 (从 Projects.vue 1:1 搬) */
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
.form-actions { display: flex; justify-content: center; gap: 16px; padding: 20px 0 4px; border-top: 1px solid #f0f0f0; margin-top: 16px; }
|
||||||
|
</style>
|
||||||
@@ -147,83 +147,7 @@
|
|||||||
|
|
||||||
<!-- ============= 新建项目 dialog(已迁移到独立页面 /manager/projects/new) ============= -->
|
<!-- ============= 新建项目 dialog(已迁移到独立页面 /manager/projects/new) ============= -->
|
||||||
|
|
||||||
<!-- ============= 项目分配 dialog(按原型 project-assign.html 1:1) ============= -->
|
<!-- 项目分配 dialog 已迁移到独立子页面 /manager/projects/assign (单条) 和 /assign-batch (批量) -->
|
||||||
<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 维度评分) ============= -->
|
<!-- ============= 批量评分 dialog(按原型 4 维度评分) ============= -->
|
||||||
<el-dialog v-model="batchOpen" :title="batchMap[batchKind]" width="626px" :show-close="false">
|
<el-dialog v-model="batchOpen" :title="batchMap[batchKind]" width="626px" :show-close="false">
|
||||||
@@ -348,13 +272,8 @@ function onAction(cmd, row) {
|
|||||||
}
|
}
|
||||||
function doCreateMeeting(row) { router.push(`/manager/meetings/new?projectId=${row.projectId}`) }
|
function doCreateMeeting(row) { router.push(`/manager/meetings/new?projectId=${row.projectId}`) }
|
||||||
function doAssign(row) {
|
function doAssign(row) {
|
||||||
resetAssignForm()
|
// 跳转独立子页面 /manager/projects/assign?projectId=X
|
||||||
assignForm.projectId = row.projectId
|
router.push({ path: '/manager/projects/assign', query: { 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}`) }
|
function doEdit(row) { router.push(`/manager/projects/edit/${row.projectId}`) }
|
||||||
|
|
||||||
@@ -548,21 +467,13 @@ function openAssign() {
|
|||||||
ElMessage.warning('请先勾选项目')
|
ElMessage.warning('请先勾选项目')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resetAssignForm()
|
// 跳转独立子页面: 单条 vs 批量
|
||||||
if (selection.value.length >= 2) {
|
if (selection.value.length === 1) {
|
||||||
assignBatchMode.value = true
|
router.push({ path: '/manager/projects/assign', query: { projectId: selection.value[0].projectId } })
|
||||||
assignBatchProjects.value = selection.value.map(r => ({ projectId: r.projectId, projectNo: r.projectNo, projectName: r.projectName }))
|
|
||||||
} else {
|
} else {
|
||||||
assignBatchMode.value = false
|
const ids = selection.value.map(r => r.projectId).join(',')
|
||||||
assignBatchProjects.value = []
|
router.push({ path: '/manager/projects/assign-batch', query: { projectIds: ids } })
|
||||||
assignForm.projectId = selection.value[0].projectId
|
|
||||||
onAssignProjectChange(assignForm.projectId)
|
|
||||||
}
|
}
|
||||||
assignOpen.value = true
|
|
||||||
setTimeout(() => {
|
|
||||||
assignForm.execRows.forEach(r => searchExecutors('', r))
|
|
||||||
loadSponsorOrgs('')
|
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
function openBatch(kind) {
|
function openBatch(kind) {
|
||||||
if (!selection.value.length) return ElMessage.warning('请先勾选项目')
|
if (!selection.value.length) return ElMessage.warning('请先勾选项目')
|
||||||
@@ -644,102 +555,17 @@ async function confirmBatch() {
|
|||||||
const newOpen = ref(false)
|
const newOpen = ref(false)
|
||||||
// 新建项目独立页面: /manager/projects/new (ManagerProjectsNew.vue)
|
// 新建项目独立页面: /manager/projects/new (ManagerProjectsNew.vue)
|
||||||
|
|
||||||
// ========== 项目分配 ==========
|
// ========== 项目分配 dialog 已迁到独立子页面 /manager/projects/assign (单条) 和 /assign-batch (批量) ==========
|
||||||
const assignOpen = ref(false)
|
// 仅保留顶部筛选区使用的远程搜索状态 (sponsorOrgOptions / executorOrgOptions), 不再保留 dialog 相关 state 和 functions
|
||||||
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([])
|
const sponsorOrgOptions = ref([])
|
||||||
const sponsorSearching = ref(false)
|
const sponsorSearching = ref(false)
|
||||||
let _supporterTimer = null
|
|
||||||
let _supporterFilterTimer = null
|
let _supporterFilterTimer = null
|
||||||
// 顶部筛选区专用: 与 assignForm.row._options 隔离 (row 级是每行一个 _options)
|
// 顶部筛选区专用
|
||||||
const executorOrgOptions = ref([])
|
const executorOrgOptions = ref([])
|
||||||
const execSearching = ref(false)
|
const execSearching = ref(false)
|
||||||
let _executorFilterTimer = null
|
let _executorFilterTimer = 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) }
|
|
||||||
|
|
||||||
// 顶部筛选区远程搜索 (多选 el-select, 复用 sponsorOrgOptions, 加 loading + 已选项重拉回)
|
// 顶部筛选区远程搜索 (多选 el-select, 复用 sponsorOrgOptions, 加 loading + 已选项重拉回)
|
||||||
function searchSponsorOrgsForFilter(query) {
|
function searchSponsorOrgsForFilter(query) {
|
||||||
if (_supporterFilterTimer) clearTimeout(_supporterFilterTimer)
|
if (_supporterFilterTimer) clearTimeout(_supporterFilterTimer)
|
||||||
@@ -794,162 +620,6 @@ function searchExecutorOrgsForFilter(query) {
|
|||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选中支持方后回填 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 previewOpen = ref(false)
|
||||||
const previewUrl = ref('')
|
const previewUrl = ref('')
|
||||||
const previewTitle = ref('附件预览')
|
const previewTitle = ref('附件预览')
|
||||||
|
|||||||
Reference in New Issue
Block a user