✨ 新增管理员代登录功能

This commit is contained in:
caojia
2026-11-04 15:20:05 +08:00
parent 93da04fe5e
commit 5cccbf510f
17 changed files with 540 additions and 18 deletions
+9
View File
@@ -10,6 +10,15 @@ export function listUser(query) {
})
}
// 创建和结束当前标签页的代登录会话
export function startImpersonation(userId) {
return request({ url: '/system/user/impersonation/' + userId, method: 'post' })
}
export function exitImpersonation() {
return request({ url: '/system/user/impersonation/exit', method: 'post' })
}
// 查询用户下拉选项(登录即可访问,仅含 id/账号/姓名/部门等展示字段)
export function optionUserList() {
return request({
+19 -2
View File
@@ -9,6 +9,10 @@
</template>
<div class="right-menu">
<div v-if="userStore.impersonating" class="impersonation-banner">
<span>当前以 {{ userStore.nickName }} 身份操作</span>
<el-button size="small" type="warning" @click="exitActingUser">返回管理员</el-button>
</div>
<template v-if="appStore.device !== 'mobile'">
<header-search id="header-search" class="right-menu-item" />
@@ -50,7 +54,7 @@
<el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
<span>布局设置</span>
</el-dropdown-item>
<el-dropdown-item command="lockScreen">
<el-dropdown-item v-if="!userStore.impersonating" command="lockScreen">
<span>锁定屏幕</span>
</el-dropdown-item>
<el-dropdown-item divided command="logout">
@@ -111,7 +115,7 @@ function handleCommand(command) {
}
function logout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
ElMessageBox.confirm(userStore.impersonating ? '结束代登录并返回管理员账号吗?' : '确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
@@ -122,6 +126,10 @@ function logout() {
}).catch(() => { })
}
function exitActingUser() {
userStore.logOut().then(() => location.replace('/index'))
}
const emits = defineEmits(['setLayout'])
function setLayout() {
emits('setLayout')
@@ -233,6 +241,15 @@ async function toggleTheme(event) {
align-items: center;
margin-left: auto;
.impersonation-banner {
display: flex;
align-items: center;
gap: 8px;
color: #b46a00;
font-size: 13px;
white-space: nowrap;
}
&:focus {
outline: none;
}
+4 -2
View File
@@ -11,7 +11,7 @@
<settings ref="settingRef" />
</div>
<!-- 全局消息通知:登录后未读弹屏 + 头像菜单“我的通知”入口 -->
<notify-popup />
<notify-popup v-if="!userStore.impersonating" />
</div>
</template>
@@ -22,8 +22,10 @@ import { AppMain, Navbar, Settings, TagsView } from './components'
import NotifyPopup from '@/components/NotifyPopup/index.vue'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import useUserStore from '@/store/modules/user'
const settingsStore = useSettingsStore()
const userStore = useUserStore()
const theme = computed(() => settingsStore.theme)
const sidebar = computed(() => useAppStore().sidebar)
const device = computed(() => useAppStore().device)
@@ -116,4 +118,4 @@ function setLayout() {
.mobile .fixed-header {
width: 100%;
}
</style>
</style>
+2 -1
View File
@@ -2,7 +2,7 @@ import router, { notFoundRoute } from './router'
import { ElMessage } from 'element-plus'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth'
import { getToken, isImpersonationRecoveryInProgress } from '@/utils/auth'
import { isHttp, isPathMatch } from '@/utils/validate'
import { isRelogin } from '@/utils/request'
import useUserStore from '@/store/modules/user'
@@ -59,6 +59,7 @@ router.beforeEach(async (to, from) => {
// 重新导航到目标路由,确保动态路由已注册
return { ...to, replace: true }
} catch (err) {
if (isImpersonationRecoveryInProgress()) return false
await useUserStore().logOut()
useDictStore().cleanDict()
ElMessage.error(err)
+4 -1
View File
@@ -1,10 +1,11 @@
import cache from '@/plugins/cache'
import useSettingsStore from '@/store/modules/settings'
import { getImpersonationToken } from '@/utils/auth'
const PERSIST_KEY = 'tags-view-visited'
function isPersistEnabled() {
return useSettingsStore().tagsViewPersist
return !getImpersonationToken() && useSettingsStore().tagsViewPersist
}
function saveVisitedViews(views) {
@@ -14,10 +15,12 @@ function saveVisitedViews(views) {
}
function loadVisitedViews() {
if (getImpersonationToken()) return []
return cache.local.getJSON(PERSIST_KEY) || []
}
function clearVisitedViews() {
if (getImpersonationToken()) return
cache.local.remove(PERSIST_KEY)
}
+17 -4
View File
@@ -2,7 +2,8 @@ import router from '@/router'
import cache from '@/plugins/cache'
import { ElMessageBox, } from 'element-plus'
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { getToken, setToken, removeToken, getImpersonationToken, clearImpersonationToken } from '@/utils/auth'
import { exitImpersonation } from '@/api/system/user'
import { isHttp, isEmpty } from "@/utils/validate"
import useLockStore from '@/store/modules/lock'
import useDictStore from '@/store/modules/dict'
@@ -18,7 +19,8 @@ const useUserStore = defineStore(
nickName: '',
avatar: '',
roles: [],
permissions: []
permissions: [],
impersonating: false
}),
actions: {
// 登录
@@ -57,6 +59,7 @@ const useUserStore = defineStore(
this.name = user.userName
this.nickName = user.nickName
this.avatar = avatar
this.impersonating = !!res.impersonating
cache.session.set('pwrChrtype', res.pwdChrtype)
/* 改密提示展示标记:通知弹屏组件读取该标记,等改密框关闭后再弹通知,避免双框冲突 */
const showPwdPrompt = (msg) => {
@@ -68,11 +71,11 @@ const useUserStore = defineStore(
})
}
/* 初始密码提示 */
if(res.isDefaultModifyPwd) {
if(!this.impersonating && res.isDefaultModifyPwd) {
showPwdPrompt('您的密码还是初始密码,请修改密码!')
}
/* 过期密码提示 */
if(!res.isDefaultModifyPwd && res.isPasswordExpired) {
if(!this.impersonating && !res.isDefaultModifyPwd && res.isPasswordExpired) {
showPwdPrompt('您的密码已过期,请尽快修改密码!')
}
resolve(res)
@@ -83,6 +86,16 @@ const useUserStore = defineStore(
},
// 退出系统
logOut() {
if (getImpersonationToken()) {
return exitImpersonation().then(() => {
clearImpersonationToken()
this.token = getToken()
this.roles = []
this.permissions = []
this.impersonating = false
useDictStore().cleanDict()
})
}
return new Promise((resolve, reject) => {
logout(this.token).then(() => {
this.token = ''
+27 -1
View File
@@ -1,9 +1,34 @@
import Cookies from 'js-cookie'
const TokenKey = 'Admin-Token'
const ImpersonationTokenKey = 'bms-impersonation-token'
let impersonationRecoveryInProgress = false
export function getToken() {
return Cookies.get(TokenKey)
return getImpersonationToken() || Cookies.get(TokenKey)
}
export function getImpersonationToken() {
return sessionStorage.getItem(ImpersonationTokenKey)
}
export function setImpersonationToken(token) {
sessionStorage.setItem(ImpersonationTokenKey, token)
}
export function clearImpersonationToken() {
sessionStorage.removeItem(ImpersonationTokenKey)
}
export function recoverImpersonation() {
if (impersonationRecoveryInProgress) return
impersonationRecoveryInProgress = true
clearImpersonationToken()
location.replace('/index')
}
export function isImpersonationRecoveryInProgress() {
return impersonationRecoveryInProgress
}
export function setToken(token) {
@@ -11,5 +36,6 @@ export function setToken(token) {
}
export function removeToken() {
clearImpersonationToken()
return Cookies.remove(TokenKey)
}
+12 -1
View File
@@ -1,6 +1,6 @@
import axios from 'axios'
import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus'
import { getToken } from '@/utils/auth'
import { getToken, getImpersonationToken, recoverImpersonation } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams, blobValidate } from '@/utils/bms'
import cache from '@/plugins/cache'
@@ -83,6 +83,11 @@ service.interceptors.response.use(res => {
return res.data
}
if (code === 401) {
const impersonationToken = getImpersonationToken()
if (impersonationToken && res.config.headers?.Authorization === 'Bearer ' + impersonationToken) {
recoverImpersonation()
return Promise.reject(new Error('代登录会话已结束,正在返回管理员账号'))
}
if (!isRelogin.show) {
isRelogin.show = true
ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
@@ -109,6 +114,12 @@ service.interceptors.response.use(res => {
}
},
error => {
const impersonationToken = getImpersonationToken()
if (impersonationToken && error.response?.status === 401 &&
error.config?.headers?.Authorization === 'Bearer ' + impersonationToken) {
recoverImpersonation()
return Promise.reject(error)
}
console.log('err' + error)
let { message } = error
if (message == "Network Error") {
+19 -2
View File
@@ -59,6 +59,9 @@
<el-tooltip content="分配角色" placement="top" v-if="row.userId !== 1">
<el-button link type="primary" icon="CircleCheck" @click="handleAuthRole(row)" v-hasPermi="['system:user:edit']"></el-button>
</el-tooltip>
<el-tooltip content="以此用户身份进入" placement="top" v-if="String(userStore.id) === '1' && String(row.userId) !== '1' && row.status === '0'">
<el-button link type="primary" icon="User" @click="handleImpersonate(row)" v-hasPermi="['system:user:impersonate']"></el-button>
</el-tooltip>
</template>
</basic-table>
</div>
@@ -163,10 +166,13 @@ import TreePanel from "@/components/TreePanel"
import ExcelImportDialog from "@/components/ExcelImportDialog"
import UserViewDrawer from "./view"
import { usePasswordRule } from "@/utils/passwordRule"
import { changeUserStatus, listUser, resetUserPwd, delUser, getUser, updateUser, addUser, deptTreeSelect, syncUsersFromOracle } from "@/api/system/user"
import { changeUserStatus, listUser, resetUserPwd, delUser, getUser, updateUser, addUser, deptTreeSelect, syncUsersFromOracle, startImpersonation } from "@/api/system/user"
import { setImpersonationToken } from '@/utils/auth'
import useUserStore from '@/store/modules/user'
import { nextTick } from "vue"
const router = useRouter()
const userStore = useUserStore()
const { proxy } = getCurrentInstance()
const { pwdValidator, pwdPromptValidator } = usePasswordRule()
const { sys_normal_disable, sys_user_sex } = useDict("sys_normal_disable", "sys_user_sex")
@@ -203,7 +209,7 @@ const columns = [
{ label: "手机号码", prop: "phonenumber", width: 120 },
{ label: "状态", prop: "status" },
{ label: "创建时间", prop: "createTime", width: 160, parseTime: true },
{ label: "操作", prop: "action", width: 150, showInToolbar: false }
{ label: "操作", prop: "action", width: 180, showInToolbar: false }
]
// 查询函数
@@ -323,6 +329,17 @@ function handleAuthRole(row) {
router.push("/system/user-auth/role/" + userId)
}
async function handleImpersonate(row) {
try {
await proxy.$modal.confirm(`将以「${row.nickName}(${row.userName})」的权限进入系统,期间的操作会记录实际管理员。是否继续?`)
const res = await startImpersonation(row.userId)
setImpersonationToken(res.token)
location.replace('/index')
} catch (error) {
// 确认取消或请求错误由公共组件处理。
}
}
/** 重置密码按钮操作 */
function handleResetPwd(row) {
proxy.$prompt(`请输入「${row.userName}」的新密码`, "重置密码", {