refactor: 合并 biz_support_unit/biz_execution_unit/biz_service_org 为 biz_org, 删除 2 张孤儿表, 改 biz_person.work_unit → org_id FK

主要改动:
- SQL: biz_support_unit → biz_org, 加 org_type, 加 business_nature; 删 biz_execution_unit, biz_service_org
- SQL: biz_project.support_unit_id/name + service_org_id/name → org_id/name/type
- SQL: biz_meeting.support_unit_name → org_name
- SQL: biz_person.work_unit → org_id (FK)
- 后端: 新 BizOrg entity/mapper/service/controller
- 后端: BizProject/BizMeeting 字段重命名
- 后端: 删 12 个 BizSupportUnit/BizExecutionUnit/BizServiceOrg Java 文件
- 后端: BizPersonImportVO.workUnit → orgName (导入时查 biz_org 取 org_id)
- 后端: BizAuthController.registerExecutor 完整实现 (原 registerSupplier stub)
- 前端: 新 admin/Orgs.vue + manager/Orgs.vue (原 SupportUnits)
- 前端: RegisterExecutor.vue (原 RegisterSupplier, 单页 2 步)
- 前端: sponsor/executor/manager 多个文件 workUnit → orgId/orgName 重命名
- 前端: 统一 supplier → executor, 业务命名 sponsor(赞助方) / executor(执行方=供应商)
- 前端: 全工程 execution → executor (company type / role / person unit_type)
This commit is contained in:
郭庆泰
2026-08-15 12:41:10 +08:00
commit cf5790229a
694 changed files with 102090 additions and 0 deletions
+433
View File
@@ -0,0 +1,433 @@
<template>
<!--
1:1 doctor/Projects.vue + doctor/Submissions.vue 容器
SIDEBAR: 项目管理
列头 (按原型 sponsor-项目管理.html left 顺序):
复选框 / 项目编号 / 项目名称 / 总场次 / 已执行 / 未执行 /
总金额 / 可用金额 / 已支付劳务费 / 已支付会务费 /
执行单位得分 / 执行单位评价 / 支持单位 / 服务机构 / 项目形式 / 是否结题 /
开始时间 / 结束时间 / 操作
操作: 建会 / 分配 / 结题 / 查看 / 编辑 / 开通 / 删除公告 / 支持单位 / 执行单位评分
业务保留: 批量评分/批量结题/批量开通/项目分配 逻辑
-->
<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.projectNo" placeholder="输入项目编号" clearable style="width: 160px" />
</el-form-item>
<el-form-item label="项目名称">
<el-input v-model="q.projectName" placeholder="输入项目名称" clearable style="width: 200px" />
</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-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>
<el-button type="primary" @click="load">查找</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="toolbar">
<el-button type="primary" @click="openCreate">新建项目</el-button>
<el-button :disabled="!selected.length" @click="onAssignSelected">项目分配</el-button>
<el-button :disabled="!selected.length" @click="onBatchRate">批量评分</el-button>
<el-button :disabled="!selected.length" @click="onBatchFinish">批量结题</el-button>
<el-button :disabled="!selected.length" @click="onBatchOpen">批量开通</el-button>
<span class="hint">已选 {{ selected.length }} </span>
</div>
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelect">
<el-table-column type="selection" width="48" />
<el-table-column prop="projectNo" label="项目编号" min-width="140" fixed>
<template #default="{ row }">
<el-link type="primary" :underline="false" @click="openDetail(row)">{{ row.projectNo }}</el-link>
</template>
</el-table-column>
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="totalSessions" label="总场次/总期数" width="120" 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 label="总金额" width="120" align="right"><template #default="{ row }">¥{{ formatMoney(row.totalAmount) }}</template></el-table-column>
<el-table-column label="可用金额" width="120" align="right"><template #default="{ row }">¥{{ formatMoney(row.availableAmount) }}</template></el-table-column>
<el-table-column label="已支付劳务费" width="120" align="right"><template #default="{ row }">¥{{ formatMoney(row.paidLaborAmount) }}</template></el-table-column>
<el-table-column label="已支付会务费" width="120" align="right"><template #default="{ row }">¥{{ formatMoney(row.paidMeetingAmount) }}</template></el-table-column>
<el-table-column prop="complianceScore" label="执行单位得分(合规)" width="150" align="center" />
<el-table-column prop="ratingScore" label="执行单位评价(支持方)" width="160" align="center" />
<el-table-column prop="orgName" label="赞助公司" min-width="140" show-overflow-tooltip />
<el-table-column prop="projectForm" label="项目形式" width="100" align="center" />
<el-table-column label="是否结题" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.isFinished === '1' ? 'success' : 'info'" size="small" disable-transitions>
{{ row.isFinished === '1' ? '已结题' : '未结题' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="startTime" label="项目开始时间" min-width="160" />
<el-table-column prop="endTime" label="项目结束时间" min-width="160" />
<el-table-column label="操作" width="480" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onCreateMeeting(row)">建会</el-button>
<el-button link type="primary" @click="openAssign(row)">分配</el-button>
<el-button link type="primary" @click="onFinish(row)">结题</el-button>
<el-button link type="primary" @click="openDetail(row)">查看</el-button>
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="primary" @click="onOpen(row)">开通</el-button>
<el-button link type="danger" @click="onDeleteNotice(row)">删除公告</el-button>
<el-button link type="primary" @click="onSupportUnit(row)">支持单位</el-button>
<el-button link type="primary" @click="openRate(row)">执行单位评分</el-button>
</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"
style="margin-top: 12px; text-align: right;"
/>
<!-- 新建/编辑项目 dialog ( doctor/Projects.vue 风格统一) -->
<el-dialog v-model="formOpen" :title="formMode === 'create' ? '新建项目' : '编辑项目'" width="600px">
<el-form :model="form" label-width="100px">
<el-form-item label="项目编号"><el-input v-model="form.projectNo" /></el-form-item>
<el-form-item label="项目名称"><el-input v-model="form.projectName" /></el-form-item>
<el-form-item label="项目形式">
<el-select v-model="form.projectForm" placeholder="请选择" clearable>
<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-input-number v-model="form.totalSessions" :min="0" :max="9999" /></el-form-item>
<el-form-item label="管理费"><el-input-number v-model="form.manageFee" :min="0" :precision="2" /></el-form-item>
<el-form-item label="开始时间"><el-date-picker v-model="form.startTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" /></el-form-item>
<el-form-item label="结束时间"><el-date-picker v-model="form.endTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="formOpen = false">取消</el-button>
<el-button type="primary" @click="submitForm">保存</el-button>
</template>
</el-dialog>
<!-- 评分 dialog -->
<el-dialog v-model="rateOpen" title="执行单位评分" width="520px">
<el-form :model="rateForm" label-width="100px">
<el-form-item label="项目编号 / 名称">
<span>{{ rateForm.projectNo }} / {{ rateForm.projectName }}</span>
</el-form-item>
<el-form-item label="评价维度">
<div class="score-row"><span>履约质量:</span><el-rate v-model="rateForm.q1" :max="5" /></div>
<div class="score-row"><span>时效响应:</span><el-rate v-model="rateForm.q2" :max="5" /></div>
<div class="score-row"><span>配合度:</span><el-rate v-model="rateForm.q3" :max="5" /></div>
<div class="score-row"><span>合规安全:</span><el-rate v-model="rateForm.q4" :max="5" /></div>
</el-form-item>
<el-form-item label="评价备注"><el-input v-model="rateForm.remark" type="textarea" rows="3" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="rateOpen = false">取消</el-button>
<el-button type="primary" @click="submitRate">提交评分</el-button>
</template>
</el-dialog>
<!-- 分配 dialog -->
<el-dialog v-model="assignOpen" title="项目分配" width="520px">
<el-form :model="assignForm" label-width="100px">
<el-form-item label="项目编号 / 名称">
<span>{{ assignForm.projectNo }} / {{ assignForm.projectName }}</span>
</el-form-item>
<el-form-item label="分配给监察员">
<el-select v-model="assignForm.supervisor" filterable placeholder="请选择监察员" style="width:100%">
<el-option v-for="p in personList" :key="p.personId" :label="p.name" :value="p.personId" />
</el-select>
</el-form-item>
<el-form-item label="监察要点"><el-input v-model="assignForm.supervisionPoint" type="textarea" rows="3" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="assignOpen = false">取消</el-button>
<el-button type="primary" @click="submitAssign">确认分配</el-button>
</template>
</el-dialog>
<!-- 详情 dialog ( doctor/Projects.vue el-descriptions 同款) -->
<el-dialog v-model="detailOpen" title="项目详情" width="640px">
<el-descriptions :column="2" border>
<el-descriptions-item label="项目编号">{{ detail.projectNo }}</el-descriptions-item>
<el-descriptions-item label="项目形式">{{ detail.projectForm }}</el-descriptions-item>
<el-descriptions-item label="项目名称" :span="2">{{ detail.projectName }}</el-descriptions-item>
<el-descriptions-item label="赞助公司" :span="2">{{ detail.orgName || '-' }}</el-descriptions-item>
<el-descriptions-item label="公司类型" :span="2">
<el-tag :type="detail.orgType === 'sponsor' ? 'success' : 'primary'" disable-transitions>
{{ detail.orgType === 'sponsor' ? '赞助方' : '执行方' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="总场次/总期数">{{ detail.totalSessions || 0 }}</el-descriptions-item>
<el-descriptions-item label="已执行/未执行">{{ detail.doneSessions || 0 }} / {{ detail.todoSessions || 0 }}</el-descriptions-item>
<el-descriptions-item label="总金额">¥{{ formatMoney(detail.totalAmount) }}</el-descriptions-item>
<el-descriptions-item label="可用金额">¥{{ formatMoney(detail.availableAmount) }}</el-descriptions-item>
<el-descriptions-item label="已支付劳务费">¥{{ formatMoney(detail.paidLaborAmount) }}</el-descriptions-item>
<el-descriptions-item label="已支付会务费">¥{{ formatMoney(detail.paidMeetingAmount) }}</el-descriptions-item>
<el-descriptions-item label="执行单位得分">{{ detail.ratingScore || '-' }}</el-descriptions-item>
<el-descriptions-item label="是否结题">
<el-tag :type="detail.isFinished === '1' ? 'success' : 'info'" disable-transitions>
{{ detail.isFinished === '1' ? '已结题' : '未结题' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="开始时间" :span="2">{{ detail.startTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="结束时间" :span="2">{{ detail.endTime || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="detailOpen = false">关闭</el-button></template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { bizList, bizAdd, bizUpdate } from '@/api/public'
import request from '@/utils/request'
import { ElMessage, ElMessageBox } from 'element-plus'
const rows = ref([])
const loading = ref(false)
const q = reactive({ projectNo: '', projectName: '', projectForm: '', isFinished: '' })
const page = reactive({ pageNum: 1, pageSize: 10, total: 0 })
const selected = ref([])
const formOpen = ref(false)
const formMode = ref('create')
const form = reactive({
projectId: null, projectNo: '', projectName: '', projectForm: '',
totalSessions: 0, manageFee: 0, startTime: '', endTime: ''
})
const rateOpen = ref(false)
const rateForm = reactive({ projectId: null, projectNo: '', projectName: '', q1: 0, q2: 0, q3: 0, q4: 0, remark: '' })
const assignOpen = ref(false)
const assignForm = reactive({ projectId: '', projectNo: '', projectName: '', supervisor: '', supervisionPoint: '' })
const personList = ref([])
const detail = ref({})
const detailOpen = ref(false)
function formatMoney(v) { return Number(v || 0).toFixed(2) }
async function load() {
loading.value = true
try {
// sponsor 专属: GET /business/project/sponsorList (后端按 mainUid 团队可见性过滤)
const { data } = await request.get('/business/project/sponsorList', {
params: { ...q, pageNum: page.pageNum, pageSize: page.pageSize }
})
const payload = data?.data || data
rows.value = payload?.rows || []
page.total = payload?.total || 0
} catch (e) { rows.value = []; page.total = 0 }
finally { loading.value = false }
}
async function loadPersons() {
try {
// sponsor 团队下"监察员"角色 + 已绑 sys_user 的子账号
const r = await bizList('person', { pageNum: 1, pageSize: 100, unitType: 'sponsor', role: '监察员' })
personList.value = (r.rows || []).filter(p => p.userId)
} catch (e) { personList.value = [] }
}
function reset() {
Object.assign(q, { projectNo: '', projectName: '', projectForm: '', isFinished: '' })
page.pageNum = 1
load()
}
function onSelect(arr) { selected.value = arr }
function openDetail(row) { detail.value = row; detailOpen.value = true }
function openCreate() {
formMode.value = 'create'
Object.assign(form, { projectId: null, projectNo: '', projectName: '', projectForm: '', totalSessions: 0, manageFee: 0, startTime: '', endTime: '' })
formOpen.value = true
}
function openEdit(row) {
formMode.value = 'edit'
Object.assign(form, {
projectId: row.projectId, projectNo: row.projectNo, projectName: row.projectName,
projectForm: row.projectForm, totalSessions: row.totalSessions, manageFee: row.manageFee,
startTime: row.startTime || '', endTime: row.endTime || ''
})
formOpen.value = true
}
function onCreateMeeting(row) {
ElMessage.info(`已为项目「${row.projectName}」启动建会流程`)
}
function onSupportUnit(row) {
if (!row.orgId) { ElMessage.warning('该项目未关联赞助公司'); return }
ElMessage.info(`查看赞助公司: ${row.orgName || row.orgId}`)
}
async function onDeleteNotice(row) {
try { await ElMessageBox.confirm(`确定删除「${row.projectName}」的所有公告吗?删除后无法恢复`, '删除公告', { type: 'warning' }) } catch { return }
try {
await bizUpdate('project', { projectId: row.projectId, noticeDeleted: '1' })
ElMessage.success('公告已删除')
load()
} catch (e) { ElMessage.error(e?.msg || '删除失败') }
}
async function submitForm() {
if (!form.projectNo || !form.projectName) { ElMessage.warning('请填写项目编号与项目名称'); return }
try {
if (formMode.value === 'create') await bizAdd('project', { ...form })
else await bizUpdate('project', form)
ElMessage.success('保存成功')
formOpen.value = false
load()
} catch (e) { ElMessage.error(e?.msg || '保存失败') }
}
function openRate(row) {
Object.assign(rateForm, { projectId: row.projectId, projectNo: row.projectNo, projectName: row.projectName, q1: 0, q2: 0, q3: 0, q4: 0, remark: '' })
rateOpen.value = true
}
async function submitRate() {
if (!rateForm.q1 && !rateForm.q2 && !rateForm.q3 && !rateForm.q4) {
ElMessage.warning('请至少完成一项评分'); return
}
try {
// sponsor 专属: POST /business/project/rate (BizProjectSponsor 字段)
const data = {
projectId: rateForm.projectId,
projectNo: rateForm.projectNo,
projectName: rateForm.projectName,
sponsorQ1: rateForm.q1,
sponsorQ2: rateForm.q2,
sponsorQ3: rateForm.q3,
sponsorQ4: rateForm.q4,
sponsorRemark: rateForm.remark
}
await request.post('/business/project/rate', data)
ElMessage.success('评分已提交')
rateOpen.value = false
} catch (e) { ElMessage.error(e?.msg || '提交失败') }
}
async function onBatchRate() {
if (!selected.value.length) return
let ok = 0
for (const r of selected.value) {
try {
await bizAdd('projectRating', { projectId: r.projectId, projectNo: r.projectNo, projectName: r.projectName, q1: 3, q2: 3, q3: 3, q4: 3, remark: '批量评分' })
ok++
} catch {}
}
ElMessage.success(`已批量评分 ${ok}`)
}
async function onBatchFinish() {
if (!selected.value.length) return
try { await ElMessageBox.confirm(`确定对选中的 ${selected.value.length} 个项目批量结题吗?`, '批量结题', { type: 'warning' }) } catch { return }
let ok = 0
for (const r of selected.value) {
try { await bizUpdate('project', { projectId: r.projectId, isFinished: '1' }); ok++ } catch {}
}
ElMessage.success(`已批量结题 ${ok}`)
load()
}
async function onBatchOpen() {
if (!selected.value.length) return
try { await ElMessageBox.confirm(`确定对选中的 ${selected.value.length} 个项目批量开通吗?`, '批量开通', { type: 'warning' }) } catch { return }
let ok = 0
for (const r of selected.value) {
try { await bizUpdate('project', { projectId: r.projectId, isPublished: '1' }); ok++ } catch {}
}
ElMessage.success(`已批量开通 ${ok}`)
load()
}
async function onFinish(row) {
try { await ElMessageBox.confirm(`确定将「${row.projectName}」结题吗?`, '结题', { type: 'warning' }) } catch { return }
try {
await bizUpdate('project', { projectId: row.projectId, isFinished: '1' })
ElMessage.success('已结题')
load()
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
}
async function onOpen(row) {
try { await ElMessageBox.confirm(`确定开通「${row.projectName}」吗?`, '开通', { type: 'warning' }) } catch { return }
try {
await bizUpdate('project', { projectId: row.projectId, isPublished: '1' })
ElMessage.success('已开通')
load()
} catch (e) { ElMessage.error(e?.msg || '操作失败') }
}
function onAssignSelected() {
if (!selected.value.length) { ElMessage.warning('请先勾选项目'); return }
ElMessage.info(`已为 ${selected.value.length} 个项目启动批量分配流程,请逐个确认`)
}
function openAssign(row) {
assignForm.projectId = row.projectId
assignForm.projectNo = row.projectNo
assignForm.projectName = row.projectName
assignForm.supervisor = ''
assignForm.supervisionPoint = ''
assignOpen.value = true
}
async function submitAssign() {
if (!assignForm.supervisor) { ElMessage.warning('请选择监察员'); return }
try {
const person = personList.value.find(p => String(p.personId) === String(assignForm.supervisor))
if (!person || !person.userId) { ElMessage.error('未找到监察员账号'); return }
// sponsor 专属: POST /business/project/sponsorAssign (BizProjectSponsorAssign 字段)
await request.post('/business/project/sponsorAssign', {
projectId: assignForm.projectId,
projectNo: assignForm.projectNo,
projectName: assignForm.projectName,
monitorUserId: person.userId,
assignPoints: assignForm.supervisionPoint || '',
assignDesc: ''
})
ElMessage.success('分配成功')
assignOpen.value = false
} catch (e) { ElMessage.error(e?.msg || '分配失败') }
}
onMounted(() => { load(); loadPersons() })
</script>
<style scoped>
/* 1:1 抄 doctor/Projects.vue + doctor/Submissions.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; }
.hint { color: #909399; font-size: 12px; margin-left: 8px; }
.score-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
:deep(.el-button--primary) { background: var(--brand-primary); border-color: var(--brand-primary); border-radius: 4px; }
:deep(.el-button--primary:hover) { background: var(--brand-primary-deep, var(--brand-primary)); border-color: var(--brand-primary-deep, var(--brand-primary)); }
:deep(.el-table .el-button) { border-radius: 4px; }
:deep(.el-table .el-button.is-link) { color: var(--brand-primary); background: transparent; border: none; }
:deep(.el-table .el-button.is-link:hover) { color: var(--brand-primary-deep, var(--brand-primary)); background: transparent; }
:deep(.el-table .el-button.is-link.is-disabled) { color: #c0c4cc; background: transparent; }
</style>