feat(attendee): Excel 批量导入 (后端 /importData + /importTemplate + 5邀请差集推送, 前端 dialog-upload 套用 dialog-import SKILL)

This commit is contained in:
郭庆泰
2026-08-22 16:35:50 +08:00
parent da7c19af6c
commit 0dafb47e7d
5 changed files with 352 additions and 1 deletions
@@ -56,6 +56,7 @@
<div class="attendee-section">
<div class="attendee-toolbar">
<el-button type="primary" size="small" :loading="attendeeLoading" @click="openAttendeeDialog()">+ 新增参会人</el-button>
<el-button size="small" @click="openAttendeeImport">批量导入</el-button>
<span class="toolbar-hint">按手机号定位; 手机号未注册将自动建 sys_user (用户名=密码=手机号)</span>
</div>
<!--
@@ -390,6 +391,35 @@
</template>
</el-dialog>
<!-- 参会人 Excel 批量导入 (沿用 dialog-import SKILL: v-if + on-error + 模板下载在框外 + ImportResultDialog) -->
<el-dialog v-model="importOpen" title="批量导入参会人" width="400px" append-to-body :close-on-click-modal="false">
<el-upload
v-if="importOpen"
ref="importUploadRef"
:limit="1"
:headers="importHeaders"
accept=".xlsx, .xls"
:action="importAction"
:on-progress="onImportProgress"
:on-success="onImportSuccess"
:on-error="onImportError"
:auto-upload="false"
drag
>
<el-icon class="el-icon--upload"><upload /></el-icon>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
</el-upload>
<div style="margin-top:10px;font-size:13px;text-align:center">
<span style="color:#909399;margin-right:8px">仅允许导入 xlsxlsx 格式文件</span>
<el-link type="primary" :underline="false" @click="downloadAttendeeImportTpl">下载模板</el-link>
</div>
<template #footer>
<el-button @click="importOpen=false"> </el-button>
<el-button type="primary" :loading="importing" @click="submitImportFile"> </el-button>
</template>
</el-dialog>
<ImportResultDialog v-model="importResultOpen" :result="importResult" />
<div class="form-actions">
<el-button @click="goBack">返回</el-button>
</div>
@@ -405,7 +435,9 @@ import { listSupporters, listExecutor } from '@/api/system'
import { useUserStore } from '@/store/user'
import { ElMessage } from 'element-plus'
import OssFileUploader from '@/components/OssFileUploader.vue'
import ImportResultDialog from '@/components/ImportResultDialog.vue'
import { ElMessageBox } from 'element-plus'
import { Upload } from '@element-plus/icons-vue'
const route = useRoute()
const router = useRouter()
@@ -709,6 +741,70 @@ async function confirmDeleteAttendee(row) {
ElMessage.error(e?.msg || e?.message || '删除失败')
}
}
// ===================== 参会人 Excel 批量导入 (沿用 dialog-import SKILL) =====================
const importOpen = ref(false)
const importing = ref(false)
const importResultOpen = ref(false)
const importResult = ref(null)
const importUploadRef = ref(null)
// el-upload 用原生 XHR, 不走 axios interceptor, 必须显式带 Authorization
const importHeaders = computed(() => ({ Authorization: 'Bearer ' + (localStorage.getItem('ry_token') || '') }))
// 后端 baseURL 与 request.js 一致 (/dev-api), 拼绝对路径给 :action; meetingId 走 query 串
const importAction = computed(() => `/dev-api/business/meetingAttendee/importData?meetingId=${meetingId.value}`)
function openAttendeeImport() {
importResult.value = null
importOpen.value = true
}
function downloadAttendeeImportTpl() {
fetch('/dev-api/business/meetingAttendee/importTemplate', { headers: importHeaders.value })
.then(r => r.blob())
.then(blob => {
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = '参会人批量导入模板.xlsx'
a.click()
})
.catch(() => ElMessage.error('模板下载失败'))
}
function submitImportFile() {
importUploadRef.value?.submit()
}
function onImportProgress() { importing.value = true }
function onImportSuccess(res) {
importing.value = false
const result = (res && res.data) ? res.data : res
if (!result || typeof result.okNum !== 'number') {
ElMessage.error(res?.msg || '导入失败, 返回数据异常')
importResult.value = null
return
}
importResult.value = result
importOpen.value = false
importResultOpen.value = true
// 刷新列表 (新参会人会出现)
loadAttendees()
}
function onImportError(err) {
importing.value = false
let msg = '上传失败, 请重试'
if (err && err.response != null) {
try {
const body = typeof err.response === 'string' ? JSON.parse(err.response) : err.response
msg = body.msg || msg
} catch { /* JSON 解析失败用兜底 */ }
} else if (err && err.message) {
msg = err.message
}
ElMessage.error(msg)
importResult.value = null
}
async function loadStaff() {
try {
const [sp, ex] = await Promise.all([