Browse Source

优化双滚动条之前

master
hejl 2 weeks ago
parent
commit
63134311b3
  1. 93
      gofaster/app/dist/renderer/js/index.js
  2. 69
      gofaster/app/src/renderer/components/MainLayout.vue
  3. 599
      gofaster/app/src/renderer/views/UserProfile.vue
  4. 1
      gofaster/backend/internal/auth/model/auth.go
  5. 1
      gofaster/backend/internal/auth/service/auth_service.go
  6. BIN
      gofaster/backend/tmp/main.exe

93
gofaster/app/dist/renderer/js/index.js vendored

@ -2732,6 +2732,8 @@ __webpack_require__.r(__webpack_exports__); @@ -2732,6 +2732,8 @@ __webpack_require__.r(__webpack_exports__);
// 计算属性
const unreadCount = (0,vue__WEBPACK_IMPORTED_MODULE_0__.computed)(() => messages.value.filter(m => !m.read).length)
const currentRoute = (0,vue__WEBPACK_IMPORTED_MODULE_0__.computed)(() => route.path)
const breadcrumbs = (0,vue__WEBPACK_IMPORTED_MODULE_0__.computed)(() => {
const path = route.path
if (path === '/') return ['欢迎']
@ -2754,6 +2756,43 @@ __webpack_require__.r(__webpack_exports__); @@ -2754,6 +2756,43 @@ __webpack_require__.r(__webpack_exports__);
showMessagePanel.value = false
}
// 关闭所有悬浮菜单
const closeAllMenus = () => {
showMessagePanel.value = false
showUserMenu.value = false
}
// 处理全局点击事件
const handleGlobalClick = (event) => {
// 检查点击是否在消息面板内
const messagePanel = document.querySelector('.message-panel')
const messageBtn = document.querySelector('.message-btn')
// 检查点击是否在用户菜单内
const userMenu = document.querySelector('.user-menu')
const userAvatar = document.querySelector('.user-avatar')
// 如果点击在消息面板或消息按钮外,且消息面板是打开的,则关闭
if (showMessagePanel.value &&
messagePanel &&
!messagePanel.contains(event.target) &&
messageBtn &&
!messageBtn.contains(event.target)) {
showMessagePanel.value = false
}
// 如果点击在用户菜单或用户头像外,且用户菜单是打开的,则关闭
if (showUserMenu.value &&
userMenu &&
!userMenu.contains(event.target) &&
userAvatar &&
!userAvatar.contains(event.target)) {
showUserMenu.value = false
}
}
const handleMenuClick = (item) => {
// 添加标签页
addTabIfNotExists({
@ -3184,7 +3223,17 @@ __webpack_require__.r(__webpack_exports__); @@ -3184,7 +3223,17 @@ __webpack_require__.r(__webpack_exports__);
window.addEventListener('show-login-modal', () => {
showLoginModalFlag.value = true
})
})
// 添加全局点击事件监听器
document.addEventListener('click', handleGlobalClick)
})
// 组件卸载时清理事件监听器
;(0,vue__WEBPACK_IMPORTED_MODULE_0__.onUnmounted)(() => {
document.removeEventListener('click', handleGlobalClick)
})
// 提供响应式数据给子组件
;(0,vue__WEBPACK_IMPORTED_MODULE_0__.provide)('isLoggedIn', isLoggedIn)
@ -3211,11 +3260,13 @@ __webpack_require__.r(__webpack_exports__); @@ -3211,11 +3260,13 @@ __webpack_require__.r(__webpack_exports__);
messages,
mainMenuItems,
favoriteMenuItems,
unreadCount,
currentRoute,
breadcrumbs,
unreadCount,
currentRoute,
breadcrumbs,
toggleMessagePanel,
toggleUserMenu,
closeAllMenus,
handleGlobalClick,
showLoginModal,
handleMenuClick,
switchTab,
@ -6318,7 +6369,7 @@ const routes = [ @@ -6318,7 +6369,7 @@ const routes = [
{
path: '/user-profile',
name: 'UserProfile',
component: () => __webpack_require__.e(/*! import() */ "src_renderer_views_UserProfile_vue").then(__webpack_require__.t.bind(__webpack_require__, /*! @/views/UserProfile.vue */ "./src/renderer/views/UserProfile.vue", 23))
component: () => __webpack_require__.e(/*! import() */ "src_renderer_views_UserProfile_vue").then(__webpack_require__.bind(__webpack_require__, /*! @/views/UserProfile.vue */ "./src/renderer/views/UserProfile.vue"))
}
]
}
@ -7432,36 +7483,6 @@ __webpack_require__.r(__webpack_exports__); @@ -7432,36 +7483,6 @@ __webpack_require__.r(__webpack_exports__);
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/create fake namespace object */
/******/ (() => {
/******/ var getProto = Object.getPrototypeOf ? (obj) => (Object.getPrototypeOf(obj)) : (obj) => (obj.__proto__);
/******/ var leafPrototypes;
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 16: return value when it's Promise-like
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = this(value);
/******/ if(mode & 8) return value;
/******/ if(typeof value === 'object' && value) {
/******/ if((mode & 4) && value.__esModule) return value;
/******/ if((mode & 16) && typeof value.then === 'function') return value;
/******/ }
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ var def = {};
/******/ leafPrototypes = leafPrototypes || [null, getProto({}), getProto([]), getProto(getProto)];
/******/ for(var current = mode & 2 && value; (typeof current == 'object' || typeof current == 'function') && !~leafPrototypes.indexOf(current); current = getProto(current)) {
/******/ Object.getOwnPropertyNames(current).forEach((key) => (def[key] = () => (value[key])));
/******/ }
/******/ def['default'] = () => (value);
/******/ __webpack_require__.d(ns, def);
/******/ return ns;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
@ -7514,7 +7535,7 @@ __webpack_require__.r(__webpack_exports__); @@ -7514,7 +7535,7 @@ __webpack_require__.r(__webpack_exports__);
/******/
/******/ /* webpack/runtime/getFullHash */
/******/ (() => {
/******/ __webpack_require__.h = () => ("5ae52292b7fe9b64")
/******/ __webpack_require__.h = () => ("00017bfd8eb65ef5")
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */

69
gofaster/app/src/renderer/components/MainLayout.vue

@ -168,10 +168,10 @@ @@ -168,10 +168,10 @@
</div>
</div>
<!-- 功能内容区 -->
<div class="content-body">
<router-view @add-tab="handleAddTab" />
</div>
<!-- 功能内容区 -->
<div class="content-body">
<router-view @add-tab="handleAddTab" />
</div>
</div>
</div>
@ -194,7 +194,7 @@ @@ -194,7 +194,7 @@
</template>
<script>
import { ref, reactive, computed, onMounted, watch, nextTick, provide } from 'vue'
import { ref, reactive, computed, onMounted, onUnmounted, watch, nextTick, provide } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { userService } from '@/services/userService'
import themeManager from '../utils/themeManager.js'
@ -304,6 +304,8 @@ export default { @@ -304,6 +304,8 @@ export default {
//
const unreadCount = computed(() => messages.value.filter(m => !m.read).length)
const currentRoute = computed(() => route.path)
const breadcrumbs = computed(() => {
const path = route.path
if (path === '/') return ['欢迎']
@ -326,6 +328,43 @@ export default { @@ -326,6 +328,43 @@ export default {
showMessagePanel.value = false
}
//
const closeAllMenus = () => {
showMessagePanel.value = false
showUserMenu.value = false
}
//
const handleGlobalClick = (event) => {
//
const messagePanel = document.querySelector('.message-panel')
const messageBtn = document.querySelector('.message-btn')
//
const userMenu = document.querySelector('.user-menu')
const userAvatar = document.querySelector('.user-avatar')
//
if (showMessagePanel.value &&
messagePanel &&
!messagePanel.contains(event.target) &&
messageBtn &&
!messageBtn.contains(event.target)) {
showMessagePanel.value = false
}
//
if (showUserMenu.value &&
userMenu &&
!userMenu.contains(event.target) &&
userAvatar &&
!userAvatar.contains(event.target)) {
showUserMenu.value = false
}
}
const handleMenuClick = (item) => {
//
addTabIfNotExists({
@ -756,7 +795,17 @@ export default { @@ -756,7 +795,17 @@ export default {
window.addEventListener('show-login-modal', () => {
showLoginModalFlag.value = true
})
})
//
document.addEventListener('click', handleGlobalClick)
})
//
onUnmounted(() => {
document.removeEventListener('click', handleGlobalClick)
})
//
provide('isLoggedIn', isLoggedIn)
@ -783,11 +832,13 @@ export default { @@ -783,11 +832,13 @@ export default {
messages,
mainMenuItems,
favoriteMenuItems,
unreadCount,
currentRoute,
breadcrumbs,
unreadCount,
currentRoute,
breadcrumbs,
toggleMessagePanel,
toggleUserMenu,
closeAllMenus,
handleGlobalClick,
showLoginModal,
handleMenuClick,
switchTab,

599
gofaster/app/src/renderer/views/UserProfile.vue

@ -0,0 +1,599 @@ @@ -0,0 +1,599 @@
<template>
<div class="user-profile">
<div class="profile-header">
<h1>👤 个人资料</h1>
<p class="profile-subtitle">查看和管理您的账户信息</p>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<p>正在加载用户信息...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<div class="error-icon"></div>
<h3>加载失败</h3>
<p>{{ error }}</p>
<button @click="loadUserProfile" class="retry-btn">重试</button>
</div>
<!-- 用户信息内容 -->
<div v-else-if="userInfo" class="profile-content">
<!-- 基本信息卡片 -->
<div class="profile-card">
<div class="card-header">
<h2>📋 基本信息</h2>
</div>
<div class="card-content">
<div class="info-grid">
<div class="info-item">
<label>用户ID</label>
<span>{{ userInfo.id || 'N/A' }}</span>
</div>
<div class="info-item">
<label>用户名</label>
<span>{{ userInfo.username || userInfo.name || 'N/A' }}</span>
</div>
<div class="info-item">
<label>邮箱</label>
<span>{{ userInfo.email || 'N/A' }}</span>
</div>
<div class="info-item">
<label>手机号</label>
<span>{{ userInfo.phone || 'N/A' }}</span>
</div>
<div class="info-item">
<label>状态</label>
<span :class="['status-badge', getStatusClass(userInfo.status)]">
{{ getStatusText(userInfo.status) }}
</span>
</div>
<div class="info-item">
<label>注册时间</label>
<span>{{ formatDate(userInfo.created_at) || formatDate(userInfo.createdAt) || 'N/A' }}</span>
</div>
</div>
</div>
</div>
<!-- 登录信息卡片 -->
<div class="profile-card">
<div class="card-header">
<h2>🔐 登录信息</h2>
</div>
<div class="card-content">
<div class="info-grid">
<div class="info-item">
<label>上次登录时间</label>
<span>{{ formatDate(userInfo.last_login_at) || formatDate(userInfo.lastLogin) || 'N/A' }}</span>
</div>
<div class="info-item">
<label>上次登录IP</label>
<span>{{ userInfo.last_login_ip || userInfo.lastLoginIP || 'N/A' }}</span>
</div>
</div>
</div>
</div>
<!-- 角色信息卡片 -->
<div class="profile-card">
<div class="card-header">
<h2>👑 角色信息</h2>
</div>
<div class="card-content">
<div v-if="userInfo.roles && userInfo.roles.length > 0" class="roles-list">
<div v-for="role in userInfo.roles" :key="role.id" class="role-item">
<div class="role-header">
<span class="role-name">{{ role.name }}</span>
<span class="role-description">{{ role.description || '无描述' }}</span>
</div>
<div class="role-permissions">
<h4>权限列表</h4>
<div class="permissions-grid">
<span
v-for="permission in role.permissions"
:key="permission.id"
class="permission-tag"
>
{{ permission.name }}
</span>
</div>
</div>
</div>
</div>
<div v-else class="no-roles">
<p>暂无角色信息</p>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="profile-actions">
<button @click="refreshProfile" class="action-btn refresh-btn">
🔄 刷新信息
</button>
<button @click="changePassword" class="action-btn password-btn">
🔒 修改密码
</button>
</div>
</div>
<!-- 空状态 -->
<div v-else class="empty-state">
<div class="empty-icon">👤</div>
<h3>暂无用户信息</h3>
<p>请先登录以查看您的个人资料</p>
<button @click="goToLogin" class="login-btn">去登录</button>
</div>
</div>
</template>
<script>
import { ref, onMounted, inject } from 'vue'
import { useRouter } from 'vue-router'
import { userService } from '@/services/userService'
export default {
name: 'UserProfile',
setup() {
const router = useRouter()
//
const currentUser = inject('currentUser', null)
const isLoggedIn = inject('isLoggedIn', false)
const showLoginModal = inject('showLoginModal', null)
//
const loading = ref(false)
const error = ref(null)
const userInfo = ref(null)
//
const loadUserProfile = async () => {
if (!isLoggedIn.value) {
error.value = '请先登录'
return
}
loading.value = true
error.value = null
try {
const token = localStorage.getItem('token')
if (!token) {
throw new Error('未找到认证token')
}
const response = await userService.getCurrentUser(token)
userInfo.value = response.data || response
console.log('加载的用户信息:', userInfo.value)
} catch (err) {
console.error('加载用户资料失败:', err)
error.value = err.response?.data?.message || err.message || '加载用户资料失败'
} finally {
loading.value = false
}
}
//
const refreshProfile = () => {
loadUserProfile()
}
//
const changePassword = () => {
// TODO:
console.log('修改密码功能待实现')
}
//
const goToLogin = () => {
if (showLoginModal) {
showLoginModal()
} else {
router.push('/')
}
}
//
const formatDate = (dateString) => {
if (!dateString) return null
try {
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
} catch (err) {
return dateString
}
}
//
const getStatusText = (status) => {
const statusMap = {
1: '正常',
0: '禁用',
2: '待验证'
}
return statusMap[status] || '未知'
}
//
const getStatusClass = (status) => {
const classMap = {
1: 'status-active',
0: 'status-inactive',
2: 'status-pending'
}
return classMap[status] || 'status-unknown'
}
//
onMounted(() => {
if (isLoggedIn.value) {
loadUserProfile()
}
})
return {
loading,
error,
userInfo,
currentUser,
isLoggedIn,
loadUserProfile,
refreshProfile,
changePassword,
goToLogin,
formatDate,
getStatusText,
getStatusClass
}
}
}
</script>
<style scoped>
.user-profile {
padding: 20px;
height: 100%;
overflow-y: auto;
}
.profile-header {
text-align: center;
margin-bottom: 30px;
}
.profile-header h1 {
font-size: 2.5rem;
color: var(--text-primary);
margin-bottom: 10px;
}
.profile-subtitle {
font-size: 1.1rem;
color: var(--text-secondary);
margin: 0;
}
.loading-container {
text-align: center;
padding: 60px 20px;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-container {
text-align: center;
padding: 60px 20px;
background: #fff5f5;
border-radius: 12px;
border: 1px solid #fed7d7;
}
.error-icon {
font-size: 3rem;
margin-bottom: 20px;
}
.retry-btn {
background: #e53e3e;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
margin-top: 20px;
}
.retry-btn:hover {
background: #c53030;
}
.profile-content {
max-width: 800px;
margin: 0 auto;
}
.profile-card {
background: var(--card-bg);
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 8px var(--shadow-color);
}
.card-header {
margin: 0 0 20px 0;
color: var(--text-primary);
font-size: 18px;
border-bottom: 2px solid var(--border-color);
padding-bottom: 8px;
}
.card-header h2 {
margin: 0;
font-size: 18px;
color: var(--text-primary);
}
.info-grid {
display: flex;
flex-direction: column;
gap: 0;
}
.info-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding: 16px 0;
border-bottom: 1px solid #f5f5f5;
}
.info-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.info-item label {
font-weight: 500;
color: var(--text-primary);
min-width: 200px;
}
.info-item span {
color: var(--text-primary);
font-size: 14px;
text-align: right;
flex: 1;
}
.status-badge {
display: inline-block;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
text-align: center;
min-width: 80px;
}
.status-active {
background: #d4edda;
color: #155724;
}
.status-inactive {
background: #f8d7da;
color: #721c24;
}
.status-pending {
background: #fff3cd;
color: #856404;
}
.status-unknown {
background: #e2e3e5;
color: #383d41;
}
.roles-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.role-item {
background: var(--card-bg);
border-radius: 8px;
padding: 20px;
border: 1px solid var(--border-color);
margin-bottom: 16px;
}
.role-header {
margin-bottom: 16px;
}
.role-name {
display: block;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
}
.role-description {
color: var(--text-secondary);
font-size: 0.9rem;
}
.role-permissions h4 {
margin: 0 0 12px 0;
color: var(--text-primary);
font-size: 1rem;
}
.permissions-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.permission-tag {
background: var(--accent-color);
color: white;
padding: 4px 12px;
border-radius: 16px;
font-size: 0.85rem;
border: none;
}
.no-roles {
text-align: center;
color: var(--text-secondary);
padding: 40px 20px;
}
.profile-actions {
display: flex;
gap: 16px;
justify-content: flex-end;
margin-top: 30px;
}
.action-btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
}
.refresh-btn {
background: #1976d2;
color: white;
}
.refresh-btn:hover {
background: #1565c0;
}
.edit-btn {
background: #f39c12;
color: white;
}
.edit-btn:hover {
background: #e67e22;
}
.password-btn {
background: #e74c3c;
color: white;
}
.password-btn:hover {
background: #c0392b;
}
.empty-state {
text-align: center;
padding: 80px 20px;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 20px;
}
.empty-state h3 {
color: var(--text-primary);
margin-bottom: 10px;
}
.empty-state p {
color: var(--text-secondary);
margin-bottom: 30px;
}
.login-btn {
background: #1976d2;
color: white;
border: none;
padding: 12px 32px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.login-btn:hover {
background: #1565c0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.user-profile {
padding: 15px;
}
.profile-header h1 {
font-size: 2rem;
}
.info-item {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.info-item label {
min-width: auto;
}
.info-item span {
text-align: left;
}
.profile-actions {
flex-direction: column;
align-items: center;
}
.action-btn {
width: 100%;
max-width: 300px;
}
}
</style>

1
gofaster/backend/internal/auth/model/auth.go

@ -27,6 +27,7 @@ type UserInfo struct { @@ -27,6 +27,7 @@ type UserInfo struct {
Email string `json:"email"`
Phone string `json:"phone"`
Status int `json:"status"`
CreatedAt time.Time `json:"created_at"`
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
LastLoginIP string `json:"last_login_ip,omitempty"`
Roles []RoleInfo `json:"roles"`

1
gofaster/backend/internal/auth/service/auth_service.go

@ -260,6 +260,7 @@ func (s *authService) buildUserInfo(user *model.User) *model.UserInfo { @@ -260,6 +260,7 @@ func (s *authService) buildUserInfo(user *model.User) *model.UserInfo {
Email: user.Email,
Phone: user.Phone,
Status: user.Status,
CreatedAt: user.CreatedAt,
LastLoginAt: user.LastLoginAt,
LastLoginIP: user.LastLoginIP,
Roles: roles,

BIN
gofaster/backend/tmp/main.exe

Binary file not shown.
Loading…
Cancel
Save