feat:增加人员导出进度条

feat:增加人员导出的时候导出人员身份
feat:增加后台api和机器人脸同步通过mqtt实现增量变化
feat:导入饭卡卡号没有绑定卡号自动绑定卡号
This commit is contained in:
dzc committed 2026-08-17 14:31:10 +08:00
1 parent 2e84e6692b
commit 4e96554554
4 files changed
+200 -174

No files matched your search

+23
View File
@@ -1,4 +1,13 @@
import request, { CommonHeaderEnum } from "/@/utils/request"
import type { AsyncTaskProgress } from "/@/api/asyncTask"
/** 用户异步导出提交接口的统一响应。 */
export interface UserExportTaskResponse {
/** 后端创建的初始异步任务进度。 */
data: AsyncTaskProgress
/** 后端返回的任务提交提示;为空时页面使用默认提示。 */
msg?: string
}
/**
* 同步用户食堂信息
@@ -41,6 +50,20 @@ export const pageList = (query: any) => {
})
}
/**
* 按当前筛选条件提交用户信息异步导出任务。
*
* @param query 用户列表筛选条件和可选用户 ID 集合
* @returns 包含任务初始进度的统一响应
*/
export const exportUsers = (query: Record<string, unknown>) => {
return request<unknown, UserExportTaskResponse>({
url: "/admin/user/export",
method: "post",
data: query,
})
}
export const addObj = (obj: Object) => {
return request({
url: "/admin/user",
+10
View File
@@ -22,6 +22,10 @@ export interface AsyncTaskProgress {
finished: boolean
/** 终态是否成功;任务未结束时为 false。 */
success: boolean
/** 导出等任务成功后生成的文件下载地址;导入任务为空。 */
resultFileUrl?: string
/** 导出等任务成功后建议使用的下载文件名称;导入任务为空。 */
resultFileName?: string
}
/** 通用异步任务轮询配置,导入、导出等业务只需提供自身命名空间和必要覆盖项。 */
@@ -38,6 +42,12 @@ export interface AsyncTaskPollingConfig {
pollTimeoutMs?: number
/** 连续查询失败多少次后暂停轮询并允许用户重试,默认 5 次。 */
maxConsecutiveErrors?: number
/** 是否等任务进入终态或轮询中断后再显示弹窗,默认 false。 */
showDialogOnFinished?: boolean
/** 任务执行期间展示的等待提示;为空时不额外展示。 */
waitingMessage?: string
/** 成功终态存在结果文件时的下载按钮文案,默认“下载结果”。 */
downloadButtonText?: string
/** 任务成功后由业务组件展示的覆盖文案。 */
successMessage?: string
/** 任务失败后由业务组件展示的覆盖文案。 */
+26 -2
View File
@@ -8,10 +8,15 @@
<span>{{ state.progress?.message || "等待执行" }}</span>
<span>{{ state.progress?.completed || 0 }} / {{ state.progress?.total || 0 }}</span>
</div>
<el-alert v-if="state.polling && prop.config.waitingMessage" :title="prop.config.waitingMessage"
type="info" :closable="false" show-icon />
<el-alert v-if="state.queryError" :title="state.queryError" type="warning" :closable="false" show-icon />
</div>
<template #footer v-if="canClose">
<el-button v-if="state.queryError" type="primary" @click="retry">重新查询</el-button>
<el-button v-if="canDownload" type="primary" :loading="state.downloading" @click="downloadResult">
{{ prop.config.downloadButtonText || "下载结果" }}
</el-button>
<el-button @click="close">关闭</el-button>
</template>
</el-dialog>
@@ -24,6 +29,7 @@ import {
type AsyncTaskPollingConfig,
type AsyncTaskProgress,
} from "/@/api/asyncTask"
import { downBlobFile } from "/@/utils/other"
/** 默认轮询间隔,兼顾进度及时性与接口压力。 */
const DEFAULT_POLL_INTERVAL_MS = 1000
@@ -59,10 +65,11 @@ const prop = defineProps({
/** 任务成功或后端终态失败事件,由具体业务决定刷新、下载或提示行为。 */
const emit = defineEmits(["success", "failure"])
/** 进度弹窗的可见状态、服务端进度和本地查询错误。 */
/** 进度弹窗的可见、轮询、下载状态,以及服务端进度和本地查询错误。 */
const state = reactive({
visible: false,
polling: false,
downloading: false,
progress: null as AsyncTaskProgress | null,
queryError: "",
})
@@ -81,6 +88,9 @@ let unmounted = false
/** 任务仍在执行时禁止关闭弹窗,避免用户误以为任务已经停止。 */
const canClose = computed(() => !state.polling)
/** 仅在任务成功且后端已经返回结果文件地址后开放下载。 */
const canDownload = computed(() => Boolean(state.progress?.finished && state.progress.success && state.progress.resultFileUrl))
/** 根据任务终态映射 Element Plus 进度条状态。 */
const progressStatus = computed(() => {
if (!state.progress?.finished) return undefined
@@ -106,6 +116,7 @@ const scheduleNextPoll = () => {
const pauseForQueryError = (message: string) => {
stopPolling()
state.queryError = message
state.visible = true
}
/** 查询一次任务进度,并根据终态、超时或网络状态决定是否继续轮询。 */
@@ -130,6 +141,7 @@ const pollProgress = async () => {
consecutiveErrors = 0
if (state.progress?.finished) {
stopPolling()
state.visible = true
emit(state.progress.success ? "success" : "failure", state.progress)
return
}
@@ -166,7 +178,8 @@ const start = (initialProgress: AsyncTaskProgress) => {
}
state.progress = { ...initialProgress, redisKey }
state.queryError = ""
state.visible = true
state.downloading = false
state.visible = !prop.config.showDialogOnFinished
state.polling = true
pollingStartedAt = Date.now()
consecutiveErrors = 0
@@ -183,6 +196,17 @@ const retry = () => {
void pollProgress()
}
/** 从成功终态保存的文件地址重新下载任务结果,并阻止连续点击发起重复请求。 */
const downloadResult = async () => {
if (!canDownload.value || state.downloading || !state.progress?.resultFileUrl) return
state.downloading = true
try {
await downBlobFile(state.progress.resultFileUrl, {}, state.progress.resultFileName || "任务结果.xlsx")
} finally {
state.downloading = false
}
}
/** 关闭已经结束或暂停查询的任务进度弹窗。 */
const close = () => {
if (state.polling) return
+141 -172
View File
@@ -1,27 +1,13 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<DraggableDepTree
ref="draggableDepTreeRef"
placeholder="请输入部门名称"
:show-expand="true"
@getSelectNode="handleGetSelectNode"
switch-inactive-text="包含"
switch-active-text="不包含"
:defaultTreeWidth="20"
:nodeClickable="false"
>
<DraggableDepTree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含"
:defaultTreeWidth="20" :nodeClickable="false">
<template #default>
<SearchPanel
v-show="showSearch"
ref="queryRef"
:model="state.queryForm"
title="用户筛选"
:loading="Boolean(state.loading)"
:hidden-active-count="advancedFilterCount"
@search="getDataList"
@reset="resetQuery"
>
<SearchPanel v-show="showSearch" ref="queryRef" :model="state.queryForm" title="用户筛选"
:loading="Boolean(state.loading)" :hidden-active-count="advancedFilterCount"
@search="getDataList" @reset="resetQuery">
<el-form-item style="width: 250px" label="用户" prop="userId">
<select-user v-model="state.queryForm.userId" clearable />
</el-form-item>
@@ -43,10 +29,12 @@
<dict-select v-model="state.queryForm.staffingType" dict-code="user_staffing_type" />
</el-form-item>
<el-form-item style="width: 250px" label="用户身份" prop="identityTypes">
<dict-select v-model="state.queryForm.identityTypes" dict-code="user_identity_type" multiple placeholder="请选择用户身份" />
<dict-select v-model="state.queryForm.identityTypes" dict-code="user_identity_type"
multiple placeholder="请选择用户身份" />
</el-form-item>
<el-form-item style="width: 180px" label="启用状态" prop="lockFlag">
<dict-select v-model="state.queryForm.lockFlag" dict-code="lock_flag" placeholder="启用/禁用" />
<dict-select v-model="state.queryForm.lockFlag" dict-code="lock_flag"
placeholder="启用/禁用" />
</el-form-item>
</template>
</SearchPanel>
@@ -54,40 +42,30 @@
<header class="mb-2 flex min-h-8 items-center justify-between gap-4">
<div class="flex min-w-0 items-center gap-2">
<span class="h-3.5 w-0.5 shrink-0 rounded-sm bg-primary" aria-hidden="true"></span>
<h2 id="user-list-title" class="m-0 text-sm font-semibold leading-5 text-[var(--el-text-color-primary)]">用户列表</h2>
<span class="text-xs text-[var(--el-text-color-secondary)]">共 {{ state.pagination?.total ?? 0 }} 位</span>
<h2 id="user-list-title"
class="m-0 text-sm font-semibold leading-5 text-[var(--el-text-color-primary)]">
用户列表</h2>
<span class="text-xs text-[var(--el-text-color-secondary)]">共 {{ state.pagination?.total
?? 0 }} 位</span>
</div>
<right-toolbar
v-model:showSearch="showSearch"
:export="'sys_user_export'"
@exportExcel="exportExcel"
@queryTable="getDataList"
/>
<right-toolbar v-model:showSearch="showSearch" :export="'sys_user_export'"
@exportExcel="exportExcel" @queryTable="getDataList" />
</header>
<div class="user-list-actions mb-2 flex flex-wrap items-center gap-2">
<el-button v-auth="'sys_user_add'" icon="folder-add" type="primary" @click="userDialogRef.openDialog()">新增</el-button>
<el-button
plain
<el-button v-auth="'sys_user_add'" icon="folder-add" type="primary"
@click="userDialogRef.openDialog()">新增</el-button>
<el-button plain
v-auths="['sys_user_export', 'sys_student_user_export', 'sys_guardian_user_export', 'sys_teacher_user_export']"
icon="upload-filled"
type="primary"
@click="excelUploadRef.show()"
>
icon="upload-filled" type="primary" @click="excelUploadRef.show()">
导入
</el-button>
<el-button plain v-auth="'sys_user_del'" :disabled="multiple" icon="Delete" type="primary" @click="handleDelete(selectObjs)">
<el-button plain v-auth="'sys_user_del'" :disabled="multiple" icon="Delete" type="primary"
@click="handleDelete(selectObjs)">
删除
</el-button>
<el-button
v-if="activeName == '1'"
plain
v-auth="'sys_user_del'"
:disabled="multiple"
icon="VideoPlay"
type="primary"
@click="doBatchDeal"
>
<el-button v-if="activeName == '1'" plain v-auth="'sys_user_del'" :disabled="multiple"
icon="VideoPlay" type="primary" @click="doBatchDeal">
审批
</el-button>
</div>
@@ -96,25 +74,13 @@
<el-tab-pane v-for="v in tabItems" :key="v.id" :label="v.label" :name="v.value" />
</el-tabs>
<el-table
v-loading="state.loading"
:data="state.dataList"
class="user-table"
row-key="userId"
border
stripe
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
@selection-change="handleSelectionChange"
>
<el-table-column
:selectable="handleSelectable"
type="selection"
width="44"
fixed="left"
class-name="user-table__selection-cell"
/>
<el-table-column label="用户信息" align="center" fixed="left" min-width="280" class-name="user-table__content-cell">
<el-table v-loading="state.loading" :data="state.dataList" class="user-table" row-key="userId"
border stripe :cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle" @selection-change="handleSelectionChange">
<el-table-column :selectable="handleSelectable" type="selection" width="44" fixed="left"
class-name="user-table__selection-cell" />
<el-table-column label="用户信息" align="center" fixed="left" min-width="280"
class-name="user-table__content-cell">
<template #default="{ row }">
<user-info-cell :user="row" />
</template>
@@ -124,13 +90,15 @@
<dl class="m-0 grid gap-1.5 text-left text-xs">
<div class="flex min-w-0 gap-2">
<dt class="w-10 shrink-0 text-[var(--el-text-color-secondary)]">商户</dt>
<dd class="m-0 truncate text-[var(--el-text-color-primary)]" :title="row.merchantName || '未关联'">
<dd class="m-0 truncate text-[var(--el-text-color-primary)]"
:title="row.merchantName || '未关联'">
{{ row.merchantName || "未关联" }}
</dd>
</div>
<div class="flex min-w-0 items-center gap-2">
<dt class="w-10 shrink-0 text-[var(--el-text-color-secondary)]">编制</dt>
<dd class="m-0 truncate text-[var(--el-text-color-primary)]" :title="getStaffingLabel(row.staffingType)">
<dd class="m-0 truncate text-[var(--el-text-color-primary)]"
:title="getStaffingLabel(row.staffingType)">
{{ getStaffingLabel(row.staffingType) }}
</dd>
</div>
@@ -153,33 +121,26 @@
<div class="flex items-center justify-between gap-3">
<span class="text-[var(--el-text-color-secondary)]">登录权限</span>
<div class="flex items-center gap-2">
<span class="text-[var(--el-text-color-regular)]">{{ row.lockFlag === "0" ? "已启用" : "已停用" }}</span>
<el-switch
v-model="row.lockFlag"
size="small"
<span class="text-[var(--el-text-color-regular)]">{{ row.lockFlag ===
"0" ? "已启用" : "已停用" }}</span>
<el-switch v-model="row.lockFlag" size="small"
:aria-label="`${row.name || row.username || '用户'}登录权限`"
:disabled="!auth('sys_user_edit') || row.processStatus != '2'"
active-value="0"
inactive-value="9"
@change="changeSwitch(row)"
/>
active-value="0" inactive-value="9" @change="changeSwitch(row)" />
</div>
</div>
<div class="flex items-start justify-between gap-3 pt-2">
<span class="text-[var(--el-text-color-secondary)]">账户状态</span>
<div class="min-w-0 text-right">
<div class="flex items-center justify-end gap-1.5">
<span
class="h-1.5 w-1.5 shrink-0 rounded-full"
<span class="h-1.5 w-1.5 shrink-0 rounded-full"
:class="row.closeAccountFlag == 1 ? 'bg-[var(--el-color-danger)]' : 'bg-[var(--el-color-success)]'"
aria-hidden="true"
></span>
<span class="text-[var(--el-text-color-regular)]">{{ row.closeAccountFlag == 1 ? "已注销" : "使用中" }}</span>
aria-hidden="true"></span>
<span class="text-[var(--el-text-color-regular)]">{{
row.closeAccountFlag == 1 ? "已注销" : "使用中" }}</span>
</div>
<div
v-if="row.closeAccountFlag == 1 && row.closeAccountApplyTime"
class="mt-1 max-w-28 break-words text-[var(--el-text-color-secondary)]"
>
<div v-if="row.closeAccountFlag == 1 && row.closeAccountApplyTime"
class="mt-1 max-w-28 break-words text-[var(--el-text-color-secondary)]">
{{ row.closeAccountApplyTime }}
</div>
</div>
@@ -192,111 +153,62 @@
<preview-image :src="row.faceImgs" />
</template>
</el-table-column>
<el-table-column v-if="false" label="全局人脸" show-overflow-tooltip class-name="user-table__content-cell">
<el-table-column v-if="false" label="全局人脸" show-overflow-tooltip
class-name="user-table__content-cell">
<template #default="{ row }">
<el-switch
:disabled="!auth('userFace_sysUserFace_global_face')"
v-if="row.isGlobalFace"
v-model="row.isGlobalFace"
size="small"
@change="changeGlobalFaceSwitch(row)"
active-value="1"
inactive-value="0"
/>
<el-switch :disabled="!auth('userFace_sysUserFace_global_face')"
v-if="row.isGlobalFace" v-model="row.isGlobalFace" size="small"
@change="changeGlobalFaceSwitch(row)" active-value="1" inactive-value="0" />
</template>
</el-table-column>
<el-table-column
v-auths="['sys_user_edit', 'userFace_sysUserFace_view', 'sys_user_del']"
label="操作"
min-width="250"
width="250"
fixed="right"
class-name="user-table__content-cell"
>
<el-table-column v-auths="['sys_user_edit', 'userFace_sysUserFace_view', 'sys_user_del']"
label="操作" min-width="250" width="250" fixed="right"
class-name="user-table__content-cell">
<template #default="{ row }">
<div class="grid grid-cols-3 justify-items-start gap-x-2 gap-y-1">
<!-- 重置密码 -->
<popover-input v-if="row.processStatus == '2'" v-model="inputPassword" @confirm="changePassword(row)">
<popover-input v-if="row.processStatus == '2'" v-model="inputPassword"
@confirm="changePassword(row)">
<template #default>
<el-button size="small" class="!m-0" v-auth="'sys_user_edit'" icon="RefreshLeft" text type="primary"
>密码</el-button
>
<el-button size="small" class="!m-0" v-auth="'sys_user_edit'"
icon="RefreshLeft" text type="primary">密码</el-button>
</template>
</popover-input>
<!-- 修改信息 -->
<el-button
v-if="row.processStatus == '2'"
class="!m-0"
v-auth="'sys_user_edit'"
icon="edit-pen"
text
type="primary"
size="small"
@click="userDialogRef.openDialog(row.userId)"
>
<el-button v-if="row.processStatus == '2'" class="!m-0" v-auth="'sys_user_edit'"
icon="edit-pen" text type="primary" size="small"
@click="userDialogRef.openDialog(row.userId)">
修改
</el-button>
<!-- 修改信息 -->
<el-button
v-auth="'admin_sysUserTag_view'"
class="!m-0"
v-if="row.processStatus == '2'"
icon="SetUp"
text
type="primary"
size="small"
@click="tagTableRef.openDialog(row.userId)"
>
<el-button v-auth="'admin_sysUserTag_view'" class="!m-0"
v-if="row.processStatus == '2'" icon="SetUp" text type="primary"
size="small" @click="tagTableRef.openDialog(row.userId)">
用户标签
</el-button>
<!-- 人脸管理 -->
<el-button
v-if="row.processStatus == '2'"
class="!m-0"
v-auth="'userFace_sysUserFace_view'"
icon="User"
size="small"
text
type="primary"
@click="faceManageRef.openDialog(row.userId)"
>
<el-button v-if="row.processStatus == '2'" class="!m-0"
v-auth="'userFace_sysUserFace_view'" icon="User" size="small" text
type="primary" @click="faceManageRef.openDialog(row.userId)">
人脸管理
</el-button>
<el-button
v-if="row.processStatus == 1"
class="!m-0"
type="primary"
size="small"
link
icon="VideoPlay"
@click="deal(row)"
>
<el-button v-if="row.processStatus == 1" class="!m-0" type="primary"
size="small" link icon="VideoPlay" @click="deal(row)">
开始处理
</el-button>
<el-button
class="!m-0"
type="primary"
size="small"
link
v-if="row.processStatus == '2'"
icon="SuitcaseLine"
@click="toRelationTree(row)"
>
<el-button class="!m-0" type="primary" size="small" link
v-if="row.processStatus == '2'" icon="SuitcaseLine"
@click="toRelationTree(row)">
关系图谱
</el-button>
<!-- 删除用户 -->
<el-tooltip content="admin 不允许被删除" :disabled="row.userId !== '1'" placement="top">
<el-tooltip content="admin 不允许被删除" :disabled="row.userId !== '1'"
placement="top">
<span>
<el-button
class="!m-0"
icon="delete"
size="small"
v-auth="'sys_user_del'"
:disabled="row.username === 'admin'"
text
<el-button class="!m-0" icon="delete" size="small"
v-auth="'sys_user_del'" :disabled="row.username === 'admin'" text
type="primary"
@click="handleDelete([{ userId: row.userId, processId: row.processId ?? null }])"
>
@click="handleDelete([{ userId: row.userId, processId: row.processId ?? null }])">
删除
</el-button>
</span>
@@ -305,7 +217,8 @@
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @current-change="currentChangeHandle" @size-change="sizeChangeHandle" />
<pagination v-bind="state.pagination" @current-change="currentChangeHandle"
@size-change="sizeChangeHandle" />
</section>
</template>
</DraggableDepTree>
@@ -314,6 +227,8 @@
<user-form ref="userDialogRef" @refresh="getDataList(false)" :deptId="state.queryForm.deptId" />
<face-manage ref="faceManageRef" @refresh="getDataList(false)" />
<user-import-excel ref="excelUploadRef" @refreshDataList="getDataList" />
<async-task-progress-component ref="exportProgressRef" :config="exportTaskConfig"
@success="handleExportSuccess" @failure="handleExportFailure" />
<!-- 审批抽屉 -->
<process-drawer ref="processDrawerRef" @task-submit-event="taskSubmitEvent" />
<!-- 批量审批对话框 -->
@@ -323,7 +238,8 @@
</template>
<script lang="ts" name="systemUser" setup>
import { delObj, pageList, putObj, type UserDeleteItem } from "/@/api/admin/user"
import { delObj, exportUsers, pageList, putObj, type UserDeleteItem } from "/@/api/admin/user"
import type { AsyncTaskPollingConfig, AsyncTaskProgress } from "/@/api/asyncTask"
import { putGlobalFace } from "/@/api/admin/userFace"
import { BasicTableProps, useTable } from "/@/hooks/table"
import { useMessage, useMessageBox } from "/@/hooks/message"
@@ -333,6 +249,8 @@ import { getDicts } from "/@/api/admin/dict"
import ProcessDrawer from "/@/components/processDrawer/index.vue"
import { useDict } from "/@/hooks/dict"
import UserRoleTags from "./components/user-role-tags.vue"
import AsyncTaskProgressComponent from "/@/components/AsyncTaskProgress.vue"
import { downBlobFile } from "/@/utils/other"
const UserForm = defineAsyncComponent(() => import("./form.vue"))
const FaceManage = defineAsyncComponent(() => import("./face-manage.vue"))
@@ -356,6 +274,20 @@ const tabItems = ref<any>([{}])
const processDrawerRef = ref()
const batchDealRef = ref()
const tagTableRef = ref()
/** 用户异步导出进度弹窗引用,用于提交成功后启动统一轮询。 */
const exportProgressRef = ref<InstanceType<typeof AsyncTaskProgressComponent>>()
/** 用户导出提交中的本地互斥状态,防止接口返回前连续点击生成重复任务。 */
const exportSubmitting = ref(false)
/** 用户导出任务轮询配置,与后端用户导出 Redis 命名空间保持一致。 */
const exportTaskConfig: AsyncTaskPollingConfig = {
progressUrl: "/admin/async-task/progress",
redisKey: "user:export",
showDialogOnFinished: false,
waitingMessage: "正在生成用户导出文件,请耐心等待进度达到 100%",
downloadButtonText: "下载 Excel",
}
// 多选rows
const selectObjs = ref<UserDeleteItem[]>([])
@@ -376,7 +308,7 @@ const state: BasicTableProps = reactive<BasicTableProps>({
},
pageList: pageList,
})
const { getDataList, currentChangeHandle, sizeChangeHandle, postDownBlobFile, tableStyle } = useTable(state)
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state)
/** 用户编制字典的响应式引用,用于组织归属复合单元格展示。 */
const { user_staffing_type } = useDict("user_staffing_type")
@@ -456,15 +388,52 @@ const taskSubmitEvent = () => {
getDataList()
}
// 导出excel
const exportExcel = () => {
/** 提交用户异步导出任务并静默轮询,待文件生成完成后再显示结果弹窗。 */
const exportExcel = async () => {
if (exportSubmitting.value) return
const _data = { ...state.queryForm }
if (_data.role && !Array.isArray(_data.role)) {
_data.role = [_data.role]
} else {
} else if (!_data.role) {
_data.role = []
}
postDownBlobFile("/admin/user/export", Object.assign(_data, { ids: selectObjs.value }), "用户管理.xlsx")
exportSubmitting.value = true
try {
const response = await exportUsers(Object.assign(_data, { userIds: selectObjs.value.map(({ userId }) => userId) }))
if (!response.data?.taskId) {
useMessage().error("导出任务提交成功,但响应中缺少任务ID")
return
}
useMessage().success(response.msg || "用户导出任务已提交")
exportProgressRef.value?.start(response.data)
} catch (error: any) {
useMessage().error(error?.msg || "用户导出任务提交失败")
} finally {
exportSubmitting.value = false
}
}
/**
* 用户导出成功后下载文件中心结果。
*
* @param progress 后端成功终态,必须包含结果文件地址
*/
const handleExportSuccess = async (progress: AsyncTaskProgress) => {
if (!progress.resultFileUrl) {
useMessage().error("导出完成,但未返回文件下载地址")
return
}
useMessage().success(progress.message || "用户导出完成")
await downBlobFile(progress.resultFileUrl, {}, progress.resultFileName || "用户信息.xlsx")
}
/**
* 用户导出失败时展示后端受控错误,避免浏览器下载异常响应。
*
* @param progress 后端失败终态
*/
const handleExportFailure = (progress: AsyncTaskProgress) => {
useMessage().error(progress.message || "用户导出失败")
}
// 是否可以多选