This commit is contained in:
张成
2025-12-22 16:26:59 +08:00
parent aa2d03ee30
commit e17d5610f5
54 changed files with 11735 additions and 3 deletions

270
app/components/UserMenu.vue Normal file
View File

@@ -0,0 +1,270 @@
<template>
<div class="user-menu">
<div class="user-info" @click="toggleMenu">
<span class="user-name">{{ userName || '未登录' }}</span>
<span class="dropdown-icon"></span>
</div>
<div v-if="menuVisible" class="user-menu-dropdown">
<div class="menu-item" @click="showUserInfo">
<span class="menu-icon">👤</span>
<span class="menu-text">个人信息</span>
</div>
<div class="menu-item" @click="showSettings">
<span class="menu-icon"></span>
<span class="menu-text">设置</span>
</div>
<div class="menu-divider"></div>
<div class="menu-item" @click="handleLogout">
<span class="menu-icon">🚪</span>
<span class="menu-text">退出登录</span>
</div>
</div>
<!-- 个人信息对话框 -->
<UserInfoDialog
:visible="showUserInfoDialog"
:user-info="userInfo"
@close="showUserInfoDialog = false"
/>
<!-- 设置对话框 -->
<SettingsDialog
:visible="showSettingsDialog"
@close="showSettingsDialog = false"
@save="handleSettingsSave"
/>
</div>
</template>
<script>
import { mapState } from 'vuex';
import UserInfoDialog from './UserInfoDialog.vue';
import SettingsDialog from './SettingsDialog.vue';
import logMixin from '../mixins/logMixin.js';
export default {
name: 'UserMenu',
mixins: [logMixin],
components: {
UserInfoDialog,
SettingsDialog
},
data() {
return {
menuVisible: false,
showUserInfoDialog: false,
showSettingsDialog: false
};
},
computed: {
...mapState('auth', ['isLoggedIn', 'userName', 'snCode', 'deviceId', 'remainingDays', 'phone']),
...mapState('mqtt', ['isConnected']),
userInfo() {
return {
userName: this.userName || '',
phone: this.phone || '',
snCode: this.snCode || '',
deviceId: this.deviceId || '',
remainingDays: this.remainingDays
};
}
},
methods: {
toggleMenu() {
this.menuVisible = !this.menuVisible;
},
showUserInfo() {
this.menuVisible = false;
this.showUserInfoDialog = true;
},
showSettings() {
this.menuVisible = false;
this.showSettingsDialog = true;
},
async handleLogout() {
this.menuVisible = false;
if (!this.isLoggedIn) {
return;
}
try {
this.addLog('info', '正在注销登录...');
// 断开MQTT连接
if (this.isConnected && window.electronAPI && window.electronAPI.mqtt) {
try {
await window.electronAPI.invoke('mqtt:disconnect');
} catch (error) {
console.error('断开MQTT连接失败:', error);
}
}
// 调用后端退出接口(如果可用)
if (window.electronAPI && window.electronAPI.invoke) {
try {
await window.electronAPI.invoke('auth:logout');
} catch (error) {
console.error('调用退出接口失败:', error);
}
}
// 使用 store 清理认证状态(无论接口是否成功都要执行)
if (this.$store) {
this.$store.dispatch('auth/logout');
// 清空平台状态
this.$store.commit('platform/SET_PLATFORM_LOGIN_STATUS', {
status: '-',
color: '#FF9800',
isLoggedIn: false
});
// 停止任务更新定时器
const taskTimer = this.$store.state.task.taskUpdateTimer;
if (taskTimer) {
clearInterval(taskTimer);
this.$store.dispatch('task/setTaskUpdateTimer', null);
}
}
// 跳转到登录页面
if (this.$router) {
this.$router.push('/login').catch(err => {
// 忽略重复导航错误
if (err.name !== 'NavigationDuplicated') {
console.error('跳转登录页失败:', err);
}
});
}
this.addLog('success', '注销登录成功');
} catch (error) {
console.error('退出登录异常:', error);
// 即使出错,也强制清空状态并跳转
if (this.$store) {
this.$store.dispatch('auth/logout');
}
if (this.$router) {
this.$router.push('/login').catch(() => {});
}
this.addLog('error', `注销登录异常: ${error.message}`);
}
},
handleSettingsSave(settings) {
// 设置已保存,可以在这里处理额外的逻辑
console.log('设置已保存:', settings);
}
},
mounted() {
// 点击外部关闭菜单
this.handleClickOutside = (e) => {
if (this.$el && !this.$el.contains(e.target)) {
this.menuVisible = false;
}
};
document.addEventListener('click', this.handleClickOutside);
},
beforeDestroy() {
// 清理事件监听器
if (this.handleClickOutside) {
document.removeEventListener('click', this.handleClickOutside);
}
}
};
</script>
<style lang="less" scoped>
.user-menu {
position: relative;
z-index: 1000;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 15px;
background: rgba(255, 255, 255, 0.2);
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
color: white;
font-size: 14px;
&:hover {
background: rgba(255, 255, 255, 0.3);
}
}
.user-name {
font-weight: 500;
}
.dropdown-icon {
font-size: 10px;
transition: transform 0.3s ease;
.user-info:hover & {
transform: rotate(180deg);
}
}
.user-menu-dropdown {
position: absolute;
top: calc(100% + 8px);
right: 0;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 160px;
overflow: hidden;
animation: slideDown 0.2s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.menu-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
cursor: pointer;
transition: background 0.2s ease;
color: #333;
font-size: 14px;
&:hover {
background: #f5f5f5;
}
}
.menu-icon {
font-size: 16px;
}
.menu-text {
flex: 1;
}
.menu-divider {
height: 1px;
background: #e0e0e0;
margin: 4px 0;
}
</style>