修改了 README

This commit is contained in:
TermiNexus committed 2026-08-01 02:23:48 +08:00
1 parent 9ea096538b
commit 2b7e02108c
5 files changed
+496 -1

No files matched your search

+3 -1
View File
@@ -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 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+259
View File
@@ -0,0 +1,259 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAppStore } from '@/stores/app'
import { useUserStore } from '@/stores/user'
import { getAvatarUrl } from '@/api/auth'
const appStore = useAppStore()
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
// 顶部导航标题(取路由 meta.title)
const navTitle = computed(() => (route.meta.title as string) || 'GERP')
// 当前激活的菜单路径
const activeMenu = computed(() => route.path)
// 头像 URL
const avatarUrl = getAvatarUrl()
onMounted(() => {
appStore.initTheme()
})
function handleMenuSelect(index: string): void {
router.push(index)
}
async function handleLogout(): Promise<void> {
await userStore.logout()
router.push('/login')
}
function handleToggleSidebar(): void {
appStore.toggleSidebar()
}
function handleToggleTheme(): void {
appStore.toggleTheme()
}
</script>
<template>
<div class="gerp-layout">
<!-- 顶部导航 -->
<header class="gerp-header">
<div class="gerp-header-left">
<el-button text class="gerp-toggle-btn" @click="handleToggleSidebar">
<el-icon :size="18"><Menu /></el-icon>
</el-button>
<div class="gerp-logo">
<img src="/favicon.svg" alt="logo" class="gerp-logo-icon" />
<span class="gerp-logo-text">GERP 管理系统</span>
</div>
</div>
<div class="gerp-header-center">
<h1 class="gerp-nav-title">{{ navTitle }}</h1>
</div>
<div class="gerp-header-right">
<div class="gerp-header-actions">
<el-button text circle title="搜索">
<el-icon><Search /></el-icon>
</el-button>
<el-badge :value="3" class="gerp-notify-badge">
<el-button text circle title="通知">
<el-icon><Bell /></el-icon>
</el-button>
</el-badge>
<el-button text circle title="主题" @click="handleToggleTheme">
<el-icon><Setting /></el-icon>
</el-button>
</div>
<el-dropdown trigger="click" @command="(cmd) => cmd === 'logout' && handleLogout()">
<div class="gerp-user-info">
<img :src="avatarUrl" alt="头像" class="gerp-user-avatar" />
<span class="gerp-user-name">{{ userStore.displayName }}</span>
<el-icon><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="router.push('/profile')">
<el-icon><User /></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>
</header>
<div class="gerp-main">
<!-- 侧边栏 -->
<aside class="gerp-sidebar" :class="{ collapsed: appStore.sidebarCollapsed }">
<el-scrollbar>
<el-menu
:default-active="activeMenu"
:collapse="appStore.sidebarCollapsed"
:collapse-transition="false"
router
@select="handleMenuSelect"
>
<template v-for="menu in userStore.menus" :key="menu.Path || menu.Title">
<!-- 含子菜单 -->
<el-sub-menu v-if="menu.Children?.length" :index="menu.Path || menu.Title">
<template #title>
<el-icon v-if="menu.Icon"><component :is="menu.Icon" /></el-icon>
<span>{{ menu.Title }}</span>
</template>
<el-menu-item
v-for="child in menu.Children"
:key="child.Path || child.Title"
:index="child.Path || ''"
>
<el-icon v-if="child.Icon"><component :is="child.Icon" /></el-icon>
<template #title>{{ child.Title }}</template>
</el-menu-item>
</el-sub-menu>
<!-- 无子菜单 -->
<el-menu-item v-else :index="menu.Path || ''">
<el-icon v-if="menu.Icon"><component :is="menu.Icon" /></el-icon>
<template #title>{{ menu.Title }}</template>
</el-menu-item>
</template>
</el-menu>
</el-scrollbar>
</aside>
<!-- 主内容区 -->
<main class="gerp-content">
<RouterView v-slot="{ Component }">
<component :is="Component" />
</RouterView>
</main>
</div>
</div>
</template>
<style scoped lang="scss">
.gerp-layout {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.gerp-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 56px;
padding: 0 16px;
background: #fff;
border-bottom: 1px solid var(--gerp-border-color);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
z-index: 100;
}
.gerp-header-left,
.gerp-header-right {
display: flex;
align-items: center;
gap: 12px;
}
.gerp-logo {
display: flex;
align-items: center;
gap: 8px;
&-icon {
width: 28px;
height: 28px;
}
&-text {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.gerp-toggle-btn {
padding: 6px;
}
.gerp-nav-title {
margin: 0;
font-size: 16px;
font-weight: 500;
}
.gerp-header-actions {
display: flex;
align-items: center;
gap: 4px;
}
.gerp-user-info {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 0 8px;
border-radius: 6px;
transition: background 0.2s;
&:hover {
background: #f5f7fa;
}
}
.gerp-user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.gerp-user-name {
font-size: 14px;
color: #303133;
}
.gerp-main {
display: flex;
flex: 1;
overflow: hidden;
}
.gerp-sidebar {
width: 220px;
background: #fff;
border-right: 1px solid var(--gerp-border-color);
transition: width 0.2s;
overflow: hidden;
&.collapsed {
width: 64px;
}
:deep(.el-menu) {
border-right: none;
}
}
.gerp-content {
flex: 1;
overflow: auto;
padding: 16px;
background: var(--gerp-bg-color);
}
</style>
+79
View File
@@ -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
+45
View File
@@ -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,
}
})
+110
View File
@@ -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<UserInfo | null>(null)
const roles = ref<string[]>([])
const menus = ref<MenuItem[]>([])
const token = ref<string>('')
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<MenuItem[]>(() => {
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<void> {
loading.value = true
try {
await apiLogin(params)
await fetchUserInfo()
await fetchMenus()
} finally {
loading.value = false
}
}
/** 注销 */
async function logout(): Promise<void> {
try {
await apiLogout()
} finally {
reset()
}
}
/** 获取当前用户信息 */
async function fetchUserInfo(): Promise<UserInfo> {
const info = await getCurrentUser()
userInfo.value = info
return info
}
/** 获取角色列表 */
async function fetchRoles(): Promise<string[]> {
const list = await getUserRoles()
roles.value = list
return list
}
/** 获取菜单 */
async function fetchMenus(): Promise<MenuItem[]> {
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,
}
})