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
+308
View File
@@ -0,0 +1,308 @@
<template>
<!--
1:1 doctor/Submissions.vue
容器: <div class="page-card">
面包屑: <div class="breadcrumb">首页 / 人员管理</div>
筛选栏: <el-form inline :model="q" class="filter-form">
工具栏: <div class="toolbar">
列表列 (按原型 sponsor-people.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.name" placeholder="输入姓名" clearable style="width: 160px" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="q.phone" placeholder="输入手机号" clearable maxlength="11" style="width: 180px" />
</el-form-item>
<el-form-item label="工作单位">
<el-input v-model="q.orgName" placeholder="输入所属公司" clearable style="width: 180px" />
</el-form-item>
<el-form-item label="部门">
<el-input v-model="q.department" placeholder="输入部门" clearable style="width: 140px" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="q.status" placeholder="全部状态" clearable style="width: 140px">
<el-option label="正常" value="0" />
<el-option label="禁用" value="1" />
</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="onCreate">新建人员</el-button>
<el-button @click="onImport">批量导入</el-button>
<el-button :disabled="!selected.length" @click="onBatchRemove">批量删除</el-button>
</div>
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelectionChange">
<el-table-column type="selection" width="48" />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column prop="orgName" label="所属公司" min-width="180" show-overflow-tooltip />
<el-table-column prop="department" label="部门" width="120" />
<el-table-column prop="position" label="职务" width="120" />
<el-table-column prop="role" label="角色" width="100" />
<el-table-column label="账号类型" width="90">
<template #default="{ row }">
<el-tag v-if="row.accountType" :type="row.accountType === 'MAIN' ? 'success' : 'warning'" disable-transitions>
{{ row.accountType === 'MAIN' ? '主账号' : '子账号' }}
</el-tag>
<span v-else style="color:#909399"></span>
</template>
</el-table-column>
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'" disable-transitions>
{{ row.status === '0' ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" />
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="onView(row)">查看</el-button>
<el-button link type="primary" @click="onEdit(row)">编辑</el-button>
<el-button link :type="row.status === '0' ? 'warning' : 'success'" @click="onToggleStatus(row)">
{{ row.status === '0' ? '禁用' : '恢复' }}
</el-button>
<el-button link type="danger" @click="onRemoveOne(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 (后端 ExcelUtil 自动生成中文列头 .xlsx) -->
<el-dialog v-model="importVisible" title="批量导入 (Excel)" width="520px">
<el-form label-width="80px">
<el-form-item label="选择文件">
<div style="display:flex; gap:8px; align-items:center">
<el-input :model-value="importFileName" placeholder="未选择文件" readonly style="flex:1" />
<input ref="fileInputEl" type="file" accept=".xls,.xlsx" style="display:none" @change="onFileChange" />
<el-button @click="fileInputEl?.click()">浏览</el-button>
<el-button type="primary" :loading="importing" @click="uploadFile">上传</el-button>
</div>
</el-form-item>
<el-form-item>
<span class="hint-link" @click="downloadTemplate">批量导入模板下载 (.xlsx)</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="importVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 导入结果 dialog ( table 展示每行成功/失败明细) -->
<el-dialog v-model="resultVisible" :title="`导入结果 (成功 ${resultOk}/${resultTotal})`" width="780px">
<el-table :data="resultRows" stripe border max-height="480">
<el-table-column prop="rowNo" label="行号" width="80" align="center" />
<el-table-column prop="name" label="姓名" width="120" />
<el-table-column prop="phone" label="手机号" width="130" />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.ok ? 'success' : 'danger'" disable-transitions size="small">
{{ row.ok ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="message" label="错误信息" show-overflow-tooltip />
</el-table>
<template #footer>
<el-button type="primary" @click="resultVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { bizUpdate, bizDelete } from '@/api/public'
import { listSponsorPerson } from '@/api/business/person'
import request from '@/utils/request'
const router = useRouter()
// sponsor 端只看自己团队 (主账号+子账号) 创建的人, 走专属接口 /business/person/sponsorList (后端强制隔离)
const q = reactive({
name: '', phone: '', orgName: '', department: '', status: ''
})
const page = reactive({ pageNum: 1, pageSize: 20, total: 0 })
const rows = ref([])
const loading = ref(false)
const selected = ref([])
async function load() {
loading.value = true
try {
const res = await listSponsorPerson({ ...q, pageNum: page.pageNum, pageSize: page.pageSize })
rows.value = res.rows || []
page.total = res.total || 0
} catch (e) {
rows.value = []; page.total = 0
} finally {
loading.value = false
}
}
function reset() {
q.name = ''
q.phone = ''
q.orgName = ''
q.department = ''
q.status = ''
page.pageNum = 1
load()
}
function onSelectionChange(arr) { selected.value = arr }
function onCreate() {
router.push({ path: '/sponsor/people/new' })
}
function onView(row) {
router.push({ path: `/sponsor/people/detail/${row.personId}` })
}
function onEdit(row) {
router.push({ path: `/sponsor/people/edit/${row.personId}` })
}
async function onToggleStatus(row) {
const newStatus = row.status === '0' ? '1' : '0'
const label = newStatus === '0' ? '恢复' : '禁用'
try {
await ElMessageBox.confirm(`确定要${label}${row.name}」吗?`, '确认', { type: 'warning' })
} catch { return }
try {
await bizUpdate('person', { personId: row.personId, status: newStatus })
ElMessage.success('已' + label)
load()
} catch (e) {
ElMessage.error(e?.msg || '操作失败')
}
}
async function onRemoveOne(row) {
try {
await ElMessageBox.confirm(`确定删除「${row.name}」? 该操作不可恢复`, '删除确认', { type: 'warning' })
} catch { return }
try {
await bizDelete('person', row.personId)
ElMessage.success('删除成功')
load()
} catch (e) {
ElMessage.error(e?.msg || '删除失败')
}
}
async function onBatchRemove() {
if (!selected.value.length) { ElMessage.warning('请先勾选要删除的人员'); return }
try {
await ElMessageBox.confirm(`确定删除选中的 ${selected.value.length} 条? 该操作不可恢复`, '批量删除', { type: 'warning' })
} catch { return }
let ok = 0
for (const r of selected.value) {
try { await bizDelete('person', r.personId); ok++ } catch {}
}
ElMessage.success(`已删除 ${ok}`)
selected.value = []
load()
}
// 批量导入 (Excel)
const importVisible = ref(false)
const importFileName = ref('')
const fileInputEl = ref(null)
const importing = ref(false)
// 导入结果 (dialog + table)
const resultVisible = ref(false)
const resultRows = ref([])
const resultOk = ref(0)
const resultTotal = ref(0)
function onImport() {
importFileName.value = ''
importVisible.value = true
}
function onFileChange(e) {
importFileName.value = e.target.files?.[0]?.name || ''
}
async function downloadTemplate() {
// 后端 ExcelUtil<BizPersonImportVO>.importTemplateExcel 自动生成中文列头 .xlsx
const res = await request.get('/business/person/sponsorImportTemplate', { responseType: 'blob' })
const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
// 优先用后端 Content-Disposition 的 filename* (utf-8) 字段
const dispo = res.headers?.['content-disposition'] || res.headers?.['Content-Disposition'] || ''
const m = /filename\*=UTF-8''([^;]+)/i.exec(dispo)
a.download = m ? decodeURIComponent(m[1]) : 'sponsor人员批量导入模板.xlsx'
a.click()
URL.revokeObjectURL(url)
ElMessage.success('模板下载完成')
}
async function uploadFile() {
const file = fileInputEl.value?.files?.[0]
if (!file) { ElMessage.warning('请先选择文件'); return }
if (!/\.(xlsx|xls)$/i.test(file.name)) { ElMessage.warning('只支持 .xlsx / .xls 文件'); return }
importing.value = true
try {
const form = new FormData()
form.append('file', file)
const res = await request.post('/business/person/sponsorImport', form, {
headers: { 'Content-Type': 'multipart/form-data' }
})
const data = res.data || res
resultOk.value = data.ok || 0
resultTotal.value = data.total || 0
resultRows.value = data.results || []
resultVisible.value = true
importVisible.value = false
fileInputEl.value.value = ''
importFileName.value = ''
load()
} catch (e) {
ElMessage.error(e?.msg || '导入失败')
} finally {
importing.value = false
}
}
load()
</script>
<style scoped>
/* 1:1 抄 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; }
.hint-link { color: var(--brand-primary); cursor: pointer; font-size: 13px; }
.hint-link:hover { color: var(--brand-primary-deep, var(--brand-primary)); }
: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>