This commit is contained in:
2026-01-16 22:06:40 +08:00
parent 060cea78ea
commit 865f7fd9d6
12 changed files with 105 additions and 433 deletions

View File

@@ -11,3 +11,24 @@ export function userLogin(params) {
data: params data: params
}) })
} }
export function userLogout() {
return request({
url: '/auth/logout',
method: 'get'
})
}
export function getUserInfo() {
return request({
url: '/auth/user',
method: 'get'
})
}
export function getMyMenu(){
return request({
url: `auth/menu/my`,
method: 'get'
})
}

View File

@@ -11,7 +11,7 @@ export default {
CORE_VER: "1.6.6", CORE_VER: "1.6.6",
//接口地址 //接口地址
API_URL: "http://localhost:8000/admin/", API_URL: "https://www.tensent.cn/admin/",
//请求超时 //请求超时
TIMEOUT: 50000, TIMEOUT: 50000,

View File

@@ -1,147 +0,0 @@
<template>
<a-menu mode="inline" theme="dark" :selected-keys="selectedKeys" @select="handleSelect" class="level1-menu">
<a-menu-item v-for="item in menuList" :key="item.path">
<template #icon>
<component :is="item.meta?.icon || 'AppstoreOutlined'" />
</template>
{{ item.meta?.title || item.name }}
</a-menu-item>
</a-menu>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useLayoutStore } from '@/stores/modules/layout'
import { getUserMenu } from '@/api/menu'
const route = useRoute()
const router = useRouter()
const layoutStore = useLayoutStore()
const menuList = ref([])
const selectedKeys = ref([])
// 获取菜单数据
const getMenuList = async () => {
try {
const res = await getUserMenu()
if (res.code === 200) {
menuList.value = res.data || []
}
} catch (error) {
console.error('获取菜单失败:', error)
// 模拟数据
menuList.value = [
{
path: '/home',
name: 'Home',
meta: { title: '首页', icon: 'HomeOutlined' }
},
{
path: '/system',
name: 'System',
meta: { title: '系统管理', icon: 'SettingOutlined' },
children: [
{
path: '/system/user',
name: 'User',
meta: { title: '用户管理', icon: 'UserOutlined' }
},
{
path: '/system/role',
name: 'Role',
meta: { title: '角色管理', icon: 'TeamOutlined' }
},
{
path: '/system/menu',
name: 'Menu',
meta: { title: '菜单管理', icon: 'MenuOutlined' }
}
]
}
]
}
}
// 更新选中的菜单
const updateSelectedKeys = () => {
// 查找当前路由对应的一级菜单
const path = route.path
const matched = route.matched
if (matched.length > 0) {
const topPath = matched[0].path
selectedKeys.value = [topPath]
// 查找对应的菜单项
const selectedMenu = menuList.value.find(item => item.path === topPath)
if (selectedMenu) {
layoutStore.setSelectedParentMenu(selectedMenu)
}
}
}
// 处理菜单选择
const handleSelect = ({ key }) => {
const selectedMenu = menuList.value.find(item => item.path === key)
if (selectedMenu) {
layoutStore.setSelectedParentMenu(selectedMenu)
// 如果没有子菜单,直接跳转
if (!selectedMenu.children || selectedMenu.children.length === 0) {
router.push(key)
}
}
}
// 监听路由变化
watch(() => route.path, () => {
updateSelectedKeys()
}, { immediate: true })
onMounted(() => {
getMenuList()
})
</script>
<style scoped lang="scss">
.level1-menu {
height: calc(100% - 64px);
border-right: none;
background-color: #001529;
:deep(.ant-menu-item) {
height: 60px;
line-height: 60px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
.anticon {
font-size: 20px;
margin-bottom: 4px;
}
.ant-menu-title-content {
font-size: 12px;
line-height: 1.2;
}
&:hover {
color: #ffffff;
background-color: #1890ff;
}
&.ant-menu-item-selected {
background-color: #1890ff;
&::after {
display: none;
}
}
}
}
</style>

View File

