feat:增加人员导出进度条
feat:增加人员导出的时候导出人员身份 feat:增加后台api和机器人脸同步通过mqtt实现增量变化 feat:导入饭卡卡号没有绑定卡号自动绑定卡号
This commit is contained in:
1 parent
2e84e6692b
commit
4e96554554
4 files changed
+200
-174
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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
|
||||
/** 任务失败后由业务组件展示的覆盖文案。 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 || "用户导出失败")
|
||||
}
|
||||
|
||||
// 是否可以多选
|
||||
|
||||
Reference in new issue
Block a user