2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 09:13:49 +08:00

feat: 前端优化

This commit is contained in:
耗子
2024-10-19 18:01:18 +08:00
parent 2d10110eef
commit 93fc5e08f6
21 changed files with 856 additions and 206 deletions

View File

@@ -32,6 +32,7 @@
"luxon": "^3.5.0",
"marked": "^14.1.2",
"pinia": "^2.2.4",
"pinia-plugin-persistedstate": "^4.1.1",
"remove": "^0.1.5",
"vue": "^3.5.11",
"vue-echarts": "^7.0.3",

745
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@
import AppMain from './AppMain.vue'
import AppHeader from './header/IndexView.vue'
import SideBar from './sidebar/IndexView.vue'
import AppTab from './tab/IndexView.vue'
import { useThemeStore } from '@/store'
@@ -47,9 +46,6 @@ const themeStore = useThemeStore()
>
<AppHeader />
</header>
<section v-if="themeStore.tab.visible" border-b p-10 bc-eee sm:block dark:border-0>
<AppTab />
</section>
<section bg="#f5f6fb" flex-1 overflow-hidden dark:bg-hex-101014>
<AppMain />
</section>

View File

@@ -1,21 +1,28 @@
<script lang="ts" setup>
import ReloadPage from '@/layout/header/components/ReloadPage.vue'
import BreadCrumb from './components/BreadCrumb.vue'
import AppTab from '@/layout/tab/IndexView.vue'
import FullScreen from './components/FullScreen.vue'
import MenuCollapse from './components/MenuCollapse.vue'
import ThemeMode from './components/ThemeMode.vue'
import UserAvatar from './components/UserAvatar.vue'
import MenuCollapse from '@/layout/header/components/MenuCollapse.vue'
import { useThemeStore } from '@/store'
const themeStore = useThemeStore()
</script>
<template>
<div flex items-center>
<div w-full flex items-center justify-between>
<MenuCollapse />
<BreadCrumb ml-15 hidden sm:block />
</div>
<div ml-auto flex items-center>
<ReloadPage />
<ThemeMode />
<FullScreen />
<UserAvatar />
<section v-if="!themeStore.isMobile && themeStore.tab.visible" w-0 flex-1 px-12>
<AppTab />
</section>
<span v-if="!themeStore.isMobile && themeStore.tab.visible" mx-6 opacity-20>|</span>
<div ml-auto flex flex-shrink-0 items-center px-12>
<ReloadPage />
<ThemeMode />
<FullScreen />
<UserAvatar />
</div>
</div>
</template>

View File

