535 lines
12 KiB
Vue
535 lines
12 KiB
Vue
<template>
|
||
<el-container class="layout-container">
|
||
<!-- 侧边栏 -->
|
||
<el-aside :width="isCollapse ? '64px' : '200px'" class="sidebar">
|
||
<div class="logo">
|
||
<img v-if="!isCollapse" src="/logo.svg" alt="Logo" class="logo-img" />
|
||
<span v-if="!isCollapse" class="logo-text">后台管理</span>
|
||
<el-icon v-else class="logo-icon"><Setting /></el-icon>
|
||
</div>
|
||
|
||
<el-menu
|
||
:default-active="$route.path"
|
||
:collapse="isCollapse"
|
||
:unique-opened="true"
|
||
router
|
||
class="sidebar-menu"
|
||
>
|
||
<el-menu-item index="/dashboard">
|
||
<el-icon><Odometer /></el-icon>
|
||
<template #title>仪表盘</template>
|
||
</el-menu-item>
|
||
|
||
<el-menu-item v-if="userStore.isAdmin" index="/products">
|
||
<el-icon><Goods /></el-icon>
|
||
<template #title>商品管理</template>
|
||
</el-menu-item>
|
||
|
||
<el-menu-item v-if="userStore.isAdmin" index="/orders">
|
||
<el-icon><List /></el-icon>
|
||
<template #title>订单管理</template>
|
||
</el-menu-item>
|
||
<el-menu-item index="/profile">
|
||
<el-icon><UserFilled /></el-icon>
|
||
<template #title>个人资料</template>
|
||
</el-menu-item>
|
||
</el-menu>
|
||
</el-aside>
|
||
|
||
<!-- 主内容区 -->
|
||
<el-container>
|
||
<!-- 顶部导航 -->
|
||
<el-header class="header">
|
||
<div class="header-left">
|
||
<el-button
|
||
type="text"
|
||
@click="toggleCollapse"
|
||
class="collapse-btn"
|
||
>
|
||
<el-icon><Expand v-if="isCollapse" /><Fold v-else /></el-icon>
|
||
</el-button>
|
||
|
||
<el-breadcrumb separator="/" class="breadcrumb">
|
||
<el-breadcrumb-item
|
||
v-for="item in breadcrumbs"
|
||
:key="item.path"
|
||
:to="item.path"
|
||
>
|
||
{{ item.title }}
|
||
</el-breadcrumb-item>
|
||
</el-breadcrumb>
|
||
</div>
|
||
|
||
<div class="header-right">
|
||
<!-- 全屏按钮 -->
|
||
<el-tooltip content="全屏" placement="bottom">
|
||
<el-button type="text" @click="toggleFullscreen" class="header-btn">
|
||
<el-icon><FullScreen /></el-icon>
|
||
</el-button>
|
||
</el-tooltip>
|
||
|
||
<!-- 刷新按钮 -->
|
||
<el-tooltip content="刷新" placement="bottom">
|
||
<el-button type="text" @click="refresh" class="header-btn">
|
||
<el-icon><Refresh /></el-icon>
|
||
</el-button>
|
||
</el-tooltip>
|
||
|
||
<!-- 用户菜单 -->
|
||
<el-dropdown @command="handleCommand" class="user-dropdown">
|
||
<div class="user-info">
|
||
<el-avatar :size="32" :src="userStore.user?.avatar">
|
||
<el-icon><UserFilled /></el-icon>
|
||
</el-avatar>
|
||
<span class="username">{{ userStore.user?.username }}</span>
|
||
<el-icon class="dropdown-icon"><ArrowDown /></el-icon>
|
||
</div>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item command="profile">
|
||
<el-icon><UserFilled /></el-icon>
|
||
个人资料
|
||
</el-dropdown-item>
|
||
<el-dropdown-item command="changePassword">
|
||
<el-icon><Lock /></el-icon>
|
||
修改密码
|
||
</el-dropdown-item>
|
||
<el-dropdown-item divided command="logout">
|
||
<el-icon><SwitchButton /></el-icon>
|
||
退出登录
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
</el-header>
|
||
|
||
<!-- 主内容 -->
|
||
<el-main class="main-content">
|
||
<transition name="fade" mode="out-in">
|
||
<router-view />
|
||
</transition>
|
||
</el-main>
|
||
</el-container>
|
||
</el-container>
|
||
|
||
<!-- 修改密码对话框 -->
|
||
<el-dialog
|
||
v-model="passwordDialogVisible"
|
||
title="修改密码"
|
||
width="400px"
|
||
:before-close="handlePasswordDialogClose"
|
||
>
|
||
<el-form
|
||
ref="passwordFormRef"
|
||
:model="passwordForm"
|
||
:rules="passwordRules"
|
||
label-width="80px"
|
||
>
|
||
<el-form-item label="当前密码" prop="currentPassword">
|
||
<el-input
|
||
v-model="passwordForm.currentPassword"
|
||
type="password"
|
||
placeholder="请输入当前密码"
|
||
show-password
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="新密码" prop="newPassword">
|
||
<el-input
|
||
v-model="passwordForm.newPassword"
|
||
type="password"
|
||
placeholder="请输入新密码"
|
||
show-password
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="确认密码" prop="confirmPassword">
|
||
<el-input
|
||
v-model="passwordForm.confirmPassword"
|
||
type="password"
|
||
placeholder="请确认新密码"
|
||
show-password
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="passwordDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleChangePassword" :loading="userStore.loading">
|
||
确定
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { useUserStore } from '@/stores/user'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import {
|
||
Odometer,
|
||
User,
|
||
UserFilled,
|
||
Setting,
|
||
Expand,
|
||
Fold,
|
||
FullScreen,
|
||
Refresh,
|
||
ArrowDown,
|
||
Lock,
|
||
SwitchButton,
|
||
Money,
|
||
Ticket,
|
||
Warning,
|
||
Goods,
|
||
List,
|
||
Coin,
|
||
DocumentChecked,
|
||
Avatar,
|
||
DataAnalysis,
|
||
Monitor,
|
||
Connection,
|
||
CreditCard,
|
||
Bell
|
||
} from '@element-plus/icons-vue'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const userStore = useUserStore()
|
||
|
||
// 组件挂载时不再自动验证token,避免登录后立即触发401错误
|
||
// token验证交给具体的API调用时处理
|
||
onMounted(() => {
|
||
// 仅确保用户状态已正确加载
|
||
console.log('Layout组件已挂载,用户状态:', userStore.isAuthenticated)
|
||
})
|
||
|
||
// 侧边栏折叠状态
|
||
const isCollapse = ref(false)
|
||
|
||
// 修改密码对话框
|
||
const passwordDialogVisible = ref(false)
|
||
const passwordFormRef = ref()
|
||
const passwordForm = ref({
|
||
currentPassword: '',
|
||
newPassword: '',
|
||
confirmPassword: ''
|
||
})
|
||
|
||
// 密码验证规则
|
||
const passwordRules = {
|
||
currentPassword: [
|
||
{ required: true, message: '请输入当前密码', trigger: 'blur' }
|
||
],
|
||
newPassword: [
|
||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' },
|
||
{
|
||
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{6,}$/,
|
||
message: '密码必须包含大小写字母和数字',
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
confirmPassword: [
|
||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
||
{
|
||
validator: (rule, value, callback) => {
|
||
if (value !== passwordForm.value.newPassword) {
|
||
callback(new Error('两次输入的密码不一致'))
|
||
} else {
|
||
callback()
|
||
}
|
||
},
|
||
trigger: 'blur'
|
||
}
|
||
]
|
||
}
|
||
|
||
// 面包屑导航
|
||
const breadcrumbs = computed(() => {
|
||
const matched = route.matched.filter(item => item.meta && item.meta.title)
|
||
const breadcrumbList = []
|
||
|
||
matched.forEach(item => {
|
||
if (item.path !== '/') {
|
||
breadcrumbList.push({
|
||
title: item.meta.title.replace(' - 后台管理系统', ''),
|
||
path: item.path
|
||
})
|
||
}
|
||
})
|
||
|
||
return breadcrumbList
|
||
})
|
||
|
||
// 切换侧边栏折叠
|
||
const toggleCollapse = () => {
|
||
isCollapse.value = !isCollapse.value
|
||
}
|
||
|
||
// 全屏切换
|
||
const toggleFullscreen = () => {
|
||
if (!document.fullscreenElement) {
|
||
document.documentElement.requestFullscreen()
|
||
} else {
|
||
document.exitFullscreen()
|
||
}
|
||
}
|
||
|
||
// 刷新页面
|
||
const refresh = () => {
|
||
window.location.reload()
|
||
}
|
||
|
||
// 处理用户菜单命令
|
||
const handleCommand = (command) => {
|
||
switch (command) {
|
||
case 'profile':
|
||
router.push('/profile')
|
||
break
|
||
case 'changePassword':
|
||
passwordDialogVisible.value = true
|
||
break
|
||
case 'logout':
|
||
handleLogout()
|
||
break
|
||
}
|
||
}
|
||
|
||
// 退出登录
|
||
const handleLogout = async () => {
|
||
try {
|
||
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
|
||
await userStore.logout()
|
||
router.push('/login')
|
||
} catch (error) {
|
||
// 用户取消
|
||
}
|
||
}
|
||
|
||
// 修改密码
|
||
const handleChangePassword = async () => {
|
||
try {
|
||
await passwordFormRef.value.validate()
|
||
|
||
const result = await userStore.changePassword({
|
||
currentPassword: passwordForm.value.currentPassword,
|
||
newPassword: passwordForm.value.newPassword
|
||
})
|
||
|
||
if (result.success) {
|
||
passwordDialogVisible.value = false
|
||
resetPasswordForm()
|
||
|
||
// 延迟跳转到登录页
|
||
setTimeout(() => {
|
||
router.push('/login')
|
||
}, 2000)
|
||
}
|
||
} catch (error) {
|
||
console.error('密码修改失败:', error)
|
||
}
|
||
}
|
||
|
||
// 关闭密码对话框
|
||
const handlePasswordDialogClose = () => {
|
||
resetPasswordForm()
|
||
passwordDialogVisible.value = false
|
||
}
|
||
|
||
// 重置密码表单
|
||
const resetPasswordForm = () => {
|
||
passwordForm.value = {
|
||
currentPassword: '',
|
||
newPassword: '',
|
||
confirmPassword: ''
|
||
}
|
||
passwordFormRef.value?.resetFields()
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.layout-container {
|
||
height: 100vh;
|
||
}
|
||
|
||
.sidebar {
|
||
background-color: #304156;
|
||
transition: width 0.3s ease;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
|
||
.sidebar .el-menu {
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
flex: 1;
|
||
}
|
||
|
||
/* 自定义滚动条样式 */
|
||
.sidebar .el-menu::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.sidebar .el-menu::-webkit-scrollbar-thumb {
|
||
background: rgba(144, 147, 153, 0.3);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.sidebar .el-menu::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(144, 147, 153, 0.5);
|
||
}
|
||
|
||
.logo {
|
||
height: 60px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #2b3a4b;
|
||
color: white;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.logo-img {
|
||
width: 32px;
|
||
height: 32px;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.logo-text {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.logo-icon {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.sidebar-menu {
|
||
border-right: none;
|
||
background-color: #304156;
|
||
}
|
||
|
||
.sidebar-menu .el-menu-item,
|
||
.sidebar-menu .el-sub-menu__title {
|
||
color: #bfcbd9;
|
||
border-bottom: 1px solid #434a50;
|
||
}
|
||
|
||
.sidebar-menu .el-menu-item:hover,
|
||
.sidebar-menu .el-sub-menu__title:hover {
|
||
background-color: #434a50 !important;
|
||
color: #fff !important;
|
||
}
|
||
|
||
.sidebar-menu .el-menu-item.is-active {
|
||
background-color: #409eff !important;
|
||
color: #fff !important;
|
||
}
|
||
|
||
.header {
|
||
background-color: #fff;
|
||
border-bottom: 1px solid #e6e6e6;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 20px;
|
||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.collapse-btn {
|
||
margin-right: 20px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.breadcrumb {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.header-btn {
|
||
font-size: 16px;
|
||
color: #606266;
|
||
}
|
||
|
||
.header-btn:hover {
|
||
color: #409eff;
|
||
}
|
||
|
||
.user-dropdown {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.user-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 5px 10px;
|
||
border-radius: 4px;
|
||
transition: background-color 0.3s ease;
|
||
}
|
||
|
||
.user-info:hover {
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.username {
|
||
font-size: 14px;
|
||
color: #606266;
|
||
}
|
||
|
||
.dropdown-icon {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
.main-content {
|
||
background-color: #f0f2f5;
|
||
padding: 20px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* 过渡动画 */
|
||
.fade-enter-active,
|
||
.fade-leave-active {
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.fade-enter-from,
|
||
.fade-leave-to {
|
||
opacity: 0;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.sidebar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
z-index: 1000;
|
||
height: 100vh;
|
||
}
|
||
|
||
.breadcrumb {
|
||
display: none;
|
||
}
|
||
|
||
.username {
|
||
display: none;
|
||
}
|
||
}
|
||
</style> |