This commit is contained in:
Your Name
2026-03-01 14:17:59 +08:00
parent 65aa12f146
commit a07e844c47
6071 changed files with 777651 additions and 0 deletions
@@ -0,0 +1,22 @@
<template>
<el-breadcrumb class="app-breadcrumb">
<el-breadcrumb-item v-for="item in breadcrumbs" :key="item.path">
{{ item.meta.title }}
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup lang="ts">
import type { RouteLocationMatched, RouteLocationNormalizedLoaded } from 'vue-router'
import { useWatchRoute } from '@/hooks/useWatchRoute'
const breadcrumbs = ref<RouteLocationMatched[]>([])
const getBreadcrumb = (route: RouteLocationNormalizedLoaded) => {
const matched = route.matched.filter((item) => item.meta && item.meta.title)
breadcrumbs.value = matched
}
useWatchRoute((route) => {
getBreadcrumb(route)
})
</script>
@@ -0,0 +1,16 @@
<template>
<div class="fold h-full cursor-pointer flex items-center px-2" @click="toggleCollapsed">
<icon :name="`local-icon-${isCollapsed ? 'expand' : 'collapse'}`" :size="18" />
</div>
</template>
<script setup lang="ts">
import useAppStore from '@/stores/modules/app'
const appStore = useAppStore()
const isCollapsed = computed(() => appStore.isCollapsed)
// 折叠展开菜单
const toggleCollapsed = () => {
appStore.toggleCollapsed()
}
</script>
@@ -0,0 +1,11 @@
<template>
<div class="full-screen h-full cursor-pointer flex items-center px-2" @click="toggleFullscreen">
<icon :size="16" :name="`local-icon-${isFullscreen ? 'fullscreen-exit' : 'fullscreen'}`" />
</div>
</template>
<script setup lang="ts">
import { useFullscreen } from '@vueuse/core'
const { toggle: toggleFullscreen, isFullscreen } = useFullscreen()
</script>
@@ -0,0 +1,83 @@
<template>
<header class="header">
<div class="navbar">
<div class="flex-1 flex">
<div class="navbar-item">
<el-tooltip
class="box-item"
effect="dark"
:content="isCollapsed ? '展开菜单' : '收起菜单'"
placement="bottom"
>
<fold />
</el-tooltip>
</div>
<div class="navbar-item">
<el-tooltip class="box-item" effect="dark" content="刷新" placement="bottom">
<refresh />
</el-tooltip>
</div>
<div class="flex items-center px-2" v-if="!isMobile && settingStore.showCrumb">
<breadcrumb />
</div>
</div>
<div class="flex">
<div class="navbar-item" v-if="!isMobile">
<el-tooltip
class="box-item"
effect="dark"
:content="isFullscreen ? '退出全屏' : '全屏模式'"
placement="bottom"
>
<full-screen />
</el-tooltip>
</div>
<div class="navbar-item">
<user-drop-down />
</div>
<div class="navbar-item">
<el-tooltip
class="box-item"
effect="dark"
content="主题设置"
placement="bottom"
>
<setting />
</el-tooltip>
</div>
</div>
</div>
<multiple-tabs v-if="settingStore.openMultipleTabs" />
</header>
</template>
<script setup lang="ts">
import { useFullscreen } from '@vueuse/core'
import useAppStore from '@/stores/modules/app'
import useSettingStore from '@/stores/modules/setting'
import Setting from '../setting/index.vue'
import Breadcrumb from './breadcrumb.vue'
import Fold from './fold.vue'
import FullScreen from './full-screen.vue'
import MultipleTabs from './multiple-tabs.vue'
import Refresh from './refresh.vue'
import UserDropDown from './user-drop-down.vue'
const appStore = useAppStore()
const isMobile = computed(() => appStore.isMobile)
const isCollapsed = computed(() => appStore.isCollapsed)
const settingStore = useSettingStore()
const { isFullscreen } = useFullscreen()
</script>
<style lang="scss">
.navbar {
height: var(--navbar-height);
@apply flex px-2 bg-body;
.navbar-item {
@apply h-full flex justify-center items-center hover:bg-page;
}
}
</style>
@@ -0,0 +1,123 @@
<template>
<div class="app-tabs pl-4 flex bg-body">
<div class="flex-1 min-w-0">
<el-tabs
:model-value="currentTab"
:closable="tabsLists.length > 1"
@tab-change="handleChange"
@tab-remove="removeTab($event)"
>
<template v-for="item in tabsLists" :key="item.fullPath">
<el-tab-pane :label="item.title" :name="item.fullPath"></el-tab-pane>
</template>
</el-tabs>
</div>
<el-dropdown @command="handleCommand">
<span class="flex items-center px-3">
<icon :size="16" name="el-icon-arrow-down" />
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="closeCurrent"> 关闭当前 </el-dropdown-item>
<el-dropdown-item command="closeOther"> 关闭其他 </el-dropdown-item>
<el-dropdown-item command="closeAll"> 关闭全部 </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script setup lang="ts">
import useMultipleTabs from '@/hooks/useMultipleTabs'
import { useWatchRoute } from '@/hooks/useWatchRoute'
import useTabsStore, { getRouteParams } from '@/stores/modules/multipleTabs'
const router = useRouter()
const tabsStore = useTabsStore()
const { removeOtherTab, addTab, removeAllTab, removeTab, tabsLists, currentTab } = useMultipleTabs()
useWatchRoute(() => {
addTab()
})
const handleChange = (fullPath: any) => {
const tabItem = tabsStore.tasMap[fullPath]
router.push(getRouteParams(tabItem))
}
const handleCommand = (command: any) => {
switch (command) {
case 'closeCurrent':
removeTab()
break
case 'closeOther':
removeOtherTab()
break
case 'closeAll':
removeAllTab()
break
}
}
</script>
<style lang="scss" scoped>
.app-tabs {
@apply border-t border-br;
:deep(.el-tabs) {
height: 40px;
.el-tabs {
&__header {
margin-bottom: 0;
}
&__content {
display: none;
}
&__nav-next,
&__nav-prev {
@apply text-xl;
}
&__nav-wrap::after {
height: 0;
}
&__item {
font-weight: normal;
padding: 0 15px !important;
box-sizing: border-box;
&.is-active {
background-color: var(--el-color-primary-light-9);
&::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
background-color: var(--el-color-primary);
margin-right: 6px;
border-radius: 50%;
vertical-align: 2px;
}
&::after {
position: absolute;
content: '';
display: block;
top: 0;
height: 2px;
left: 0;
width: 100%;
background-color: var(--el-color-primary);
}
}
.is-icon-close {
color: var(--el-text-color-regular);
vertical-align: -2px;
&:hover {
color: var(--color-white);
background-color: var(--el-color-danger);
}
}
}
&__active-bar {
display: none;
}
}
}
}
</style>
@@ -0,0 +1,15 @@
<template>
<div class="refresh cursor-pointer h-full flex items-center px-2" @click="refreshView">
<icon name="el-icon-RefreshRight" :size="18" />
</div>
</template>
<script setup lang="ts">
import useAppStore from '@/stores/modules/app'
const appStore = useAppStore()
// 刷新页面
const refreshView = () => {
appStore.refreshView()
}
</script>
@@ -0,0 +1,43 @@
<template>
<el-dropdown class="px-2" @command="handleCommand">
<div class="flex items-center">
<el-avatar :size="34" :src="userInfo.avatar" />
<div class="ml-3 mr-1">{{ userInfo.name }}</div>
<icon name="el-icon-ArrowDown" />
</div>
<template #dropdown>
<el-dropdown-menu>
<router-link to="/user/setting">
<el-dropdown-item>个人设置</el-dropdown-item>
</router-link>
<el-dropdown-item command="cache">清除缓存</el-dropdown-item>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { systemCacheClear } from '@/api/setting/system'
import useUserStore from '@/stores/modules/user'
import feedback from '@/utils/feedback'
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const handleCommand = async (command: string) => {
switch (command) {
case 'logout':
await feedback.confirm('确定退出登录吗?')
userStore.logout()
break
case 'cache':
// 清理缓存
await systemCacheClear()
window.location.reload()
break
}
}
</script>
@@ -0,0 +1,27 @@
<template>
<main class="main-wrap h-full bg-page">
<el-scrollbar>
<div class="p-4">
<router-view v-if="isRouteShow" v-slot="{ Component, route }">
<keep-alive :include="includeList" :max="20">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
</div>
</el-scrollbar>
</main>
</template>
<script setup lang="ts">
import useAppStore from '@/stores/modules/app'
import useTabsStore from '@/stores/modules/multipleTabs'
import useSettingStore from '@/stores/modules/setting'
const appStore = useAppStore()
const tabsStore = useTabsStore()
const settingStore = useSettingStore()
const isRouteShow = computed(() => appStore.isRouteShow)
const includeList = computed(() => (settingStore.openMultipleTabs ? tabsStore.getCacheTabList : []))
</script>
<style></style>
@@ -0,0 +1,231 @@
<template>
<div class="setting-drawer">
<el-drawer
v-model="showSetting"
append-to-body
direction="rtl"
size="250px"
title="主题设置"
>
<div class="h-full flex flex-col">
<div class="setting-item mb-5">
<span class="text-tx-secondary">风格设置</span>
<div class="flex mt-4 cursor-pointer">
<div
class="mr-4 flex relative text-primary"
v-for="item in sideThemeList"
:key="item.type"
@click="sideTheme = item.type"
>
<img :src="item.image" width="52" height="36" />
<icon
v-if="sideTheme == item.type"
class="icon-select"
name="el-icon-Select"
/>
</div>
</div>
</div>
<div class="setting-item mb-5 flex justify-between items-center">
<span class="text-tx-secondary">主题颜色</span>
<div>
<el-color-picker v-model="theme" :predefine="predefineColors" />
</div>
</div>
<div class="setting-item mb-5 flex justify-between items-center">
<span class="text-tx-secondary">开启黑暗模式</span>
<div>
<el-switch :model-value="isDark" @change="toggleDark" />
</div>
</div>
<div class="setting-item mb-5 flex justify-between items-center">
<span class="text-tx-secondary">开启多页签栏</span>
<div>
<el-switch
v-model="openMultipleTabs"
:active-value="true"
:inactive-value="false"
/>
</div>
</div>
<div class="setting-item mb-5 flex justify-between items-center">
<span class="text-tx-secondary">只展开一个一级菜单</span>
<div>
<el-switch
v-model="isUniqueOpened"
:active-value="true"
:inactive-value="false"
/>
</div>
</div>
<div class="setting-item mb-5 flex justify-between items-center">
<div class="text-tx-secondary flex-none mr-3">菜单栏宽度</div>
<div>
<el-input-number v-model="sideWidth" :min="180" :max="250" />
</div>
</div>
<div class="setting-item mb-5 flex justify-between items-center">
<div class="text-tx-secondary flex-none mr-3">显示LOGO</div>
<div>
<el-switch
v-model="showLogo"
:active-value="true"
:inactive-value="false"
/>
</div>
</div>
<div class="setting-item mb-5 flex justify-between items-center">
<div class="text-tx-secondary flex-none mr-3">显示面包屑</div>
<div>
<el-switch
v-model="showCrumb"
:active-value="true"
:inactive-value="false"
/>
</div>
</div>
<div class="setting-item flex justify-between items-center mt-auto">
<el-button @click="resetTheme" class="w-full">重置主题</el-button>
</div>
</div>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core'
import theme_dark from '@/assets/images/theme_black.png'
import theme_light from '@/assets/images/theme_white.png'
import useSettingStore from '@/stores/modules/setting'
const settingStore = useSettingStore()
const predefineColors = ref(['#409EFF', '#28C76F', '#EA5455', '#FF9F43', '#01CFE8', '#4A5DFF'])
const sideThemeList = [
{
type: 'dark',
image: theme_dark
},
{
type: 'light',
image: theme_light
}
]
const sideTheme = computed({
get() {
return settingStore.sideTheme
},
set(value) {
settingStore.setSetting({
key: 'sideTheme',
value
})
}
})
const openMultipleTabs = computed({
get() {
return settingStore.openMultipleTabs
},
set(value) {
settingStore.setSetting({
key: 'openMultipleTabs',
value
})
}
})
const isUniqueOpened = computed({
get() {
return settingStore.isUniqueOpened
},
set(value) {
settingStore.setSetting({
key: 'isUniqueOpened',
value
})
}
})
const sideWidth = computed({
get() {
return settingStore.sideWidth
},
set(value) {
settingStore.setSetting({
key: 'sideWidth',
value
})
}
})
const showSetting = computed({
get() {
return settingStore.showDrawer
},
set(value) {
settingStore.setSetting({
key: 'showDrawer',
value
})
}
})
const theme = computed({
get() {
return settingStore.theme
},
set(value) {
settingStore.setSetting({
key: 'theme',
value
})
themeChange()
}
})
const showLogo = computed({
get() {
return settingStore.showLogo
},
set(value) {
settingStore.setSetting({
key: 'showLogo',
value
})
}
})
const showCrumb = computed({
get() {
return settingStore.showCrumb
},
set(value) {
settingStore.setSetting({
key: 'showCrumb',
value
})
}
})
const isDark = useDark()
const themeChange = () => {
settingStore.setTheme(isDark.value)
}
const toggleDark = () => {
useToggle(isDark)()
themeChange()
}
const resetTheme = () => {
isDark.value = false
settingStore.resetTheme()
themeChange()
}
</script>
<style lang="scss" scoped>
.icon-select {
@apply absolute left-1/2 top-1/2;
transform: translate(-50%, -50%);
}
</style>
@@ -0,0 +1,21 @@
<template>
<div class="setting flex cursor-pointer h-full items-center px-2" @click="openSetting">
<icon :size="16" name="el-icon-Setting" />
<layout-setting />
</div>
</template>
<script setup lang="ts">
import useSettingStore from '@/stores/modules/setting'
import LayoutSetting from './drawer.vue'
const settingStore = useSettingStore()
const openSetting = () => {
settingStore.setSetting({
key: 'showDrawer',
value: true
})
}
</script>
@@ -0,0 +1,45 @@
<template>
<aside class="sidebar h-full">
<el-drawer
v-model="showMenuDrawer"
direction="ltr"
:size="drawderSize"
title="主题设置"
:with-header="false"
>
<side />
</el-drawer>
<side v-show="!isMobile" />
</aside>
</template>
<script setup lang="ts">
import useAppStore from '@/stores/modules/app'
import useSettingStore from '@/stores/modules/setting'
import Side from './side.vue'
const appStore = useAppStore()
const settingStore = useSettingStore()
const isMobile = computed(() => appStore.isMobile)
const showMenuDrawer = computed({
get() {
return !appStore.isCollapsed && isMobile.value
},
set(value) {
appStore.toggleCollapsed(!value)
}
})
const drawderSize = computed(() => {
return `${settingStore.sideWidth + 1}px`
})
</script>
<style lang="scss" scoped>
.sidebar {
:deep(.el-drawer__body) {
padding: 0;
}
}
</style>
@@ -0,0 +1,69 @@
<template>
<div class="logo" @click="handleClick">
<image-contain :width="szie" :height="szie" :src="config.web_logo" />
<transition name="title-width">
<div
v-show="showTitle"
class="logo-title overflow-hidden whitespace-nowrap"
:class="{ 'text-white': theme == ThemeEnum.DARK }"
:style="{ left: `${szie + 16}px` }"
>
<overflow-tooltip
:content="title || config.web_name"
:teleported="true"
placement="bottom"
overflo-type="unset"
>
</overflow-tooltip>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { ThemeEnum } from '@/enums/appEnums'
import useAppStore from '@/stores/modules/app'
defineProps({
szie: { type: Number, default: 34 },
title: { type: String },
theme: { type: String },
showTitle: { type: Boolean, default: true }
})
const appStore = useAppStore()
const config = computed(() => appStore.config)
const router = useRouter()
const handleClick = () => {
router.push('/')
}
</script>
<style lang="scss" scoped>
.logo {
height: var(--navbar-height);
overflow: hidden;
cursor: pointer;
user-select: none;
@apply flex items-center p-2 relative;
.logo-title {
width: 70%;
position: absolute;
@apply text-xl;
}
.title-width-enter-active {
opacity: 0;
transition: all 0.3s ease-out;
}
.title-width-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.title-width-enter-from,
.title-width-leave-to {
width: 0;
opacity: 0;
}
}
</style>
@@ -0,0 +1,89 @@
<template>
<template v-if="!route.meta?.hidden">
<app-link v-if="!hasShowChild" :to="`${routePath}?${queryStr}`">
<el-menu-item :index="routePath">
<icon
class="menu-item-icon"
:size="16"
v-if="routeMeta?.icon"
:name="routeMeta?.icon"
/>
<template #title>
<span>{{ routeMeta?.title }}</span>
</template>
</el-menu-item>
</app-link>
<el-sub-menu v-else :index="routePath" :popper-class="popperClass">
<template #title>
<icon
class="menu-item-icon"
:size="16"
v-if="routeMeta?.icon"
:name="routeMeta?.icon"
/>
<span>{{ routeMeta?.title }}</span>
</template>
<menu-item
v-for="item in route?.children"
:key="resolvePath(item.path)"
:route="item"
:route-path="resolvePath(item.path)"
:popper-class="popperClass"
/>
</el-sub-menu>
</template>
</template>
<script lang="ts" setup>
import type { RouteRecordRaw } from 'vue-router'
import { getNormalPath, objectToQuery } from '@/utils/util'
import { isExternal } from '@/utils/validate'
interface Props {
route: RouteRecordRaw
routePath: string
popperClass: string
}
const props = defineProps<Props>()
const hasShowChild = computed(() => {
const children: RouteRecordRaw[] = props.route.children ?? []
return !!children.filter((item) => !item.meta?.hidden).length
})
const routeMeta = computed(() => {
return props.route.meta
})
const resolvePath = (path: string) => {
if (isExternal(path)) {
return path
}
const newPath = getNormalPath(`${props.routePath}/${path}`)
return newPath
}
const queryStr = computed<string>(() => {
const query = props.route.meta?.query as string
try {
const queryObj = JSON.parse(query)
return objectToQuery(queryObj)
} catch (error) {
// console.log(error)
return query
}
})
</script>
<style lang="scss" scoped>
.el-menu-item,
.el-sub-menu__title {
.menu-item-icon {
margin-right: 8px;
width: var(--el-menu-icon-width);
text-align: center;
vertical-align: middle;
}
}
</style>
@@ -0,0 +1,102 @@
<template>
<div
class="menu flex-1 min-h-0"
:class="themeClass"
:style="isCollapsed ? '' : `--aside-width: ${width}px`"
>
<el-scrollbar>
<el-menu
v-bind="config"
:default-active="activeMenu"
:collapse="isCollapsed"
mode="vertical"
:unique-opened="uniqueOpened"
@select="$emit('select')"
>
<menu-item
v-for="route in routes"
:key="route.path"
:route="route"
:route-path="route.path"
:popper-class="themeClass"
/>
</el-menu>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import type { PropType } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import MenuItem from './menu-item.vue'
const props = defineProps({
routes: {
type: Object as PropType<RouteRecordRaw[]>
},
config: {
type: Object
},
uniqueOpened: {
type: Boolean,
default: false
},
isCollapsed: {
type: Boolean,
default: false
},
theme: {
type: String
},
width: {
type: Number,
default: 200
}
})
defineEmits(['select'])
const route = useRoute()
const activeMenu = computed<string>(() => route.meta?.activeMenu || route.path)
const themeClass = computed(() => `theme-${props.theme}`)
</script>
<style lang="scss" scoped>
.menu {
&.theme-dark {
.el-menu {
:deep(.el-menu-item) {
&.is-active {
@apply bg-primary border-primary;
}
}
}
:deep(.el-menu--collapse) {
.el-sub-menu.is-active .el-sub-menu__title {
@apply bg-primary #{!important};
}
}
}
&.theme-light {
:deep(.el-menu) {
.el-menu-item {
border-color: transparent;
&.is-active {
@apply bg-primary-light-9 border-r-2 border-primary;
}
}
.el-menu-item:hover,
.el-sub-menu__title:hover {
color: var(--el-color-primary);
}
}
}
.el-menu {
border-right: none;
&:not(.el-menu--collapse) {
width: var(--aside-width);
}
}
}
</style>
@@ -0,0 +1,67 @@
<template>
<div class="side" :style="sideStyle">
<side-logo v-if="settingStore.showLogo" :show-title="!isCollapsed" :theme="sideTheme" />
<side-menu
:routes="routes"
:is-collapsed="isCollapsed"
:width="settingStore.sideWidth"
:unique-opened="settingStore.isUniqueOpened"
:config="menuProp"
:theme="sideTheme"
@select="handleSelect"
/>
</div>
</template>
<script setup lang="ts">
import useAppStore from '@/stores/modules/app'
import useSettingStore from '@/stores/modules/setting'
import useUserStore from '@/stores/modules/user'
import SideLogo from './logo.vue'
import SideMenu from './menu.vue'
const appStore = useAppStore()
const isCollapsed = computed(() => {
if (appStore.isMobile) {
return false
} else {
return appStore.isCollapsed
}
})
const settingStore = useSettingStore()
const sideTheme = computed(() => settingStore.sideTheme)
const userStore = useUserStore()
const routes = computed(() => userStore.routes)
const sideStyle = computed(() => {
return sideTheme.value == 'dark'
? {
'--side-dark-color': settingStore.sideDarkColor
}
: ''
})
const menuProp = computed(() => {
return {
backgroundColor: sideTheme.value == 'dark' ? settingStore.sideDarkColor : '',
textColor: sideTheme.value == 'dark' ? 'var(--el-color-white)' : '',
activeTextColor: sideTheme.value == 'dark' ? 'var(--el-color-white)' : ''
}
})
const handleSelect = () => {
if (appStore.isMobile) {
appStore.toggleCollapsed(true)
}
}
</script>
<style lang="scss" scoped>
.side {
position: relative;
z-index: 999;
@apply border-r border-br-light h-full flex flex-col;
background-color: var(--side-dark-color, var(--el-bg-color));
}
</style>