feat: setup core architecture (router, pinia, axios, permission)

- Vue Router 4 with navigation guards and dynamic route loading
- Pinia stores for user, permission, and app state management
- Axios API layer with request/response interceptors
- Permission control (v-hasPermi, v-hasRole directives)
- Utility functions (auth, validate, permission)
- Layout component with sidebar, navbar, and breadcrumbs
- Error pages (401, 404) and redirect view
- Login API integration with user store
feature/20260628/refactor-frontend-modernization
Lxy 3 weeks ago
parent 6762d22d8e
commit 2eb6a4444e

@ -1,3 +1,3 @@
# 开发环境配置 # 开发环境配置
VITE_API_BASE_URL=/dev-api VITE_APP_BASE_API=/dev-api
VITE_APP_TITLE=RuoYi 管理系统 VITE_APP_TITLE=RuoYi 管理系统

@ -1,3 +1,3 @@
# 生产环境配置 # 生产环境配置
VITE_API_BASE_URL=/prod-api VITE_APP_BASE_API=/prod-api
VITE_APP_TITLE=RuoYi 管理系统 VITE_APP_TITLE=RuoYi 管理系统

@ -0,0 +1,42 @@
import request from '@/api/request'
/**
*
*/
export function login(username: string, password: string, code?: string, uuid?: string) {
return request({
url: '/login',
method: 'post',
data: { username, password, code, uuid },
})
}
/**
*
*/
export function getInfo() {
return request({
url: '/getInfo',
method: 'get',
})
}
/**
* 退
*/
export function logout() {
return request({
url: '/logout',
method: 'post',
})
}
/**
*
*/
export function getCodeImg() {
return request({
url: '/captchaImage',
method: 'get',
})
}

@ -0,0 +1,11 @@
import request from '@/api/request'
/**
*
*/
export function getRouters() {
return request({
url: '/getRouters',
method: 'get',
})
}

@ -0,0 +1,88 @@
import request from '@/api/request'
/**
*
*/
export function getUserList(params: any) {
return request({
url: '/system/user/list',
method: 'get',
params,
})
}
/**
*
*/
export function getUser(userId: number | string) {
return request({
url: `/system/user/${userId}`,
method: 'get',
})
}
/**
*
*/
export function addUser(data: any) {
return request({
url: '/system/user',
method: 'post',
data,
})
}
/**
*
*/
export function updateUser(data: any) {
return request({
url: '/system/user',
method: 'put',
data,
})
}
/**
*
*/
export function delUser(userId: number | string | (number | string)[]) {
return request({
url: `/system/user/${userId}`,
method: 'delete',
})
}
/**
*
*/
export function resetUserPwd(userId: number | string, password: string) {
return request({
url: `/system/user/resetPwd`,
method: 'put',
data: { userId, password },
})
}
/**
*
*/
export function changeUserStatus(userId: number | string, status: string) {
return request({
url: `/system/user/changeStatus`,
method: 'put',
data: { userId, status },
})
}
/**
*
*/
export function exportUser(params: any) {
return request({
url: '/system/user/export',
method: 'post',
data: params,
responseType: 'blob',
})
}

