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
@@ -0,0 +1,393 @@
<template>
<!--
我的项目 (sponsor 专属, 团队模式: 主账号+子账号关联的项目; sponsor 角色可见, router meta.role='sponsor' + beforeEach 守卫拦截)
容器: <div class="page-card"> ( People.vue / NewPerson.vue 同款)
面包屑: <div class="breadcrumb">首页 / 我的项目</div>
1:1 抄原型: proto/html/components/sponsor-projects.html
15 : checkbox + 项目编号 + 项目名称 + 总场次/已执行/未执行 + 4 金额 + 开始/结束时间 + 是否结题 + 执行单位评价 + 操作
Modal 1: 项目评价 (4 维度星级 1-5 + 备注) -> POST /business/project/rate [hmr fix]
Modal 2: 项目分配 (监察员 chip + 说明 + 要点) -> POST /business/project/sponsorAssign
-->
<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-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 260px"
/>
</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="onAssignBatch">项目分配</el-button>
<el-button @click="onEvalBatch">批量评价</el-button>
</div>
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelectionChange">
<el-table-column type="selection" width="40" />
<el-table-column label="项目编号" width="160">
<template #default="{ row }">
<a class="link">{{ row.projectNo }}</a>
</template>
</el-table-column>
<el-table-column label="项目名称" min-width="220" show-overflow-tooltip>
<template #default="{ row }">
<a class="link-bold">{{ row.projectName }}</a>
</template>
</el-table-column>
<el-table-column prop="totalSessions" label="总场次" width="80" 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="120" align="right" :formatter="fmtMoney" />
<el-table-column prop="availableAmount" label="可用金额" width="120" align="right" :formatter="fmtMoney" />
<el-table-column prop="paidLaborAmount" label="劳务费" width="120" align="right" :formatter="fmtMoney" />
<el-table-column prop="paidMeetingAmount" label="会务费" width="120" align="right" :formatter="fmtMoney" />
<el-table-column label="项目开始时间" width="170" align="center">
<template #default="{ row }">
<span class="mono">{{ fmtDate(row.startTime) }}</span>
</template>
</el-table-column>
<el-table-column label="项目结束时间" width="170" align="center">
<template #default="{ row }">
<span class="mono">{{ fmtDate(row.endTime) }}</span>
</template>
</el-table-column>
<el-table-column label="是否结题" width="90" align="center">
<template #default="{ row }">
<span>{{ row.isFinished === '1' ? '已结题' : '未结题' }}</span>
</template>
</el-table-column>
<el-table-column label="执行单位评价" width="120" align="center">
<template #default="{ row }">
<a v-if="row.ratingScore" class="link">{{ row.ratingScore }}</a>
<span v-else style="color:#909399"></span>
</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEval(row)">项目评价</el-button>
<el-button link type="primary" @click="openAssign(row)">分配</el-button>
<el-button link type="primary" @click="onView(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
v-model:current-page="page.pageNum"
v-model:page-size="page.pageSize"
:page-sizes="[10, 20, 50]"
:total="page.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="load"
@current-change="load"
/>
<!-- ============ Modal 1: 项目评价 ============ -->
<el-dialog v-model="evalVisible" title="执行单位评分" width="600px" append-to-body destroy-on-close>
<div class="modal-project-info">
<div><span class="lbl">项目编号</span><span>{{ evalForm.projectNo }}</span></div>
<div><span class="lbl">项目名称</span><span>{{ evalForm.projectName }}</span></div>
</div>
<el-form :model="evalForm" label-width="120px" class="modal-form">
<el-form-item label="履约质量">
<el-rate v-model="evalForm.q1" :max="5" :colors="['#99A9BF','#F7BA2A','#FF9900']" show-text :texts="['1', '2', '3', '4', '5']" />
</el-form-item>
<el-form-item label="时效响应">
<el-rate v-model="evalForm.q2" :max="5" :colors="['#99A9BF','#F7BA2A','#FF9900']" show-text :texts="['1', '2', '3', '4', '5']" />
</el-form-item>
<el-form-item label="配合度">
<el-rate v-model="evalForm.q3" :max="5" :colors="['#99A9BF','#F7BA2A','#FF9900']" show-text :texts="['1', '2', '3', '4', '5']" />
</el-form-item>
<el-form-item label="合规安全">
<el-rate v-model="evalForm.q4" :max="5" :colors="['#99A9BF','#F7BA2A','#FF9900']" show-text :texts="['1', '2', '3', '4', '5']" />
</el-form-item>
<el-form-item label="评价备注">
<el-input v-model="evalForm.remark" type="textarea" :rows="4" placeholder="请输入评价备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="evalVisible = false">取消</el-button>
<el-button type="primary" @click="saveEval">确定</el-button>
</template>
</el-dialog>
<!-- ============ Modal 2: 项目分配 (单选 / 批量复用) ============ -->
<el-dialog :title="assignBatchMode ? `批量项目分配 (${assignBatchRows.length} 个项目)` : '项目分配'" width="600px" append-to-body destroy-on-close>
<!-- 单选模式: 显示项目编号 / 名称 -->
<div v-if="!assignBatchMode" class="modal-project-info">
<div><span class="lbl">项目编号</span><span>{{ assignForm.projectNo }}</span></div>
<div><span class="lbl">项目名称</span><span>{{ assignForm.projectName }}</span></div>
</div>
<!-- 批量模式: 显示已选项目摘要 -->
<div v-else class="modal-project-info">
<div><span class="lbl">已选项目数</span><span>{{ assignBatchRows.length }}</span></div>
<div><span class="lbl">项目编号预览</span><span style="color:#606266;font-size:12px">{{ assignBatchRows.slice(0,5).map(r=>r.projectNo).join('、') }}{{ assignBatchRows.length > 5 ? ' 等' : '' }}</span></div>
</div>
<el-form :model="assignForm" label-width="100px" class="modal-form">
<el-form-item label="分配给监察员">
<el-input v-model="assignForm.monitorInput" placeholder="输入监察员姓名筛选" clearable style="width: 240px" @input="filterMonitors" />
</el-form-item>
<el-form-item label="候选监察员">
<div class="chip-list">
<el-tag v-for="m in candidateMonitors" :key="m.userId" class="chip" type="info" effect="plain" @click="pickMonitor(m)">{{ m.name }}</el-tag>
<span v-if="!candidateMonitors.length" style="color:#909399">无可分配监察员</span>
</div>
</el-form-item>
<el-form-item label="分配说明">
<el-input v-model="assignForm.assignDesc" type="textarea" :rows="3" placeholder="请输入分配说明" />
</el-form-item>
<el-form-item label="监察要点">
<el-input v-model="assignForm.assignPoints" type="textarea" :rows="3" placeholder="请输入监察要点" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="assignVisible = false">取消</el-button>
<el-button type="primary" :loading="assignSubmitting" @click="saveAssign">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { listSponsorProjects, rateProject, sponsorAssignProject, sponsorAssignBatch } from '@/api/business/project'
import { listBizPerson } from '@/api/business/person'
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
const loading = ref(false)
const rows = ref([])
const selected = ref([])
const dateRange = ref([])
const q = reactive({
projectNo: '',
projectName: '',
isFinished: ''
})
const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
function fmtMoney(_row, _col, val) {
if (val === null || val === undefined || val === '') return '—'
return Number(val).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
function fmtDate(val) {
if (!val) return '—'
return String(val).substring(0, 10)
}
async function load() {
loading.value = true
try {
const params = {
pageNum: page.pageNum,
pageSize: page.pageSize,
projectNo: q.projectNo || undefined,
projectName: q.projectName || undefined,
isFinished: q.isFinished || undefined,
startTime: dateRange.value?.[0] ? dateRange.value[0] + ' 00:00:00' : undefined,
endTime: dateRange.value?.[1] ? dateRange.value[1] + ' 23:59:59' : undefined
}
const res = await listSponsorProjects(params)
rows.value = res.rows || []
page.total = res.total || 0
} finally {
loading.value = false
}
}
function reset() {
q.projectNo = ''
q.projectName = ''
q.isFinished = ''
dateRange.value = []
page.pageNum = 1
load()
}
function onSelectionChange(val) { selected.value = val }
function onView(row) {
ElMessageBox.alert(`项目编号: ${row.projectNo}\n项目名称: ${row.projectName}`, '项目详情', { confirmButtonText: '确定' })
}
// ========== Modal 1: 项目评价 ==========
const evalVisible = ref(false)
const evalForm = reactive({ projectId: null, projectNo: '', projectName: '', q1: 5, q2: 5, q3: 5, q4: 5, remark: '' })
function openEval(row) {
evalForm.projectId = row.projectId
evalForm.projectNo = row.projectNo
evalForm.projectName = row.projectName
evalForm.q1 = row.sponsorQ1 || 5
evalForm.q2 = row.sponsorQ2 || 5
evalForm.q3 = row.sponsorQ3 || 5
evalForm.q4 = row.sponsorQ4 || 5
evalForm.remark = row.sponsorRemark || ''
evalVisible.value = true
}
async function saveEval() {
const avg = ((evalForm.q1 + evalForm.q2 + evalForm.q3 + evalForm.q4) / 4).toFixed(2)
await rateProject({
projectId: evalForm.projectId,
sponsorRating: avg,
sponsorQ1: evalForm.q1,
sponsorQ2: evalForm.q2,
sponsorQ3: evalForm.q3,
sponsorQ4: evalForm.q4,
sponsorRemark: evalForm.remark
})
ElMessage.success('评价已保存')
evalVisible.value = false
load()
}
function onEvalBatch() {
if (!selected.value.length) return ElMessage.warning('请先选择项目')
if (selected.value.length > 1) return ElMessage.warning('批量评价一次仅可评价一个项目')
openEval(selected.value[0])
}
// ========== Modal 2: 项目分配 (单选 / 批量) ==========
const assignVisible = ref(false)
const assignBatchMode = ref(false) // 批量模式开关
const assignBatchRows = ref([]) // 批量模式下被分配的项目列表
const assignSubmitting = ref(false)
const assignForm = reactive({
projectId: null, projectNo: '', projectName: '',
monitorInput: '', monitorUserId: null, monitorUserName: '',
assignDesc: '', assignPoints: ''
})
const allMonitors = ref([])
const candidateMonitors = ref([])
async function loadMonitors() {
// 业务隔离: 仅查 login 主账号创建的人员 (parent_user_id = mainUserId)
const mainUid = userStore.id
const res = await listBizPerson({ parentUserId: mainUid, role: '监察员', pageNum: 1, pageSize: 100 })
allMonitors.value = res.rows || []
candidateMonitors.value = allMonitors.value.slice()
}
function filterMonitors() {
const kw = assignForm.monitorInput.trim()
candidateMonitors.value = kw ? allMonitors.value.filter(m => m.name.includes(kw)) : allMonitors.value.slice()
}
function pickMonitor(m) {
assignForm.monitorUserId = m.userId
assignForm.monitorUserName = m.name
assignForm.monitorInput = m.name
}
function openAssign(row) {
assignBatchMode.value = false
assignBatchRows.value = []
assignForm.projectId = row.projectId
assignForm.projectNo = row.projectNo
assignForm.projectName = row.projectName
assignForm.monitorInput = ''
assignForm.monitorUserId = null
assignForm.monitorUserName = ''
assignForm.assignDesc = ''
assignForm.assignPoints = ''
assignVisible.value = true
loadMonitors()
}
function openAssignBatch(rows) {
assignBatchMode.value = true
assignBatchRows.value = rows.map(r => ({ projectId: r.projectId, projectNo: r.projectNo, projectName: r.projectName }))
assignForm.monitorInput = ''
assignForm.monitorUserId = null
assignForm.monitorUserName = ''
assignForm.assignDesc = ''
assignForm.assignPoints = ''
assignVisible.value = true
loadMonitors()
}
async function saveAssign() {
if (!assignForm.monitorUserId) return ElMessage.warning('请选择监察员')
if (!assignForm.assignDesc) return ElMessage.warning('请填写分配说明')
assignSubmitting.value = true
try {
if (assignBatchMode.value) {
// 批量模式: 循环逐条 POST (后端先删后插, 每个项目都会被覆盖)
let ok = 0, errs = []
for (const r of assignBatchRows.value) {
try {
await sponsorAssignProject({
projectId: r.projectId,
monitorUserId: assignForm.monitorUserId,
assignDesc: assignForm.assignDesc,
assignPoints: assignForm.assignPoints
})
ok++
} catch (e) { errs.push(`${r.projectNo}: ${e?.msg || e.message}`) }
}
if (errs.length) ElMessageBox.alert(`批量分配: 成功 ${ok}/${assignBatchRows.value.length}\n\n失败:\n${errs.join('\n')}`, '分配结果', { type: 'warning' })
else ElMessage.success(`批量分配成功 (${ok}/${assignBatchRows.value.length})`)
} else {
// 单条模式
await sponsorAssignProject({
projectId: assignForm.projectId,
monitorUserId: assignForm.monitorUserId,
assignDesc: assignForm.assignDesc,
assignPoints: assignForm.assignPoints
})
ElMessage.success('分配成功')
}
assignVisible.value = false
load()
} finally {
assignSubmitting.value = false
}
}
function onAssignBatch() {
if (!selected.value.length) return ElMessage.warning('请先选择项目')
// 单选走 openAssign, 多选走批量分配
if (selected.value.length === 1) openAssign(selected.value[0])
else openAssignBatch(selected.value)
}
onMounted(load)
</script>
<style scoped>
.page-card { background: #fff; border: 1px solid #f0f0f0; border-radius: 6px; padding: 16px 20px; }
.breadcrumb { color: #909399; font-size: 13px; margin-bottom: 12px; }
.filter-form { margin-bottom: 12px; }
.toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.link { color: var(--brand-primary, #1E3A8A); }
.link-bold { color: #303133; font-weight: 600; }
.mono { font-family: monospace; color: #606266; font-size: 12px; }
.pagination { margin-top: 12px; justify-content: flex-end; display: flex; }
.modal-project-info { background: #f5f7fa; padding: 12px 16px; border-radius: 4px; margin-bottom: 12px; }
.modal-project-info > div { display: flex; line-height: 22px; }
.modal-project-info .lbl { color: #909399; width: 80px; }
.modal-form .el-form-item { margin-bottom: 16px; }
.hint-inline { color: #909399; font-size: 12px; margin-top: 4px; }
.chip-list { display: flex; flex-wrap: wrap; gap: 8px; }
.chip { cursor: pointer; }
</style>