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:
@@ -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>
|
||||
Reference in New Issue
Block a user