@ -1,10 +1,11 @@
import axios from 'axios' import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' import type { AxiosInstance, AxiosResponse } from 'axios'
import { ElMessage } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { getToken, removeToken } from '@/utils/auth'
const service: AxiosInstance = axios.create({ const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/dev-api', baseURL: import.meta.env.VITE_APP_BASE_API || '/dev-api',
timeout: 15000, timeout: 60000,
headers: { headers: {
'Content-Type': 'application/json;charset=utf-8', 'Content-Type': 'application/json;charset=utf-8',
}, },
@ -13,10 +14,23 @@ const service: AxiosInstance = axios.create({
// 请求拦截器 // 请求拦截器
service.interceptors.request.use( service.interceptors.request.use(
(config) => { (config) => {
// 添加 token // 自动注入 Token
const token = localStorage.getItem('token') const token = getToken()
if (token) { if (token) {
config.headers.Authorization = `Bearer ${token}` config.headers['Authorization'] = `Bearer ${token}`
}
// GET 请求参数处理
if (config.method === 'get' && config.params) {
const url = config.url || ''
const separator = url.includes('?') ? '&' : '?'
const queryString = Object.entries(config.params)
.filter(([, v]) => v !== undefined && v !== null && v !== '')
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v as string)}`)
.join('&')
if (queryString) {
config.url = `${url}${separator}${queryString}`
}
config.params = {}
} }
return config return config
}, },
@ -28,22 +42,39 @@ service.interceptors.request.use(
// 响应拦截器 // 响应拦截器
service.interceptors.response.use( service.interceptors.response.use(
(response: AxiosResponse) => { (response: AxiosResponse) => {
const { code, msg, data } = response.data const res = response.data
// 二进制数据直接返回
if (res instanceof Blob || response.config.responseType === 'blob') {
return res
}
const { code, msg, data } = res
if (code === 200) { if (code === 200) {
return data return { data, msg, code }
} else if (code === 401) { } else if (code === 401) {
ElMessage.error('登录已过期,请重新登录') ElMessageBox.confirm('登录状态已过期,请重新登录', '系统提示', {
localStorage.removeItem('token') confirmButtonText: '重新登录',
window.location.href = '/login' cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
removeToken()
window.location.href = '/login'
})
.catch(() => {})
return Promise.reject(new Error(msg || '登录已过期')) return Promise.reject(new Error(msg || '登录已过期'))
} else if (code === 500) {
ElMessage.error(msg || '系统内部错误')
return Promise.reject(new Error(msg || '系统内部错误'))
} else { } else {
ElMessage.error(msg || '请求失败') ElMessage.error(msg || '请求失败')
return Promise.reject(new Error(msg || '请求失败')) return Promise.reject(new Error(msg || '请求失败'))
} }
}, },
(error) => { (error) => {
let message = '网络错误' let message = '网络异常'
if (error.response) { if (error.response) {
switch (error.response.status) { switch (error.response.status) {
case 401: case 401:
@ -59,31 +90,16 @@ service.interceptors.response.use(
message = '服务器内部错误' message = '服务器内部错误'
break break
default: default:
message = `连接错误${error.response.status}` message = `连接错误 ${error.response.status}`
} }
} else if (error.code === 'ECONNABORTED') { } else if (error.code === 'ECONNABORTED') {
message = '请求超时' message = '请求超时'
} else {
message = error.message || '网络异常'
} }
ElMessage.error(message) ElMessage.error(message)
return Promise.reject(error) return Promise.reject(error)
} }
) )
// 封装请求方法
export const get = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<T> => {
return service.get(url, { params, ...config })
}
export const post = <T = any>(url: string, data?: object, config?: AxiosRequestConfig): Promise<T> => {
return service.post(url, data, config)
}
export const put = <T = any>(url: string, data?: object, config?: AxiosRequestConfig): Promise<T> => {
return service.put(url, data, config)
}
export const del = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<T> => {
return service.delete(url, { params, ...config })
}
export default service export default service

@ -11,12 +11,23 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElForm: typeof import('element-plus/es')['ElForm'] ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
HelloWorld: typeof import('./components/HelloWorld.vue')['default'] HelloWorld: typeof import('./components/HelloWorld.vue')['default']
ParentView: typeof import('./components/ParentView.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
Sidebar: typeof import('./components/layout/Sidebar/index.vue')['default'] Sidebar: typeof import('./components/layout/Sidebar/index.vue')['default']

@ -0,0 +1,7 @@
<template>
<router-view />
</template>
<script setup lang="ts">
// Parent view for routes with children
</script>

@ -0,0 +1,38 @@
import type { Directive, DirectiveBinding } from 'vue'
import { hasPermission, hasRole } from '@/utils/permission'
/**
* v-hasPermi
* : <el-button v-hasPermi="'system:user:add'"></el-button>
* <el-button v-hasPermi="['system:user:add', 'system:user:edit']"></el-button>
*/
export const hasPermi: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding
if (value) {
const permissions = Array.isArray(value) ? value : [value]
const hasPerm = permissions.some((p: string) => hasPermission(p))
if (!hasPerm) {
el.parentNode?.removeChild(el)
}
}
},
}
/**
* v-hasRole
* : <div v-hasRole="'admin'"></div>
* <div v-hasRole="['admin', 'editor']"></div>
*/
export const hasRoleDir: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding
if (value) {
const roles = Array.isArray(value) ? value : [value]
const hasRolePerm = roles.some((r: string) => hasRole(r))
if (!hasRolePerm) {
el.parentNode?.removeChild(el)
}
}
},
}

@ -0,0 +1,215 @@
<template>
<div class="app-wrapper" :class="{ 'sidebar-collapsed': appStore.sidebarCollapsed }">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-logo">
<h1>RuoYi</h1>
</div>
<el-menu
:default-active="activeMenu"
:collapse="appStore.sidebarCollapsed"
background-color="var(--bg1)"
text-color="var(--t2)"
active-text-color="var(--primary)"
router
>
<template v-for="route in permissionStore.sidebarRoutes" :key="route.path">
<el-menu-item v-if="!route.children || route.children.length === 0" :index="resolvePath(route.path)">
<el-icon v-if="route.meta?.icon">
<component :is="route.meta.icon" />
</el-icon>
<template #title>{{ route.meta?.title }}</template>
</el-menu-item>
<el-sub-menu v-else :index="resolvePath(route.path)">
<template #title>
<el-icon v-if="route.meta?.icon">
<component :is="route.meta.icon" />
</el-icon>
<span>{{ route.meta?.title }}</span>
</template>
<el-menu-item
v-for="child in route.children"
:key="child.path"
:index="resolvePath(route.path, child.path)"
>
{{ child.meta?.title }}
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</aside>
<!-- Main Container -->
<div class="main-container">
<!-- Navbar -->
<header class="navbar">
<div class="navbar-left">
<el-icon class="hamburger" @click="appStore.toggleSidebar">
<Fold v-if="!appStore.sidebarCollapsed" />
<Expand v-else />
</el-icon>
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="item in breadcrumbs" :key="item.path">
{{ item.meta?.title }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="navbar-right">
<el-dropdown @command="handleCommand">
<span class="user-info">
<el-avatar :size="28" :src="userStore.userInfo?.avatar" />
<span class="username">{{ userStore.userInfo?.userName }}</span>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">个人中心</el-dropdown-item>
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</header>
<!-- App Main -->
<main class="app-main">
<router-view v-slot="{ Component }">
<keep-alive :include="cachedViews">
<component :is="Component" />
</keep-alive>
</router-view>
</main>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Fold, Expand } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/modules/user'
import { usePermissionStore } from '@/stores/modules/permission'
import { useAppStore } from '@/stores/modules/app'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const permissionStore = usePermissionStore()
const appStore = useAppStore()
const activeMenu = computed(() => route.path)
const breadcrumbs = computed(() => {
return route.matched.filter(item => item.meta?.title)
})
const cachedViews = computed(() => permissionStore.cachedViews)
function resolvePath(parentPath: string, childPath?: string): string {
if (!childPath) return parentPath
if (parentPath.endsWith('/')) return `${parentPath}${childPath}`
return `${parentPath}/${childPath}`
}
async function handleCommand(command: string) {
if (command === 'logout') {
await userStore.logout()
router.push('/login')
} else if (command === 'profile') {
// TODO: navigate to profile page
}
}
</script>
<style scoped lang="scss">
.app-wrapper {
display: flex;
height: 100vh;
width: 100%;
}
.sidebar {
width: 210px;
background-color: var(--bg1);
border-right: 1px solid var(--bd);
transition: width 0.3s;
overflow: hidden;
display: flex;
flex-direction: column;
.sidebar-logo {
height: 50px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg2);
border-bottom: 1px solid var(--bd);
h1 {
margin: 0;
font-size: 18px;
color: var(--primary);
white-space: nowrap;
}
}
.el-menu {
flex: 1;
border-right: none;
overflow-y: auto;
}
}
.sidebar-collapsed .sidebar {
width: 64px;
}
.main-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.navbar {
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
background-color: var(--bg1);
border-bottom: 1px solid var(--bd);
.navbar-left {
display: flex;
align-items: center;
gap: 12px;
.hamburger {
cursor: pointer;
font-size: 20px;
color: var(--t1);
}
}
.navbar-right {
.user-info {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
.username {
font-size: 14px;
color: var(--t1);
}
}
}
}
.app-main {
flex: 1;
padding: 16px;
background-color: var(--bg0);
overflow-y: auto;
}
</style>

@ -1,15 +1,13 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue' import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import { createPinia } from 'pinia' import pinia from './stores'
import '@/styles/index.scss' import { hasPermi, hasRoleDir } from '@/directives/permission'
const app = createApp(App) const app = createApp(App)
@ -18,8 +16,12 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component) app.component(key, component)
} }
app.use(createPinia()) // Register custom directives
app.directive('hasPermi', hasPermi)
app.directive('hasRole', hasRoleDir)
app.use(pinia)
app.use(router) app.use(router)
app.use(ElementPlus, { locale: zhCn }) app.use(ElementPlus)
app.mount('#app') app.mount('#app')

@ -1,7 +1,7 @@
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
/** /**
* * 访
*/ */
export const constantRoutes: RouteRecordRaw[] = [ export const constantRoutes: RouteRecordRaw[] = [
{ {
@ -11,15 +11,40 @@ export const constantRoutes: RouteRecordRaw[] = [
meta: { title: '登录', hidden: true }, meta: { title: '登录', hidden: true },
}, },
{ {
path: '/', path: '/redirect/:path(.*)',
name: 'Home', name: 'Redirect',
component: () => import('@/views/index.vue'), component: () => import('@/views/redirect.vue'),
meta: { title: '首页' }, meta: { hidden: true },
}, },
{ {
path: '/:pathMatch(.*)*', path: '/404',
name: 'NotFound', name: 'NotFound',
component: () => import('@/views/404.vue'), component: () => import('@/views/error/404.vue'),
meta: { title: '404', hidden: true }, meta: { title: '404', hidden: true },
}, },
{
path: '/401',
name: 'Unauthorized',
component: () => import('@/views/error/401.vue'),
meta: { title: '401', hidden: true },
},
{
path: '/',
component: () => import('@/layout/index.vue'),
redirect: '/index',
children: [
{
path: 'index',
name: 'Index',
component: () => import('@/views/index.vue'),
meta: { title: '首页', icon: 'HomeFilled', affix: true },
},
],
},
// Catch-all 404 - must be added last
{
path: '/:pathMatch(.*)*',
redirect: '/404',
meta: { hidden: true },
},
] ]

@ -1,22 +1,52 @@
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
/** /**
* *
*
*
* 1.
* 2. /getRouters
* 3. 使 usePermissionStore.generateRoutes()
* 4. router.addRoute()
*
*
* {
* path: '/system',
* menuName: '系统管理',
* component: 'Layout',
* icon: 'Setting',
* visible: '0', // '0' 显示, '1' 隐藏
* children: [...]
* }
*/ */
export const dynamicRoutes: RouteRecordRaw[] = [
// 示例:系统管理路由 // 动态路由占位(实际由 generateRoutes 从服务端获取)
// { export const dynamicRoutes: RouteRecordRaw[] = []
// path: '/system',
// name: 'System', /**
// component: () => import('@/components/layout/index.vue'), *
// meta: { title: '系统管理' }, * @param routes
// children: [ * @param roles
// { */
// path: 'user', export function filterAsyncRoutes(routes: RouteRecordRaw[], roles: string[]): RouteRecordRaw[] {
// name: 'SystemUser', const result: RouteRecordRaw[] = []
// component: () => import('@/views/system/user/index.vue'),
// meta: { title: '用户管理' }, routes.forEach(route => {
// }, const tmp = { ...route }
// ], if (hasPermission(roles, tmp)) {
// }, if (tmp.children) {
] tmp.children = filterAsyncRoutes(tmp.children, roles)
}
result.push(tmp)
}
})
return result
}
function hasPermission(roles: string[], route: RouteRecordRaw): boolean {
if (!route.meta?.roles) {
return true
}
return roles.some(role => (route.meta?.roles as string[])?.includes(role))
}

@ -1,6 +1,12 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
import { constantRoutes } from './constant' import { constantRoutes } from './constant'
import { getToken } from '@/utils/auth'
import { useUserStore } from '@/stores/modules/user'
import { usePermissionStore } from '@/stores/modules/permission'
// White list of paths that don't require authentication
const whiteList = ['/login', '/404', '/401', '/register']
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -8,4 +14,52 @@ const router = createRouter({
scrollBehavior: () => ({ top: 0 }), scrollBehavior: () => ({ top: 0 }),
}) })
// Navigation guard
router.beforeEach(async (to, _from, next) => {
const token = getToken()
const userStore = useUserStore()
const permissionStore = usePermissionStore()
if (token) {
// Already logged in
if (to.path === '/login') {
next({ path: '/' })
} else {
// Check if user info has been loaded
if (userStore.roles.length === 0) {
try {
// Get user info
await userStore.getInfo()
// Generate dynamic routes
const accessRoutes = await permissionStore.generateRoutes()
// Add dynamic routes to router
accessRoutes.forEach(route => {
if (!router.hasRoute(route.name || route.path)) {
router.addRoute(route)
}
})
// Re-navigate to the target route with newly added routes
next({ ...to, replace: true })
} catch (error) {
// Failed to get user info, reset token and redirect to login
await userStore.fedLogout()
next(`/login?redirect=${to.path}`)
}
} else {
next()
}
}
} else {
// No token
if (whiteList.includes(to.path)) {
next()
} else {
next(`/login?redirect=${to.path}`)
}
}
})
export default router export default router

@ -0,0 +1,50 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
const SIDEBAR_STATUS_KEY = 'sidebarStatus'
function getSidebarStatus(): boolean {
const status = localStorage.getItem(SIDEBAR_STATUS_KEY)
return status !== null ? status === '1' : false
}
export const useAppStore = defineStore('app', () => {
const sidebarCollapsed = ref<boolean>(getSidebarStatus())
const device = ref<'desktop' | 'mobile'>('desktop')
const size = ref<'default' | 'large' | 'small'>('default')
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value
localStorage.setItem(SIDEBAR_STATUS_KEY, sidebarCollapsed.value ? '1' : '0')
}
function closeSidebar() {
sidebarCollapsed.value = true
localStorage.setItem(SIDEBAR_STATUS_KEY, '1')
}
function openSidebar() {
sidebarCollapsed.value = false
localStorage.setItem(SIDEBAR_STATUS_KEY, '0')
}
function setDevice(val: 'desktop' | 'mobile') {
device.value = val
}
function setSize(val: 'default' | 'large' | 'small') {
size.value = val
localStorage.setItem('size', val)
}
return {
sidebarCollapsed,
device,
size,
toggleSidebar,
closeSidebar,
openSidebar,
setDevice,
setSize,
}
})

@ -0,0 +1,160 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import { constantRoutes } from '@/router/constant'
import request from '@/api/request'
// Component loader for dynamic routes
const modules = import.meta.glob('@/views/**/*.vue')
// Layout component - lazy import
const Layout = () => import('@/layout/index.vue')
const ParentView = () => import('@/components/ParentView.vue')
export interface RouteMeta {
title?: string
icon?: string
hidden?: boolean
noCache?: boolean
affix?: boolean
link?: string
}
export const usePermissionStore = defineStore('permission', () => {
const routes = ref<RouteRecordRaw[]>([])
const addRoutes = ref<RouteRecordRaw[]>([])
const sidebarRoutes = ref<RouteRecordRaw[]>([])
const cachedViews = ref<string[]>([])
/**
*
*/
async function generateRoutes(): Promise<RouteRecordRaw[]> {
try {
const res = await request({
url: '/getRouters',
method: 'get',
})
const rawMenus = res.data || res
const accessedRoutes = filterAsyncRouter(rawMenus)
routes.value = [...constantRoutes, ...accessedRoutes]
addRoutes.value = accessedRoutes
sidebarRoutes.value = filterHiddenRoutes(accessedRoutes)
return accessedRoutes
} catch (error) {
console.error('Failed to generate routes:', error)
return []
}
}
/**
*
*/
function filterAsyncRouter(menuList: any[]): RouteRecordRaw[] {
return menuList
.filter((menu: any) => menu.visible !== '1' || menu.children)
.map((menu: any) => {
const route: any = {
path: menu.path,
name: menu.name || convertRouteName(menu.path),
meta: {
title: menu.menuName || menu.meta?.title,
icon: menu.icon || menu.meta?.icon,
hidden: menu.visible === '1',
noCache: menu.noCache === true,
link: menu.isFrame === 0 ? menu.path : undefined,
},
}
// Handle component
if (menu.component === 'Layout') {
route.component = Layout
} else if (menu.component === 'ParentView') {
route.component = ParentView
} else if (menu.component) {
const componentPath = menu.component.startsWith('/')
? menu.component
: `/src/views/${menu.component}.vue`
const componentLoader = modules[componentPath]
route.component = componentLoader || (() => import('@/views/error/404.vue'))
}
// Handle children
if (menu.children && menu.children.length > 0) {
route.children = filterAsyncRouter(menu.children)
}
// Handle redirect
if (menu.redirect) {
route.redirect = menu.redirect
}
return route as RouteRecordRaw
})
}
/**
*
*/
function filterHiddenRoutes(routeList: RouteRecordRaw[]): RouteRecordRaw[] {
return routeList
.filter(r => !r.meta?.hidden)
.map(r => ({
...r,
children: r.children ? filterHiddenRoutes(r.children) : undefined,
})) as RouteRecordRaw[]
}
/**
* Convert path to route name (e.g., /system/user -> SystemUser)
*/
function convertRouteName(path: string): string {
return path
.split('/')
.filter(Boolean)
.map(segment => segment.charAt(0).toUpperCase() + segment.slice(1))
.join('')
}
/**
*
*/
function addCachedView(name: string) {
if (name && !cachedViews.value.includes(name)) {
cachedViews.value.push(name)
}
}
/**
*
*/
function delCachedView(name: string) {
const index = cachedViews.value.indexOf(name)
if (index > -1) {
cachedViews.value.splice(index, 1)
}
}
/**
*
*/
function resetRoutes() {
routes.value = []
addRoutes.value = []
sidebarRoutes.value = []
cachedViews.value = []
}
return {
routes,
addRoutes,
sidebarRoutes,
cachedViews,
generateRoutes,
addCachedView,
delCachedView,
resetRoutes,
}
})

@ -1,5 +1,12 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import { getToken, setToken, removeToken } from '@/utils/auth'
import axios from 'axios'
const apiClient = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API || '/dev-api',
timeout: 60000,
})
export interface UserInfo { export interface UserInfo {
userId: number userId: number
@ -11,27 +18,56 @@ export interface UserInfo {
} }
export const useUserStore = defineStore('user', () => { export const useUserStore = defineStore('user', () => {
const token = ref<string>('') const token = ref<string>(getToken())
const userInfo = ref<UserInfo | null>(null) const userInfo = ref<UserInfo | null>(null)
const roles = ref<string[]>([])
const permissions = ref<string[]>([])
async function login(username: string, password: string, code?: string, uuid?: string) {
const { data } = await apiClient.post('/login', { username, password, code, uuid })
setToken(data.token)
token.value = data.token
return data
}
async function getInfo() {
const { data } = await apiClient.get('/getInfo')
const user = data.user
userInfo.value = user
roles.value = user.roles || []
permissions.value = user.permissions || []
return data
}
function setToken(newToken: string) { async function logout() {
token.value = newToken try {
await apiClient.post('/logout')
} finally {
resetState()
}
} }
function setUserInfo(info: UserInfo) { async function fedLogout() {
userInfo.value = info resetState()
} }
function logout() { function resetState() {
token.value = '' token.value = ''
userInfo.value = null userInfo.value = null
roles.value = []
permissions.value = []
removeToken()
} }
return { return {
token, token,
userInfo, userInfo,
setToken, roles,
setUserInfo, permissions,
login,
getInfo,
logout, logout,
fedLogout,
resetState,
} }
}) })

@ -1,7 +1,5 @@
/// <reference types="vite/client" /> export interface RouteMeta {
title?: string
declare module '*.vue' { icon?: string
import type { DefineComponent } from 'vue' hidden?: boolean
const component: DefineComponent<object, object, any>
export default component
} }

@ -0,0 +1,13 @@
const TOKEN_KEY = 'Admin-Token'
export function getToken(): string {
return localStorage.getItem(TOKEN_KEY) || ''
}
export function setToken(token: string): void {
localStorage.setItem(TOKEN_KEY, token)
}
export function removeToken(): void {
localStorage.removeItem(TOKEN_KEY)
}

@ -0,0 +1,48 @@
import { useUserStore } from '@/stores/modules/user'
/**
*
*/
export function hasPermission(permission: string): boolean {
const userStore = useUserStore()
const allPermission = '*:*:*'
if (userStore.roles.includes('admin')) {
return true
}
return userStore.permissions.includes(allPermission) || userStore.permissions.includes(permission)
}
/**
*
*/
export function hasRole(role: string): boolean {
const userStore = useUserStore()
if (userStore.roles.includes('admin')) {
return true
}
return userStore.roles.includes(role)
}
/**
*
*/
export function checkPermi(permissions: string[]): boolean {
if (!permissions || permissions.length === 0) {
return false
}
return permissions.some(p => hasPermission(p))
}
/**
*
*/
export function checkRole(roles: string[]): boolean {
if (!roles || roles.length === 0) {
return false
}
return roles.some(r => hasRole(r))
}

@ -0,0 +1,34 @@
/**
* URL
*/
export function isExternal(path: string): boolean {
return /^(https?:|mailto:|tel:)/.test(path)
}
/**
*
*/
export function validUsername(str: string): boolean {
return str.trim().length >= 2
}
/**
*
*/
export function validPassword(str: string): boolean {
return str.length >= 5
}
/**
*
*/
export function isPhone(str: string): boolean {
return /^1[3-9]\d{9}$/.test(str)
}
/**
*
*/
export function isEmail(str: string): boolean {
return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(str)
}

@ -0,0 +1,40 @@
<template>
<div class="error-container">
<div class="error-content">
<h1 class="error-code">401</h1>
<p class="error-message">未授权请重新登录</p>
<el-button type="primary" @click="$router.push('/login')"></el-button>
</div>
</div>
</template>
<script setup lang="ts">
// 401 error page
</script>
<style scoped lang="scss">
.error-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: var(--bg0);
}
.error-content {
text-align: center;
.error-code {
font-size: 120px;
font-weight: bold;
color: var(--danger);
margin: 0;
}
.error-message {
font-size: 24px;
color: var(--t2);
margin: 16px 0 32px;
}
}
</style>

@ -0,0 +1,40 @@
<template>
<div class="error-container">
<div class="error-content">
<h1 class="error-code">404</h1>
<p class="error-message">页面不存在</p>
<el-button type="primary" @click="$router.push('/')"></el-button>
</div>
</div>
</template>
<script setup lang="ts">
// 404 error page
</script>
<style scoped lang="scss">
.error-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: var(--bg0);
}
.error-content {
text-align: center;
.error-code {
font-size: 120px;
font-weight: bold;
color: var(--primary);
margin: 0;
}
.error-message {
font-size: 24px;
color: var(--t2);
margin: 16px 0 32px;
}
}
</style>

@ -36,12 +36,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { User, Lock } from '@element-plus/icons-vue' import { User, Lock } from '@element-plus/icons-vue'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores/modules/user'
const router = useRouter() const userStore = useUserStore()
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const loading = ref(false) const loading = ref(false)
@ -62,11 +61,8 @@ const handleLogin = async () => {
if (valid) { if (valid) {
loading.value = true loading.value = true
try { try {
// TODO: API await userStore.login(loginForm.username, loginForm.password)
// const res = await login(loginForm.username, loginForm.password) window.location.href = '/'
// localStorage.setItem('token', res.token)
// router.push('/')
ElMessage.info('登录功能待实现')
} catch (error) { } catch (error) {
console.error('Login failed:', error) console.error('Login failed:', error)
} finally { } finally {

@ -0,0 +1,17 @@
<template>
<div></div>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
import { onMounted } from 'vue'
const route = useRoute()
const router = useRouter()
onMounted(() => {
const { params, query } = route
const { path } = params
router.replace({ path: `/${path}`, query })
})
</script>

@ -4,6 +4,7 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"], "types": ["vite/client"],
"baseUrl": ".", "baseUrl": ".",
"ignoreDeprecations": "6.0",
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
}, },

Loading…
Cancel
Save