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:
sladro 2025-09-19 09:50:22 +08:00
parent f6f4744e0b
commit 23d94cf0ab
5 changed files with 123 additions and 10 deletions

View File

@ -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: {

View File

@ -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 {

View File

@ -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秒

View File

@ -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() {
// 后处理钩子:将来可扩展为发送到远程日志服务 // 后处理钩子:将来可扩展为发送到远程日志服务
// 当前只保留接口,不实现具体逻辑 // 当前只保留接口,不实现具体逻辑

View File

@ -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)