feat(auth): 添加超级管理员用户切换功能和相关日志记录
This commit is contained in:
parent
f3da61b10a
commit
431f06f211
@ -1,5 +1,6 @@
|
||||
import re
|
||||
import uuid
|
||||
from src.utils import logger
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status, UploadFile, File
|
||||
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.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.user_utils import generate_unique_user_id, validate_username, is_valid_phone_number
|
||||
from server.utils.common_utils import log_operation
|
||||
@ -769,3 +770,58 @@ async def upload_user_avatar(
|
||||
|
||||
except Exception as 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>
|
||||
<div :class="['log-viewer', { fullscreen: state.isFullscreen }]" ref="logViewer">
|
||||
<div class="control-panel">
|
||||
<div class="button-group">
|
||||
<a-button
|
||||
@click="fetchLogs"
|
||||
:loading="state.fetching"
|
||||
:icon="h(ReloadOutlined)"
|
||||
class="icon-only"
|
||||
>
|
||||
</a-button>
|
||||
<a-button @click="clearLogs" :icon="h(ClearOutlined)" class="icon-only"> </a-button>
|
||||
<a-button @click="printSystemConfig">
|
||||
<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-modal
|
||||
v-model:open="showModal"
|
||||
title="调试面板(请在生产环境中谨慎使用)"
|
||||
width="90%"
|
||||
:footer="null"
|
||||
:maskClosable="true"
|
||||
:destroyOnClose="true"
|
||||
class="debug-modal"
|
||||
>
|
||||
<div :class="['log-viewer', { fullscreen: state.isFullscreen }]" ref="logViewer">
|
||||
<div class="control-panel">
|
||||
<div class="button-group">
|
||||
<a-button
|
||||
:type="state.autoRefresh ? 'primary' : 'default'"
|
||||
:class="{ 'auto-refresh-button': state.autoRefresh }"
|
||||
@click="toggleAutoRefresh(!state.autoRefresh)"
|
||||
@click="fetchLogs"
|
||||
:loading="state.fetching"
|
||||
: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-tooltip>
|
||||
</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)"
|
||||
<a-button @click="clearLogs" :icon="h(ClearOutlined)" class="icon-only"> </a-button>
|
||||
<a-button @click="printSystemConfig">
|
||||
<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
|
||||
:type="state.autoRefresh ? 'primary' : 'default'"
|
||||
:class="{ 'auto-refresh-button': state.autoRefresh }"
|
||||
@click="toggleAutoRefresh(!state.autoRefresh)"
|
||||
>
|
||||
<div class="option-content">
|
||||
<span class="option-text">{{ level.label }}</span>
|
||||
<div class="option-indicator">
|
||||
<CheckCircleOutlined v-if="isLogLevelSelected(level.value)" />
|
||||
<PlusCircleOutlined v-else />
|
||||
<template #icon>
|
||||
<SyncOutlined :spin="state.autoRefresh" />
|
||||
</template>
|
||||
自动刷新
|
||||
<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 ref="logContainer" class="log-container">
|
||||
<div v-if="processedLogs.length" class="log-lines">
|
||||
<div
|
||||
v-for="(log, index) in processedLogs"
|
||||
:key="index"
|
||||
:class="['log-line', `level-${log.level.toLowerCase()}`]"
|
||||
>
|
||||
<span class="timestamp">{{ formatTimestamp(log.timestamp) }}</span>
|
||||
<span class="level">{{ log.level }}</span>
|
||||
<span class="module">{{ log.module }}</span>
|
||||
<span class="message">{{ log.message }}</span>
|
||||
<div ref="logContainer" class="log-container">
|
||||
<div v-if="processedLogs.length" class="log-lines">
|
||||
<div
|
||||
v-for="(log, index) in processedLogs"
|
||||
:key="index"
|
||||
:class="['log-line', `level-${log.level.toLowerCase()}`]"
|
||||
>
|
||||
<span class="timestamp">{{ formatTimestamp(log.timestamp) }}</span>
|
||||
<span class="level">{{ log.level }}</span>
|
||||
<span class="module">{{ log.module }}</span>
|
||||
<span class="message">{{ log.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-logs">暂无日志</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>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
onMounted,
|
||||
onActivated,
|
||||
onUnmounted,
|
||||
nextTick,
|
||||
reactive,
|
||||
computed,
|
||||
h,
|
||||
toRaw
|
||||
} from 'vue'
|
||||
import { ref, reactive, computed, defineModel, onMounted, onActivated, onUnmounted, nextTick, toRaw, h } from 'vue'
|
||||
|
||||
const showModal = defineModel('show')
|
||||
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useDatabaseStore } from '@/stores/database'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import { useInfoStore } from '@/stores/info'
|
||||
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 {
|
||||
FullscreenOutlined,
|
||||
FullscreenExitOutlined,
|
||||
@ -132,11 +158,12 @@ import {
|
||||
UserOutlined,
|
||||
DatabaseOutlined,
|
||||
RobotOutlined,
|
||||
BugOutlined
|
||||
BugOutlined,
|
||||
SwapOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import dayjs from '@/utils/time'
|
||||
import { configApi } from '@/apis/system_api'
|
||||
import { checkAdminPermission } from '@/stores/user'
|
||||
import { checkSuperAdminPermission } from '@/stores/user'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const userStore = useUserStore()
|
||||
@ -162,7 +189,10 @@ const state = reactive({
|
||||
searchText: '',
|
||||
selectedLevels: logLevels.map((l) => l.value),
|
||||
rawLogs: [],
|
||||
isFullscreen: false
|
||||
isFullscreen: false,
|
||||
showUserSwitcher: false,
|
||||
users: [],
|
||||
switchingUser: false
|
||||
})
|
||||
|
||||
const error = ref('')
|
||||
@ -219,7 +249,7 @@ const processedLogs = computed(() => {
|
||||
|
||||
// 获取日志数据
|
||||
const fetchLogs = async () => {
|
||||
if (!checkAdminPermission()) return
|
||||
if (!checkSuperAdminPermission()) return
|
||||
|
||||
state.fetching = true
|
||||
try {
|
||||
@ -245,7 +275,7 @@ const fetchLogs = async () => {
|
||||
|
||||
// 清空日志
|
||||
const clearLogs = () => {
|
||||
if (!checkAdminPermission()) return
|
||||
if (!checkSuperAdminPermission()) return
|
||||
state.rawLogs = []
|
||||
}
|
||||
|
||||
@ -280,7 +310,7 @@ const toggleLogLevel = (level) => {
|
||||
|
||||
// 自动刷新
|
||||
const toggleAutoRefresh = (value) => {
|
||||
if (!checkAdminPermission()) return
|
||||
if (!checkSuperAdminPermission()) return
|
||||
|
||||
if (value) {
|
||||
autoRefreshInterval = setInterval(fetchLogs, 5000)
|
||||
@ -296,7 +326,7 @@ const toggleAutoRefresh = (value) => {
|
||||
|
||||
// 全屏切换
|
||||
const toggleFullscreen = async () => {
|
||||
if (!checkAdminPermission()) return
|
||||
if (!checkSuperAdminPermission()) return
|
||||
|
||||
try {
|
||||
if (!state.isFullscreen) {
|
||||
@ -329,7 +359,7 @@ const handleFullscreenChange = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (checkAdminPermission()) {
|
||||
if (checkSuperAdminPermission()) {
|
||||
fetchLogs()
|
||||
}
|
||||
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
||||
@ -338,7 +368,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
if (!checkAdminPermission()) return
|
||||
if (!checkSuperAdminPermission()) return
|
||||
|
||||
if (state.autoRefresh) {
|
||||
toggleAutoRefresh(true)
|
||||
@ -359,14 +389,14 @@ onUnmounted(() => {
|
||||
|
||||
// 打印系统配置
|
||||
const printSystemConfig = () => {
|
||||
if (!checkAdminPermission()) return
|
||||
if (!checkSuperAdminPermission()) return
|
||||
console.log('=== 系统配置 ===')
|
||||
console.log(config)
|
||||
}
|
||||
|
||||
// 打印用户信息
|
||||
const printUserInfo = () => {
|
||||
if (!checkAdminPermission()) return
|
||||
if (!checkSuperAdminPermission()) return
|
||||
console.log('=== 用户信息 ===')
|
||||
const userInfo = {
|
||||
token: userStore.token ? '*** (已隐藏)' : null,
|
||||
@ -385,7 +415,7 @@ const printUserInfo = () => {
|
||||
|
||||
// 打印知识库信息
|
||||
const printDatabaseInfo = async () => {
|
||||
if (!checkAdminPermission()) return
|
||||
if (!checkSuperAdminPermission()) return
|
||||
|
||||
try {
|
||||
console.log('=== 知识库信息 ===')
|
||||
@ -418,13 +448,13 @@ const printDatabaseInfo = async () => {
|
||||
|
||||
// 切换Debug模式
|
||||
const toggleDebugMode = () => {
|
||||
if (!checkAdminPermission()) return
|
||||
if (!checkSuperAdminPermission()) return
|
||||
infoStore.toggleDebugMode()
|
||||
}
|
||||
|
||||
// 打印智能体配置
|
||||
const printAgentConfig = async () => {
|
||||
if (!checkAdminPermission()) return
|
||||
if (!checkSuperAdminPermission()) return
|
||||
|
||||
try {
|
||||
console.log('=== 智能体配置信息 ===')
|
||||
@ -486,6 +516,66 @@ const printAgentConfig = async () => {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -26,28 +26,36 @@
|
||||
</div>
|
||||
</a-menu-item>
|
||||
<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>
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
key="theme"
|
||||
@click="toggleTheme"
|
||||
:icon="h(themeStore.isDark ? Sun : Moon, { size: '16' })"
|
||||
:icon="themeStore.isDark ? SunIcon : MoonIcon"
|
||||
>
|
||||
<span class="menu-text">{{
|
||||
themeStore.isDark ? '切换到浅色模式' : '切换到深色模式 (Beta)'
|
||||
}}</span>
|
||||
</a-menu-item>
|
||||
<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
|
||||
v-if="userStore.isAdmin"
|
||||
key="setting"
|
||||
@click="goToSetting"
|
||||
:icon="h(Settings, { size: '16' })"
|
||||
:icon="SettingsIcon"
|
||||
>
|
||||
<span class="menu-text">系统设置</span>
|
||||
</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>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
@ -165,6 +173,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 调试面板 Modal -->
|
||||
<DebugComponent v-model:show="showDebug" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -172,10 +183,7 @@
|
||||
import { computed, ref, inject, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
import DebugComponent from '@/components/DebugComponent.vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
CircleUser,
|
||||
@ -186,7 +194,8 @@ import {
|
||||
User,
|
||||
LogOut,
|
||||
Upload,
|
||||
Settings
|
||||
Settings,
|
||||
Terminal
|
||||
} from 'lucide-vue-next'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
|
||||
@ -194,6 +203,17 @@ const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
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
|
||||
const { openSettingsModal } = inject('settingsModal', {})
|
||||
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
<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 { GithubOutlined } from '@ant-design/icons-vue'
|
||||
import { Bot, Waypoints, LibraryBig, BarChart3, CircleCheck } from 'lucide-vue-next'
|
||||
import { onLongPress } from '@vueuse/core'
|
||||
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import { useDatabaseStore } from '@/stores/database'
|
||||
@ -32,7 +31,6 @@ const isLoadingStars = ref(false)
|
||||
|
||||
// Add state for debug modal
|
||||
const showDebugModal = ref(false)
|
||||
const htmlRefHook = useTemplateRef('htmlRefHook')
|
||||
|
||||
// Add state for settings modal
|
||||
const showSettingsModal = ref(false)
|
||||
@ -42,21 +40,6 @@ const openSettingsModal = () => {
|
||||
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
|
||||
const handleDebugModalClose = () => {
|
||||
showDebugModal.value = false
|
||||
@ -181,7 +164,7 @@ provide('settingsModal', {
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="htmlRefHook" class="fill debug-trigger"></div>
|
||||
<div class="fill"></div>
|
||||
<div class="github nav-item">
|
||||
<a-tooltip placement="right">
|
||||
<template #title>欢迎 Star</template>
|
||||
@ -233,15 +216,6 @@ provide('settingsModal', {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
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,
|
||||
@ -277,12 +251,7 @@ div.header,
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
// 添加debug触发器样式
|
||||
.debug-trigger {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 20px;
|
||||
.fill {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user