@@ -1,127 +0,0 @@
<template>
<div v-if="parentMenu && parentMenu.children && parentMenu.children.length > 0" class="level2-menu-wrapper">
<div class="menu-header">
<span class="menu-title">{{ parentMenu.meta?.title || parentMenu.name }}</span>
</div>
<a-menu mode="inline" :theme="theme" :selected-keys="selectedKeys" :open-keys="openKeys" @select="handleSelect"
@open-change="handleOpenChange" class="level2-menu">
<template v-for="item in parentMenu.children">
<!-- 有子菜单 -->
<a-sub-menu v-if="item.children && item.children.length > 0" :key="item.path + '-sub'">
<template #icon>
<component :is="item.meta?.icon || 'MenuOutlined'" />
</template>
<template #title>{{ item.meta?.title || item.name }}</template>
<a-menu-item v-for="child in item.children" :key="child.path">
<template #icon>
<component :is="child.meta?.icon || 'FileOutlined'" />
</template>
{{ child.meta?.title || child.name }}
</a-menu-item>
</a-sub-menu>
<!-- 无子菜单 -->
<a-menu-item v-else :key="item.path + '-item'">
<template #icon>
<component :is="item.meta?.icon || 'FileOutlined'" />
</template>
{{ item.meta?.title || item.name }}
</a-menu-item>
</template>
</a-menu>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useLayoutStore } from '@/stores/modules/layout'
const props = defineProps({
theme: {
type: String,
default: 'light'
}
})
const route = useRoute()
const router = useRouter()
const layoutStore = useLayoutStore()
const parentMenu = computed(() => layoutStore.selectedParentMenu)
const selectedKeys = ref([])
const openKeys = ref([])
// 更新选中的菜单
const updateSelectedKeys = () => {
selectedKeys.value = [route.path]
// 获取父级菜单路径
const matched = route.matched
.filter(item => item.path !== '/' && item.path !== route.path)
.map(item => item.path)
openKeys.value = matched
}
// 处理菜单选择
const handleSelect = ({ key }) => {
router.push(key)
}
// 处理菜单展开/收起
const handleOpenChange = (keys) => {
openKeys.value = keys
}
// 监听路由变化
watch(() => route.path, () => {
updateSelectedKeys()
}, { immediate: true })
// 监听父菜单变化
watch(() => parentMenu.value, () => {
updateSelectedKeys()
})
</script>
<style scoped lang="scss">
.level2-menu-wrapper {
height: 100%;
display: flex;
flex-direction: column;
.menu-header {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #f0f0f0;
.menu-title {
font-size: 16px;
font-weight: 500;
color: #333;
}
}
.level2-menu {
flex: 1;
overflow-y: auto;
border-right: none;
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
}
</style>

View File

@@ -0,0 +1,5 @@
<template>
</template>
<script setup>
</script>

View File

@@ -1,91 +0,0 @@
<template>
<a-menu mode="horizontal" :selected-keys="selectedKeys" @select="handleSelect" class="top-menu">
<a-menu-item v-for="item in menuList" :key="item.path">
<template #icon>
<component :is="item.meta?.icon || 'AppstoreOutlined'" />
</template>
{{ item.meta?.title || item.name }}
</a-menu-item>
</a-menu>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getUserMenu } from '@/api/menu'
const route = useRoute()
const router = useRouter()
const menuList = ref([])
const selectedKeys = ref([])
// 获取菜单数据
const getMenuList = async () => {
try {
const res = await getUserMenu()
if (res.code === 200) {
// 只显示一级菜单
menuList.value = (res.data || []).filter(item => !item.meta?.hidden)
}
} catch (error) {
console.error('获取菜单失败:', error)
// 模拟数据
menuList.value = [
{
path: '/home',
name: 'Home',
meta: { title: '首页', icon: 'HomeOutlined' }
},
{
path: '/system',
name: 'System',
meta: { title: '系统管理', icon: 'SettingOutlined' }
}
]
}
}
// 更新选中的菜单
const updateSelectedKeys = () => {
// 查找当前路由对应的顶级菜单
const path = route.path
const matched = route.matched
if (matched.length > 0) {
const topPath = matched[0].path
selectedKeys.value = [topPath]
}
}
// 处理菜单选择
const handleSelect = ({ key }) => {
router.push(key)
}
// 监听路由变化
watch(() => route.path, () => {
updateSelectedKeys()
}, { immediate: true })
onMounted(() => {
getMenuList()
})
</script>
<style scoped lang="scss">
.top-menu {
border-bottom: none;
flex: 1;
background: transparent;
:deep(.ant-menu-item) {
&:hover {
color: #1890ff;
}
&.ant-menu-item-selected {
background-color: rgba(24, 144, 255, 0.1);
}
}
}
</style>

View File

