From 36b62a26b91041d0558577f33e471d0f585be74c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=AD=E5=BA=86=E6=B3=B0?= <12369755+htcloud1@user.noreply.gitee.com> Date: Thu, 20 Aug 2026 23:02:08 +0800 Subject: [PATCH] =?UTF-8?q?refactor(projects):=20=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=88=86=E9=85=8D=20dialog=20=E6=8B=86=E4=B8=BA=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E5=AD=90=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 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 --- ry-vue3/src/router/index.js | 2 + .../views/manager/ManagerProjectsAssign.vue | 424 ++++++++++++++++++ ry-vue3/src/views/manager/Projects.vue | 354 +-------------- 3 files changed, 438 insertions(+), 342 deletions(-) create mode 100644 ry-vue3/src/views/manager/ManagerProjectsAssign.vue diff --git a/ry-vue3/src/router/index.js b/ry-vue3/src/router/index.js index d9f7a3d..73b7311 100644 --- a/ry-vue3/src/router/index.js +++ b/ry-vue3/src/router/index.js @@ -61,6 +61,8 @@ const routes = [ { 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/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/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: '专家审核' } }, diff --git a/ry-vue3/src/views/manager/ManagerProjectsAssign.vue b/ry-vue3/src/views/manager/ManagerProjectsAssign.vue new file mode 100644 index 0000000..acd20cf --- /dev/null +++ b/ry-vue3/src/views/manager/ManagerProjectsAssign.vue @@ -0,0 +1,424 @@ + + + + + \ No newline at end of file diff --git a/ry-vue3/src/views/manager/Projects.vue b/ry-vue3/src/views/manager/Projects.vue index c44ff5c..dbeaa16 100644 --- a/ry-vue3/src/views/manager/Projects.vue +++ b/ry-vue3/src/views/manager/Projects.vue @@ -147,83 +147,7 @@ - - - - - - - - - - - -
{{ assignBatchProjects.length }} 个项目
-
- - - - - - 执行方分配 - -
- 项目总场次: {{ assignForm.totalSessions || 0 }} - 已分配场次: {{ assignedSessions }} - ⚠ 已超出 {{ assignedSessions - assignForm.totalSessions }} 场 - | - 总金额(元): {{ assignForm.totalAmount || 0 }} - 已分配金额: ¥ {{ assignedAmount }} - ⚠ 已超出 ¥ {{ Math.round((assignedAmount - assignForm.totalAmount) * 100) / 100 }} -
- -
- 本次分配: {{ assignedSessions }} 场 / ¥ {{ assignedAmount }} - (保存时按各项目自身的总场次和总金额逐项校验) -
- - - - - - - - - - - - - - - -
- + 增加执行方 -
- - - -
- -
+ @@ -348,13 +272,8 @@ function onAction(cmd, 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) + // 跳转独立子页面 /manager/projects/assign?projectId=X + router.push({ path: '/manager/projects/assign', query: { projectId: row.projectId } }) } function doEdit(row) { router.push(`/manager/projects/edit/${row.projectId}`) } @@ -548,21 +467,13 @@ function openAssign() { 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 })) + // 跳转独立子页面: 单条 vs 批量 + if (selection.value.length === 1) { + router.push({ path: '/manager/projects/assign', query: { projectId: selection.value[0].projectId } }) } else { - assignBatchMode.value = false - assignBatchProjects.value = [] - assignForm.projectId = selection.value[0].projectId - onAssignProjectChange(assignForm.projectId) + const ids = selection.value.map(r => r.projectId).join(',') + router.push({ path: '/manager/projects/assign-batch', query: { projectIds: ids } }) } - assignOpen.value = true - setTimeout(() => { - assignForm.execRows.forEach(r => searchExecutors('', r)) - loadSponsorOrgs('') - }, 100) } function openBatch(kind) { if (!selection.value.length) return ElMessage.warning('请先勾选项目') @@ -644,102 +555,17 @@ async function confirmBatch() { 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 -}) +// ========== 项目分配 dialog 已迁到独立子页面 /manager/projects/assign (单条) 和 /assign-batch (批量) ========== +// 仅保留顶部筛选区使用的远程搜索状态 (sponsorOrgOptions / executorOrgOptions), 不再保留 dialog 相关 state 和 functions + const sponsorOrgOptions = ref([]) const sponsorSearching = ref(false) -let _supporterTimer = null let _supporterFilterTimer = null -// 顶部筛选区专用: 与 assignForm.row._options 隔离 (row 级是每行一个 _options) +// 顶部筛选区专用 const executorOrgOptions = ref([]) const execSearching = ref(false) 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 + 已选项重拉回) function searchSponsorOrgsForFilter(query) { if (_supporterFilterTimer) clearTimeout(_supporterFilterTimer) @@ -794,162 +620,6 @@ function searchExecutorOrgsForFilter(query) { }, 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 previewUrl = ref('') const previewTitle = ref('附件预览')