feat: 实现用户退出登录功能并完善日志系统用户信息
- 头部用户菜单添加下拉框和退出登录选项 - 退出登录功能集成认证Store和路由跳转 - WebSocket连接动态获取当前登录用户信息 - 日志记录系统添加用户名和用户ID字段 - 移除vite-plugin-vue-devtools避免开发环境干扰 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f6f4744e0b
commit
23d94cf0ab
@ -2,13 +2,11 @@ import { fileURLToPath, URL } from 'node:url'
|
|||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
vueDevTools(),
|
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
@ -57,10 +57,18 @@
|
|||||||
<i class="fas fa-bell"></i>
|
<i class="fas fa-bell"></i>
|
||||||
<span class="badge">3</span>
|
<span class="badge">3</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-menu">
|
<div class="user-menu" @click="toggleUserDropdown">
|
||||||
<img src="@/assets/images/adminlogo.png" alt="用户头像" class="user-avatar">
|
<img src="@/assets/images/adminlogo.png" alt="用户头像" class="user-avatar">
|
||||||
<span class="username">管理员</span>
|
<span class="username">管理员</span>
|
||||||
<i class="fas fa-chevron-down"></i>
|
<i class="fas fa-chevron-down" :class="{ 'rotated': showUserDropdown }"></i>
|
||||||
|
|
||||||
|
<!-- 用户下拉菜单 -->
|
||||||
|
<div v-show="showUserDropdown" class="user-dropdown">
|
||||||
|
<div class="dropdown-item" @click.stop="handleLogout">
|
||||||
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
|
<span>退出登录</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="system-settings">
|
<div class="system-settings">
|
||||||
<i class="fas fa-cog"></i>
|
<i class="fas fa-cog"></i>
|
||||||
@ -70,8 +78,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { useCADStore } from '@/stores/cad'
|
import { useCADStore } from '@/stores/cad'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { PAGE_TYPES, CAD_REQUIRED_PAGES } from '@/config/pages'
|
import { PAGE_TYPES, CAD_REQUIRED_PAGES } from '@/config/pages'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
@ -87,6 +97,13 @@ const emit = defineEmits(['page-change'])
|
|||||||
|
|
||||||
// Store
|
// Store
|
||||||
const cadStore = useCADStore()
|
const cadStore = useCADStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// Router
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 用户下拉菜单状态
|
||||||
|
const showUserDropdown = ref(false)
|
||||||
|
|
||||||
// 检查是否有CAD连接
|
// 检查是否有CAD连接
|
||||||
const isCADConnected = computed(() => {
|
const isCADConnected = computed(() => {
|
||||||
@ -102,6 +119,17 @@ const handleNavClick = (page) => {
|
|||||||
|
|
||||||
emit('page-change', page)
|
emit('page-change', page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换用户下拉菜单
|
||||||
|
const toggleUserDropdown = () => {
|
||||||
|
showUserDropdown.value = !showUserDropdown.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const handleLogout = () => {
|
||||||
|
authStore.logout()
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -237,6 +265,7 @@ const handleNavClick = (page) => {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-menu:hover {
|
.user-menu:hover {
|
||||||
@ -258,6 +287,56 @@ const handleNavClick = (page) => {
|
|||||||
.user-menu i {
|
.user-menu i {
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-menu i.rotated {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px var(--color-shadow-black-1);
|
||||||
|
min-width: 140px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background: var(--color-white-rgb-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:first-child {
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:last-child {
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:only-child {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item i {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.system-settings {
|
.system-settings {
|
||||||
|
|||||||
@ -11,7 +11,7 @@ const API_CONFIG = {
|
|||||||
BASE_URL: 'ws://localhost:8000',
|
BASE_URL: 'ws://localhost:8000',
|
||||||
ENDPOINT: '/api/v1/ws/connect',
|
ENDPOINT: '/api/v1/ws/connect',
|
||||||
CLIENT_ID: 'web_client',
|
CLIENT_ID: 'web_client',
|
||||||
USER_ID: 'admin',
|
USER_ID: null, // 动态设置,从auth store获取
|
||||||
RECONNECT_ATTEMPTS: 5,
|
RECONNECT_ATTEMPTS: 5,
|
||||||
RECONNECT_DELAY: 1000, // 1秒
|
RECONNECT_DELAY: 1000, // 1秒
|
||||||
HEARTBEAT_INTERVAL: 30000 // 30秒
|
HEARTBEAT_INTERVAL: 30000 // 30秒
|
||||||
|
|||||||
@ -8,6 +8,9 @@ import websocketService from './websocketService'
|
|||||||
// 日志记录系统
|
// 日志记录系统
|
||||||
class ApiLogger {
|
class ApiLogger {
|
||||||
static log(method, url, requestData, response, duration) {
|
static log(method, url, requestData, response, duration) {
|
||||||
|
// 获取当前用户信息
|
||||||
|
const currentUser = this.getCurrentUser()
|
||||||
|
|
||||||
const logEntry = {
|
const logEntry = {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
method: method.toUpperCase(),
|
method: method.toUpperCase(),
|
||||||
@ -16,13 +19,25 @@ class ApiLogger {
|
|||||||
status: response.ok ? 'success' : 'error',
|
status: response.ok ? 'success' : 'error',
|
||||||
statusCode: response.status,
|
statusCode: response.status,
|
||||||
duration: `${duration}ms`,
|
duration: `${duration}ms`,
|
||||||
responseSize: response.headers?.get('content-length') || 'unknown'
|
responseSize: response.headers?.get('content-length') || 'unknown',
|
||||||
|
user: currentUser?.username || 'anonymous' // 添加用户信息
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为将来扩展预留:可以发送到日志服务
|
// 为将来扩展预留:可以发送到日志服务
|
||||||
this.sendToLogService(logEntry)
|
this.sendToLogService(logEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取当前登录用户信息
|
||||||
|
static getCurrentUser() {
|
||||||
|
try {
|
||||||
|
const userKey = 'user' // 与auth.js中的tokenConfig.userKey保持一致
|
||||||
|
const savedUser = localStorage.getItem(userKey)
|
||||||
|
return savedUser ? JSON.parse(savedUser) : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static sendToLogService() {
|
static sendToLogService() {
|
||||||
// 后处理钩子:将来可扩展为发送到远程日志服务
|
// 后处理钩子:将来可扩展为发送到远程日志服务
|
||||||
// 当前只保留接口,不实现具体逻辑
|
// 当前只保留接口,不实现具体逻辑
|
||||||
|
|||||||
@ -35,8 +35,23 @@ class WebSocketService {
|
|||||||
|
|
||||||
// 获取WebSocket连接URL
|
// 获取WebSocket连接URL
|
||||||
getConnectionUrl() {
|
getConnectionUrl() {
|
||||||
const { BASE_URL, ENDPOINT, CLIENT_ID, USER_ID } = this.config
|
const { BASE_URL, ENDPOINT, CLIENT_ID } = this.config
|
||||||
return `${BASE_URL}${ENDPOINT}?client_id=${CLIENT_ID}&user_id=${USER_ID}`
|
// 动态获取当前用户信息
|
||||||
|
const currentUser = this.getCurrentUser()
|
||||||
|
const userId = currentUser?.username || 'anonymous'
|
||||||
|
return `${BASE_URL}${ENDPOINT}?client_id=${CLIENT_ID}&user_id=${userId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前登录用户信息
|
||||||
|
getCurrentUser() {
|
||||||
|
try {
|
||||||
|
// 从localStorage读取用户信息
|
||||||
|
const userKey = 'user' // 与auth.js中的tokenConfig.userKey保持一致
|
||||||
|
const savedUser = localStorage.getItem(userKey)
|
||||||
|
return savedUser ? JSON.parse(savedUser) : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接WebSocket
|
// 连接WebSocket
|
||||||
@ -241,6 +256,10 @@ class WebSocketService {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
const currentUser = this.getCurrentUser()
|
||||||
|
const username = currentUser?.username || 'anonymous'
|
||||||
|
|
||||||
const logData = {
|
const logData = {
|
||||||
type: 'log_operation',
|
type: 'log_operation',
|
||||||
operation: operation.substring(0, 100), // 限制长度
|
operation: operation.substring(0, 100), // 限制长度
|
||||||
@ -249,7 +268,9 @@ class WebSocketService {
|
|||||||
target_object: options.target_object || '',
|
target_object: options.target_object || '',
|
||||||
status: options.status || 'success',
|
status: options.status || 'success',
|
||||||
duration: options.duration || 0,
|
duration: options.duration || 0,
|
||||||
operation_category: options.operation_category || '软件控制'
|
operation_category: options.operation_category || '软件控制',
|
||||||
|
user_name: username, // 添加用户名信息
|
||||||
|
user_id: username // 添加用户ID信息
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.send(logData)
|
return this.send(logData)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user