603 lines
15 KiB
Vue
603 lines
15 KiB
Vue
<template>
|
||
<div class="header-container">
|
||
<!-- 顶部导航栏 -->
|
||
<nav class="navbar">
|
||
<!-- 左侧:导航按钮 -->
|
||
<div class="nav-left">
|
||
<router-link
|
||
v-for="nav in navigation"
|
||
:key="nav.path"
|
||
:to="nav.path"
|
||
class="nav-btn"
|
||
:class="{ active: isNavActive(nav.path) }"
|
||
:title="nav.description"
|
||
>
|
||
<span class="nav-icon">{{ nav.icon }}</span>
|
||
<span class="nav-text">{{ nav.name }}</span>
|
||
<span v-if="nav.path === '/cart'" class="cart-count">({{ cartItemCount }})</span>
|
||
</router-link>
|
||
</div>
|
||
|
||
<!-- 右侧:用户登录状态 -->
|
||
<div class="nav-right">
|
||
<!-- 加载中:显示占位符(防止闪烁) -->
|
||
<div v-if="isLoginStatusLoading" class="auth-loading">
|
||
<span class="loading-placeholder"></span>
|
||
</div>
|
||
|
||
<!-- 未登录:显示注册/登录按钮 -->
|
||
<div v-else-if="!isLoggedIn" class="auth-buttons">
|
||
<button class="auth-btn register-btn" @click="goToRegister">
|
||
注册
|
||
</button>
|
||
<button class="auth-btn login-btn" @click="goToLogin">
|
||
登录
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 已登录:显示用户邮箱、安全设置和退出按钮 -->
|
||
<div v-else class="user-info">
|
||
<span class="user-email">{{ userEmail }}</span>
|
||
<router-link
|
||
to="/account/security-settings"
|
||
class="security-link"
|
||
active-class="active"
|
||
>
|
||
安全设置
|
||
</router-link>
|
||
<el-button
|
||
type="text"
|
||
size="small"
|
||
class="logout-btn"
|
||
@click="handleLogout"
|
||
>
|
||
退出
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- 面包屑导航 -->
|
||
<!-- <div class="breadcrumb">
|
||
<router-link
|
||
v-for="(crumb, index) in breadcrumbs"
|
||
:key="index"
|
||
:to="getBreadcrumbPath(index)"
|
||
class="breadcrumb-item"
|
||
:class="{ active: index === breadcrumbs.length - 1 }"
|
||
>
|
||
{{ crumb }}
|
||
</router-link>
|
||
</div> -->
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { readCart } from '../utils/cartManager'
|
||
import { mainNavigation, getBreadcrumb } from '../utils/navigation'
|
||
import { getGoodsListV2 } from '../api/shoppingCart'
|
||
import { getToken } from '../utils/request'
|
||
|
||
export default {
|
||
name: 'Header',
|
||
data() {
|
||
return {
|
||
user: null,
|
||
cart: [],
|
||
// 服务端购物车数量(统计 productMachineDtoList 的总条数)
|
||
cartServerCount: 0,
|
||
navigation: mainNavigation,
|
||
// 用户邮箱
|
||
userEmail: '',
|
||
// 登录状态(改为 data 属性,支持响应式更新)
|
||
isLoggedIn: false,
|
||
// 登录状态初始化中(防止闪烁)
|
||
isLoginStatusLoading: true
|
||
}
|
||
},
|
||
computed: {
|
||
// 计算购物车数量
|
||
cartItemCount() {
|
||
// 只使用服务端数量,避免与本地缓存数量不一致
|
||
return Number.isFinite(this.cartServerCount) ? this.cartServerCount : 0
|
||
},
|
||
// 计算面包屑导航
|
||
breadcrumbs() {
|
||
return getBreadcrumb(this.$route.path)
|
||
}
|
||
},
|
||
watch: {},
|
||
async mounted() {
|
||
// 初始化登录状态(异步等待 token 初始化完成)
|
||
await this.updateLoginStatus()
|
||
this.loadCart()
|
||
// 监听购物车变化
|
||
window.addEventListener('storage', this.handleStorageChange)
|
||
// 首次加载服务端购物车数量
|
||
this.loadServerCartCount()
|
||
// 监听应用内购物车更新事件
|
||
window.addEventListener('cart-updated', this.handleCartUpdated)
|
||
// 监听登录状态变化事件(当 token 被清除时触发)
|
||
window.addEventListener('login-status-changed', this.handleLoginStatusChanged)
|
||
// 加载用户信息(邮箱)
|
||
this.loadUserEmail()
|
||
},
|
||
beforeDestroy() {
|
||
window.removeEventListener('storage', this.handleStorageChange)
|
||
window.removeEventListener('cart-updated', this.handleCartUpdated)
|
||
window.removeEventListener('login-status-changed', this.handleLoginStatusChanged)
|
||
},
|
||
methods: {
|
||
/**
|
||
* 判断导航按钮是否应该高亮
|
||
* 在安全设置页面时,个人中心不应该高亮
|
||
*/
|
||
isNavActive(path) {
|
||
const currentPath = (this.$route && this.$route.path) || ''
|
||
// 如果是安全设置页面,个人中心不高亮
|
||
if (currentPath === '/account/security-settings' && path === '/account') {
|
||
return false
|
||
}
|
||
// 其他情况使用 Vue Router 的默认匹配逻辑(包含匹配)
|
||
return currentPath === path || (path !== '/' && currentPath.startsWith(path + '/'))
|
||
},
|
||
loadCart() {
|
||
this.cart = readCart()
|
||
},
|
||
/**
|
||
* 加载服务器购物车数量
|
||
* 根据新接口结构:res.rows[].cartMachineInfoDtoList.length 累加
|
||
*/
|
||
async loadServerCartCount() {
|
||
try {
|
||
const res = await getGoodsListV2()
|
||
let total = 0
|
||
|
||
// 新接口结构:res.rows 是店铺数组,每个店铺有 cartMachineInfoDtoList
|
||
if (Array.isArray(res && res.rows)) {
|
||
total = res.rows.reduce((sum, shop) => {
|
||
const machineList = Array.isArray(shop && shop.cartMachineInfoDtoList)
|
||
? shop.cartMachineInfoDtoList
|
||
: []
|
||
return sum + machineList.length
|
||
}, 0)
|
||
} else if (Array.isArray(res && res.data && res.data.rows)) {
|
||
// 兼容:如果数据在 res.data.rows 中
|
||
total = res.data.rows.reduce((sum, shop) => {
|
||
const machineList = Array.isArray(shop && shop.cartMachineInfoDtoList)
|
||
? shop.cartMachineInfoDtoList
|
||
: []
|
||
return sum + machineList.length
|
||
}, 0)
|
||
}
|
||
|
||
this.cartServerCount = Number.isFinite(total) ? total : 0
|
||
} catch (e) {
|
||
// 忽略错误,保持当前显示
|
||
console.error('加载购物车数量失败:', e)
|
||
}
|
||
},
|
||
async handleStorageChange(event) {
|
||
if (event.key === 'power_leasing_cart_v1') {
|
||
this.loadCart()
|
||
this.loadServerCartCount()
|
||
} else if (event.key === 'leasToken') {
|
||
// 当 token 变化时,更新登录状态
|
||
await this.updateLoginStatus()
|
||
// 如果 token 被清除,同时清除用户信息
|
||
if (!event.newValue) {
|
||
this.userEmail = ''
|
||
} else {
|
||
this.loadUserEmail()
|
||
}
|
||
}
|
||
},
|
||
/**
|
||
* 处理登录状态变化事件
|
||
*/
|
||
async handleLoginStatusChanged() {
|
||
// 登录状态变化时不需要显示加载状态(已经有数据了)
|
||
this.isLoginStatusLoading = false
|
||
await this.updateLoginStatus()
|
||
// 如果未登录,清除用户信息
|
||
if (!this.isLoggedIn) {
|
||
this.userEmail = ''
|
||
} else {
|
||
this.loadUserEmail()
|
||
}
|
||
},
|
||
/**
|
||
* 更新登录状态
|
||
* 使用 getToken() 从内存缓存或加密存储中读取 token
|
||
* 支持等待异步初始化完成
|
||
*/
|
||
async updateLoginStatus() {
|
||
try {
|
||
// 使用 getToken(true) 等待 token 初始化完成(如果还在初始化中)
|
||
const token = await getToken(true)
|
||
this.isLoggedIn = !!token && token !== 'null' && token !== 'undefined'
|
||
|
||
if (process.env.NODE_ENV === 'development') {
|
||
console.log('[Header] 登录状态更新:', this.isLoggedIn, token ? '有 token' : '无 token')
|
||
}
|
||
} catch (e) {
|
||
console.error('更新登录状态失败:', e)
|
||
this.isLoggedIn = false
|
||
} finally {
|
||
// 无论成功失败,都结束加载状态
|
||
this.isLoginStatusLoading = false
|
||
}
|
||
},
|
||
handleCartUpdated(event) {
|
||
// 支持事件携带数量 { detail: { count } }
|
||
try {
|
||
const next = event && event.detail && Number(event.detail.count)
|
||
if (Number.isFinite(next)) {
|
||
this.cartServerCount = next
|
||
return
|
||
}
|
||
} catch (e) { /* ignore malformed event detail */ }
|
||
// 无显式数量则主动刷新
|
||
this.loadServerCartCount()
|
||
},
|
||
/**
|
||
* 跳转到登录页
|
||
*/
|
||
goToLogin() {
|
||
this.$router.push('/login')
|
||
},
|
||
/**
|
||
* 跳转到注册页
|
||
*/
|
||
goToRegister() {
|
||
this.$router.push('/register')
|
||
},
|
||
/**
|
||
* 加载用户邮箱
|
||
* 从localStorage读取用户信息,获取用户名
|
||
*/
|
||
loadUserEmail() {
|
||
try {
|
||
// 从localStorage读取用户信息
|
||
const userInfoStr = localStorage.getItem('userInfo')
|
||
if (userInfoStr) {
|
||
const userInfo = JSON.parse(userInfoStr)
|
||
// 获取用户名(userName字段),如果没有就显示默认值
|
||
this.userEmail = userInfo.userName || userInfo.email || userInfo.username || '用户'
|
||
}
|
||
} catch (e) {
|
||
console.error('读取用户信息失败:', e)
|
||
this.userEmail = ''
|
||
}
|
||
},
|
||
/**
|
||
* 退出登录
|
||
* 清除所有登录信息,跳转到登录页
|
||
*/
|
||
async handleLogout() {
|
||
// 动态导入 clearToken 函数
|
||
const { clearToken } = await import('../utils/request')
|
||
|
||
// 清除加密存储的 token(包括内存缓存)
|
||
await clearToken()
|
||
|
||
// 清除其他登录信息
|
||
localStorage.removeItem('userInfo')
|
||
localStorage.removeItem('leasEmail')
|
||
localStorage.removeItem('userId')
|
||
localStorage.removeItem('username')
|
||
|
||
// 更新登录状态
|
||
this.updateLoginStatus()
|
||
|
||
// 清空购物车
|
||
this.user = null
|
||
this.cart = []
|
||
this.userEmail = ''
|
||
|
||
// 触发登录状态变化事件
|
||
window.dispatchEvent(new CustomEvent('login-status-changed'))
|
||
|
||
// 提示用户
|
||
this.$message.success('退出登录成功')
|
||
|
||
// 跳转到登录页
|
||
this.$router.push('/login')
|
||
},
|
||
getBreadcrumbPath(index) {
|
||
const paths = ['/productList', '/cart', '/checkout']
|
||
if (index === 0) return '/productList'
|
||
if (index < paths.length) return paths[index - 1]
|
||
return '/productList'
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.header-container {
|
||
width: 100%;
|
||
}
|
||
|
||
/* 导航栏布局:导航按钮在中间,登录状态在右边 */
|
||
.navbar {
|
||
display: flex;
|
||
justify-content: center; /* 主内容居中 */
|
||
align-items: center;
|
||
background: #fff;
|
||
border-bottom: 1px solid #eee;
|
||
padding: 16px 32px;
|
||
margin-bottom: 16px;
|
||
position: relative; /* 让右侧元素可以绝对定位 */
|
||
}
|
||
|
||
/* 左侧导航按钮区域(实际在中间显示) */
|
||
.nav-left {
|
||
display: flex;
|
||
gap: 24px;
|
||
}
|
||
|
||
/* 右侧用户登录区域(绝对定位到右边) */
|
||
.nav-right {
|
||
display: flex;
|
||
align-items: center;
|
||
position: absolute;
|
||
right: 32px; /* 距离右边32px */
|
||
}
|
||
|
||
.nav-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
background: none;
|
||
border: none;
|
||
font-size: 16px;
|
||
color: #2c3e50;
|
||
cursor: pointer;
|
||
padding: 12px 20px;
|
||
border-radius: 8px;
|
||
transition: all 0.3s ease;
|
||
text-decoration: none;
|
||
outline: none;
|
||
position: relative;
|
||
}
|
||
|
||
/* 导航按钮悬停效果 */
|
||
.nav-btn:hover {
|
||
background: #f5f7ff;
|
||
color: #667eea;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
/* 导航按钮激活状态 - 紫色渐变,跟登录按钮一样 */
|
||
.nav-btn.active {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: #fff;
|
||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.nav-icon {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.nav-text {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.cart-count {
|
||
background: #e74c3c;
|
||
color: white;
|
||
padding: 2px 8px;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
font-weight: bold;
|
||
min-width: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 面包屑导航样式 */
|
||
.breadcrumb {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 12px 20px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
margin: 0 20px 20px 20px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.breadcrumb-item {
|
||
color: #666;
|
||
text-decoration: none;
|
||
transition: color 0.3s ease;
|
||
}
|
||
|
||
.breadcrumb-item:hover {
|
||
color: #42b983;
|
||
}
|
||
|
||
.breadcrumb-item.active {
|
||
color: #2c3e50;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.breadcrumb-item:not(:last-child)::after {
|
||
content: '>';
|
||
margin-left: 8px;
|
||
color: #ccc;
|
||
}
|
||
|
||
/* 未登录:注册/登录按钮样式 */
|
||
.auth-buttons {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* 登录状态加载中占位符 */
|
||
.auth-loading {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8px 20px;
|
||
}
|
||
|
||
.loading-placeholder {
|
||
display: inline-block;
|
||
width: 180px;
|
||
height: 32px;
|
||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||
background-size: 200% 100%;
|
||
animation: loading-shimmer 1.5s infinite;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
@keyframes loading-shimmer {
|
||
0% {
|
||
background-position: 200% 0;
|
||
}
|
||
100% {
|
||
background-position: -200% 0;
|
||
}
|
||
}
|
||
|
||
.auth-btn {
|
||
padding: 8px 20px;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
text-decoration: none;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* 注册按钮 - 白色背景 */
|
||
.register-btn {
|
||
color: #667eea;
|
||
border: 1px solid #667eea;
|
||
background: white;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.register-btn:hover {
|
||
background: #f5f7ff;
|
||
}
|
||
|
||
/* 登录按钮 - 紫色背景 */
|
||
.login-btn {
|
||
color: white;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.login-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
/* 已登录:用户信息样式 */
|
||
.user-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
padding: 8px 16px;
|
||
/* 去掉灰色背景,更简洁 */
|
||
}
|
||
|
||
/* 用户邮箱显示 */
|
||
.user-email {
|
||
color: #2c3e50;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
/* 安全设置链接样式 - 与导航按钮一致 */
|
||
.security-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
color: #2c3e50;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
text-decoration: none;
|
||
padding: 12px 20px;
|
||
margin-right: 8px;
|
||
border-radius: 8px;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
position: relative;
|
||
}
|
||
|
||
.security-link:hover {
|
||
background: #f5f7ff;
|
||
color: #667eea;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.security-link.active {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: #fff;
|
||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
/* 退出按钮样式 */
|
||
.logout-btn {
|
||
color: #e74c3c;
|
||
font-size: 14px;
|
||
padding: 4px 12px;
|
||
}
|
||
|
||
.logout-btn:hover {
|
||
color: #c0392b;
|
||
background: #fee;
|
||
}
|
||
|
||
/* 响应式设计 - 手机端适配 */
|
||
@media (max-width: 768px) {
|
||
.navbar {
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
.nav-left {
|
||
flex-direction: column;
|
||
width: 100%;
|
||
gap: 8px;
|
||
}
|
||
|
||
.nav-btn {
|
||
width: 100%;
|
||
justify-content: center;
|
||
padding: 16px 20px;
|
||
}
|
||
|
||
.nav-right {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
.auth-buttons {
|
||
width: 100%;
|
||
}
|
||
|
||
.auth-btn {
|
||
flex: 1;
|
||
text-align: center;
|
||
}
|
||
|
||
.user-info {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
.breadcrumb {
|
||
margin: 0 12px 16px 12px;
|
||
padding: 8px 16px;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
</style>
|
||
|