feat(sponsor/people): 批量删除自我保护 + 批量导入 UI 用 el-upload 对齐其他页面

1. 批量删除主账号自我保护 (防误删):
   - 复选框 :selectable 排除主账号自己 (row.userId === store.user?.userId)
   - 批量删除按钮 :disabled 在包含主账号时置灰 (computed: hasSelfInSelection)
   - onBatchRemove 函数入口再校验一次 (defense in depth)

2. 批量导入 dialog 用 el-upload 对齐 manager/admin/executor:
   - 替换手写 <input type=file> + 浏览/上传 button, 改用标准 el-upload
     (:auto-upload=false :limit=1 :on-change :on-remove)
   - footer 加'开始导入'按钮 (disabled by !importFile)
   - 文件名显示 + 删除按钮 el-upload 自带, 不再手写
   - uploadFile() 改 submitImport(), 语义更准
   - onImport() 开头 clearFiles() 防止关闭再打开时残留

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
郭庆泰
2026-08-18 21:08:01 +08:00
co-authored by Claude
parent 8aba05aa0f
commit 25ec74114f
+29 -21
View File
@@ -39,11 +39,11 @@
<div class="toolbar"> <div class="toolbar">
<el-button type="primary" @click="onCreate">新建人员</el-button> <el-button type="primary" @click="onCreate">新建人员</el-button>
<el-button @click="onImport">批量导入</el-button> <el-button @click="onImport">批量导入</el-button>
<el-button :disabled="!selected.length" @click="onBatchRemove">批量删除</el-button> <el-button :disabled="!selected.length || hasSelfInSelection" @click="onBatchRemove">批量删除</el-button>
</div> </div>
<el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelectionChange"> <el-table :data="rows" v-loading="loading" stripe border @selection-change="onSelectionChange">
<el-table-column type="selection" width="48" /> <el-table-column type="selection" width="48" :selectable="(row) => row.userId !== store.user?.userId" />
<el-table-column prop="name" label="姓名" width="100" /> <el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="phone" label="手机号" width="130" /> <el-table-column prop="phone" label="手机号" width="130" />
<el-table-column prop="orgName" label="所属公司" min-width="180" show-overflow-tooltip /> <el-table-column prop="orgName" label="所属公司" min-width="180" show-overflow-tooltip />
@@ -102,12 +102,11 @@
<el-dialog v-model="importVisible" title="批量导入 (Excel)" width="520px"> <el-dialog v-model="importVisible" title="批量导入 (Excel)" width="520px">
<el-form label-width="80px"> <el-form label-width="80px">
<el-form-item label="选择文件"> <el-form-item label="选择文件">
<div style="display:flex; gap:8px; align-items:center"> <!-- 对齐 manager/admin/executor: el-upload :auto-upload=false 自管 file, footer 按钮触发提交 -->
<el-input :model-value="importFileName" placeholder="未选择文件" readonly style="flex:1" /> <el-upload ref="uploadRef" :auto-upload="false" :limit="1" accept=".xls,.xlsx"
<input ref="fileInputEl" type="file" accept=".xls,.xlsx" style="display:none" @change="onFileChange" /> :on-change="onFileChange" :on-remove="onFileRemove">
<el-button @click="fileInputEl?.click()">浏览</el-button> <el-button>选择文件</el-button>
<el-button type="primary" :loading="importing" @click="uploadFile">上传</el-button> </el-upload>
</div>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<span class="hint-link" @click="downloadTemplate">批量导入模板下载 (.xlsx)</span> <span class="hint-link" @click="downloadTemplate">批量导入模板下载 (.xlsx)</span>
@@ -115,6 +114,7 @@
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="importVisible = false">关闭</el-button> <el-button @click="importVisible = false">关闭</el-button>
<el-button type="primary" :loading="importing" :disabled="!importFile" @click="submitImport">开始导入</el-button>
</template> </template>
</el-dialog> </el-dialog>
@@ -141,7 +141,7 @@
</template> </template>
<script setup> <script setup>
import { reactive, ref } from 'vue' import { reactive, ref, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { bizUpdate, bizDelete } from '@/api/public' import { bizUpdate, bizDelete } from '@/api/public'
@@ -153,6 +153,9 @@ import request from '@/utils/request'
const router = useRouter() const router = useRouter()
const store = useUserStore() const store = useUserStore()
// 批量操作禁用判断: 选了主账号自己 (跟"操作列禁用/删除隐藏"是同一条规则, 防止误删)
const hasSelfInSelection = computed(() => selected.value.some(r => r.userId === store.user?.userId))
// sponsor 端只看自己团队 (主账号+子账号) 创建的人, 走专属接口 /business/person/sponsorList (后端强制隔离) // sponsor 端只看自己团队 (主账号+子账号) 创建的人, 走专属接口 /business/person/sponsorList (后端强制隔离)
const q = reactive({ const q = reactive({
name: '', phone: '', orgName: '', department: '', status: '' name: '', phone: '', orgName: '', department: '', status: ''
@@ -229,6 +232,7 @@ async function onRemoveOne(row) {
async function onBatchRemove() { async function onBatchRemove() {
if (!selected.value.length) { ElMessage.warning('请先勾选要删除的人员'); return } if (!selected.value.length) { ElMessage.warning('请先勾选要删除的人员'); return }
if (hasSelfInSelection.value) { ElMessage.warning('选中的包含您本人, 不能删除'); return }
try { try {
await ElMessageBox.confirm(`确定删除选中的 ${selected.value.length} 条? 该操作不可恢复`, '批量删除', { type: 'warning' }) await ElMessageBox.confirm(`确定删除选中的 ${selected.value.length} 条? 该操作不可恢复`, '批量删除', { type: 'warning' })
} catch { return } } catch { return }
@@ -243,8 +247,8 @@ async function onBatchRemove() {
// 批量导入 (Excel) // 批量导入 (Excel)
const importVisible = ref(false) const importVisible = ref(false)
const importFileName = ref('') const uploadRef = ref()
const fileInputEl = ref(null) const importFile = ref(null) // el-upload 选中的原始 File 对象 (file.raw)
const importing = ref(false) const importing = ref(false)
// 导入结果 (dialog + table) // 导入结果 (dialog + table)
const resultVisible = ref(false) const resultVisible = ref(false)
@@ -252,11 +256,16 @@ const resultRows = ref([])
const resultOk = ref(0) const resultOk = ref(0)
const resultTotal = ref(0) const resultTotal = ref(0)
function onImport() { function onImport() {
importFileName.value = '' importFile.value = null
// 关闭后下次再打开 el-upload 内部 fileList 可能残留, 用 ref 清掉
uploadRef.value?.clearFiles()
importVisible.value = true importVisible.value = true
} }
function onFileChange(e) { function onFileChange(file) {
importFileName.value = e.target.files?.[0]?.name || '' importFile.value = file.raw
}
function onFileRemove() {
importFile.value = null
} }
async function downloadTemplate() { async function downloadTemplate() {
// 后端 ExcelUtil<BizPersonImportVO>.importTemplateExcel 自动生成中文列头 .xlsx // 后端 ExcelUtil<BizPersonImportVO>.importTemplateExcel 自动生成中文列头 .xlsx
@@ -273,14 +282,13 @@ async function downloadTemplate() {
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
ElMessage.success('模板下载完成') ElMessage.success('模板下载完成')
} }
async function uploadFile() { async function submitImport() {
const file = fileInputEl.value?.files?.[0] if (!importFile.value) { ElMessage.warning('请先选择文件'); return }
if (!file) { ElMessage.warning('请先选择文件'); return } if (!/\.(xlsx|xls)$/i.test(importFile.value.name)) { ElMessage.warning('只支持 .xlsx / .xls 文件'); return }
if (!/\.(xlsx|xls)$/i.test(file.name)) { ElMessage.warning('只支持 .xlsx / .xls 文件'); return }
importing.value = true importing.value = true
try { try {
const form = new FormData() const form = new FormData()
form.append('file', file) form.append('file', importFile.value)
const res = await request.post('/business/person/sponsorImport', form, { const res = await request.post('/business/person/sponsorImport', form, {
headers: { 'Content-Type': 'multipart/form-data' } headers: { 'Content-Type': 'multipart/form-data' }
}) })
@@ -290,8 +298,8 @@ async function uploadFile() {
resultRows.value = data.results || [] resultRows.value = data.results || []
resultVisible.value = true resultVisible.value = true
importVisible.value = false importVisible.value = false
fileInputEl.value.value = '' importFile.value = null
importFileName.value = '' uploadRef.value?.clearFiles()
load() load()
} catch (e) { } catch (e) {
ElMessage.error(e?.msg || '导入失败') ElMessage.error(e?.msg || '导入失败')