refactor(executor): 对齐 sponsor 人员管理模式 + 修 3 个 bug

后端:
- BizAuthController.registerExecutor step 6 同步建 biz_person (主账号自己=管理员, name=unitName, role='admin', unit_type='executor'), 让"人员管理"能看见自己 (跟 registerSponsor 同模式, 走 BizPersonMapper 直插, 绕过 SUB 子账号逻辑)
- BizPersonMapper.xml selectExecutorList 加 OR u.user_id=主账号 (主账号自己能查到), + countAdminByOrgId (校验公司唯一管理员)
- SysUserMapper.xml 手机号正则 1[3-9] → 1[0-9] (允许 10 开头测试号段, 跟 sponsor 一致)

前端 executor/NewPerson.vue 1:1 对齐 sponsor/NewPerson.vue:
- el-card form-card 包裹 + el-row :gutter=12 两列布局
- 用户名/初始密码 行 (v-if=!isEdit)
- 状态改用 el-radio-group, 值 0=正常 1=禁用
- 修了状态值反向 bug: 原 executor 正常→"1" 禁用→"0" 跟 DB/UserStatus enum 相反, 编辑时反向生效 (BizPersonServiceImpl.insert 硬编码 status='0' 掩盖了 bug)
- role 统一 el-select + admin 选项 :disabled=true (管理员只能注册时建)
- 编辑模式 el-select :disabled=true (不能改角色)
- phone 正则 /^1[0-9]\d{9}$/, userName 3-30 模式, password 6-30, department maxlength=100
- 新建成功 ElMessageBox.alert 展示子账号+密码
- 重复手机号走 warning 黄 (isDuplicateError)
- confirmCancel 取消确认
- loadMyOrg 自动带出主账号公司

前端 executor/People.vue:
- 状态列对齐 sponsor: row.status === '0' ? '正常' : '禁用' (修原 || '正常' fallback bug 导致显示原始 '0' + 颜色永远红的两个 bug)
- 批量导入 完整实装 (原 downloadTemplate/submitImport 都是 stub, 只弹提示):
  - downloadTemplate GET /business/person/executorImportTemplate + blob 下载 + 解析 Content-Disposition 文件名
  - submitImport POST /business/person/executorImport + FormData + 导入结果 dialog (行号/姓名/手机号/成功+错误信息)
  - el-upload 加 :on-change=:on-remove 自管 file, footer 改 "关闭/开始导入" 带 loading/disabled
  - 新增 onImport 先 clearFiles 再开 dialog