@@ -7,15 +7,21 @@
<div class="logo-box"> <div class="logo-box">
<span class="logo-text">VUE</span> <span class="logo-text">VUE</span>
</div> </div>
<level1-menu /> <ul class="left-nav">
<li v-for="(item, index) in menuList" :key="index">
<component :is="item.meta?.icon" />
<span>{{ item.meta?.title }}</span>
</li>
</ul>
</a-layout-sider> </a-layout-sider>
<!-- 第二个侧边栏显示选中的父菜单的子菜单 --> <!-- 第二个侧边栏显示选中的父菜单的子菜单 -->
<a-layout-sider <a-layout-sider
v-if="selectedParentMenu && selectedParentMenu.children && selectedParentMenu.children.length > 0" v-if="selectedParentMenu && selectedParentMenu.children && selectedParentMenu.children.length > 0"
theme="light" :collapsed="sidebarCollapsed" :collapsible="true" @collapse="handleCollapse" theme="light" :collapsed="sidebarCollapsed" :collapsible="true" @collapse="handleCollapse" width="200" :collapsed-width="64">
class="level2-sidebar" width="200" :collapsed-width="64"> <a-menu v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline" :items="menuList">
<level2-menu /> <navMenu />
</a-menu>
</a-layout-sider> </a-layout-sider>
<a-layout class="main-layout"> <a-layout class="main-layout">
@@ -45,7 +51,9 @@
<span v-if="!sidebarCollapsed" class="logo-text">VUE ADMIN</span> <span v-if="!sidebarCollapsed" class="logo-text">VUE ADMIN</span>
<span v-else class="logo-text-mini">V</span> <span v-else class="logo-text-mini">V</span>
</div> </div>
<side-menu :collapsed="sidebarCollapsed" /> <a-menu v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline" :items="menuList">
<navMenu />
</a-menu>
</a-layout-sider> </a-layout-sider>
<a-layout class="main-layout"> <a-layout class="main-layout">
<a-layout-header class="app-header"> <a-layout-header class="app-header">
@@ -72,7 +80,9 @@
<div class="logo-box-top"> <div class="logo-box-top">
<span class="logo-text">VUE ADMIN</span> <span class="logo-text">VUE ADMIN</span>
</div> </div>
<topMenu /> <a-menu v-model:selectedKeys="selectedKeys" mode="horizontal" :items="menuList">
<navMenu />
</a-menu>
</div> </div>
<userbar /> <userbar />
</a-layout-header> </a-layout-header>
@@ -102,14 +112,12 @@
import { computed, defineOptions, ref } from 'vue' import { computed, defineOptions, ref } from 'vue'
import { useLayoutStore } from '@/stores/modules/layout' import { useLayoutStore } from '@/stores/modules/layout'
import { SettingOutlined } from '@ant-design/icons-vue' import { SettingOutlined } from '@ant-design/icons-vue'
import { useUserStore } from '@/stores/modules/user'
import userbar from './components/userbar.vue' import userbar from './components/userbar.vue'
import navMenu from './components/navMenu.vue'
import breadcrumb from './components/breadcrumb.vue' import breadcrumb from './components/breadcrumb.vue'
import tags from './components/tags.vue' import tags from './components/tags.vue'
import topMenu from './components/topMenu.vue'
import sideMenu from './components/sideMenu.vue'
import level1Menu from './components/level1Menu.vue'
import level2Menu from './components/level2Menu.vue'
import setting from './components/setting.vue' import setting from './components/setting.vue'
// 定义组件名称(多词命名) // 定义组件名称(多词命名)
@@ -142,6 +150,12 @@ const layoutClass = computed(() => {
} }
}) })
const openKeys = ref([])
const selectedKeys = ref([])
const menuList = computed(() => {
return useUserStore().menu
})
// 处理折叠 // 处理折叠
const handleCollapse = (collapsed) => { const handleCollapse = (collapsed) => {
layoutStore.sidebarCollapsed = collapsed layoutStore.sidebarCollapsed = collapsed

View File

@@ -51,6 +51,7 @@ import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue' import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { useUserStore } from '@/stores/modules/user' import { useUserStore } from '@/stores/modules/user'
import { userLogin, getUserInfo, getMyMenu } from '@/api/auth'
// 定义组件名称(多词命名) // 定义组件名称(多词命名)
defineOptions({ defineOptions({
@@ -70,26 +71,24 @@ const formState = reactive({
const handleLogin = async () => { const handleLogin = async () => {
loading.value = true; loading.value = true;
try { try {
// 模拟登录请求
await new Promise((resolve) => setTimeout(resolve, 1000)) let res = await userLogin({ username: formState.username, password: formState.password })
// 模拟登录成功 if (res.code == 1) {
const mockUserInfo = { userStore.setToken(res.data.access_token)
id: 1, let userInfo = await getUserInfo()
username: formState.username, if (userInfo.code == 1) {
nickname: '管理员', userStore.setUserInfo(userInfo.data)
email: 'admin@example.com', }
role: ['admin'], let menu = await getMyMenu()
avatar: '' if (menu.code == 1){
userStore.setMenu(menu.data)
}
message.success('登录成功');
// 跳转到首页或重定向页面
const redirect = router.currentRoute.value.query.redirect || '/'
router.push(redirect)
} }
const mockToken = 'mock-token-' + Date.now()
const mockRefreshToken = 'mock-refresh-token-' + Date.now()
userStore.setToken(mockToken)
userStore.setRefreshToken(mockRefreshToken)
userStore.setUserInfo(mockUserInfo)
message.success('登录成功');
// 跳转到首页或重定向页面
const redirect = router.currentRoute.value.query.redirect || '/'
router.push(redirect)
} catch { } catch {
message.error('登录失败,请检查用户名和密码'); message.error('登录失败,请检查用户名和密码');
} finally { } finally {

View File

@@ -2,7 +2,6 @@ import { createRouter, createWebHashHistory } from 'vue-router'
import NProgress from 'nprogress' import NProgress from 'nprogress'
import 'nprogress/nprogress.css' import 'nprogress/nprogress.css'
import config from '../config' import config from '../config'
import userRoutes from '../config/routes'
import { useUserStore } from '../stores/modules/user' import { useUserStore } from '../stores/modules/user'
import systemRoutes from './systemRoutes' import systemRoutes from './systemRoutes'
@@ -141,38 +140,8 @@ router.beforeEach(async (to, from, next) => {
// 4. 动态路由加载 // 4. 动态路由加载
if (!isDynamicRouteLoaded) { if (!isDynamicRouteLoaded) {
try { try {
// 获取静态菜单配置
const staticMenus = userRoutes || []
// 获取后端返回的用户菜单 // 获取后端返回的用户菜单
const backendMenus = userStore.getMenu() const mergedMenus = userStore.getMenu()
// 合并静态菜单和后端菜单
// 如果后端菜单为空,只使用静态菜单
// 如果后端菜单不为空,合并两个菜单,后端菜单优先
let mergedMenus = [...staticMenus]
if (backendMenus && backendMenus.length > 0) {
// 创建菜单映射,用于去重(以路径为唯一标识)
const menuMap = new Map()
// 先添加静态菜单
staticMenus.forEach(menu => {
if (menu.path) {
menuMap.set(menu.path, menu)
}
})
// 添加后端菜单,如果路径重复则覆盖
backendMenus.forEach(menu => {
if (menu.path) {
menuMap.set(menu.path, menu)
}
})
// 转换为数组
mergedMenus = Array.from(menuMap.values())
}
if (mergedMenus && mergedMenus.length > 0) { if (mergedMenus && mergedMenus.length > 0) {
// 将合并后的菜单转换为路由 // 将合并后的菜单转换为路由

View File

@@ -2,6 +2,7 @@ import { ref } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { resetRouter } from '../../router' import { resetRouter } from '../../router'
import { customStorage } from '../persist' import { customStorage } from '../persist'
import userRoutes from '@/config/routes'
export const useUserStore = defineStore( export const useUserStore = defineStore(
'user', 'user',
@@ -28,7 +29,35 @@ export const useUserStore = defineStore(
// 设置菜单 // 设置菜单
function setMenu(newMenu) { function setMenu(newMenu) {
menu.value = newMenu const staticMenus = userRoutes || []
// 合并静态菜单和后端菜单
// 如果后端菜单为空,只使用静态菜单
// 如果后端菜单不为空,合并两个菜单,后端菜单优先
let mergedMenus = [...staticMenus]
if (newMenu && newMenu.length > 0) {
// 创建菜单映射,用于去重(以路径为唯一标识)
const menuMap = new Map()
// 先添加静态菜单
staticMenus.forEach(menu => {
if (menu.path) {
menuMap.set(menu.path, menu)
}
})
// 添加后端菜单,如果路径重复则覆盖
newMenu.forEach(menu => {
if (menu.path) {
menuMap.set(menu.path, menu)
}
})
// 转换为数组
mergedMenus = Array.from(menuMap.values())
}
menu.value = mergedMenus
} }
// 获取菜单 // 获取菜单

View File

@@ -6,7 +6,7 @@ import router from '@/router'
const http = axios.create({ const http = axios.create({
timeout: 30000, timeout: 30000,
baseURL: config.baseURL baseURL: config.API_URL
}) })
// 是否正在刷新 token // 是否正在刷新 token
@@ -40,8 +40,8 @@ http.interceptors.response.use(
const { code, data, message } = response.data const { code, data, message } = response.data
// 请求成功 // 请求成功
if (code === 200 || code === 0) { if (code === 200 || code === 1) {
return data return { code, data, message }
} }
// 其他错误码处理 // 其他错误码处理

View File

@@ -161,7 +161,7 @@ tool.data = {
return value.content; return value.content;
} }
return null; return null;
} catch (err) { } catch {
return null; return null;
} }
}, },
@@ -183,7 +183,7 @@ tool.session = {
const data = sessionStorage.getItem(table); const data = sessionStorage.getItem(table);
try { try {
return JSON.parse(data); return JSON.parse(data);
} catch (err) { } catch {
return null; return null;
} }
}, },