From 2b7e02108cc60062b6257769536628e3e1f19c58 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 1 Aug 2026 02:23:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- src/layouts/DefaultLayout.vue | 259 ++++++++++++++++++++++++++++++++++ src/router/index.ts | 79 +++++++++++ src/stores/app.ts | 45 ++++++ src/stores/user.ts | 110 +++++++++++++++ 5 files changed, 496 insertions(+), 1 deletion(-) create mode 100644 src/layouts/DefaultLayout.vue create mode 100644 src/router/index.ts create mode 100644 src/stores/app.ts create mode 100644 src/stores/user.ts diff --git a/README.md b/README.md index 33895ab..32dfb20 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# Vue 3 + TypeScript + Vite +# GERP + +An ERP backend built using Vue, TypeScript, and Vite, handed over for use in the SAAS scenarios under GVSDS. This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + + + diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..d874fa0 --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,79 @@ +import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' +import { useUserStore } from '@/stores/user' + +/** 静态路由(无需权限) */ +export const staticRoutes: RouteRecordRaw[] = [ + { + path: '/login', + name: 'Login', + component: () => import('@/views/Login.vue'), + meta: { title: '登录', requiresAuth: false, layout: 'blank' }, + }, + { + path: '/', + component: () => import('@/layouts/DefaultLayout.vue'), + redirect: '/dashboard', + children: [ + { + path: 'dashboard', + name: 'Dashboard', + component: () => import('@/views/Dashboard.vue'), + meta: { title: '仪表盘', icon: 'Odometer', requiresAuth: true }, + }, + { + path: 'profile', + name: 'Profile', + component: () => import('@/views/Profile.vue'), + meta: { title: '个人中心', icon: 'User', requiresAuth: true }, + }, + ], + }, + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: () => import('@/views/NotFound.vue'), + meta: { title: '页面不存在', requiresAuth: false, layout: 'blank' }, + }, +] + +/** 动态路由(从后端菜单加载后注入) */ +export const dynamicRoutes: RouteRecordRaw[] = [] + +const router = createRouter({ + history: createWebHistory(), + routes: staticRoutes, + scrollBehavior(_to, _from, savedPosition) { + return savedPosition || { top: 0 } + }, +}) + +/** 全局前置守卫:登录态校验、动态路由注入 */ +router.beforeEach(async (to, _from, next) => { + // 设置页面标题 + document.title = (to.meta.title as string) || 'GERP 管理系统' + + const userStore = useUserStore() + + // 不需要登录的页面直接放行 + if (to.meta.requiresAuth === false) { + next() + return + } + + // 已登录但未加载用户信息:尝试加载 + if (!userStore.isLoggedIn) { + try { + await userStore.fetchUserInfo() + await userStore.fetchMenus() + // TODO: 注入动态路由后 next({ ...to, replace: true }) + } catch { + // 加载失败:跳转登录 + next({ name: 'Login', query: { redirect: to.fullPath } }) + return + } + } + + next() +}) + +export default router diff --git a/src/stores/app.ts b/src/stores/app.ts new file mode 100644 index 0000000..ee6f4d0 --- /dev/null +++ b/src/stores/app.ts @@ -0,0 +1,45 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +/** 应用状态:侧边栏折叠、主题、语言等全局 UI 状态 */ +export const useAppStore = defineStore('app', () => { + /** 侧边栏是否折叠 */ + const sidebarCollapsed = ref(false) + /** 当前主题:light / dark */ + const theme = ref<'light' | 'dark'>('light') + /** 当前语言 */ + const locale = ref<'zh-CN' | 'en-US'>('zh-CN') + + /** 切换侧边栏 */ + function toggleSidebar(): void { + sidebarCollapsed.value = !sidebarCollapsed.value + } + + /** 设置主题 */ + function setTheme(value: 'light' | 'dark'): void { + theme.value = value + document.documentElement.classList.toggle('dark', value === 'dark') + localStorage.setItem('gerp-theme', value) + } + + /** 初始化主题(从 localStorage 恢复) */ + function initTheme(): void { + const saved = localStorage.getItem('gerp-theme') as 'light' | 'dark' | null + if (saved) setTheme(saved) + } + + /** 切换主题 */ + function toggleTheme(): void { + setTheme(theme.value === 'light' ? 'dark' : 'light') + } + + return { + sidebarCollapsed, + theme, + locale, + toggleSidebar, + setTheme, + initTheme, + toggleTheme, + } +}) diff --git a/src/stores/user.ts b/src/stores/user.ts new file mode 100644 index 0000000..ff824d2 --- /dev/null +++ b/src/stores/user.ts @@ -0,0 +1,110 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { UserInfo, MenuItem } from '@/types/api' +import { getCurrentUser, getUserRoles, login as apiLogin, logout as apiLogout, type LoginParams } from '@/api/auth' +import { getMenuList } from '@/api/menu' + +/** 用户状态管理:当前登录用户、角色、菜单、权限 */ +export const useUserStore = defineStore('user', () => { + // === 状态 === + const userInfo = ref(null) + const roles = ref([]) + const menus = ref([]) + const token = ref('') + const loading = ref(false) + + // === 计算属性 === + const isLoggedIn = computed(() => !!userInfo.value) + const isAdmin = computed(() => userInfo.value?.IsAdmin === true) + const currentRole = computed(() => userInfo.value?.CurrentRole || '') + const avatar = computed(() => userInfo.value?.Avatar || '') + const displayName = computed(() => userInfo.value?.DisplayName || userInfo.value?.UserName || '未登录') + + /** 扁平化后的菜单(用于面包屑、菜单搜索) */ + const flatMenus = computed(() => { + const result: MenuItem[] = [] + const walk = (items: MenuItem[]) => { + for (const item of items) { + result.push(item) + if (item.Children?.length) walk(item.Children) + } + } + walk(menus.value) + return result + }) + + // === 动作 === + /** 登录 */ + async function login(params: LoginParams): Promise { + loading.value = true + try { + await apiLogin(params) + await fetchUserInfo() + await fetchMenus() + } finally { + loading.value = false + } + } + + /** 注销 */ + async function logout(): Promise { + try { + await apiLogout() + } finally { + reset() + } + } + + /** 获取当前用户信息 */ + async function fetchUserInfo(): Promise { + const info = await getCurrentUser() + userInfo.value = info + return info + } + + /** 获取角色列表 */ + async function fetchRoles(): Promise { + const list = await getUserRoles() + roles.value = list + return list + } + + /** 获取菜单 */ + async function fetchMenus(): Promise { + const list = await getMenuList() + menus.value = list + return list + } + + /** 重置状态(登出或登录失效时调用) */ + function reset(): void { + userInfo.value = null + roles.value = [] + menus.value = [] + token.value = '' + localStorage.removeItem('gerp-user') + } + + return { + // 状态 + userInfo, + roles, + menus, + token, + loading, + // 计算属性 + isLoggedIn, + isAdmin, + currentRole, + avatar, + displayName, + flatMenus, + // 动作 + login, + logout, + fetchUserInfo, + fetchRoles, + fetchMenus, + reset, + } +})