This commit is contained in:
郭庆泰
2026-08-18 21:32:10 +08:00
parent 6716074eee
commit 40f296b59d
5 changed files with 348 additions and 155 deletions
+104 -20
View File
@@ -18,7 +18,7 @@
<!-- ========== 批量按钮 (top=203: 新建人员 / 批量导入) ========== -->
<div class="batch-bar">
<el-button type="primary" @click="goNew">新建人员</el-button>
<el-button @click="importOpen = true">批量导入</el-button>
<el-button @click="onImport">批量导入</el-button>
<span class="filter-tip">*批量导入模板与列表表项一致</span>
</div>
@@ -34,17 +34,22 @@
<el-tag size="small" :type="row.role === 'meetingExecutor' ? 'warning' : 'info'">{{ translatePersonRole(row.role) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag size="small" :type="row.status === '正常' ? 'success' : 'danger'">{{ row.status || '正常' }}</el-tag>
<el-tag :type="row.status === '0' ? 'success' : 'danger'" disable-transitions>
{{ row.status === '0' ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="onView(row)">查看</el-button>
<el-button size="small" link type="primary" @click="goEdit(row)">编辑</el-button>
<el-button v-if="(row.status || '正常') === '正常'" size="small" link type="danger" @click="onToggleStatus(row, '禁用')">禁用</el-button>
<el-button v-else size="small" link type="success" @click="onToggleStatus(row, '恢复')">恢复</el-button>
<!-- 本人不显示禁用/恢复按钮 ( sponsor 一样, 避免主账号把自己禁用) -->
<template v-if="row.userId !== store.user?.userId">
<el-button v-if="(row.status || '正常') === '正常'" size="small" link type="danger" @click="onToggleStatus(row, '禁用')">禁用</el-button>
<el-button v-else size="small" link type="success" @click="onToggleStatus(row, '恢复')">恢复</el-button>
</template>
</template>
</el-table-column>
</el-table>
@@ -58,21 +63,42 @@
<!-- (旧的 formOpen dialog 已删) -->
<!-- ========== Dialog: 批量导入 ========== -->
<el-dialog v-model="importOpen" title="批量导入" width="520px">
<el-form label-width="100px">
<el-dialog v-model="importOpen" title="批量导入 (Excel)" width="520px">
<el-form label-width="80px">
<el-form-item label="选择文件">
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" accept=".xls,.xlsx">
<!-- 对齐 manager/admin/sponsor: el-upload :auto-upload=false 自管 file, footer 按钮触发提交 -->
<el-upload ref="uploadRef" :auto-upload="false" :limit="1" accept=".xls,.xlsx"
:on-change="onFileChange" :on-remove="onFileRemove">
<el-button>选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="">
<el-button link type="primary" @click="downloadTemplate">批量导入模板下载</el-button>
<el-form-item>
<span class="hint-link" @click="downloadTemplate">批量导入模板下载 (.xlsx)</span>
</el-form-item>
<el-form-item><span class="filter-tip">*批量导入模板xls与列表表项一致</span></el-form-item>
</el-form>
<template #footer>
<el-button @click="importOpen = false">取消</el-button>
<el-button type="primary" @click="submitImport">确定</el-button>
<el-button @click="importOpen = false">关闭</el-button>
<el-button type="primary" :loading="importing" :disabled="!importFile" @click="submitImport">开始导入</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>
@@ -97,10 +123,13 @@ import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { bizUpdate } from '@/api/public'
import { listExecutorPerson } from '@/api/business/person'
import { useUserStore } from '@/store/user'
import { ElMessage, ElMessageBox } from 'element-plus'
import { translatePersonRole } from '@/utils/roleMap'
import request from '@/utils/request'
const router = useRouter()
const store = useUserStore()
const list = ref([])
const loading = ref(false)
@@ -111,6 +140,15 @@ const importOpen = ref(false)
const view = ref({})
const viewOpen = ref(false)
const uploadRef = ref()
const importFile = ref(null) // el-upload 选中的原始 File 对象 (file.raw)
const importing = ref(false)
// 导入结果 (dialog + table)
const resultVisible = ref(false)
const resultRows = ref([])
const resultOk = ref(0)
const resultTotal = ref(0)
async function loadList() {
loading.value = true
try {
@@ -142,14 +180,58 @@ async function onToggleStatus(row, action) {
} catch (e) { if (e !== 'cancel') ElMessage.error(e?.msg || `${action}失败`) }
}
function downloadTemplate() {
ElMessage.info('模板下载中...')
// 批量导入 (Excel) - 对齐 sponsor/SponsorPeople.vue
function onImport() {
importFile.value = null
// 关闭后下次再打开 el-upload 内部 fileList 可能残留, 用 ref 清掉
uploadRef.value?.clearFiles()
importOpen.value = true
}
function submitImport() {
ElMessage.success('导入成功')
importOpen.value = false
loadList()
function onFileChange(file) {
importFile.value = file.raw
}
function onFileRemove() {
importFile.value = null
}
async function downloadTemplate() {
// 后端 ExcelUtil<BizPersonImportVO>.importTemplateExcel 自动生成中文列头 .xlsx
const res = await request.get('/business/person/executorImportTemplate', { 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]) : 'executor人员批量导入模板.xlsx'
a.click()
URL.revokeObjectURL(url)
ElMessage.success('模板下载完成')
}
async function submitImport() {
if (!importFile.value) { ElMessage.warning('请先选择文件'); return }
if (!/\.(xlsx|xls)$/i.test(importFile.value.name)) { ElMessage.warning('只支持 .xlsx / .xls 文件'); return }
importing.value = true
try {
const form = new FormData()
form.append('file', importFile.value)
const res = await request.post('/business/person/executorImport', 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
importOpen.value = false
importFile.value = null
uploadRef.value?.clearFiles()
loadList()
} catch (e) {
ElMessage.error(e?.msg || '导入失败')
} finally {
importing.value = false
}
}
onMounted(loadList)
@@ -160,6 +242,8 @@ onMounted(loadList)
.page-sub { font-size: 13px; color: #909399; margin-bottom: 16px; }
.batch-bar { margin-bottom: 12px; display: flex; gap: 8px; align-items: center; }
.filter-tip { color: #909399; font-size: 12px; }
.hint-link { color: var(--brand-primary); cursor: pointer; font-size: 13px; }
.hint-link:hover { color: var(--brand-primary-deep, var(--brand-primary)); }
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
.filter-form { margin-bottom: 12px; }
</style>