✨ init Project
This commit is contained in:
66
src/view/layout/aside/asideComponent/asyncSubmenu.vue
Normal file
66
src/view/layout/aside/asideComponent/asyncSubmenu.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<el-sub-menu
|
||||
ref="subMenu"
|
||||
:index="routerInfo.name"
|
||||
class="gva-sub-menu dark:text-slate-300 relative"
|
||||
>
|
||||
<template #title>
|
||||
<div
|
||||
v-if="!isCollapse"
|
||||
class="flex items-center"
|
||||
:style="{
|
||||
height: sideHeight
|
||||
}"
|
||||
>
|
||||
<el-icon v-if="routerInfo.meta.icon">
|
||||
<component :is="routerInfo.meta.icon" />
|
||||
</el-icon>
|
||||
<span>{{ routerInfo.meta.title }}</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<el-icon v-if="routerInfo.meta.icon">
|
||||
<component :is="routerInfo.meta.icon" />
|
||||
</el-icon>
|
||||
<span>{{ routerInfo.meta.title }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<slot />
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject, computed } from 'vue'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
const { config } = storeToRefs(appStore)
|
||||
|
||||
defineOptions({
|
||||
name: 'AsyncSubmenu'
|
||||
})
|
||||
|
||||
defineProps({
|
||||
routerInfo: {
|
||||
default: function () {
|
||||
return null
|
||||
},
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
const isCollapse = inject('isCollapse', {
|
||||
default: false
|
||||
})
|
||||
|
||||
const sideHeight = computed(() => {
|
||||
return config.value.layout_side_item_height + 'px'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.gva-sub-menu {
|
||||
.el-sub-menu__title {
|
||||
height: v-bind('sideHeight') !important;
|
||||
}
|
||||
}
|
||||
</style>
|
47
src/view/layout/aside/asideComponent/index.vue
Normal file
47
src/view/layout/aside/asideComponent/index.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<component
|
||||
:is="menuComponent"
|
||||
v-if="!routerInfo.hidden"
|
||||
:router-info="routerInfo"
|
||||
>
|
||||
<template v-if="routerInfo.children && routerInfo.children.length">
|
||||
<AsideComponent
|
||||
v-for="item in routerInfo.children"
|
||||
:key="item.name"
|
||||
:router-info="item"
|
||||
/>
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MenuItem from './menuItem.vue'
|
||||
import AsyncSubmenu from './asyncSubmenu.vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'AsideComponent'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
routerInfo: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'vertical'
|
||||
}
|
||||
})
|
||||
|
||||
const menuComponent = computed(() => {
|
||||
if (
|
||||
props.routerInfo.children &&
|
||||
props.routerInfo.children.filter((item) => !item.hidden).length
|
||||
) {
|
||||
return AsyncSubmenu
|
||||
} else {
|
||||
return MenuItem
|
||||
}
|
||||
})
|
||||
</script>
|
43
src/view/layout/aside/asideComponent/menuItem.vue
Normal file
43
src/view/layout/aside/asideComponent/menuItem.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<el-menu-item
|
||||
:index="routerInfo.name"
|
||||
class="dark:text-slate-300 overflow-hidden"
|
||||
:style="{
|
||||
height: sideHeight
|
||||
}"
|
||||
>
|
||||
<el-icon v-if="routerInfo.meta.icon">
|
||||
<component :is="routerInfo.meta.icon" />
|
||||
</el-icon>
|
||||
<template #title>
|
||||
{{ routerInfo.meta.title }}
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
const { config } = storeToRefs(appStore)
|
||||
|
||||
defineOptions({
|
||||
name: 'MenuItem'
|
||||
})
|
||||
|
||||
defineProps({
|
||||
routerInfo: {
|
||||
default: function () {
|
||||
return null
|
||||
},
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
const sideHeight = computed(() => {
|
||||
return config.value.layout_side_item_height + 'px'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
146
src/view/layout/aside/combinationMode.vue
Normal file
146
src/view/layout/aside/combinationMode.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<div
|
||||
v-if="mode === 'head'"
|
||||
class="bg-white h-[calc(100%-4px)] text-slate-700 dark:text-slate-300 mx-2 dark:bg-slate-900 flex items-center w-[calc(100vw-600px)] overflow-auto"
|
||||
>
|
||||
<el-menu
|
||||
:default-active="routerStore.topActive"
|
||||
mode="horizontal"
|
||||
class="border-r-0 border-b-0 w-full flex gap-1 items-center box-border h-[calc(100%-1px)]"
|
||||
unique-opened
|
||||
@select="(index, _, ele) => selectMenuItem(index, _, ele, true)"
|
||||
>
|
||||
<template v-for="item in routerStore.topMenu">
|
||||
<aside-component
|
||||
v-if="!item.hidden"
|
||||
:key="item.name"
|
||||
:router-info="item"
|
||||
mode="horizontal"
|
||||
/>
|
||||
</template>
|
||||
</el-menu>
|
||||
</div>
|
||||
<div
|
||||
v-if="mode === 'normal'"
|
||||
class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700"
|
||||
:class="isCollapse ? '' : ' px-2'"
|
||||
:style="{
|
||||
width: layoutSideWidth + 'px'
|
||||
}"
|
||||
>
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
:default-active="active"
|
||||
class="border-r-0 w-full"
|
||||
unique-opened
|
||||
@select="(index, _, ele) => selectMenuItem(index, _, ele, false)"
|
||||
>
|
||||
<template v-for="item in routerStore.leftMenu">
|
||||
<aside-component
|
||||
v-if="!item.hidden"
|
||||
:key="item.name"
|
||||
:router-info="item"
|
||||
/>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
<div
|
||||
class="absolute bottom-8 right-2 w-8 h-8 bg-gray-50 dark:bg-slate-800 flex items-center justify-center rounded cursor-pointer"
|
||||
:class="isCollapse ? 'right-0 left-0 mx-auto' : 'right-2'"
|
||||
@click="toggleCollapse"
|
||||
>
|
||||
<el-icon v-if="!isCollapse">
|
||||
<DArrowLeft />
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<DArrowRight />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import AsideComponent from '@/view/layout/aside/asideComponent/index.vue'
|
||||
import { ref, provide, watchEffect, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
const { device, config } = storeToRefs(appStore)
|
||||
|
||||
defineOptions({
|
||||
name: 'GvaAside'
|
||||
})
|
||||
|
||||
defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'normal'
|
||||
}
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const routerStore = useRouterStore()
|
||||
const isCollapse = ref(false)
|
||||
const active = ref('')
|
||||
const layoutSideWidth = computed(() => {
|
||||
if (!isCollapse.value) {
|
||||
return config.value.layout_side_width
|
||||
} else {
|
||||
return config.value.layout_side_collapsed_width
|
||||
}
|
||||
})
|
||||
watchEffect(() => {
|
||||
active.value = route.meta.activeName || route.name
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (device.value === 'mobile') {
|
||||
isCollapse.value = true
|
||||
} else {
|
||||
isCollapse.value = false
|
||||
}
|
||||
})
|
||||
|
||||
provide('isCollapse', isCollapse)
|
||||
|
||||
const selectMenuItem = (index, _, ele, top) => {
|
||||
const query = {}
|
||||
const params = {}
|
||||
routerStore.routeMap[index]?.parameters &&
|
||||
routerStore.routeMap[index]?.parameters.forEach((item) => {
|
||||
if (item.type === 'query') {
|
||||
query[item.key] = item.value
|
||||
} else {
|
||||
params[item.key] = item.value
|
||||
}
|
||||
})
|
||||
if (index === route.name) return
|
||||
if (index.indexOf('http://') > -1 || index.indexOf('https://') > -1) {
|
||||
window.open(index, '_blank')
|
||||
return
|
||||
}
|
||||
|
||||
if (!top) {
|
||||
router.push({ name: index, query, params })
|
||||
return
|
||||
}
|
||||
const leftMenu = routerStore.setLeftMenu(index)
|
||||
if (!leftMenu) {
|
||||
router.push({ name: index, query, params })
|
||||
return;
|
||||
}
|
||||
const firstMenu = leftMenu.find((item) => !item.hidden && item.path.indexOf("http://") === -1 && item.path.indexOf("https://") === -1)
|
||||
router.push({ name: firstMenu.name, query, params })
|
||||
|
||||
}
|
||||
|
||||
const toggleCollapse = () => {
|
||||
isCollapse.value = !isCollapse.value
|
||||
}
|
||||
</script>
|
106
src/view/layout/aside/headMode.vue
Normal file
106
src/view/layout/aside/headMode.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-white h-[calc(100%-4px)] text-slate-700 dark:text-slate-300 mx-2 dark:bg-slate-900 flex items-center w-[calc(100vw-600px)] overflow-auto"
|
||||
>
|
||||
<el-menu
|
||||
:default-active="active"
|
||||
mode="horizontal"
|
||||
class="border-r-0 w-full flex gap-1 items-center box-border h-[calc(100%-1px)]"
|
||||
unique-opened
|
||||
@select="selectMenuItem"
|
||||
>
|
||||
<template v-for="item in routerStore.asyncRouters[0].children">
|
||||
<aside-component
|
||||
v-if="!item.hidden"
|
||||
:key="item.name"
|
||||
:router-info="item"
|
||||
mode="horizontal"
|
||||
/>
|
||||
</template>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AsideComponent from '@/view/layout/aside/asideComponent/index.vue'
|
||||
import { ref, provide, watchEffect } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
const { device } = storeToRefs(appStore)
|
||||
|
||||
defineOptions({
|
||||
name: 'GvaAside'
|
||||
})
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const routerStore = useRouterStore()
|
||||
const isCollapse = ref(false)
|
||||
const active = ref('')
|
||||
watchEffect(() => {
|
||||
if (route.name === 'Iframe') {
|
||||
active.value = decodeURIComponent(route.query.url)
|
||||
return
|
||||
}
|
||||
active.value = route.meta.activeName || route.name
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (device.value === 'mobile') {
|
||||
isCollapse.value = true
|
||||
} else {
|
||||
isCollapse.value = false
|
||||
}
|
||||
})
|
||||
|
||||
provide('isCollapse', isCollapse)
|
||||
|
||||
const selectMenuItem = (index) => {
|
||||
const query = {}
|
||||
const params = {}
|
||||
routerStore.routeMap[index]?.parameters &&
|
||||
routerStore.routeMap[index]?.parameters.forEach((item) => {
|
||||
if (item.type === 'query') {
|
||||
query[item.key] = item.value
|
||||
} else {
|
||||
params[item.key] = item.value
|
||||
}
|
||||
})
|
||||
if (index === route.name) return
|
||||
if (index.indexOf('http://') > -1 || index.indexOf('https://') > -1) {
|
||||
if (index === 'Iframe') {
|
||||
query.url = decodeURIComponent(index)
|
||||
router.push({
|
||||
name: 'Iframe',
|
||||
query,
|
||||
params
|
||||
})
|
||||
return
|
||||
} else {
|
||||
window.open(index, '_blank')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
router.push({ name: index, query, params })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-menu--horizontal.el-menu,
|
||||
.el-menu--horizontal > .el-menu-item.is-active {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
|
||||
.dark {
|
||||
.el-menu-item.is-active {
|
||||
background-color: var(--el-color-primary-bg) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
34
src/view/layout/aside/index.vue
Normal file
34
src/view/layout/aside/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<normal-mode
|
||||
v-if="
|
||||
config.side_mode === 'normal' ||
|
||||
(device === 'mobile' && config.side_mode == 'head') ||
|
||||
(device === 'mobile' && config.side_mode == 'combination')
|
||||
"
|
||||
/>
|
||||
<head-mode v-if="config.side_mode === 'head' && device !== 'mobile'" />
|
||||
<combination-mode
|
||||
v-if="config.side_mode === 'combination' && device !== 'mobile'"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import NormalMode from './normalMode.vue'
|
||||
import HeadMode from './headMode.vue'
|
||||
import CombinationMode from './combinationMode.vue'
|
||||
|
||||
defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'normal'
|
||||
}
|
||||
})
|
||||
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useAppStore } from '@/pinia'
|
||||
const appStore = useAppStore()
|
||||
const { config, device } = storeToRefs(appStore)
|
||||
</script>
|
120
src/view/layout/aside/normalMode.vue
Normal file
120
src/view/layout/aside/normalMode.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700"
|
||||
:class="isCollapse ? '' : ' px-2'"
|
||||
:style="{
|
||||
width: layoutSideWidth + 'px'
|
||||
}"
|
||||
>
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
:default-active="active"
|
||||
class="border-r-0 w-full"
|
||||
unique-opened
|
||||
@select="selectMenuItem"
|
||||
>
|
||||
<template v-for="item in routerStore.asyncRouters[0]?.children || []">
|
||||
<aside-component
|
||||
v-if="!item.hidden"
|
||||
:key="item.name"
|
||||
:router-info="item"
|
||||
/>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
<div
|
||||
class="absolute bottom-8 right-2 w-8 h-8 bg-gray-50 dark:bg-slate-800 flex items-center justify-center rounded cursor-pointer"
|
||||
:class="isCollapse ? 'right-0 left-0 mx-auto' : 'right-2'"
|
||||
@click="toggleCollapse"
|
||||
>
|
||||
<el-icon v-if="!isCollapse">
|
||||
<DArrowLeft />
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<DArrowRight />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AsideComponent from '@/view/layout/aside/asideComponent/index.vue'
|
||||
import { ref, provide, watchEffect, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
const { device, config } = storeToRefs(appStore)
|
||||
|
||||
defineOptions({
|
||||
name: 'GvaAside'
|
||||
})
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const routerStore = useRouterStore()
|
||||
const isCollapse = ref(false)
|
||||
const active = ref('')
|
||||
const layoutSideWidth = computed(() => {
|
||||
if (!isCollapse.value) {
|
||||
return config.value.layout_side_width
|
||||
} else {
|
||||
return config.value.layout_side_collapsed_width
|
||||
}
|
||||
})
|
||||
watchEffect(() => {
|
||||
if (route.name === 'Iframe') {
|
||||
active.value = decodeURIComponent(route.query.url)
|
||||
return
|
||||
}
|
||||
active.value = route.meta.activeName || route.name
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (device.value === 'mobile') {
|
||||
isCollapse.value = true
|
||||
} else {
|
||||
isCollapse.value = false
|
||||
}
|
||||
})
|
||||
|
||||
provide('isCollapse', isCollapse)
|
||||
|
||||
const selectMenuItem = (index) => {
|
||||
const query = {}
|
||||
const params = {}
|
||||
routerStore.routeMap[index]?.parameters &&
|
||||
routerStore.routeMap[index]?.parameters.forEach((item) => {
|
||||
if (item.type === 'query') {
|
||||
query[item.key] = item.value
|
||||
} else {
|
||||
params[item.key] = item.value
|
||||
}
|
||||
})
|
||||
if (index === route.name) return
|
||||
if (index.indexOf('http://') > -1 || index.indexOf('https://') > -1) {
|
||||
if (index === 'Iframe') {
|
||||
query.url = decodeURIComponent(index)
|
||||
router.push({
|
||||
name: 'Iframe',
|
||||
query,
|
||||
params
|
||||
})
|
||||
return
|
||||
} else {
|
||||
window.open(index, '_blank')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
router.push({ name: index, query, params })
|
||||
}
|
||||
}
|
||||
|
||||
const toggleCollapse = () => {
|
||||
isCollapse.value = !isCollapse.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
140
src/view/layout/header/index.vue
Normal file
140
src/view/layout/header/index.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<!--
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/7
|
||||
!-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex justify-between fixed top-0 left-0 right-0 z-10 h-16 bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 shadow dark:shadow-gray-700 items-center px-2"
|
||||
>
|
||||
<div class="flex items-center cursor-pointer flex-1">
|
||||
<div
|
||||
class="flex items-center cursor-pointer"
|
||||
:class="isMobile ? '' : 'min-w-48'"
|
||||
@click="router.push({ path: '/' })"
|
||||
>
|
||||
<img
|
||||
alt
|
||||
class="h-12 bg-white rounded-full"
|
||||
:src="$GIN_VUE_ADMIN.appLogo"
|
||||
/>
|
||||
<div
|
||||
v-if="!isMobile"
|
||||
class="inline-flex font-bold text-2xl ml-2"
|
||||
:class="
|
||||
(config.side_mode === 'head' ||
|
||||
config.side_mode === 'combination') &&
|
||||
'min-w-fit'
|
||||
"
|
||||
>
|
||||
{{ $GIN_VUE_ADMIN.appName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-breadcrumb
|
||||
v-show="!isMobile"
|
||||
v-if="config.side_mode !== 'head' && config.side_mode !== 'combination'"
|
||||
class="ml-4"
|
||||
>
|
||||
<el-breadcrumb-item
|
||||
v-for="item in matched.slice(1, matched.length)"
|
||||
:key="item.path"
|
||||
>
|
||||
{{ fmtTitle(item.meta.title, route) }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
<gva-aside
|
||||
v-if="config.side_mode === 'head' && !isMobile"
|
||||
class="flex-1"
|
||||
/>
|
||||
<gva-aside
|
||||
v-if="config.side_mode === 'combination' && !isMobile"
|
||||
mode="head"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ml-2 flex items-center">
|
||||
<tools />
|
||||
<el-dropdown>
|
||||
<div class="flex justify-center items-center h-full w-full">
|
||||
<span
|
||||
class="cursor-pointer flex justify-center items-center text-black dark:text-gray-100"
|
||||
>
|
||||
<CustomPic />
|
||||
<span v-show="!isMobile" class="w-16">{{
|
||||
userStore.userInfo.nickName
|
||||
}}</span>
|
||||
<el-icon>
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
<span class="font-bold">
|
||||
当前角色:{{ userStore.userInfo.authority.authorityName }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
<template v-if="userStore.userInfo.authorities">
|
||||
<el-dropdown-item
|
||||
v-for="item in userStore.userInfo.authorities.filter(
|
||||
(i) => i.authorityId !== userStore.userInfo.authorityId
|
||||
)"
|
||||
:key="item.authorityId"
|
||||
@click="changeUserAuth(item.authorityId)"
|
||||
>
|
||||
<span> 切换为:{{ item.authorityName }} </span>
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
<el-dropdown-item icon="avatar" @click="toPerson">
|
||||
个人信息
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item icon="reading-lamp" @click="userStore.LoginOut">
|
||||
登 出
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import tools from './tools.vue'
|
||||
import CustomPic from '@/components/customPic/index.vue'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { setUserAuthority } from '@/api/user'
|
||||
import { fmtTitle } from '@/utils/fmtRouterTitle'
|
||||
import gvaAside from '@/view/layout/aside/index.vue'
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
const { device, config } = storeToRefs(appStore)
|
||||
const isMobile = computed(() => {
|
||||
return device.value === 'mobile'
|
||||
})
|
||||
const toPerson = () => {
|
||||
router.push({ name: 'person' })
|
||||
}
|
||||
const matched = computed(() => route.meta.matched)
|
||||
|
||||
const changeUserAuth = async (id) => {
|
||||
const res = await setUserAuthority({
|
||||
authorityId: id
|
||||
})
|
||||
if (res.code === 0) {
|
||||
window.sessionStorage.setItem('needCloseAll', 'true')
|
||||
window.sessionStorage.setItem('needToHome', 'true')
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
192
src/view/layout/header/tools.vue
Normal file
192
src/view/layout/header/tools.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<!--
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/7
|
||||
!-->
|
||||
|
||||
<template>
|
||||
<div class="flex items-center mx-4 gap-4">
|
||||
<el-tooltip class="" effect="dark" content="视频教程" placement="bottom">
|
||||
<el-dropdown @command="toDoc">
|
||||
<el-icon
|
||||
class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
|
||||
>
|
||||
<Film />
|
||||
</el-icon>
|
||||
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="item in videoList"
|
||||
:key="item.link"
|
||||
:command="item.link"
|
||||
>{{ item.title }}</el-dropdown-item
|
||||
>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip class="" effect="dark" content="搜索" placement="bottom">
|
||||
<el-icon
|
||||
@click="handleCommand"
|
||||
class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
|
||||
>
|
||||
<Search />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip class="" effect="dark" content="系统设置" placement="bottom">
|
||||
<el-icon
|
||||
class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
|
||||
@click="toggleSetting"
|
||||
>
|
||||
<Setting />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip class="" effect="dark" content="刷新" placement="bottom">
|
||||
<el-icon
|
||||
class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
|
||||
:class="showRefreshAnmite ? 'animate-spin' : ''"
|
||||
@click="toggleRefresh"
|
||||
>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
class=""
|
||||
effect="dark"
|
||||
content="切换主题"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-icon
|
||||
v-if="appStore.isDark"
|
||||
class="w-8 h-8 shadow rounded-full border border-gray-600 cursor-pointer border-solid"
|
||||
@click="appStore.toggleTheme(false)"
|
||||
>
|
||||
<Sunny />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
v-else
|
||||
class="w-8 h-8 shadow rounded-full border border-gray-200 cursor-pointer border-solid"
|
||||
@click="appStore.toggleTheme(true)"
|
||||
>
|
||||
<Moon />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
|
||||
<gva-setting v-model:drawer="showSettingDrawer"></gva-setting>
|
||||
<command-menu ref="command" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/pinia'
|
||||
import GvaSetting from '@/view/layout/setting/index.vue'
|
||||
import { ref } from 'vue'
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import CommandMenu from '@/components/commandMenu/index.vue'
|
||||
import { toDoc } from '@/utils/doc'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const showSettingDrawer = ref(false)
|
||||
const showRefreshAnmite = ref(false)
|
||||
const toggleRefresh = () => {
|
||||
showRefreshAnmite.value = true
|
||||
emitter.emit('reload')
|
||||
setTimeout(() => {
|
||||
showRefreshAnmite.value = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const toggleSetting = () => {
|
||||
showSettingDrawer.value = true
|
||||
}
|
||||
|
||||
const first = ref('')
|
||||
const command = ref()
|
||||
|
||||
const handleCommand = () => {
|
||||
command.value.open()
|
||||
}
|
||||
const initPage = () => {
|
||||
// 判断当前用户的操作系统
|
||||
if (window.localStorage.getItem('osType') === 'WIN') {
|
||||
first.value = 'Ctrl'
|
||||
} else {
|
||||
first.value = '⌘'
|
||||
}
|
||||
// 当用户同时按下ctrl和k键的时候
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.ctrlKey && e.key === 'k') {
|
||||
// 阻止浏览器默认事件
|
||||
e.preventDefault()
|
||||
handleCommand()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
|
||||
initPage()
|
||||
|
||||
const videoList = [
|
||||
{
|
||||
title: '1.clone项目和安装依赖',
|
||||
link: 'https://www.bilibili.com/video/BV1jx4y1s7xx'
|
||||
},
|
||||
{
|
||||
title: '2.初始化项目',
|
||||
link: 'https://www.bilibili.com/video/BV1sr421K7sv'
|
||||
},
|
||||
{
|
||||
title: '3.开启调试工具+创建初始化包',
|
||||
link: 'https://www.bilibili.com/video/BV1iH4y1c7Na'
|
||||
},
|
||||
{
|
||||
title: '4.手动使用自动化创建功能',
|
||||
link: 'https://www.bilibili.com/video/BV1UZ421T7fV'
|
||||
},
|
||||
{
|
||||
title: '5.使用已有表格创建业务',
|
||||
link: 'https://www.bilibili.com/video/BV1NE4m1977s'
|
||||
},
|
||||
{
|
||||
title: '6.使用AI创建业务和创建数据源模式的可选项',
|
||||
link: 'https://www.bilibili.com/video/BV17i421a7DE'
|
||||
},
|
||||
{
|
||||
title: '7.创建自己的后端方法',
|
||||
link: 'https://www.bilibili.com/video/BV1Yw4m1k7fg'
|
||||
},
|
||||
{
|
||||
title: '8.新增一个前端页面',
|
||||
link: 'https://www.bilibili.com/video/BV12y411i7oE'
|
||||
},
|
||||
{
|
||||
title: '9.配置一个前端二级页面',
|
||||
link: 'https://www.bilibili.com/video/BV1ZM4m1y7i3'
|
||||
},
|
||||
{
|
||||
title: '10.配置一个前端菜单参数',
|
||||
link: 'https://www.bilibili.com/video/BV1WS42197DZ'
|
||||
},
|
||||
{
|
||||
title: '11.菜单参数实战+动态菜单标题+菜单高亮配置',
|
||||
link: 'https://www.bilibili.com/video/BV1NE4m1979c'
|
||||
},
|
||||
{
|
||||
title: '12.增加菜单可控按钮',
|
||||
link: 'https://www.bilibili.com/video/BV1Sw4m1k746'
|
||||
},
|
||||
{
|
||||
title: '14.新增客户角色和其相关配置教学',
|
||||
link: 'https://www.bilibili.com/video/BV1Ki421a7X2'
|
||||
},
|
||||
{
|
||||
title: '15.发布项目上线',
|
||||
link: 'https://www.bilibili.com/video/BV1Lx4y1s77D'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
73
src/view/layout/iframe.vue
Normal file
73
src/view/layout/iframe.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-gray-50 text-slate-700 dark:text-slate-500 dark:bg-slate-800 w-screen h-screen"
|
||||
>
|
||||
<iframe
|
||||
v-if="reloadFlag"
|
||||
id="gva-base-load-dom"
|
||||
class="gva-body-h bg-gray-50 dark:bg-slate-800 w-full border-t border-gray-200 dark:border-slate-700"
|
||||
:src="url"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import useResponsive from '@/hooks/responsive'
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import { ref, onMounted, nextTick, reactive, watchEffect } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
const { isDark } = storeToRefs(appStore)
|
||||
|
||||
|
||||
defineOptions({
|
||||
name: 'GvaLayoutIframe'
|
||||
})
|
||||
|
||||
useResponsive(true)
|
||||
const font = reactive({
|
||||
color: 'rgba(0, 0, 0, .15)'
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
font.color = isDark.value ? 'rgba(255,255,255, .15)' : 'rgba(0, 0, 0, .15)'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const url = route.query.url || 'https://www.gin-vue-admin.com'
|
||||
|
||||
onMounted(() => {
|
||||
// 挂载一些通用的事件
|
||||
emitter.on('reload', reload)
|
||||
if (userStore.loadingInstance) {
|
||||
userStore.loadingInstance.close()
|
||||
}
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const reloadFlag = ref(true)
|
||||
let reloadTimer = null
|
||||
const reload = async () => {
|
||||
if (reloadTimer) {
|
||||
window.clearTimeout(reloadTimer)
|
||||
}
|
||||
reloadTimer = window.setTimeout(async () => {
|
||||
if (route.meta.keepAlive) {
|
||||
reloadFlag.value = false
|
||||
await nextTick()
|
||||
reloadFlag.value = true
|
||||
} else {
|
||||
const title = route.meta.title
|
||||
router.push({ name: 'Reload', params: { title } })
|
||||
}
|
||||
}, 400)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
114
src/view/layout/index.vue
Normal file
114
src/view/layout/index.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-gray-50 text-slate-700 dark:text-slate-500 dark:bg-slate-800 w-screen h-screen"
|
||||
>
|
||||
<el-watermark
|
||||
v-if="config.show_watermark"
|
||||
:font="font"
|
||||
:z-index="9999"
|
||||
:gap="[180, 150]"
|
||||
class="absolute inset-0 pointer-events-none"
|
||||
:content="userStore.userInfo.nickName"
|
||||
/>
|
||||
<gva-header />
|
||||
<div class="flex flex-row w-full gva-container pt-16 box-border h-full">
|
||||
<gva-aside
|
||||
v-if="
|
||||
config.side_mode === 'normal' ||
|
||||
(device === 'mobile' && config.side_mode == 'head') ||
|
||||
(device === 'mobile' && config.side_mode == 'combination')
|
||||
"
|
||||
/>
|
||||
<gva-aside
|
||||
v-if="config.side_mode === 'combination' && device !== 'mobile'"
|
||||
mode="normal"
|
||||
/>
|
||||
<div class="flex-1 px-2 w-0 h-full">
|
||||
<gva-tabs v-if="config.showTabs" />
|
||||
<div
|
||||
class="overflow-auto"
|
||||
:class="config.showTabs ? 'gva-container2' : 'gva-container pt-1'"
|
||||
>
|
||||
<router-view v-if="reloadFlag" v-slot="{ Component, route }">
|
||||
<div
|
||||
id="gva-base-load-dom"
|
||||
class="gva-body-h bg-gray-50 dark:bg-slate-800"
|
||||
>
|
||||
<transition mode="out-in" :name="config.transition_type">
|
||||
<keep-alive :include="routerStore.keepAliveRouters">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</div>
|
||||
</router-view>
|
||||
<BottomInfo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import GvaAside from '@/view/layout/aside/index.vue'
|
||||
import GvaHeader from '@/view/layout/header/index.vue'
|
||||
import useResponsive from '@/hooks/responsive'
|
||||
import GvaTabs from './tabs/index.vue'
|
||||
import BottomInfo from '@/components/bottomInfo/bottomInfo.vue'
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import { ref, onMounted, nextTick, reactive, watchEffect } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import '@/style/transition.scss'
|
||||
const appStore = useAppStore()
|
||||
const { config, isDark, device } = storeToRefs(appStore)
|
||||
|
||||
defineOptions({
|
||||
name: 'GvaLayout'
|
||||
})
|
||||
|
||||
useResponsive(true)
|
||||
const font = reactive({
|
||||
color: 'rgba(0, 0, 0, .15)'
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
font.color = isDark.value ? 'rgba(255,255,255, .15)' : 'rgba(0, 0, 0, .15)'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const routerStore = useRouterStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 挂载一些通用的事件
|
||||
emitter.on('reload', reload)
|
||||
if (userStore.loadingInstance) {
|
||||
userStore.loadingInstance.close()
|
||||
}
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const reloadFlag = ref(true)
|
||||
let reloadTimer = null
|
||||
const reload = async () => {
|
||||
if (reloadTimer) {
|
||||
window.clearTimeout(reloadTimer)
|
||||
}
|
||||
reloadTimer = window.setTimeout(async () => {
|
||||
if (route.meta.keepAlive) {
|
||||
reloadFlag.value = false
|
||||
await nextTick()
|
||||
reloadFlag.value = true
|
||||
} else {
|
||||
const title = route.meta.title
|
||||
router.push({ name: 'Reload', params: { title } })
|
||||
}
|
||||
}, 400)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
62
src/view/layout/screenfull/index.vue
Normal file
62
src/view/layout/screenfull/index.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div @click="clickFull">
|
||||
<div v-if="isShow" class="gvaIcon gvaIcon-fullscreen-expand" />
|
||||
<div v-else class="gvaIcon gvaIcon-fullscreen-shrink" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import screenfull from 'screenfull' // 引入screenfull
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'Screenfull'
|
||||
})
|
||||
|
||||
defineProps({
|
||||
width: {
|
||||
type: Number,
|
||||
default: 22
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 22
|
||||
},
|
||||
fill: {
|
||||
type: String,
|
||||
default: '#48576a'
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.on('change', changeFullShow)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
screenfull.off('change')
|
||||
})
|
||||
|
||||
const clickFull = () => {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
const isShow = ref(true)
|
||||
const changeFullShow = () => {
|
||||
isShow.value = !screenfull.isFullscreen
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.screenfull-svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
margin-right: 32px;
|
||||
fill: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
</style>
|
98
src/view/layout/search/search.vue
Normal file
98
src/view/layout/search/search.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="search-component items-center">
|
||||
<div
|
||||
class="gvaIcon gvaIcon-refresh"
|
||||
:class="[reload ? 'reloading' : '']"
|
||||
@click="handleReload"
|
||||
/>
|
||||
<Screenfull class="search-icon" />
|
||||
<div class="gvaIcon gvaIcon-customer-service" @click="toService" />
|
||||
<el-switch
|
||||
v-model="isDark"
|
||||
:active-action-icon="Moon"
|
||||
:inactive-action-icon="Sunny"
|
||||
@change="handleDarkSwitch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Screenfull from '@/view/layout/screenfull/index.vue'
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import { Sunny, Moon } from '@element-plus/icons-vue'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'BtnBox'
|
||||
})
|
||||
const isDark = ref(localStorage.getItem('isDark') === 'true' || true)
|
||||
|
||||
watchEffect(() => {
|
||||
if (isDark.value) {
|
||||
document.documentElement.classList.add('dark')
|
||||
localStorage.setItem('isDark', true)
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
localStorage.setItem('isDark', false)
|
||||
}
|
||||
})
|
||||
const reload = ref(false)
|
||||
const handleReload = () => {
|
||||
reload.value = true
|
||||
emitter.emit('reload')
|
||||
setTimeout(() => {
|
||||
reload.value = false
|
||||
}, 500)
|
||||
}
|
||||
const toService = () => {
|
||||
window.open('https://support.qq.com/product/371961')
|
||||
}
|
||||
|
||||
const handleDarkSwitch = (e) => {
|
||||
isDark.value = e
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.search-component {
|
||||
@apply inline-flex overflow-hidden text-center gap-5 mr-5 text-black dark:text-gray-100;
|
||||
div {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
.el-input__inner {
|
||||
@apply border-b border-solid border-gray-300;
|
||||
}
|
||||
.el-dropdown-link {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.reload {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.reloading {
|
||||
animation: turn 0.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes turn {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
212
src/view/layout/setting/index.vue
Normal file
212
src/view/layout/setting/index.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="drawer"
|
||||
title="系统配置"
|
||||
direction="rtl"
|
||||
:size="width"
|
||||
:show-close="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">系统配置</span>
|
||||
<el-button type="primary" @click="saveConfig">保存配置</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col">
|
||||
<div class="mb-8">
|
||||
<Title title="默认主题"></Title>
|
||||
<div class="mt-2 text-sm p-2 flex items-center justify-center gap-2">
|
||||
<el-segmented
|
||||
v-model="config.darkMode"
|
||||
:options="options"
|
||||
size="default"
|
||||
@change="appStore.toggleDarkMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<Title title="主题色"></Title>
|
||||
<div class="mt-2 text-sm p-2 flex items-center gap-2 justify-center">
|
||||
<div
|
||||
v-for="item in colors"
|
||||
:key="item"
|
||||
class="w-5 h-5 rounded cursor-pointer flex items-center justify-center"
|
||||
:style="`background:${item}`"
|
||||
@click="appStore.togglePrimaryColor(item)"
|
||||
>
|
||||
<el-icon v-if="config.primaryColor === item">
|
||||
<Select />
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-color-picker
|
||||
v-model="customColor"
|
||||
@change="appStore.togglePrimaryColor"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<Title title="主题配置"></Title>
|
||||
<div class="mt-2 text-md p-2 flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>展示水印</div>
|
||||
<el-switch
|
||||
v-model="config.show_watermark"
|
||||
@change="appStore.toggleConfigWatermark"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>灰色模式</div>
|
||||
<el-switch v-model="config.grey" @change="appStore.toggleGrey" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>色弱模式</div>
|
||||
<el-switch
|
||||
v-model="config.weakness"
|
||||
@change="appStore.toggleWeakness"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>菜单模式</div>
|
||||
<el-segmented
|
||||
v-model="config.side_mode"
|
||||
:options="sideModes"
|
||||
size="default"
|
||||
@change="appStore.toggleSideMode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>显示标签页</div>
|
||||
<el-switch
|
||||
v-model="config.showTabs"
|
||||
@change="appStore.toggleTabs"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex-shrink-0">页面切换动画</div>
|
||||
<el-select
|
||||
v-model="config.transition_type"
|
||||
@change="appStore.toggleTransition"
|
||||
class="w-40"
|
||||
>
|
||||
<el-option value="fade" label="淡入淡出" />
|
||||
<el-option value="slide" label="滑动" />
|
||||
<el-option value="zoom" label="缩放" />
|
||||
<el-option value="none" label="无动画" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<Title title="layout 大小配置"></Title>
|
||||
<div class="mt-2 text-md p-2 flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div>侧边栏展开宽度</div>
|
||||
<el-input-number
|
||||
v-model="config.layout_side_width"
|
||||
:min="150"
|
||||
:max="400"
|
||||
:step="10"
|
||||
></el-input-number>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div>侧边栏收缩宽度</div>
|
||||
<el-input-number
|
||||
v-model="config.layout_side_collapsed_width"
|
||||
:min="60"
|
||||
:max="100"
|
||||
></el-input-number>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div>侧边栏子项高度</div>
|
||||
<el-input-number
|
||||
v-model="config.layout_side_item_height"
|
||||
:min="30"
|
||||
:max="50"
|
||||
></el-input-number>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <el-alert type="warning" :closable="false">
|
||||
请注意,所有配置请保存到本地文件的
|
||||
<el-tag>config.json</el-tag> 文件中,否则刷新页面后会丢失配置
|
||||
</el-alert>-->
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { setSelfSetting } from '@/api/user'
|
||||
import Title from './title.vue'
|
||||
const appStore = useAppStore()
|
||||
const { config, device } = storeToRefs(appStore)
|
||||
defineOptions({
|
||||
name: 'GvaSetting'
|
||||
})
|
||||
|
||||
const width = computed(() => {
|
||||
return device.value === 'mobile' ? '100%' : '500px'
|
||||
})
|
||||
|
||||
const colors = [
|
||||
'#EB2F96',
|
||||
'#3b82f6',
|
||||
'#2FEB54',
|
||||
'#EBEB2F',
|
||||
'#EB2F2F',
|
||||
'#2FEBEB'
|
||||
]
|
||||
|
||||
const drawer = defineModel('drawer', {
|
||||
default: true,
|
||||
type: Boolean
|
||||
})
|
||||
|
||||
const options = ['dark', 'light', 'auto']
|
||||
const sideModes = [
|
||||
{
|
||||
label: '正常模式',
|
||||
value: 'normal'
|
||||
},
|
||||
{
|
||||
label: '顶部菜单栏模式',
|
||||
value: 'head'
|
||||
},
|
||||
{
|
||||
label: '组合模式',
|
||||
value: 'combination'
|
||||
}
|
||||
]
|
||||
|
||||
const saveConfig = async () => {
|
||||
/*const input = document.createElement("textarea");
|
||||
input.value = JSON.stringify(config.value);
|
||||
// 添加回车
|
||||
input.value = input.value.replace(/,/g, ",\n");
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(input);
|
||||
ElMessage.success("复制成功, 请自行保存到本地文件中");*/
|
||||
const res = await setSelfSetting(config.value)
|
||||
if (res.code === 0) {
|
||||
localStorage.setItem('originSetting', JSON.stringify(config.value))
|
||||
ElMessage.success('保存成功')
|
||||
drawer.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const customColor = ref('')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep(.el-drawer__header) {
|
||||
@apply border-gray-400 dark:border-gray-600;
|
||||
}
|
||||
</style>
|
34
src/view/layout/setting/title.vue
Normal file
34
src/view/layout/setting/title.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="title relative my-2">
|
||||
<div class="flex-shrink-0 text-center text-xl text-gray-600">
|
||||
{{ title }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'layoutSettingTitle'
|
||||
})
|
||||
|
||||
defineProps({
|
||||
title: String
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.title::before,
|
||||
.title::after {
|
||||
content: '';
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: #e3e3e3;
|
||||
}
|
||||
</style>
|
425
src/view/layout/tabs/index.vue
Normal file
425
src/view/layout/tabs/index.vue
Normal file
@@ -0,0 +1,425 @@
|
||||
<!--
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/7
|
||||
!-->
|
||||
<template>
|
||||
<div class="gva-tabs">
|
||||
<el-tabs
|
||||
v-model="activeValue"
|
||||
:closable="!(historys.length === 1 && $route.name === defaultRouter)"
|
||||
type="card"
|
||||
class="bg-white text-slate-700 dark:text-slate-500 dark:bg-slate-900"
|
||||
@contextmenu.prevent="openContextMenu($event)"
|
||||
@tab-click="changeTab"
|
||||
@tab-remove="removeTab"
|
||||
@click.middle.prevent="middleCloseTab($event)"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="item in historys"
|
||||
:key="getFmtString(item)"
|
||||
:label="item.meta.title"
|
||||
:name="getFmtString(item)"
|
||||
:tab="item"
|
||||
class="border-none"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
:tab="item"
|
||||
:class="
|
||||
activeValue === getFmtString(item)
|
||||
? 'text-active'
|
||||
: 'text-gray-600 dark:text-slate-400 '
|
||||
"
|
||||
><i
|
||||
:class="
|
||||
activeValue === getFmtString(item)
|
||||
? 'text-active'
|
||||
: 'text-gray-600 dark:text-slate-400'
|
||||
"
|
||||
/>
|
||||
{{ fmtTitle(item.meta.title, item) }}</span
|
||||
>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!--自定义右键菜单html代码-->
|
||||
<ul
|
||||
v-show="contextMenuVisible"
|
||||
:style="{ left: left + 'px', top: top + 'px' }"
|
||||
class="contextmenu"
|
||||
>
|
||||
<li @click="closeAll">关闭所有</li>
|
||||
<li @click="closeLeft">关闭左侧</li>
|
||||
<li @click="closeRight">关闭右侧</li>
|
||||
<li @click="closeOther">关闭其他</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import { computed, onUnmounted, ref, watch, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { fmtTitle } from '@/utils/fmtRouterTitle'
|
||||
|
||||
defineOptions({
|
||||
name: 'HistoryComponent'
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const getFmtString = (item) => {
|
||||
return item.name + JSON.stringify(item.query) + JSON.stringify(item.params)
|
||||
}
|
||||
|
||||
const historys = ref([])
|
||||
const activeValue = ref('')
|
||||
const contextMenuVisible = ref(false)
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const left = ref(0)
|
||||
const top = ref(0)
|
||||
const isCollapse = ref(false)
|
||||
const isMobile = ref(false)
|
||||
const rightActive = ref('')
|
||||
const defaultRouter = computed(
|
||||
() => userStore.userInfo.authority.defaultRouter
|
||||
)
|
||||
const openContextMenu = (e) => {
|
||||
if (historys.value.length === 1 && route.name === defaultRouter.value) {
|
||||
return false
|
||||
}
|
||||
let id = ''
|
||||
if (e.srcElement.nodeName === 'SPAN') {
|
||||
id = e.srcElement.offsetParent.id
|
||||
} else {
|
||||
id = e.srcElement.id
|
||||
}
|
||||
if (id) {
|
||||
contextMenuVisible.value = true
|
||||
|
||||
left.value = e.clientX
|
||||
top.value = e.clientY + 10
|
||||
rightActive.value = id.substring(4)
|
||||
}
|
||||
}
|
||||
const closeAll = () => {
|
||||
historys.value = [
|
||||
{
|
||||
name: defaultRouter.value,
|
||||
meta: {
|
||||
title: '首页'
|
||||
},
|
||||
query: {},
|
||||
params: {}
|
||||
}
|
||||
]
|
||||
router.push({ name: defaultRouter.value })
|
||||
contextMenuVisible.value = false
|
||||
sessionStorage.setItem('historys', JSON.stringify(historys.value))
|
||||
}
|
||||
const closeLeft = () => {
|
||||
let right
|
||||
const rightIndex = historys.value.findIndex((item) => {
|
||||
if (getFmtString(item) === rightActive.value) {
|
||||
right = item
|
||||
}
|
||||
return getFmtString(item) === rightActive.value
|
||||
})
|
||||
const activeIndex = historys.value.findIndex(
|
||||
(item) => getFmtString(item) === activeValue.value
|
||||
)
|
||||
historys.value.splice(0, rightIndex)
|
||||
if (rightIndex > activeIndex) {
|
||||
router.push(right)
|
||||
}
|
||||
sessionStorage.setItem('historys', JSON.stringify(historys.value))
|
||||
}
|
||||
const closeRight = () => {
|
||||
let right
|
||||
const leftIndex = historys.value.findIndex((item) => {
|
||||
if (getFmtString(item) === rightActive.value) {
|
||||
right = item
|
||||
}
|
||||
return getFmtString(item) === rightActive.value
|
||||
})
|
||||
const activeIndex = historys.value.findIndex(
|
||||
(item) => getFmtString(item) === activeValue.value
|
||||
)
|
||||
historys.value.splice(leftIndex + 1, historys.value.length)
|
||||
if (leftIndex < activeIndex) {
|
||||
router.push(right)
|
||||
}
|
||||
sessionStorage.setItem('historys', JSON.stringify(historys.value))
|
||||
}
|
||||
const closeOther = () => {
|
||||
let right
|
||||
historys.value = historys.value.filter((item) => {
|
||||
if (getFmtString(item) === rightActive.value) {
|
||||
right = item
|
||||
}
|
||||
return getFmtString(item) === rightActive.value
|
||||
})
|
||||
router.push(right)
|
||||
sessionStorage.setItem('historys', JSON.stringify(historys.value))
|
||||
}
|
||||
const isSame = (route1, route2) => {
|
||||
if (route1.name !== route2.name) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
Object.keys(route1.query).length !== Object.keys(route2.query).length ||
|
||||
Object.keys(route1.params).length !== Object.keys(route2.params).length
|
||||
) {
|
||||
return false
|
||||
}
|
||||
for (const key in route1.query) {
|
||||
if (route1.query[key] !== route2.query[key]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (const key in route1.params) {
|
||||
if (route1.params[key] !== route2.params[key]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
const setTab = (route) => {
|
||||
if (!historys.value.some((item) => isSame(item, route))) {
|
||||
const obj = {}
|
||||
obj.name = route.name
|
||||
obj.meta = { ...route.meta }
|
||||
delete obj.meta.matched
|
||||
obj.query = route.query
|
||||
obj.params = route.params
|
||||
historys.value.push(obj)
|
||||
}
|
||||
window.sessionStorage.setItem('activeValue', getFmtString(route))
|
||||
}
|
||||
|
||||
const historyMap = ref({})
|
||||
|
||||
const changeTab = (TabsPaneContext) => {
|
||||
const name = TabsPaneContext?.props?.name
|
||||
if (!name) return
|
||||
const tab = historyMap.value[name]
|
||||
router.push({
|
||||
name: tab.name,
|
||||
query: tab.query,
|
||||
params: tab.params
|
||||
})
|
||||
}
|
||||
const removeTab = (tab) => {
|
||||
const index = historys.value.findIndex((item) => getFmtString(item) === tab)
|
||||
if (getFmtString(route) === tab) {
|
||||
if (historys.value.length === 1) {
|
||||
router.push({ name: defaultRouter.value })
|
||||
} else {
|
||||
if (index < historys.value.length - 1) {
|
||||
router.push({
|
||||
name: historys.value[index + 1].name,
|
||||
query: historys.value[index + 1].query,
|
||||
params: historys.value[index + 1].params
|
||||
})
|
||||
} else {
|
||||
router.push({
|
||||
name: historys.value[index - 1].name,
|
||||
query: historys.value[index - 1].query,
|
||||
params: historys.value[index - 1].params
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
historys.value.splice(index, 1)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => contextMenuVisible.value,
|
||||
() => {
|
||||
if (contextMenuVisible.value) {
|
||||
document.body.addEventListener('click', () => {
|
||||
contextMenuVisible.value = false
|
||||
})
|
||||
} else {
|
||||
document.body.removeEventListener('click', () => {
|
||||
contextMenuVisible.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => route,
|
||||
(to) => {
|
||||
if (to.name === 'Login' || to.name === 'Reload') {
|
||||
return
|
||||
}
|
||||
historys.value = historys.value.filter((item) => !item.meta.closeTab)
|
||||
setTab(to)
|
||||
sessionStorage.setItem('historys', JSON.stringify(historys.value))
|
||||
activeValue.value = window.sessionStorage.getItem('activeValue')
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => historys.value,
|
||||
() => {
|
||||
sessionStorage.setItem('historys', JSON.stringify(historys.value))
|
||||
historyMap.value = {}
|
||||
historys.value.forEach((item) => {
|
||||
historyMap.value[getFmtString(item)] = item
|
||||
})
|
||||
emitter.emit('setKeepAlive', historys.value)
|
||||
},
|
||||
{
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
const initPage = () => {
|
||||
// 全局监听 关闭当前页面函数
|
||||
emitter.on('closeThisPage', () => {
|
||||
removeTab(getFmtString(route))
|
||||
})
|
||||
// 全局监听 关闭所有页面函数
|
||||
emitter.on('closeAllPage', () => {
|
||||
closeAll()
|
||||
})
|
||||
emitter.on('mobile', (data) => {
|
||||
isMobile.value = data
|
||||
})
|
||||
emitter.on('collapse', (data) => {
|
||||
isCollapse.value = data
|
||||
})
|
||||
|
||||
emitter.on('setQuery', (data) => {
|
||||
const index = historys.value.findIndex(
|
||||
(item) => getFmtString(item) === activeValue.value
|
||||
)
|
||||
historys.value[index].query = data
|
||||
activeValue.value = getFmtString(historys.value[index])
|
||||
const currentUrl = window.location.href.split('?')[0]
|
||||
const currentSearchParams = new URLSearchParams(data).toString()
|
||||
window.history.replaceState(
|
||||
{},
|
||||
'',
|
||||
`${currentUrl}?${currentSearchParams}`
|
||||
)
|
||||
sessionStorage.setItem('historys', JSON.stringify(historys.value))
|
||||
})
|
||||
|
||||
emitter.on('switchTab', async (data) => {
|
||||
const index = historys.value.findIndex((item) => item.name === data.name)
|
||||
if (index < 0) {
|
||||
return
|
||||
}
|
||||
for (const key in data.query) {
|
||||
data.query[key] = String(data.query[key])
|
||||
}
|
||||
for (const key in data.params) {
|
||||
data.params[key] = String(data.params[key])
|
||||
}
|
||||
|
||||
historys.value[index].query = data.query || {}
|
||||
historys.value[index].params = data.params || {}
|
||||
await nextTick()
|
||||
router.push(historys.value[index])
|
||||
})
|
||||
const initHistorys = [
|
||||
{
|
||||
name: defaultRouter.value,
|
||||
meta: {
|
||||
title: '首页'
|
||||
},
|
||||
query: {},
|
||||
params: {}
|
||||
}
|
||||
]
|
||||
setTab(route)
|
||||
historys.value =
|
||||
JSON.parse(sessionStorage.getItem('historys')) || initHistorys
|
||||
if (!window.sessionStorage.getItem('activeValue')) {
|
||||
activeValue.value = getFmtString(route)
|
||||
} else {
|
||||
activeValue.value = window.sessionStorage.getItem('activeValue')
|
||||
}
|
||||
if (window.sessionStorage.getItem('needCloseAll') === 'true') {
|
||||
closeAll()
|
||||
window.sessionStorage.removeItem('needCloseAll')
|
||||
}
|
||||
}
|
||||
initPage()
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.off('collapse')
|
||||
emitter.off('mobile')
|
||||
})
|
||||
|
||||
const middleCloseTab = (e) => {
|
||||
if (historys.value.length === 1 && route.name === defaultRouter.value) {
|
||||
return false
|
||||
}
|
||||
let id = ''
|
||||
if (e.srcElement.nodeName === 'SPAN') {
|
||||
id = e.srcElement.offsetParent.id
|
||||
} else {
|
||||
id = e.srcElement.id
|
||||
}
|
||||
if (id) {
|
||||
removeTab(id.substring(4))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.contextmenu {
|
||||
@apply bg-white dark:bg-slate-900 w-28 m-0 py-2.5 px-0 border border-gray-200 text-sm shadow-md rounded absolute z-50 border-solid dark:border-slate-800;
|
||||
}
|
||||
|
||||
.contextmenu li {
|
||||
@apply text-slate-700 dark:text-slate-200 text-base list-none px-4 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer;
|
||||
}
|
||||
|
||||
$base-tag-item-height: 4rem;
|
||||
|
||||
.gva-tabs {
|
||||
::v-deep(.el-tabs--card > .el-tabs__header) {
|
||||
border: none;
|
||||
}
|
||||
::v-deep(.el-tabs__nav-scroll) {
|
||||
padding: 4px 4px;
|
||||
}
|
||||
|
||||
::v-deep(.el-tabs__nav) {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
::v-deep(.el-tabs__header) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
::v-deep(.el-tabs__item) {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--el-border-color-darker);
|
||||
border-radius: 2px;
|
||||
margin-right: 5px;
|
||||
margin-left: 2px;
|
||||
transition: padding 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !important;
|
||||
height: 34px;
|
||||
&.is-active {
|
||||
border: 1px solid var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
::v-deep(.el-tabs__item):first-child {
|
||||
border: 1px solid var(--el-border-color-darker);
|
||||
&.is-active {
|
||||
border: 1px solid var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user