修改了 README
This commit is contained in:
1 parent
9ea096538b
commit
2b7e02108c
5 files changed
+496
-1
No files matched your search
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in new issue
Block a user