feat(auth): 添加超级管理员用户切换功能和相关日志记录
This commit is contained in:
parent
f3da61b10a
commit
431f06f211
@ -1,5 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
|
from src.utils import logger
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status, UploadFile, File
|
from fastapi import APIRouter, Depends, HTTPException, Request, status, UploadFile, File
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
@ -9,7 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from src.storage.db.manager import db_manager
|
from src.storage.db.manager import db_manager
|
||||||
from src.storage.db.models import User, Department
|
from src.storage.db.models import User, Department
|
||||||
from server.utils.auth_middleware import get_admin_user, get_current_user, get_db, get_required_user
|
from server.utils.auth_middleware import get_admin_user, get_superadmin_user, get_current_user, get_db, get_required_user
|
||||||
from server.utils.auth_utils import AuthUtils
|
from server.utils.auth_utils import AuthUtils
|
||||||
from server.utils.user_utils import generate_unique_user_id, validate_username, is_valid_phone_number
|
from server.utils.user_utils import generate_unique_user_id, validate_username, is_valid_phone_number
|
||||||
from server.utils.common_utils import log_operation
|
from server.utils.common_utils import log_operation
|
||||||
@ -769,3 +770,58 @@ async def upload_user_avatar(
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"头像上传失败: {str(e)}")
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"头像上传失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# 路由:模拟用户登录(超级管理员专用)
|
||||||
|
@auth.post("/impersonate/{user_id}", response_model=Token)
|
||||||
|
async def impersonate_user(
|
||||||
|
user_id: int,
|
||||||
|
request: Request,
|
||||||
|
current_user: User = Depends(get_superadmin_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""超级管理员模拟其他用户登录"""
|
||||||
|
# 查找目标用户
|
||||||
|
result = await db.execute(select(User).filter(User.id == user_id, User.is_deleted == 0))
|
||||||
|
target_user = result.scalar_one_or_none()
|
||||||
|
if target_user is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="用户不存在",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 不能模拟超级管理员
|
||||||
|
if target_user.role == "superadmin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="不能模拟超级管理员账户",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 生成访问令牌
|
||||||
|
token_data = {"sub": str(target_user.id)}
|
||||||
|
access_token = AuthUtils.create_access_token(token_data)
|
||||||
|
|
||||||
|
# 获取部门名称
|
||||||
|
department_name = None
|
||||||
|
if target_user.department_id:
|
||||||
|
result = await db.execute(select(Department.name).filter(Department.id == target_user.department_id))
|
||||||
|
department_name = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
# 记录操作(危险操作标记)
|
||||||
|
await log_operation(db, current_user.id, "⚠️ 危险操作-模拟用户", f"模拟用户: {target_user.username}", request)
|
||||||
|
|
||||||
|
# 控制台警告日志
|
||||||
|
logger.warning(f"⚠️ [危险操作] 超级管理员 {current_user.username} 模拟登录用户: {target_user.username}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"access_token": access_token,
|
||||||
|
"token_type": "bearer",
|
||||||
|
"user_id": target_user.id,
|
||||||
|
"username": target_user.username,
|
||||||
|
"user_id_login": target_user.user_id,
|
||||||
|
"phone_number": target_user.phone_number,
|
||||||
|
"avatar": target_user.avatar,
|
||||||
|
"role": target_user.role,
|
||||||
|
"department_id": target_user.department_id,
|
||||||
|
"department_name": department_name,
|
||||||
|
}
|
||||||
|
|||||||
@ -1,125 +1,151 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="['log-viewer', { fullscreen: state.isFullscreen }]" ref="logViewer">
|
<a-modal
|
||||||
<div class="control-panel">
|
v-model:open="showModal"
|
||||||
<div class="button-group">
|
title="调试面板(请在生产环境中谨慎使用)"
|
||||||
<a-button
|
width="90%"
|
||||||
@click="fetchLogs"
|
:footer="null"
|
||||||
:loading="state.fetching"
|
:maskClosable="true"
|
||||||
:icon="h(ReloadOutlined)"
|
:destroyOnClose="true"
|
||||||
class="icon-only"
|
class="debug-modal"
|
||||||
>
|
>
|
||||||
</a-button>
|
<div :class="['log-viewer', { fullscreen: state.isFullscreen }]" ref="logViewer">
|
||||||
<a-button @click="clearLogs" :icon="h(ClearOutlined)" class="icon-only"> </a-button>
|
<div class="control-panel">
|
||||||
<a-button @click="printSystemConfig">
|
<div class="button-group">
|
||||||
<template #icon><SettingOutlined /></template>
|
|
||||||
系统配置
|
|
||||||
</a-button>
|
|
||||||
<a-button @click="printUserInfo">
|
|
||||||
<template #icon><UserOutlined /></template>
|
|
||||||
用户信息
|
|
||||||
</a-button>
|
|
||||||
<a-button @click="printDatabaseInfo">
|
|
||||||
<template #icon><DatabaseOutlined /></template>
|
|
||||||
知识库信息
|
|
||||||
</a-button>
|
|
||||||
<a-button @click="printAgentConfig">
|
|
||||||
<template #icon><RobotOutlined /></template>
|
|
||||||
智能体配置
|
|
||||||
</a-button>
|
|
||||||
<a-button @click="toggleDebugMode" :type="infoStore.debugMode ? 'primary' : 'default'">
|
|
||||||
<template #icon><BugOutlined /></template>
|
|
||||||
Debug 模式: {{ infoStore.debugMode ? '开启' : '关闭' }}
|
|
||||||
</a-button>
|
|
||||||
<a-button @click="toggleFullscreen">
|
|
||||||
<template #icon>
|
|
||||||
<FullscreenOutlined v-if="!state.isFullscreen" />
|
|
||||||
<FullscreenExitOutlined v-else />
|
|
||||||
</template>
|
|
||||||
{{ state.isFullscreen ? '退出全屏' : '全屏' }}
|
|
||||||
</a-button>
|
|
||||||
<a-tooltip :title="state.autoRefresh ? '点击停止自动刷新' : '点击开启自动刷新'">
|
|
||||||
<a-button
|
<a-button
|
||||||
:type="state.autoRefresh ? 'primary' : 'default'"
|
@click="fetchLogs"
|
||||||
:class="{ 'auto-refresh-button': state.autoRefresh }"
|
:loading="state.fetching"
|
||||||
@click="toggleAutoRefresh(!state.autoRefresh)"
|
:icon="h(ReloadOutlined)"
|
||||||
|
class="icon-only"
|
||||||
>
|
>
|
||||||
<template #icon>
|
|
||||||
<SyncOutlined :spin="state.autoRefresh" />
|
|
||||||
</template>
|
|
||||||
自动刷新
|
|
||||||
<span v-if="state.autoRefresh" class="refresh-interval">(5s)</span>
|
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
<a-button @click="clearLogs" :icon="h(ClearOutlined)" class="icon-only"> </a-button>
|
||||||
</div>
|
<a-button @click="printSystemConfig">
|
||||||
<div class="filter-group">
|
<template #icon><SettingOutlined /></template>
|
||||||
<a-input-search
|
系统配置
|
||||||
v-model:value="state.searchText"
|
</a-button>
|
||||||
placeholder="搜索日志..."
|
<a-button @click="printUserInfo">
|
||||||
style="width: 200px; height: 32px"
|
<template #icon><UserOutlined /></template>
|
||||||
@search="onSearch"
|
用户信息
|
||||||
/>
|
</a-button>
|
||||||
<div class="log-level-selector">
|
<a-button @click="printDatabaseInfo">
|
||||||
<div class="multi-select-cards">
|
<template #icon><DatabaseOutlined /></template>
|
||||||
<div
|
知识库信息
|
||||||
v-for="level in logLevels"
|
</a-button>
|
||||||
:key="level.value"
|
<a-button @click="printAgentConfig">
|
||||||
class="option-card"
|
<template #icon><RobotOutlined /></template>
|
||||||
:class="{
|
智能体配置
|
||||||
selected: isLogLevelSelected(level.value),
|
</a-button>
|
||||||
unselected: !isLogLevelSelected(level.value)
|
<a-button @click="toggleDebugMode" :type="infoStore.debugMode ? 'primary' : 'default'">
|
||||||
}"
|
<template #icon><BugOutlined /></template>
|
||||||
@click="toggleLogLevel(level.value)"
|
Debug 模式: {{ infoStore.debugMode ? '开启' : '关闭' }}
|
||||||
|
</a-button>
|
||||||
|
<a-button @click="toggleFullscreen">
|
||||||
|
<template #icon>
|
||||||
|
<FullscreenOutlined v-if="!state.isFullscreen" />
|
||||||
|
<FullscreenExitOutlined v-else />
|
||||||
|
</template>
|
||||||
|
{{ state.isFullscreen ? '退出全屏' : '全屏' }}
|
||||||
|
</a-button>
|
||||||
|
<a-tooltip :title="state.autoRefresh ? '点击停止自动刷新' : '点击开启自动刷新'">
|
||||||
|
<a-button
|
||||||
|
:type="state.autoRefresh ? 'primary' : 'default'"
|
||||||
|
:class="{ 'auto-refresh-button': state.autoRefresh }"
|
||||||
|
@click="toggleAutoRefresh(!state.autoRefresh)"
|
||||||
>
|
>
|
||||||
<div class="option-content">
|
<template #icon>
|
||||||
<span class="option-text">{{ level.label }}</span>
|
<SyncOutlined :spin="state.autoRefresh" />
|
||||||
<div class="option-indicator">
|
</template>
|
||||||
<CheckCircleOutlined v-if="isLogLevelSelected(level.value)" />
|
自动刷新
|
||||||
<PlusCircleOutlined v-else />
|
<span v-if="state.autoRefresh" class="refresh-interval">(5s)</span>
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-button @click="openUserSwitcher">
|
||||||
|
<template #icon><SwapOutlined /></template>
|
||||||
|
切换用户
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<a-input-search
|
||||||
|
v-model:value="state.searchText"
|
||||||
|
placeholder="搜索日志..."
|
||||||
|
style="width: 200px; height: 32px"
|
||||||
|
@search="onSearch"
|
||||||
|
/>
|
||||||
|
<div class="log-level-selector">
|
||||||
|
<div class="multi-select-cards">
|
||||||
|
<div
|
||||||
|
v-for="level in logLevels"
|
||||||
|
:key="level.value"
|
||||||
|
class="option-card"
|
||||||
|
:class="{
|
||||||
|
selected: isLogLevelSelected(level.value),
|
||||||
|
unselected: !isLogLevelSelected(level.value)
|
||||||
|
}"
|
||||||
|
@click="toggleLogLevel(level.value)"
|
||||||
|
>
|
||||||
|
<div class="option-content">
|
||||||
|
<span class="option-text">{{ level.label }}</span>
|
||||||
|
<div class="option-indicator">
|
||||||
|
<CheckCircleOutlined v-if="isLogLevelSelected(level.value)" />
|
||||||
|
<PlusCircleOutlined v-else />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div ref="logContainer" class="log-container">
|
||||||
<div ref="logContainer" class="log-container">
|
<div v-if="processedLogs.length" class="log-lines">
|
||||||
<div v-if="processedLogs.length" class="log-lines">
|
<div
|
||||||
<div
|
v-for="(log, index) in processedLogs"
|
||||||
v-for="(log, index) in processedLogs"
|
:key="index"
|
||||||
:key="index"
|
:class="['log-line', `level-${log.level.toLowerCase()}`]"
|
||||||
:class="['log-line', `level-${log.level.toLowerCase()}`]"
|
>
|
||||||
>
|
<span class="timestamp">{{ formatTimestamp(log.timestamp) }}</span>
|
||||||
<span class="timestamp">{{ formatTimestamp(log.timestamp) }}</span>
|
<span class="level">{{ log.level }}</span>
|
||||||
<span class="level">{{ log.level }}</span>
|
<span class="module">{{ log.module }}</span>
|
||||||
<span class="module">{{ log.module }}</span>
|
<span class="message">{{ log.message }}</span>
|
||||||
<span class="message">{{ log.message }}</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="empty-logs">暂无日志</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="empty-logs">暂无日志</div>
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
|
<!-- 用户切换 Modal -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="state.showUserSwitcher"
|
||||||
|
title="切换用户"
|
||||||
|
:confirmLoading="state.switchingUser"
|
||||||
|
:footer="null"
|
||||||
|
:bodyStyle="{ padding: '12px' }"
|
||||||
|
>
|
||||||
|
<a-list item-layout="horizontal" :data-source="state.users">
|
||||||
|
<template #renderItem="{ item }">
|
||||||
|
<a-list-item @click="switchToUser(item)" style="cursor: pointer">
|
||||||
|
<a-list-item-meta :title="item.username" :description="item.role" />
|
||||||
|
</a-list-item>
|
||||||
|
</template>
|
||||||
|
<template #empty>
|
||||||
|
<a-empty description="暂无用户" />
|
||||||
|
</template>
|
||||||
|
</a-list>
|
||||||
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
</a-modal>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import { ref, reactive, computed, defineModel, onMounted, onActivated, onUnmounted, nextTick, toRaw, h } from 'vue'
|
||||||
ref,
|
|
||||||
onMounted,
|
const showModal = defineModel('show')
|
||||||
onActivated,
|
|
||||||
onUnmounted,
|
|
||||||
nextTick,
|
|
||||||
reactive,
|
|
||||||
computed,
|
|
||||||
h,
|
|
||||||
toRaw
|
|
||||||
} from 'vue'
|
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { useDatabaseStore } from '@/stores/database'
|
import { useDatabaseStore } from '@/stores/database'
|
||||||
import { useAgentStore } from '@/stores/agent'
|
import { useAgentStore } from '@/stores/agent'
|
||||||
import { useInfoStore } from '@/stores/info'
|
import { useInfoStore } from '@/stores/info'
|
||||||
import { useThrottleFn } from '@vueuse/core'
|
import { useThrottleFn } from '@vueuse/core'
|
||||||
import { message } from 'ant-design-vue'
|
import { message, Modal, List as AList, ListItem as AListItem, ListItemMeta as AListItemMeta, Empty as AEmpty } from 'ant-design-vue'
|
||||||
import {
|
import {
|
||||||
FullscreenOutlined,
|
FullscreenOutlined,
|
||||||
FullscreenExitOutlined,
|
FullscreenExitOutlined,
|
||||||
@ -132,11 +158,12 @@ import {
|
|||||||
UserOutlined,
|
UserOutlined,
|
||||||
DatabaseOutlined,
|
DatabaseOutlined,
|
||||||
RobotOutlined,
|
RobotOutlined,
|
||||||
BugOutlined
|
BugOutlined,
|
||||||
|
SwapOutlined
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import dayjs from '@/utils/time'
|
import dayjs from '@/utils/time'
|
||||||
import { configApi } from '@/apis/system_api'
|
import { configApi } from '@/apis/system_api'
|
||||||
import { checkAdminPermission } from '@/stores/user'
|
import { checkSuperAdminPermission } from '@/stores/user'
|
||||||
|
|
||||||
const configStore = useConfigStore()
|
const configStore = useConfigStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@ -162,7 +189,10 @@ const state = reactive({
|
|||||||
searchText: '',
|
searchText: '',
|
||||||
selectedLevels: logLevels.map((l) => l.value),
|
selectedLevels: logLevels.map((l) => l.value),
|
||||||
rawLogs: [],
|
rawLogs: [],
|
||||||
isFullscreen: false
|
isFullscreen: false,
|
||||||
|
showUserSwitcher: false,
|
||||||
|
users: [],
|
||||||
|
switchingUser: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
@ -219,7 +249,7 @@ const processedLogs = computed(() => {
|
|||||||
|
|
||||||
// 获取日志数据
|
// 获取日志数据
|
||||||
const fetchLogs = async () => {
|
const fetchLogs = async () => {
|
||||||
if (!checkAdminPermission()) return
|
if (!checkSuperAdminPermission()) return
|
||||||
|
|
||||||
state.fetching = true
|
state.fetching = true
|
||||||
try {
|
try {
|
||||||
@ -245,7 +275,7 @@ const fetchLogs = async () => {
|
|||||||
|
|
||||||
// 清空日志
|
// 清空日志
|
||||||
const clearLogs = () => {
|
const clearLogs = () => {
|
||||||
if (!checkAdminPermission()) return
|
if (!checkSuperAdminPermission()) return
|
||||||
state.rawLogs = []
|
state.rawLogs = []
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,7 +310,7 @@ const toggleLogLevel = (level) => {
|
|||||||
|
|
||||||
// 自动刷新
|
// 自动刷新
|
||||||
const toggleAutoRefresh = (value) => {
|
const toggleAutoRefresh = (value) => {
|
||||||
if (!checkAdminPermission()) return
|
if (!checkSuperAdminPermission()) return
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
autoRefreshInterval = setInterval(fetchLogs, 5000)
|
autoRefreshInterval = setInterval(fetchLogs, 5000)
|
||||||
@ -296,7 +326,7 @@ const toggleAutoRefresh = (value) => {
|
|||||||
|
|
||||||
// 全屏切换
|
// 全屏切换
|
||||||
const toggleFullscreen = async () => {
|
const toggleFullscreen = async () => {
|
||||||
if (!checkAdminPermission()) return
|
if (!checkSuperAdminPermission()) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!state.isFullscreen) {
|
if (!state.isFullscreen) {
|
||||||
@ -329,7 +359,7 @@ const handleFullscreenChange = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (checkAdminPermission()) {
|
if (checkSuperAdminPermission()) {
|
||||||
fetchLogs()
|
fetchLogs()
|
||||||
}
|
}
|
||||||
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
||||||
@ -338,7 +368,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
if (!checkAdminPermission()) return
|
if (!checkSuperAdminPermission()) return
|
||||||
|
|
||||||
if (state.autoRefresh) {
|
if (state.autoRefresh) {
|
||||||
toggleAutoRefresh(true)
|
toggleAutoRefresh(true)
|
||||||
@ -359,14 +389,14 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
// 打印系统配置
|
// 打印系统配置
|
||||||
const printSystemConfig = () => {
|
const printSystemConfig = () => {
|
||||||
if (!checkAdminPermission()) return
|
if (!checkSuperAdminPermission()) return
|
||||||
console.log('=== 系统配置 ===')
|
console.log('=== 系统配置 ===')
|
||||||
console.log(config)
|
console.log(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打印用户信息
|
// 打印用户信息
|
||||||
const printUserInfo = () => {
|
const printUserInfo = () => {
|
||||||
if (!checkAdminPermission()) return
|
if (!checkSuperAdminPermission()) return
|
||||||
console.log('=== 用户信息 ===')
|
console.log('=== 用户信息 ===')
|
||||||
const userInfo = {
|
const userInfo = {
|
||||||
token: userStore.token ? '*** (已隐藏)' : null,
|
token: userStore.token ? '*** (已隐藏)' : null,
|
||||||
@ -385,7 +415,7 @@ const printUserInfo = () => {
|
|||||||
|
|
||||||
// 打印知识库信息
|
// 打印知识库信息
|
||||||
const printDatabaseInfo = async () => {
|
const printDatabaseInfo = async () => {
|
||||||
if (!checkAdminPermission()) return
|
if (!checkSuperAdminPermission()) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('=== 知识库信息 ===')
|
console.log('=== 知识库信息 ===')
|
||||||
@ -418,13 +448,13 @@ const printDatabaseInfo = async () => {
|
|||||||
|
|
||||||
// 切换Debug模式
|
// 切换Debug模式
|
||||||
const toggleDebugMode = () => {
|
const toggleDebugMode = () => {
|
||||||
if (!checkAdminPermission()) return
|
if (!checkSuperAdminPermission()) return
|
||||||
infoStore.toggleDebugMode()
|
infoStore.toggleDebugMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打印智能体配置
|
// 打印智能体配置
|
||||||
const printAgentConfig = async () => {
|
const printAgentConfig = async () => {
|
||||||
if (!checkAdminPermission()) return
|
if (!checkSuperAdminPermission()) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('=== 智能体配置信息 ===')
|
console.log('=== 智能体配置信息 ===')
|
||||||
@ -486,6 +516,66 @@ const printAgentConfig = async () => {
|
|||||||
message.error('获取智能体配置失败: ' + error.message)
|
message.error('获取智能体配置失败: ' + error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取用户列表
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/users', {
|
||||||
|
headers: userStore.getAuthHeaders()
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('获取用户列表失败')
|
||||||
|
}
|
||||||
|
state.users = await response.json()
|
||||||
|
} catch (err) {
|
||||||
|
message.error(`获取用户列表失败: ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开用户选择器
|
||||||
|
const openUserSwitcher = () => {
|
||||||
|
if (!checkSuperAdminPermission()) return
|
||||||
|
state.showUserSwitcher = true
|
||||||
|
fetchUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换用户
|
||||||
|
const switchToUser = async (user) => {
|
||||||
|
if (!checkSuperAdminPermission()) return
|
||||||
|
|
||||||
|
// 危险操作确认
|
||||||
|
Modal.confirm({
|
||||||
|
title: '⚠️ 危险操作确认',
|
||||||
|
content: `确定要切换为用户 "${user.username}" 吗?此操作将被记录。`,
|
||||||
|
okText: '确认切换',
|
||||||
|
cancelText: '取消',
|
||||||
|
okType: 'danger',
|
||||||
|
onOk: async () => {
|
||||||
|
state.switchingUser = true
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/auth/impersonate/${user.id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: userStore.getAuthHeaders()
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.detail || '切换用户失败')
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
// 设置新 token
|
||||||
|
localStorage.setItem('user_token', data.access_token)
|
||||||
|
message.success(`已切换用户: ${user.username}`)
|
||||||
|
state.showUserSwitcher = false
|
||||||
|
// 刷新页面以重新初始化应用
|
||||||
|
window.location.reload()
|
||||||
|
} catch (err) {
|
||||||
|
message.error(`切换失败: ${err.message}`)
|
||||||
|
} finally {
|
||||||
|
state.switchingUser = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -26,28 +26,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-divider />
|
<a-menu-divider />
|
||||||
<a-menu-item key="docs" @click="openDocs" :icon="h(BookOpen, { size: '16' })">
|
<a-menu-item key="docs" @click="openDocs" :icon="BookOpenIcon">
|
||||||
<span class="menu-text">文档中心</span>
|
<span class="menu-text">文档中心</span>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item
|
<a-menu-item
|
||||||
key="theme"
|
key="theme"
|
||||||
@click="toggleTheme"
|
@click="toggleTheme"
|
||||||
:icon="h(themeStore.isDark ? Sun : Moon, { size: '16' })"
|
:icon="themeStore.isDark ? SunIcon : MoonIcon"
|
||||||
>
|
>
|
||||||
<span class="menu-text">{{
|
<span class="menu-text">{{
|
||||||
themeStore.isDark ? '切换到浅色模式' : '切换到深色模式 (Beta)'
|
themeStore.isDark ? '切换到浅色模式' : '切换到深色模式 (Beta)'
|
||||||
}}</span>
|
}}</span>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-divider v-if="userStore.isAdmin" />
|
<a-menu-divider v-if="userStore.isAdmin" />
|
||||||
|
<a-menu-item
|
||||||
|
v-if="userStore.isSuperAdmin"
|
||||||
|
key="debug"
|
||||||
|
@click="showDebug = true"
|
||||||
|
:icon="TerminalIcon"
|
||||||
|
>
|
||||||
|
<span class="menu-text">调试面板(非生产环境)</span>
|
||||||
|
</a-menu-item>
|
||||||
<a-menu-item
|
<a-menu-item
|
||||||
v-if="userStore.isAdmin"
|
v-if="userStore.isAdmin"
|
||||||
key="setting"
|
key="setting"
|
||||||
@click="goToSetting"
|
@click="goToSetting"
|
||||||
:icon="h(Settings, { size: '16' })"
|
:icon="SettingsIcon"
|
||||||
>
|
>
|
||||||
<span class="menu-text">系统设置</span>
|
<span class="menu-text">系统设置</span>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item key="logout" @click="logout" :icon="h(LogOut, { size: '16' })">
|
<a-menu-item key="logout" @click="logout" :icon="LogOutIcon">
|
||||||
<span class="menu-text">退出登录</span>
|
<span class="menu-text">退出登录</span>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
@ -165,6 +173,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 调试面板 Modal -->
|
||||||
|
<DebugComponent v-model:show="showDebug" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -172,10 +183,7 @@
|
|||||||
import { computed, ref, inject, h } from 'vue'
|
import { computed, ref, inject, h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
//
|
import DebugComponent from '@/components/DebugComponent.vue'
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import {
|
import {
|
||||||
CircleUser,
|
CircleUser,
|
||||||
@ -186,7 +194,8 @@ import {
|
|||||||
User,
|
User,
|
||||||
LogOut,
|
LogOut,
|
||||||
Upload,
|
Upload,
|
||||||
Settings
|
Settings,
|
||||||
|
Terminal
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
|
||||||
@ -194,6 +203,17 @@ const router = useRouter()
|
|||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const themeStore = useThemeStore()
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
|
// 预定义图标组件,避免 Vue 警告
|
||||||
|
const BookOpenIcon = h(BookOpen, { size: '16' })
|
||||||
|
const SunIcon = h(Sun, { size: '16' })
|
||||||
|
const MoonIcon = h(Moon, { size: '16' })
|
||||||
|
const TerminalIcon = h(Terminal, { size: '16' })
|
||||||
|
const SettingsIcon = h(Settings, { size: '16' })
|
||||||
|
const LogOutIcon = h(LogOut, { size: '16' })
|
||||||
|
|
||||||
|
// 调试面板状态
|
||||||
|
const showDebug = ref(false)
|
||||||
|
|
||||||
// Inject settings modal methods
|
// Inject settings modal methods
|
||||||
const { openSettingsModal } = inject('settingsModal', {})
|
const { openSettingsModal } = inject('settingsModal', {})
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, useTemplateRef, computed, provide } from 'vue'
|
import { ref, reactive, onMounted, computed, provide } from 'vue'
|
||||||
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
||||||
import { GithubOutlined } from '@ant-design/icons-vue'
|
import { GithubOutlined } from '@ant-design/icons-vue'
|
||||||
import { Bot, Waypoints, LibraryBig, BarChart3, CircleCheck } from 'lucide-vue-next'
|
import { Bot, Waypoints, LibraryBig, BarChart3, CircleCheck } from 'lucide-vue-next'
|
||||||
import { onLongPress } from '@vueuse/core'
|
|
||||||
|
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
import { useDatabaseStore } from '@/stores/database'
|
import { useDatabaseStore } from '@/stores/database'
|
||||||
@ -32,7 +31,6 @@ const isLoadingStars = ref(false)
|
|||||||
|
|
||||||
// Add state for debug modal
|
// Add state for debug modal
|
||||||
const showDebugModal = ref(false)
|
const showDebugModal = ref(false)
|
||||||
const htmlRefHook = useTemplateRef('htmlRefHook')
|
|
||||||
|
|
||||||
// Add state for settings modal
|
// Add state for settings modal
|
||||||
const showSettingsModal = ref(false)
|
const showSettingsModal = ref(false)
|
||||||
@ -42,21 +40,6 @@ const openSettingsModal = () => {
|
|||||||
showSettingsModal.value = true
|
showSettingsModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup long press for debug modal
|
|
||||||
onLongPress(
|
|
||||||
htmlRefHook,
|
|
||||||
() => {
|
|
||||||
console.log('long press')
|
|
||||||
showDebugModal.value = true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
delay: 1000, // 1秒长按
|
|
||||||
modifiers: {
|
|
||||||
prevent: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handle debug modal close
|
// Handle debug modal close
|
||||||
const handleDebugModalClose = () => {
|
const handleDebugModalClose = () => {
|
||||||
showDebugModal.value = false
|
showDebugModal.value = false
|
||||||
@ -181,7 +164,7 @@ provide('settingsModal', {
|
|||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="htmlRefHook" class="fill debug-trigger"></div>
|
<div class="fill"></div>
|
||||||
<div class="github nav-item">
|
<div class="github nav-item">
|
||||||
<a-tooltip placement="right">
|
<a-tooltip placement="right">
|
||||||
<template #title>欢迎 Star</template>
|
<template #title>欢迎 Star</template>
|
||||||
@ -233,15 +216,6 @@ provide('settingsModal', {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
min-width: var(--min-width);
|
min-width: var(--min-width);
|
||||||
|
|
||||||
.debug-panel {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 100;
|
|
||||||
right: 0;
|
|
||||||
bottom: 50px;
|
|
||||||
border-radius: 20px 0 0 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.header,
|
div.header,
|
||||||
@ -277,12 +251,7 @@ div.header,
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加debug触发器样式
|
.fill {
|
||||||
.debug-trigger {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 20px;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user