Files
jurong_supplier_management/src/layout/Layout.vue
2025-09-25 14:38:57 +08:00

535 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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