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 管理系统

@ -1,3 +1,3 @@
# 生产环境配置
VITE_API_BASE_URL=/prod-api
VITE_APP_BASE_API=/prod-api
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 type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'
import type { AxiosInstance, AxiosResponse } from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getToken, removeToken } from '@/utils/auth'
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/dev-api',
timeout: 15000,
baseURL: import.meta.env.VITE_APP_BASE_API || '/dev-api',
timeout: 60000,
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
@ -13,10 +14,23 @@ const service: AxiosInstance = axios.create({
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 添加 token
const token = localStorage.getItem('token')
// 自动注入 Token
const token = getToken()
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
},
@ -28,22 +42,39 @@ service.interceptors.request.use(
// 响应拦截器
service.interceptors.response.use(
(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) {
return data
return { data, msg, code }
} else if (code === 401) {
ElMessage.error('登录已过期,请重新登录')
localStorage.removeItem('token')
window.location.href = '/login'
ElMessageBox.confirm('登录状态已过期,请重新登录', '系统提示', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
removeToken()
window.location.href = '/login'
})
.catch(() => {})
return Promise.reject(new Error(msg || '登录已过期'))
} else if (code === 500) {
ElMessage.error(msg || '系统内部错误')
return Promise.reject(new Error(msg || '系统内部错误'))
} else {
ElMessage.error(msg || '请求失败')
return Promise.reject(new Error(msg || '请求失败'))
}
},
(error) => {
let message = '网络错误'
let message = '网络异常'
if (error.response) {
switch (error.response.status) {
case 401:
@ -59,31 +90,16 @@ service.interceptors.response.use(
message = '服务器内部错误'
break
default:
message = `连接错误${error.response.status}`
message = `连接错误 ${error.response.status}`
}
} else if (error.code === 'ECONNABORTED') {
message = '请求超时'
} else {
message = error.message || '网络异常'
}
ElMessage.error(message)
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

@ -11,12 +11,23 @@ export {}
/* prettier-ignore */
declare module 'vue' {
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']
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']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
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']
ParentView: typeof import('./components/ParentView.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
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 ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import App from './App.vue'
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)
@ -18,8 +16,12 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
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(ElementPlus, { locale: zhCn })
app.use(ElementPlus)
app.mount('#app')

@ -1,7 +1,7 @@
import type { RouteRecordRaw } from 'vue-router'
/**
*
* 访
*/
export const constantRoutes: RouteRecordRaw[] = [
{
@ -11,15 +11,40 @@ export const constantRoutes: RouteRecordRaw[] = [
meta: { title: '登录', hidden: true },
},
{
path: '/',
name: 'Home',
component: () => import('@/views/index.vue'),
meta: { title: '首页' },
path: '/redirect/:path(.*)',
name: 'Redirect',
component: () => import('@/views/redirect.vue'),
meta: { hidden: true },
},
{
path: '/:pathMatch(.*)*',
path: '/404',
name: 'NotFound',
component: () => import('@/views/404.vue'),
component: () => import('@/views/error/404.vue'),
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'
/**
*
*
*
*
* 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[] = [
// 示例:系统管理路由
// {
// path: '/system',
// name: 'System',
// component: () => import('@/components/layout/index.vue'),
// meta: { title: '系统管理' },
// children: [
// {
// path: 'user',
// name: 'SystemUser',
// component: () => import('@/views/system/user/index.vue'),
// meta: { title: '用户管理' },
// },
// ],
// },
]
// 动态路由占位(实际由 generateRoutes 从服务端获取)
export const dynamicRoutes: RouteRecordRaw[] = []
/**
*
* @param routes
* @param roles
*/
export function filterAsyncRoutes(routes: RouteRecordRaw[], roles: string[]): RouteRecordRaw[] {
const result: RouteRecordRaw[] = []
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 type { RouteRecordRaw } from 'vue-router'
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({
history: createWebHistory(import.meta.env.BASE_URL),
@ -8,4 +14,52 @@ const router = createRouter({
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

@ -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 { 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 {
userId: number
@ -11,27 +18,56 @@ export interface UserInfo {
}
export const useUserStore = defineStore('user', () => {
const token = ref<string>('')
const token = ref<string>(getToken())
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) {
token.value = newToken
async function logout() {
try {
await apiClient.post('/logout')
} finally {
resetState()
}
}
function setUserInfo(info: UserInfo) {
userInfo.value = info
async function fedLogout() {
resetState()
}
function logout() {
function resetState() {
token.value = ''
userInfo.value = null
roles.value = []
permissions.value = []
removeToken()
}
return {
token,
userInfo,
setToken,
setUserInfo,
roles,
permissions,
login,
getInfo,
logout,
fedLogout,
resetState,
}
})

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

@ -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">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { User, Lock } from '@element-plus/icons-vue'
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 loading = ref(false)
@ -57,16 +56,13 @@ const rules = {
const handleLogin = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
loading.value = true
try {
// TODO: API
// const res = await login(loginForm.username, loginForm.password)
// localStorage.setItem('token', res.token)
// router.push('/')
ElMessage.info('登录功能待实现')
await userStore.login(loginForm.username, loginForm.password)
window.location.href = '/'
} catch (error) {
console.error('Login failed:', error)
} 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",
"types": ["vite/client"],
"baseUrl": ".",
"ignoreDeprecations": "6.0",
"paths": {
"@/*": ["src/*"]
},

Loading…
Cancel
Save