✨ 新增管理员代登录功能
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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}」的新密码`, "重置密码", {
|
||||
|
||||
Reference in New Issue
Block a user