@@ -1,63 +0,0 @@
<script lang="ts" setup>
import { renderIcon } from '@/utils'
import { useI18n } from 'vue-i18n'
import type { Meta } from '~/types/router'
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const generator: any = (routerMap: any) => {
return routerMap.map((item: any) => {
const currentMenu = {
...item,
label: t(String(item.meta.title)),
key: item.path,
disabled: item.path === '/',
icon: getIcon(item.meta)
}
// 是否有子菜单,并递归处理
if (item.children && item.children.length > 0) {
currentMenu.children = generator(item.children, currentMenu)
}
return currentMenu
})
}
const breadcrumbList = computed(() => {
return generator(route.matched)
})
function handleBreadClick(path: string) {
if (path === route.path) return
router.push(path)
}
function getIcon(meta?: Meta, size = 16) {
if (meta?.icon) return renderIcon(meta.icon, { size })
return ''
}
</script>
<template>
<n-breadcrumb>
<template v-for="routeItem in breadcrumbList" :key="routeItem.name">
<n-breadcrumb-item v-if="routeItem.meta.title">
<n-dropdown
v-if="routeItem.children.length"
:options="routeItem.children"
@select="handleBreadClick"
>
<span class="link-text">
<component :is="routeItem.icon" v-if="routeItem.icon" />
{{ $t(routeItem.meta.title) }}
</span>
</n-dropdown>
<span v-else class="link-text">
<component :is="routeItem.icon" v-if="routeItem.icon" />
{{ $t(routeItem.meta.title) }}
</span>
</n-breadcrumb-item>
</template>
</n-breadcrumb>
</template>

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup>
import user from '@/api/panel/user'
import { router } from '@/router'
import { useUserStore } from '@/store'
import { renderIcon } from '@/utils'
@@ -18,7 +19,7 @@ const options = [
}
]
function handleSelect(key: string) {
const handleSelect = (key: string) => {
if (key === 'logout') {
window.$dialog.info({
content: '确认退出?',
@@ -26,7 +27,9 @@ function handleSelect(key: string) {
positiveText: '确定',
negativeText: '取消',
onPositiveClick() {
userStore.logout()
user.logout().then(() => {
userStore.logout()
})
window.$message.success('已退出登录!')
}
})
@@ -35,12 +38,19 @@ function handleSelect(key: string) {
router.push({ name: 'setting-index' })
}
}
const username = computed(() => {
if (userStore.username !== '') {
return userStore.username
}
return '未知'
})
</script>
<template>
<n-dropdown :options="options" @select="handleSelect">
<div flex cursor-pointer items-center>
<span hidden sm:block>{{ userStore.username }}</span>
<span>{{ username }}</span>
</div>
</n-dropdown>
</template>

View File

@@ -3,7 +3,6 @@ import type { TabItem } from '@/store'
import { useTabStore } from '@/store'
import ContextMenu from './components/ContextMenu.vue'
const route = useRoute()
const router = useRouter()
const tabStore = useTabStore()
@@ -21,16 +20,6 @@ const contextMenuOption = reactive<ContextMenuOption>({
currentPath: ''
})
watch(
() => route.path,
() => {
const { name, fullPath: path } = route
const title = (route.meta?.title as string) || ''
tabStore.addTab({ name: name as string, path, title })
},
{ immediate: true }
)
function handleTagClick(path: string) {
tabStore.setActiveTab(path)
router.push(path)
@@ -61,12 +50,10 @@ async function handleContextMenu(e: MouseEvent, tabItem: TabItem) {
<template>
<div>
<n-tabs
:value="tabStore.activeTab"
:value="tabStore.active"
:closable="tabStore.tabs.length > 1"
type="card"
@close="(path: string) => tabStore.removeTab(path)"
bg-white
dark:bg-dark
>
<n-tab
v-for="item in tabStore.tabs"

View File

@@ -23,7 +23,7 @@ const options = computed(() => [
{
label: '重新加载',
key: 'reload',
disabled: props.currentPath !== tabStore.activeTab,
disabled: props.currentPath !== tabStore.active,
icon: renderIcon('mdi:refresh', { size: 14 })
},
{

View File

@@ -1,3 +1,4 @@
import { createTabGuard } from '@/router/guard/tab-guard'
import type { Router } from 'vue-router'
import { createAppInstallGuard } from './app-install-guard'
import { createPageLoadingGuard } from './page-loading-guard'
@@ -6,5 +7,6 @@ import { createPageTitleGuard } from './page-title-guard'
export function setupRouterGuard(router: Router) {
createPageLoadingGuard(router)
createPageTitleGuard(router)
createTabGuard(router)
createAppInstallGuard(router)
}

View File

@@ -0,0 +1,18 @@
import { useTabStore } from '@/store'
import type { Router } from 'vue-router'
export const EXCLUDE_TAB = ['/404', '/403', '/login']
export function createTabGuard(router: Router) {
router.afterEach((to) => {
if (EXCLUDE_TAB.includes(to.path)) return
const tabStore = useTabStore()
const { name, fullPath: path } = to
const title = String(to.meta?.title)
tabStore.addTab({
name: String(name),
path,
title
})
})
}

View File

@@ -1,4 +1,4 @@
import { usePermissionStore, useUserStore } from '@/store'
import { usePermissionStore } from '@/store'
import type { App } from 'vue'
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import type { RoutesType, RouteType } from '~/types/router'
@@ -22,8 +22,6 @@ export async function setupRouter(app: App) {
export async function addDynamicRoutes() {
try {
const userStore = useUserStore()
await userStore.getUserInfo()
const permissionStore = usePermissionStore()
const accessRoutes = permissionStore.generateRoutes(['admin'])
accessRoutes.forEach((route: RouteType) => {

View File

@@ -1,8 +1,11 @@
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import type { App } from 'vue'
export async function setupStore(app: App) {
app.use(createPinia())
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
}
export * from './modules'

View File

@@ -1,6 +0,0 @@
import { getSession } from '@/utils'
export const activeTab = getSession('activeTab')
export const tabs = getSession('tabs')
export const WITHOUT_TAB_PATHS = ['/404', '/login']

View File

@@ -1,29 +1,32 @@
import { router } from '@/router'
import { setSession } from '@/utils'
import { defineStore } from 'pinia'
import { activeTab, tabs, WITHOUT_TAB_PATHS } from './helpers'
export const WITHOUT_TAB_PATHS = ['/404', '/login']
export interface Tab {
active: string
tabs: Array<TabItem>
}
export interface TabItem {
name: string
path: string
title?: string
title: string
}
export const useTabStore = defineStore('tab', {
state() {
state: (): Tab => {
return {
tabs: <Array<TabItem>>tabs || [],
activeTab: <string>activeTab || ''
active: '',
tabs: []
}
},
actions: {
setActiveTab(path: string) {
this.activeTab = path
setSession('activeTab', path)
this.active = path
},
setTabs(tabs: Array<TabItem>) {
this.tabs = tabs
setSession('tabs', tabs)
},
addTab(tab: TabItem) {
this.setActiveTab(tab.path)
@@ -32,7 +35,7 @@ export const useTabStore = defineStore('tab', {
this.setTabs([...this.tabs, tab])
},
removeTab(path: string) {
if (path === this.activeTab) {
if (path === this.active) {
const activeIndex = this.tabs.findIndex((item) => item.path === path)
if (activeIndex > 0) router.push(this.tabs[activeIndex - 1].path)
else router.push(this.tabs[activeIndex + 1].path)
@@ -41,25 +44,26 @@ export const useTabStore = defineStore('tab', {
},
removeOther(curPath: string) {
this.setTabs(this.tabs.filter((tab) => tab.path === curPath))
if (curPath !== this.activeTab) router.push(this.tabs[this.tabs.length - 1].path)
if (curPath !== this.active) router.push(this.tabs[this.tabs.length - 1].path)
},
removeLeft(curPath: string) {
const curIndex = this.tabs.findIndex((item) => item.path === curPath)
const filterTabs = this.tabs.filter((item, index) => index >= curIndex)
this.setTabs(filterTabs)
if (!filterTabs.find((item) => item.path === this.activeTab))
if (!filterTabs.find((item) => item.path === this.active))
router.push(filterTabs[filterTabs.length - 1].path)
},
removeRight(curPath: string) {
const curIndex = this.tabs.findIndex((item) => item.path === curPath)
const filterTabs = this.tabs.filter((item, index) => index <= curIndex)
this.setTabs(filterTabs)
if (!filterTabs.find((item) => item.path === this.activeTab))
if (!filterTabs.find((item) => item.path === this.active))
router.push(filterTabs[filterTabs.length - 1].path)
},
resetTabs() {
this.setTabs([])
this.setActiveTab('')
}
}
},
persist: true
})

View File

@@ -13,7 +13,7 @@ interface ColorAction {
}
/** 初始化主题配置 */
export function initThemeSettings(): Theme.Setting {
export function defaultSettings(): Theme.Setting {
const isMobile = themeSetting.isMobile || false
const darkMode = themeSetting.darkMode || false
const sider = themeSetting.sider || {

View File

@@ -10,7 +10,7 @@ import {
} from 'naive-ui'
import type { BuiltInGlobalTheme } from 'naive-ui/es/themes/interface'
import { defineStore } from 'pinia'
import { getNaiveThemeOverrides, initThemeSettings } from './helpers'
import { defaultSettings, getNaiveThemeOverrides } from './helpers'
type ThemeState = Theme.Setting
@@ -19,8 +19,8 @@ const locales: Record<string, { locale: NLocale; dateLocale: NDateLocale }> = {
en: { locale: enUS, dateLocale: dateEnUS }
}
export const useThemeStore = defineStore('theme-store', {
state: (): ThemeState => initThemeSettings(),
export const useThemeStore = defineStore('theme', {
state: (): ThemeState => defaultSettings(),
getters: {
naiveThemeOverrides(): GlobalThemeOverrides {
return getNaiveThemeOverrides({
@@ -66,5 +66,6 @@ export const useThemeStore = defineStore('theme-store', {
setLocale(locale: string) {
this.locale = locale
}
}
},
persist: true
})

View File

@@ -1,56 +1,37 @@
import user from '@/api/panel/user'
import { resetRouter } from '@/router'
import { usePermissionStore, useTabStore } from '@/store'
import { toLogin } from '@/utils'
import { defineStore } from 'pinia'
interface UserInfo {
export interface UserInfo {
id?: string
username?: string
role?: Array<string>
}
export const useUserStore = defineStore('user', {
state() {
state: (): UserInfo => {
return {
userInfo: <UserInfo>{}
}
},
getters: {
userId(): string {
return this.userInfo.id || ''
},
username(): string {
return this.userInfo.username || ''
},
role(): Array<string> {
return this.userInfo.role || []
id: '',
username: '',
role: []
}
},
actions: {
async getUserInfo() {
try {
const res: any = await user.info()
const { id, username, role } = res.data
this.userInfo = { id, username, role }
return Promise.resolve(res.data)
} catch (error) {
return Promise.reject(error)
}
set(info: UserInfo) {
this.id = info.id
this.username = info.username
this.role = info.role
},
async logout() {
user.logout().then(() => {
const { resetTabs } = useTabStore()
const { resetPermission } = usePermissionStore()
resetPermission()
resetTabs()
resetRouter()
this.$reset()
toLogin()
})
},
setUserInfo(userInfo = {}) {
this.userInfo = { ...this.userInfo, ...userInfo }
logout() {
const { resetTabs } = useTabStore()
const { resetPermission } = usePermissionStore()
resetPermission()
resetTabs()
resetRouter()
this.$reset()
toLogin()
}
}
},
persist: true
})

View File

@@ -1,2 +1 @@
export * from './local'
export * from './session'

View File

@@ -1,31 +1,28 @@
import { decrypto, encrypto } from '@/utils'
interface StorageData {
value: unknown
value: any
expire: number | null
}
/** 默认存期限为7天 */
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7
/** 默认存期限为永久 */
const DEFAULT_CACHE_TIME = 0
export function setLocal(key: string, value: unknown, expire: number | null = DEFAULT_CACHE_TIME) {
export function setLocal(key: string, value: any, expire: number | null = DEFAULT_CACHE_TIME) {
const storageData: StorageData = {
value,
expire: expire !== null ? new Date().getTime() + expire * 1000 : null
expire: expire !== 0 && expire !== null ? new Date().getTime() + expire * 1000 : 0
}
const json = encrypto(storageData)
const json = JSON.stringify(storageData)
window.localStorage.setItem(key, json)
}
export function getLocal<T>(key: string) {
const json = window.localStorage.getItem(key)
if (json) {
let storageData: StorageData | null = null
storageData = decrypto(json)
const storageData = JSON.parse(json)
if (storageData) {
const { value, expire } = storageData
// 没有过期时间或者在有效期内则直接返回
if (expire === null || expire >= Date.now()) return value as T
if (expire === 0 || expire >= Date.now()) return value as T
}
removeLocal(key)
return null
@@ -36,8 +33,7 @@ export function getLocal<T>(key: string) {
export function getLocalExpire(key: string): number | null {
const json = window.localStorage.getItem(key)
if (json) {
let storageData: StorageData | null = null
storageData = decrypto(json)
const storageData = JSON.parse(json)
if (storageData) {
return storageData.expire
}

View File

@@ -1,23 +0,0 @@
import { decrypto, encrypto } from '@/utils'
export function setSession(key: string, value: unknown) {
const json = encrypto(value)
sessionStorage.setItem(key, json)
}
export function getSession<T>(key: string) {
const json = sessionStorage.getItem(key)
let data: T | null = null
if (json) {
data = decrypto(json)
}
return data
}
export function removeSession(key: string) {
window.sessionStorage.removeItem(key)
}
export function clearSession() {
window.sessionStorage.clear()
}

View File

@@ -28,9 +28,9 @@ if (localLoginInfo) {
loginInfo.value.password = localLoginInfo.password || ''
}
const userStore = useUserStore()
const loging = ref<boolean>(false)
const isRemember = useStorage('isRemember', false)
const userStore = useUserStore()
async function handleLogin() {
const { username, password } = loginInfo.value
@@ -49,7 +49,8 @@ async function handleLogin() {
}
await addDynamicRoutes()
await userStore.getUserInfo()
const { data } = await user.info()
userStore.set(data)
if (query.redirect) {
const path = query.redirect as string
Reflect.deleteProperty(query, 'redirect')
@@ -69,7 +70,8 @@ onMounted(async () => {
await user.isLogin().then(async (res) => {
if (res.data) {
await addDynamicRoutes()
await userStore.getUserInfo()
const { data } = await user.info()
userStore.set(data)
if (query.redirect) {
const path = query.redirect as string
Reflect.deleteProperty(query, 'redirect')