🧹 清理重复配置文件
- 删除根目录中重复的 NestJS 配置文件 - 删除 tsconfig.json, tsconfig.build.json, eslint.config.mjs, .prettierrc - 保留 wwjcloud-nest/ 目录中的完整配置 - 避免配置冲突,确保项目结构清晰
This commit is contained in:
314
admin-vben/src/layout/admin/components/aside/index.vue
Normal file
314
admin-vben/src/layout/admin/components/aside/index.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div :class="['layout-aside ease-in duration-200 flex box-border', { 'bright': !dark}]">
|
||||
<div class="flex flex-col border-0 border-r-[1px] border-solid border-[var(--el-color-info-light-8)] box-border overflow-hidden">
|
||||
|
||||
<div :class="['w-[150px] one-menu hide-scrollbar', { 'expanded': systemStore.menuIsCollapse }]" >
|
||||
<!-- <div class="flex items-center justify-center bg-primary text-white h-[40px] text-[16px] rounded-[4px] mb-[10px]"><span class="text-[20px]">+</span><span v-if="systemStore.menuIsCollapse" class="ml-[10px]">应用市场</span></div> -->
|
||||
<div class="flex flex-col items-center">
|
||||
<template v-for="(item, index) in oneMenuData">
|
||||
<div v-if="item.meta.show" :title="systemStore.menuIsCollapse ? item.meta.title : item.meta.short_title" class="menu-item my-[2px] p-2 flex w-full box-border cursor-pointer relative" :class="{'is-active':oneMenuActive===item.original_name,'hover-left': systemStore.menuIsCollapse, 'vertical': !systemStore.menuIsCollapse , 'horizontal': systemStore.menuIsCollapse }" :style="{ height: (systemStore.menuIsCollapse ) ? '40px' : '55px' }" @click="router.push({ name: item.name })">
|
||||
<div class="w-[20px] h-[20px] flex items-center justify-center menu-icon" :class="{'is-active':oneMenuActive===item.original_name}">
|
||||
<template v-if="item.meta.icon">
|
||||
<el-image class="w-[20px] h-[20px] overflow-hidden" :src="item.meta.icon" fit="fill" v-if="isUrl(item.meta.icon)"/>
|
||||
<icon :name="item.meta.icon" size="20px" color="#1D1F3A" v-else />
|
||||
</template>
|
||||
<icon v-else :name="'iconfont iconshezhi1'" color="#1D1F3A" />
|
||||
</div>
|
||||
|
||||
<div v-if="systemStore.menuIsCollapse" class="text-left text-[14px] mt-[3px] w-[75px] using-hidden ml-[10px]">{{ item.meta.title || item.meta.short_title }}</div>
|
||||
<div v-else class="text-center text-[12px] using-hidden mt-1">{{ item.meta.short_title || item.meta.title }}</div>
|
||||
<div v-if="systemStore.menuIsCollapse && item.name=='app_store' && recentlyUpdated.length>0" class="text-[11px] bg-[#DA203E] px-[10px] rounded-[12px] text-[#fff] absolute right-[6px]">更新</div>
|
||||
<div v-if="!systemStore.menuIsCollapse && item.name=='app_store' && recentlyUpdated.length>0" class="w-[7px] h-[7px] bg-[#DA203E] absolute flex items-center justify-center rounded-full top-[4px] right-[14px]"></div>
|
||||
<div v-if="systemStore.menuIsCollapse && item.original_name=='tool' && isNewVersion" class="text-[11px] bg-[#DA203E] px-[10px] rounded-[12px] text-[#fff] absolute right-[6px]">更新</div>
|
||||
<div v-if="!systemStore.menuIsCollapse && item.original_name=='tool' && isNewVersion" class="w-[7px] h-[7px] bg-[#DA203E] absolute flex items-center justify-center rounded-full top-[4px] right-[14px]"></div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-col two-menu w-[185px] " v-if="twoMenuData.length">
|
||||
<!-- <div class="w-[185px] h-[64px] flex items-center justify-center text-[16px]">{{ route.matched[1].meta.title }}</div> -->
|
||||
<el-scrollbar class="flex-1" >
|
||||
<el-menu :default-active="route.name" :default-openeds="defaultOpeneds" :router="true" class="aside-menu">
|
||||
<menu-item v-for="(route, index) in twoMenuData" :routes="route" :key="index" :isNewVersion="isNewVersion" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { ADMIN_ROUTE,findFirstValidRoute } from "@/router/routers"
|
||||
import { isUrl } from '@/utils/common'
|
||||
import menuItem from './menu-item.vue'
|
||||
import { getAddonLocal} from '@/app/api/addon'
|
||||
import { getVersions } from "@/app/api/auth"
|
||||
import { getFrameworkVersionList } from "@/app/api/module"
|
||||
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const routers = userStore.routers
|
||||
const systemStore = useSystemStore()
|
||||
const router = useRouter()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
|
||||
const logoUrl = computed(() => {
|
||||
return userStore.siteInfo.icon ? userStore.siteInfo.icon : systemStore.website.icon
|
||||
})
|
||||
const twoMenuData = ref<Record<string, any>[]>([])
|
||||
const defaultOpeneds = ref<string[]>([]) // 默认打开的菜单项路径数组
|
||||
|
||||
const oneMenuData = ref<Record<string, any>[]>([])
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
}
|
||||
oneMenuData.value.push(item)
|
||||
})
|
||||
|
||||
const oneMenuActive = ref(oneMenuData.value[0].name)
|
||||
watch(route, () => {
|
||||
twoMenuData.value = route.matched[1].children ?? []
|
||||
oneMenuActive.value = route.matched[1].name == ADMIN_ROUTE.children[0].name ? route.matched[2].name : route.matched[1].name
|
||||
defaultOpeneds.value = twoMenuData.value.map(item => item.name)
|
||||
}, { immediate: true })
|
||||
|
||||
const recentlyUpdated = ref([])
|
||||
const localListFn = () => {
|
||||
getAddonLocal({}).then((res) => {
|
||||
const data = res.data.list
|
||||
recentlyUpdated.value = []
|
||||
for (const i in data) {
|
||||
if (data[i].install_info && Object.keys(data[i].install_info)?.length) {
|
||||
if (data[i].install_info.version != data[i].version) {
|
||||
recentlyUpdated.value.push(data[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
localListFn()
|
||||
const frameworkVersionList = ref([])
|
||||
const isNewVersion = computed(() => {
|
||||
if (!newVersion.value || newVersion.value.version_no === version.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 将版本号转为字符串再处理
|
||||
const currentVersionStr = String(version.value);
|
||||
const latestVersionStr = String(newVersion.value.version_no);
|
||||
// 移除点号并转为数字比较
|
||||
const currentVersionNum = parseInt(currentVersionStr.replace(/\./g, ''), 10);
|
||||
const latestVersionNum = parseInt(latestVersionStr.replace(/\./g, ''), 10);
|
||||
return latestVersionNum > currentVersionNum;
|
||||
})
|
||||
|
||||
const getFrameworkVersionListFn = () => {
|
||||
getFrameworkVersionList().then(({ data }) => {
|
||||
frameworkVersionList.value = data
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
getFrameworkVersionListFn()
|
||||
|
||||
const newVersion: any = computed(() => {
|
||||
return frameworkVersionList.value.length ? frameworkVersionList.value[0] : null
|
||||
})
|
||||
const version = ref('')
|
||||
const getVersionsInfo = () => {
|
||||
getVersions().then((res) => {
|
||||
version.value = res.data.version.version
|
||||
})
|
||||
}
|
||||
getVersionsInfo()
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.one-menu{
|
||||
padding: 20px 10px 10px;
|
||||
width: 78px;
|
||||
overflow-y: auto;
|
||||
// transition: width 0.1s ease-out;
|
||||
&.expanded {
|
||||
width: 185px;
|
||||
padding: 18px 15px 15px;
|
||||
}
|
||||
.menu-item{
|
||||
border-radius: 2px;
|
||||
justify-content: center;
|
||||
&.vertical {
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.menu-icon {
|
||||
// background-color: transparent; /* 默认无背景色 */
|
||||
color: #1D1F3A;
|
||||
}
|
||||
|
||||
// .menu-icon.is-active {
|
||||
// background-color: var(--el-color-primary); /* 选中时背景色 */
|
||||
// color: white; /* 选中时图标颜色变白 */
|
||||
// border-radius: 4px; /* 可选:使图标背景为圆形 */
|
||||
// }
|
||||
|
||||
&:hover{
|
||||
background-color: #EAEBF0 !important;
|
||||
border-radius: 6px;
|
||||
// background-color: var(--el-color-primary-light-9) !important;
|
||||
// color:var(--el-color-primary);
|
||||
}
|
||||
&.is-active{
|
||||
background-color: #EAEBF0 !important;
|
||||
border-radius: 6px;
|
||||
// background-color: var(--el-color-primary-light-9) !important;
|
||||
// border: none;
|
||||
// color:var(--el-color-primary);
|
||||
}
|
||||
span{
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
.menu-item.hover-left {
|
||||
justify-content: flex-start;
|
||||
padding-left: 5px;
|
||||
}
|
||||
&.expanded .menu-item .text-center {
|
||||
opacity: 1;
|
||||
}
|
||||
.el-menu{
|
||||
border: 0;
|
||||
}
|
||||
.el-scrollbar{
|
||||
height: calc(100vh - 65px);
|
||||
}
|
||||
}
|
||||
.two-menu{
|
||||
.aside-menu:not(.el-menu--collapse) {
|
||||
width: 185px;
|
||||
border: 0;
|
||||
padding-top: 15px;
|
||||
.el-menu-item{
|
||||
height: 40px;
|
||||
margin: 4px 15px;
|
||||
padding: 0 8px !important;
|
||||
border-radius: 2px;
|
||||
span{
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
&.is-active{
|
||||
background-color: #EAEBF0 !important;
|
||||
border-radius: 6px;
|
||||
color: inherit;
|
||||
// background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
&:hover{
|
||||
background-color: #EAEBF0 !important;
|
||||
border-radius: 6px;
|
||||
// background-color: var(--el-color-primary-light-9) !important;
|
||||
// color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.el-sub-menu{
|
||||
width: 185px;
|
||||
margin: 4px 0;
|
||||
// margin-bottom: 8px;
|
||||
.el-sub-menu__title{
|
||||
margin: 0 15px;
|
||||
height: 40px;
|
||||
padding-left: 8px;
|
||||
border-radius: 2px;
|
||||
span{
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
&:hover{
|
||||
background-color:#EAEBF0 !important;
|
||||
border-radius: 6px;
|
||||
// background-color: var(--el-color-primary-light-9) !important;
|
||||
// color: var(--el-color-primary);
|
||||
}
|
||||
.el-icon.el-sub-menu__icon-arrow{
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
.el-menu-item{
|
||||
padding-left: 25px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-wrap {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
|
||||
.logo {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: var(--el-font-size-base);
|
||||
}
|
||||
}
|
||||
// :deep(.el-scrollbar__bar){
|
||||
// display: none !important;
|
||||
// }
|
||||
// .layout-aside .el-scrollbar__wrap--hidden-default, .layout-aside .el-scrollbar{
|
||||
// overflow: inherit !important;
|
||||
// }
|
||||
// 隐藏滚动条
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
/* Chrome/Safari/Edge */
|
||||
}
|
||||
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
/* IE/Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
}
|
||||
// .layout-aside .menu-item.is-active{
|
||||
// position: relative;
|
||||
// &:after{
|
||||
// content: "";
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// width: 1px;
|
||||
// background: var(--el-color-primary);
|
||||
// right: -1px;
|
||||
// }
|
||||
// }
|
||||
</style>
|
||||
80
admin-vben/src/layout/admin/components/aside/menu-item.vue
Normal file
80
admin-vben/src/layout/admin/components/aside/menu-item.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<template v-if="meta.show">
|
||||
<el-sub-menu v-if="routes.children" :index="String(routes.name)">
|
||||
<template #title>
|
||||
<div v-if="meta.icon && props.level != 2" class="w-[13px] h-[13px] mr-[10rpx] relative flex justify-center items-center">
|
||||
<icon v-if="meta.icon && props.level != 2" :name="meta.icon" color="#1D1F3A" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="using-hidden" :class="['ml-[10px]', {'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}]">{{ meta.title }}</span>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :level="props.level + 1" :key="index" :isNewVersion="props.isNewVersion" />
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else :index="String(routes.name)" :route="routes.path">
|
||||
<template #title>
|
||||
<div v-if="meta.icon && props.level != 2" class="w-[13px] h-[13px] mr-[10rpx] relative flex justify-center items-center">
|
||||
<icon v-if="meta.icon && props.level != 2" color="#1D1F3A" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="using-hidden" :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}
|
||||
<div v-if="meta.view=='app/upgrade'&& props.isNewVersion" class="w-[7px] h-[7px] bg-[#DA203E] absolute flex items-center justify-center rounded-full top-[10px] right-[65px]"></div>
|
||||
</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, computed } from 'vue'
|
||||
import menuItem from './menu-item.vue'
|
||||
import { CollectionTag } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
routes: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
isNewVersion: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const meta = computed(() => props.routes.meta)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-sub-menu{
|
||||
.el-icon{
|
||||
width: auto;
|
||||
}
|
||||
li{
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
.el-alert .el-alert__description{
|
||||
margin-top: 0;
|
||||
}
|
||||
.index-item {
|
||||
border: 1px solid;
|
||||
border-color: var(--el-color-primary);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style scoped>
|
||||
.using-hidden {
|
||||
word-break: break-all;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
141
admin-vben/src/layout/admin/components/aside/side.vue
Normal file
141
admin-vben/src/layout/admin/components/aside/side.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<el-container class="w-100" :class="[{ 'sidebar-dark-mode': systemStore.sidebar == 'twoType' }, { 'sidebar-brightness-mode': systemStore.sidebar == 'oneType' }]">
|
||||
<el-main class="menu-wrap">
|
||||
<el-scrollbar>
|
||||
<el-menu :default-active="menuActive" :router="true" class="aside-menu h-full" :unique-opened="true" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in userStore.routers[0].children" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import menuItem from './menu-item.vue'
|
||||
|
||||
const logo = ref('@/app/assets/images/login_logo.png')
|
||||
const systemStore = useSystemStore()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const siteInfo = userStore.siteInfo
|
||||
|
||||
const menuActive = computed(() => String(route.name))
|
||||
|
||||
userStore.routers = userStore.routers.filter((item, index) => {
|
||||
if (item.name == 'setting_manage') {
|
||||
// item.meta.class = 1
|
||||
if (item.children) {
|
||||
item.children.forEach((subItem, subIndex) => {
|
||||
subItem.meta.class = 1
|
||||
if (subItem.children) {
|
||||
subItem.children.forEach((threeItem, threeIndex) => {
|
||||
threeItem.meta.class = 2
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return item.children
|
||||
}
|
||||
})
|
||||
|
||||
// userStore.routers.forEach((item, index) => {
|
||||
// item.meta.class = 1
|
||||
// if (item.children) {
|
||||
// item.children.forEach((subItem, subIndex) => {
|
||||
// subItem.meta.class = 2
|
||||
// if (subItem.children) {
|
||||
// subItem.children.forEach((threeItem, threeIndex) => {
|
||||
// threeItem.meta.class = 3
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.logo-wrap {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
|
||||
.logo {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: var(--el-font-size-base);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-wrap {
|
||||
flex: 1 !important;
|
||||
padding: 0 !important;
|
||||
|
||||
.el-menu {
|
||||
border-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-dark-mode {
|
||||
background-color: #191a23;
|
||||
|
||||
&>.logo-wrap {
|
||||
.logo>i {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
border-bottom: 2px solid #101117;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
background-color: #191a23;
|
||||
|
||||
.el-sub-menu {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.el-sub-menu__title,
|
||||
.el-menu-item {
|
||||
background: transparent !important;
|
||||
color: #B7B7ba;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
color: #fff !important;
|
||||
background-color: var(--el-color-primary) !important;
|
||||
}
|
||||
|
||||
li::after {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-brightness-mode {
|
||||
&>.logo-wrap {
|
||||
.logo>i {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
200
admin-vben/src/layout/admin/components/header/index.vue
Normal file
200
admin-vben/src/layout/admin/components/header/index.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<el-container class="h-[64px] w-full layout-admin flex items-center justify-between pr-[15px] border-b-[1px] border-solid border-[var(--el-color-info-light-8)]" >
|
||||
<!-- :class="['h-full px-[10px]',{'layout-header border-b border-color': !dark}]" -->
|
||||
<div class="flex items-center">
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element Expand" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element Fold" v-else />
|
||||
</div> -->
|
||||
<div class="flex justify-center items-center flex-shrink-0" :class="{'w-[185px]': systemStore.menuIsCollapse,'w-[78px]': !systemStore.menuIsCollapse}">
|
||||
<div class="w-full h-[40px] overflow-hidden">
|
||||
<el-image style="width: 100%; height: 100%" :src="img(logoUrl)" fit="contain" v-if="!systemStore.menuIsCollapse">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-full"><img class="max-w-[70px]" src="@/app/assets/images/logo.default.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
<el-image style="width: 100%; height: 100%" :src="img(longLogoUrl)" fit="contain" v-else>
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-full"><img class="max-w-[180px]" src="@/app/assets/images/logo.default.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-panel flex items-center text-[14px] leading-[1]">
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element Fold" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element Expand" v-else />
|
||||
</div>
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element Refresh" />
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px] hidden-xs-only">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index" :to="route.path" class="inter">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div>
|
||||
<el-input placeholder="搜索站点或应用" v-model.trim="keywords" />
|
||||
</div> -->
|
||||
|
||||
<div>
|
||||
<div class="right-panel h-full flex items-center justify-end">
|
||||
<div class="border-primary border-[1px] h-[30px] px-[15px] flex items-center rounded-[6px] mr-[10px] cursor-pointer" @click="toHome()">
|
||||
<span class="iconfont iconguanliduan !text-primary mr-1"></span>
|
||||
<span class="text-[14px] text-primary">客户端</span>
|
||||
</div>
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<message />
|
||||
</div>
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" v-model="comparisonToken" />
|
||||
<input type="hidden" v-model="comparisonSiteId" />
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detectionLoginFn">{{ t('layout.detectionLoginOperation') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showDialog" :title="t('indexTemplate')" width="550px" :destroy-on-close="true" >
|
||||
<div class="flex flex-wrap">
|
||||
<div v-for="(items, index) in indexList" :key="index" v-if="index_path == ''">
|
||||
<div @click="index_path = items.view_path" class="index-item py-[5px] px-[10px] mr-[10px] rounded-[3px] cursor-pointer" :class="items.is_use == 1 ? 'bg-primary text-[#fff]' : '' ">
|
||||
<span >{{ items.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(itemTo, indexTo) in indexList" :key="indexTo" v-else>
|
||||
<div @click="index_path = itemTo.view_path" class="index-item py-[5px] px-[10px] mr-[10px] rounded-[3px] cursor-pointer" :class="index_path == itemTo.view_path ? 'bg-primary text-[#fff]' : '' ">
|
||||
<span >{{ itemTo.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="submitIndex">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useRoute,useRouter } from 'vue-router'
|
||||
import { img, getToken } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
import storage from '@/utils/storage'
|
||||
import userInfo from './user-info.vue'
|
||||
import layoutSetting from './layout-setting.vue'
|
||||
import message from './message.vue'
|
||||
|
||||
const showDialog = ref<boolean>(false)
|
||||
const systemStore = useSystemStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
const logoUrl = computed(() => {
|
||||
return userStore.siteInfo.icon ? userStore.siteInfo.icon : systemStore.website.icon
|
||||
})
|
||||
const longLogoUrl = computed(() => {
|
||||
return userStore.siteInfo.logo ? userStore.siteInfo.logo : systemStore.website.logo
|
||||
})
|
||||
// 检测登录 start
|
||||
const detectionLoginDialog = ref(false)
|
||||
const comparisonToken = ref('')
|
||||
const comparisonSiteId = ref('')
|
||||
if (storage.get('comparisonTokenStorage')) {
|
||||
comparisonToken.value = storage.get('comparisonTokenStorage')
|
||||
}
|
||||
if (storage.get('comparisonSiteIdStorage')) {
|
||||
comparisonSiteId.value = storage.get('comparisonSiteIdStorage')
|
||||
}
|
||||
// 监听标签页面切换
|
||||
document.addEventListener('visibilitychange', e => {
|
||||
if (document.visibilityState === 'visible' && (comparisonSiteId.value != storage.get('siteId') || comparisonToken.value != storage.get('token'))) {
|
||||
detectionLoginDialog.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const detectionLoginFn = () => {
|
||||
detectionLoginDialog.value = false
|
||||
location.reload()
|
||||
}
|
||||
// 检测登录 end
|
||||
|
||||
// 刷新路由
|
||||
const refreshRouter = () => {
|
||||
if (!appStore.routeRefreshTag) return
|
||||
appStore.refreshRouterView()
|
||||
}
|
||||
|
||||
// 面包屑导航
|
||||
const breadcrumb = computed(() => {
|
||||
const matched = route.matched.filter(item => { return item.meta.title })
|
||||
if (matched[0] && matched[0].path == '/') matched.splice(0, 1)
|
||||
return matched
|
||||
})
|
||||
storage.set({ key: 'currHeadMenuName', data: "" })
|
||||
systemStore.toggleMenuCollapse(storage.get('menuiscollapse') || false)
|
||||
const toggleMenuCollapse = () => {
|
||||
systemStore.toggleMenuCollapse(!systemStore.menuIsCollapse)
|
||||
}
|
||||
|
||||
const toHome = () => {
|
||||
if (!window.localStorage.getItem('site.token')) {
|
||||
window.localStorage.setItem('site.token', getToken())
|
||||
window.localStorage.setItem('site.comparisonTokenStorage', getToken())
|
||||
}
|
||||
if (!window.localStorage.getItem('site.userinfo')) {
|
||||
window.localStorage.setItem('site.userinfo', JSON.stringify(useUserStore().userInfo))
|
||||
}
|
||||
router.push({ path: '/home/index'})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-header{
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
border-bottom: 1px solid #e8e9eb;
|
||||
}
|
||||
.navbar-item {
|
||||
padding: 0 8px;
|
||||
}
|
||||
.index-item {
|
||||
border: 1px solid;
|
||||
border-color: var(--el-color-primary);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
// :deep(.el-input__wrapper) {
|
||||
// box-shadow: none !important;
|
||||
// border-radius: 4px !important;
|
||||
// background: #F7F7FA !important;
|
||||
// min-width: 638px;
|
||||
// height: 40px;
|
||||
// border-radius: 4px !important;
|
||||
// }
|
||||
</style>
|
||||
104
admin-vben/src/layout/admin/components/header/layout-setting.vue
Normal file
104
admin-vben/src/layout/admin/components/header/layout-setting.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<icon name="element Setting" @click="drawer = true" />
|
||||
|
||||
<el-drawer v-model="drawer" :title="t('layout.layoutSetting')" size="300px">
|
||||
<el-scrollbar>
|
||||
<!-- 黑暗模式 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.darkMode') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="dark" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主题颜色 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.themeColor') }}</div>
|
||||
<div>
|
||||
<el-color-picker v-model="theme" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 布局风格 -->
|
||||
<div class="setting-item mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.layoutStyle') }}</div>
|
||||
<div class="flex mt-[10px] layout-style flex-wrap">
|
||||
<div class="relative w-[125px] h-[100px] border mr-[10px] mb-[10px] hover:border-primary"
|
||||
:class="{ 'border-primary': currLayout == item.key }" v-for="(item, index) in layouts"
|
||||
@click="handleSetLayout(item.key)">
|
||||
<img :src="item.image" alt="" class="w-full h-full">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { setThemeColor, img } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
import Storage from '@/utils/storage'
|
||||
|
||||
const layouts = ref([
|
||||
{ key: 'admin', image: img('static/resource/images/system/layout_bussiness.png') },
|
||||
{ key: 'admin_simplicity', image: img('static/resource/images/system/layout_darkside.png') }
|
||||
])
|
||||
const currLayout = ref(Storage.get('admin_layout') || 'admin')
|
||||
|
||||
const drawer = ref(false)
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
const dark = computed({
|
||||
get() {
|
||||
return systemStore.dark
|
||||
},
|
||||
set(val) {
|
||||
systemStore.setTheme('dark', val)
|
||||
toggleDark(val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const sidebar = computed({
|
||||
get() {
|
||||
return systemStore.sidebar
|
||||
},
|
||||
set(val) {
|
||||
systemStore.setTheme('sidebar', val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const theme = computed({
|
||||
get() {
|
||||
return systemStore.theme
|
||||
},
|
||||
set(val) {
|
||||
systemStore.setTheme('theme', val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const handleSetLayout = (key: string) => {
|
||||
Storage.set({ key: 'admin_layout', data: key })
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.layout-style {
|
||||
&>div:nth-child(2n+2) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
64
admin-vben/src/layout/admin/components/header/message.vue
Normal file
64
admin-vben/src/layout/admin/components/header/message.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<el-popover class="box-item" :width="322">
|
||||
<template #reference>
|
||||
<div class="relative">
|
||||
<icon name="iconfont iconFramec-1" />
|
||||
<span v-if="showRedDot" class="absolute top-[-3px] right-[-5px] w-[12px] h-[12px] rounded-full bg-[#DA203E] text-white text-[12px] flex justify-center items-center">1</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center bg-[#F8FAFF] p-[10px] rounded-[8px]" v-if="showRedDot">
|
||||
<div class="w-[36px] h-[36px] rounded-full flex justify-center items-center">
|
||||
<img src="@/app/assets/images/index/cloud.png" alt="" class="w-[36px] h-[36px]" />
|
||||
</div>
|
||||
<div class="py-[3px] ml-[5px] flex-1">
|
||||
<div class="text-[16px] font-bold text-[#1D1F3A] mb-[5px]">云编译</div>
|
||||
<div class="text-[12px] text-[#4F516D] flex justify-between items-center">
|
||||
<span>有正在执行的编译任务</span>
|
||||
<span class="text-primary cursor-pointer ml-auto" @click="cloudBuildRef?.elNotificationClick()">点击查看</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center" v-else>
|
||||
<img src="@/app/assets/images/index/message_empty.png" alt="">
|
||||
</div>
|
||||
</el-popover>
|
||||
<!-- <i class="iconfont iconFramec-1 cursor-pointer" title="消息" v-else></i> -->
|
||||
<cloud-build ref="cloudBuildRef"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import CloudBuild from "@/app/components/cloud-build/index.vue"
|
||||
|
||||
const cloudBuildRef = ref<any>(null)
|
||||
|
||||
const showRedDot = ref(false)
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const startPolling = () => {
|
||||
if (pollTimer) return
|
||||
cloudBuildRef.value?.getCloudBuildTaskFn()
|
||||
|
||||
pollTimer = setInterval(() => {
|
||||
const startTime = localStorage.getItem('cloud_build_task')
|
||||
showRedDot.value = !!startTime
|
||||
if (!startTime) {
|
||||
stopPolling()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
}
|
||||
startPolling()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.box-item .el-popover.el-popper){
|
||||
padding: 13px 16px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-dropdown @command="switchLang" :tabindex="1">
|
||||
<icon name="iconfont iconfanyi" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="zh-cn" :disabled="systemStore.lang == 'zh-cn'">简体中文</el-dropdown-item>
|
||||
<el-dropdown-item command="en" :disabled="systemStore.lang == 'en'">English</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { language } from '@/lang'
|
||||
import { useRoute } from 'vue-router'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const route = useRoute()
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const switchLang = (command: string) => {
|
||||
systemStore.$patch((state) => {
|
||||
state.lang = command
|
||||
storage.set({ key: 'lang', data: command })
|
||||
})
|
||||
language.loadLocaleMessages(route.path, systemStore.lang)
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
159
admin-vben/src/layout/admin/components/header/user-info.vue
Normal file
159
admin-vben/src/layout/admin/components/header/user-info.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dropdown @command="clickEvent" :tabindex="1">
|
||||
<div class="userinfo flex h-full items-center">
|
||||
<el-avatar v-if="userStore.userInfo.head_img" :size="25" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
|
||||
<img v-else src="@/app/assets/images/member_head.png" class="w-[25px] rounded-full" />
|
||||
<div class="user-name pl-[8px]">{{ userStore.userInfo.username }}</div>
|
||||
<icon name="element ArrowDown" class="ml-[5px]" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<div class="p-[10px]">
|
||||
<div class="userinfo flex h-full items-center pb-[10px] border-b-[1px] border-solid border-[#e5e5e5]">
|
||||
<el-avatar v-if="userStore.userInfo.head_img" :size="45" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
|
||||
<img v-else src="@/app/assets/images/member_head.png" class="w-[45px] rounded-full" />
|
||||
<div>
|
||||
<div class="user-name pl-[8px] text-[14px]">{{ userStore.userInfo.username }}</div>
|
||||
<div class="pl-[8px] text-[13px] text-[#9699B6]">个人中心</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="getUserInfoFn" class="rounded-[4px]">
|
||||
<!-- <router-link to="/user/center"> -->
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconshezhi1 ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">账号设置</span>
|
||||
</div>
|
||||
<!-- </router-link> -->
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item class="rounded-[4px]">
|
||||
<router-link to="/tools/authorize">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconshouquanxinxi2 ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">授权信息</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="changePasswordDialog=true" class="rounded-[4px]">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconxiugai ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">修改密码</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="logout" class="rounded-[4px]">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont icontuichudenglu ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">退出登录</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码">
|
||||
<div>
|
||||
<el-form :model="saveInfo" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-form-item :label="t('originalPassword')" prop="original_password">
|
||||
<el-input v-model="saveInfo.original_password" type="password" :placeholder="t('originalPasswordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('newPassword')" prop="password">
|
||||
<el-input v-model="saveInfo.password" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
<div class="form-tip">{{t('passwordTip')}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('passwordCopy')" prop="password_copy">
|
||||
<el-input v-model="saveInfo.password_copy" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="changePasswordDialog = false">{{t('cancel')}}</el-button>
|
||||
<el-button type="primary" @click="submitForm(formRef)">{{t('save')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<user-info-edit ref="userInfoEditRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import type { FormInstance, FormRules, ElNotification } from 'element-plus'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { setUserInfo } from '@/app/api/personal'
|
||||
import { t } from '@/lang'
|
||||
import { img } from '@/utils/common'
|
||||
import userInfoEdit from '@/app/components/user-info-edit/index.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const clickEvent = (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
userStore.logout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userStore.logout()
|
||||
}
|
||||
const userInfoEditRef = ref(null)
|
||||
const getUserInfoFn = () => {
|
||||
userInfoEditRef.value?.open()
|
||||
}
|
||||
// 修改密码 --- start
|
||||
const changePasswordDialog = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
// 提交信息
|
||||
const saveInfo = reactive({
|
||||
original_password: '',
|
||||
password: '',
|
||||
password_copy: ''
|
||||
})
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
original_password: [
|
||||
{ required: true, message: t('originalPasswordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password_copy: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
let msg = ''
|
||||
if (saveInfo.password && !saveInfo.original_password) msg = t('originalPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && !saveInfo.password_copy) msg = t('newPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && saveInfo.password_copy && saveInfo.password != saveInfo.password_copy) msg = t('doubleCipherHint')
|
||||
if (msg) {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: msg
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setUserInfo(saveInfo).then((res: any) => {
|
||||
changePasswordDialog.value = false
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-popper .el-dropdown-menu{
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
</style>
|
||||
136
admin-vben/src/layout/admin/components/tabs.vue
Normal file
136
admin-vben/src/layout/admin/components/tabs.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="tab-wrap w-full px-[16px]">
|
||||
<el-tabs :closable="tabbarStore.tabLength > 1" :model-value="route.path" @tab-click="tabClick" @tab-remove="removeTab">
|
||||
<el-tab-pane v-for="(tab, key, index) in tabbarStore.tabs" :name="tab.path" :key="index">
|
||||
<template #label>
|
||||
<el-dropdown trigger="contextmenu" placement="bottom-start">
|
||||
<span :class="{ 'text-primary': route.path == tab.path }" class="tab-name">{{ tab.title }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Back" :disabled="index == 0" @click="closeLeft(tab.path)">{{t('tabs.closeLeft') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Right" :disabled="index == (tabbarStore.tabLength - 1)" @click="closeRight(tab.path)">{{t('tabs.closeRight') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Close" :disabled="tabbarStore.tabLength == 1" @click="closeOther(tab.path)">{{t('tabs.closeOther') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted } from 'vue'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const tabbarStore = useTabbarStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
tabbarStore.addTab(route)
|
||||
})
|
||||
|
||||
watch(route, (nval: any) => {
|
||||
tabbarStore.addTab(nval)
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加tab
|
||||
* @param content
|
||||
*/
|
||||
const tabClick = (content: any) => {
|
||||
const tabRoute = tabbarStore.tabs[content.props.name]
|
||||
router.push({ path: tabRoute.path, query: tabRoute.query })
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除tab
|
||||
* @param content
|
||||
*/
|
||||
const removeTab = (content: any) => {
|
||||
if (route.path == content) {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
router.push({ path: tabs[tabs.indexOf(content) - 1] })
|
||||
}
|
||||
tabbarStore.removeTab(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭左侧
|
||||
* @param path
|
||||
*/
|
||||
const closeLeft = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(path) - 1; i >= 0; i--) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ path })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧
|
||||
* @param path
|
||||
*/
|
||||
const closeRight = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(path) + 1; i < tabs.length; i++) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ path })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭其他
|
||||
* @param path
|
||||
*/
|
||||
const closeOther = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
tabs.forEach((key: string) => { key != path && delete tabbarStore.tabs[key] })
|
||||
router.push({ path })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs) {
|
||||
.el-tabs--border-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
margin-bottom: 0;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
display: inline-flex !important;
|
||||
padding: 0 20px !important;
|
||||
align-items: center;
|
||||
|
||||
.tab-name:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
45
admin-vben/src/layout/admin/index.vue
Normal file
45
admin-vben/src/layout/admin/index.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<el-container>
|
||||
<!-- 顶部 -->
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
<!-- 顶部 end -->
|
||||
<el-container :style="{height:'calc(100vh - 64px)'}">
|
||||
<!-- 左侧边栏 -->
|
||||
<layout-aside></layout-aside>
|
||||
<!-- 左侧边栏 end -->
|
||||
<!-- 主体 -->
|
||||
<el-main class="h-full p-0 bg-page">
|
||||
<el-scrollbar>
|
||||
<div class="p-[15px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<!-- 主体 end -->
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import layoutHeader from './components/header/index.vue'
|
||||
import layoutAside from './components/aside/index.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tabbarStore = useTabbarStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bg-page {
|
||||
background-color: #F7F7FA;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<el-aside class="layout-aside dark w-auto">
|
||||
<el-scrollbar>
|
||||
<side class="hidden-xs-only slide" />
|
||||
</el-scrollbar>
|
||||
</el-aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import side from './side.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
watch(route, () => {
|
||||
systemStore.$patch(state => {
|
||||
state.menuDrawer = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.layout-aside {
|
||||
//--side-dark-color: #141414;
|
||||
//background-color: var(--side-dark-color, var(--el-bg-color));
|
||||
|
||||
&.bright {
|
||||
// background-color: #F5F7F9;
|
||||
|
||||
li {
|
||||
// background-color: #F5F7F9;
|
||||
|
||||
&.is-active:not(.is-opened) {
|
||||
position: relative;
|
||||
color: #333;
|
||||
// background-color: #fff;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
//background-color: var(--el-menu-active-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slide {
|
||||
border-right: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
}
|
||||
|
||||
.aside-drawer {
|
||||
.el-drawer__body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<template v-if="meta.show">
|
||||
<el-sub-menu v-if="meta.type == 0 && routes.children" :index="String(routes.name)">
|
||||
<template #title>
|
||||
<div class="flex h-full flex-col items-center justify-center" v-if="systemStore.menuIsCollapse && props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" :size="'20px'" class="leading-none"/>
|
||||
<div class="leading-none mt-[5px]">{{ meta.short_title || meta.title }}</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<icon v-if="meta.icon && props.level != 1" :name="meta.icon" :size="props.level == 1 ? '20px' : '16px'" class="absolute" />
|
||||
</template>
|
||||
<span class="text-[14px]" :class="{ 'font-bold': props.level === 1 , ' ml-[20px]': (meta.icon && props.level != 1) }">{{ meta.title }}</span>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :key="index" :level="props.level + 1" />
|
||||
</el-sub-menu>
|
||||
<template v-else>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" >
|
||||
<div class="flex h-full flex-col items-center justify-center" v-if="systemStore.menuIsCollapse && props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" :size="'20px'" class="leading-none"/>
|
||||
<div class="leading-none mt-[5px]">{{ meta.short_title || meta.title }}</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<icon v-if="meta.icon && props.level != 1" :name="meta.icon" :size="props.level == 1 ? '20px' : '16px'" class="absolute" />
|
||||
</template>
|
||||
<template #title>
|
||||
<span class="text-[14px]" :class="{ 'font-bold': props.level === 1 , ' ml-[20px]': (meta.icon && props.level != 1) }">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import menuItem from './menu-item.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const routers = useUserStore().routers
|
||||
const props = defineProps({
|
||||
routes: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
})
|
||||
const systemStore = useSystemStore()
|
||||
const meta = computed(() => props.routes.meta)
|
||||
|
||||
const addons = computed(() => {
|
||||
const addons:Record<string, any> = {}
|
||||
userStore.siteInfo?.apps.forEach((item: any) => { addons[item.key] = item })
|
||||
userStore.siteInfo?.site_addons.forEach((item: any) => { addons[item.key] = item })
|
||||
return addons
|
||||
})
|
||||
|
||||
const systemAddonKeys = computed(() => {
|
||||
return userStore.siteInfo?.site_addons.map((item: any) => item.key)
|
||||
})
|
||||
|
||||
const addonRouters: Record<string, any> = {}
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon) {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
})
|
||||
|
||||
const addonsMenus = ref(null)
|
||||
|
||||
watch(route, () => {
|
||||
if (props.routes.name != 'addon_list') return
|
||||
|
||||
if (systemAddonKeys.value.includes(route.meta.addon) && addonRouters[route.meta.addon]) {
|
||||
addonsMenus.value = addonRouters[route.meta.addon]
|
||||
} else {
|
||||
addonsMenus.value = null
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-sub-menu{
|
||||
.el-icon{
|
||||
// width: auto;
|
||||
}
|
||||
}
|
||||
.el-menu {
|
||||
.el-sub-menu__title,
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
// background-color: #F1F5FF !important;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.el-icon.el-sub-menu__icon-arrow{
|
||||
font-size: 15px;
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
133
admin-vben/src/layout/admin_simplicity/components/aside/side.vue
Normal file
133
admin-vben/src/layout/admin_simplicity/components/aside/side.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<el-container class="flex flex-col">
|
||||
<el-main class="menu-wrap">
|
||||
<el-scrollbar class="menu-scrollbar flex-1 h-0">
|
||||
<el-menu :default-active="route.name" :default-openeds="defaultOpeneds" :router="true" :unique-opened="false" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in menuData" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import menuItem from './menu-item.vue'
|
||||
import { findFirstValidRoute } from '@/router/routers'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const siteInfo = userStore.siteInfo
|
||||
const routers = userStore.routers
|
||||
const addonIndexRoute = userStore.addonIndexRoute
|
||||
const menuData = ref<Record<string, any>[]>([])
|
||||
const addonRouters: Record<string, any> = {}
|
||||
const website = computed(() => {
|
||||
return systemStore.website
|
||||
})
|
||||
// 默认打开的菜单项路径数组
|
||||
const defaultOpeneds = ref<string[]>([])
|
||||
// 在组件挂载后,生成默认打开的菜单项路径数组
|
||||
onMounted(() => {
|
||||
|
||||
})
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon == '') {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
}
|
||||
menuData.value.push(item)
|
||||
} else if (item.meta.addon != '' && siteInfo?.apps.length == 1 && siteInfo?.apps[0].key == item.meta.addon) {
|
||||
if (item.children) {
|
||||
item.children.forEach((citem: Record<string, any>) => {
|
||||
citem.original_name = citem.name
|
||||
if (citem.children && citem.children.length) {
|
||||
citem.name = findFirstValidRoute(citem.children)
|
||||
}
|
||||
})
|
||||
menuData.value.unshift(...item.children)
|
||||
} else {
|
||||
menuData.value.unshift(item)
|
||||
}
|
||||
} else {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
})
|
||||
|
||||
// 多应用时将应用插入菜单
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
const routers:Record<string, any>[] = []
|
||||
siteInfo?.apps.forEach((item: Record<string, any>) => {
|
||||
if (addonRouters[item.key]) {
|
||||
addonRouters[item.key].name = addonIndexRoute[item.key]
|
||||
routers.push(addonRouters[item.key])
|
||||
}
|
||||
})
|
||||
menuData.value.unshift(...routers)
|
||||
}
|
||||
|
||||
defaultOpeneds.value = menuData.value.map(item => item.name)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.logo-wrap {
|
||||
// background: #1f2531;
|
||||
background: #fff;
|
||||
transition: transform getCssVar('transition-duration');
|
||||
}
|
||||
:root{
|
||||
--aside-width: 200px;
|
||||
}
|
||||
.menu-wrap {
|
||||
padding: 0!important;
|
||||
// background: #1f2531;
|
||||
// background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-menu {
|
||||
border-right: 0!important;
|
||||
|
||||
&:not(.el-menu--collapse) {
|
||||
width: var(--aside-width);
|
||||
}
|
||||
|
||||
.el-menu-item, .el-sub-menu__title {
|
||||
--el-menu-item-height: 40px;
|
||||
}
|
||||
|
||||
.el-sub-menu .el-menu-item {
|
||||
--el-menu-sub-item-height: 40px;
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
background: var(--el-color-primary) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
&.el-menu--inline {
|
||||
// background: #282e3a;
|
||||
// background: #fff;
|
||||
}
|
||||
.el-menu-item .el-menu-tooltip__trigger{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.el-menu--collapse {
|
||||
.el-menu-item, .el-sub-menu__title {
|
||||
--el-menu-item-height: 60px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<el-container class="h-[64px] layout-admin flex items-center justify-between px-[15px]" >
|
||||
<!-- :class="['h-full px-[10px]',{'layout-header border-b border-color': !dark}]" -->
|
||||
<div class="flex items-center">
|
||||
<div class="max-w-[120px] flex justify-center items-center flex-shrink-0">
|
||||
<div class="max-w-[120px] h-[40px] overflow-hidden">
|
||||
<el-image class="w-full h-full" :src="img(website.icon)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-full"><img class="max-w-[120px]" src="@/app/assets/images/logo.default.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-panel flex items-center text-[14px] leading-[1]">
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element Expand" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element Fold" v-else />
|
||||
</div>
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element Refresh" />
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px] hidden-xs-only">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index" :to="route.path" class="inter">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="right-panel h-full flex items-center justify-end">
|
||||
<div class="border-primary border-[1px] h-[30px] px-[15px] flex items-center rounded-[6px] mr-[10px] cursor-pointer" @click="toHome()">
|
||||
<span class="iconfont iconguanliduan !text-primary mr-1"></span>
|
||||
<span class="text-[14px] text-primary">客户端</span>
|
||||
</div>
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<message />
|
||||
</div>
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detectionLoginFn">{{ t('layout.detectionLoginOperation') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showDialog" :title="t('indexTemplate')" width="550px" :destroy-on-close="true" >
|
||||
<div class="flex flex-wrap">
|
||||
<div v-for="(items, index) in indexList" :key="index" v-if="index_path == ''">
|
||||
<div @click="index_path = items.view_path" class="index-item py-[5px] px-[10px] mr-[10px] rounded-[3px] cursor-pointer" :class="items.is_use == 1 ? 'bg-primary text-[#fff]' : '' ">
|
||||
<span >{{ items.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(itemTo, indexTo) in indexList" :key="indexTo" v-else>
|
||||
<div @click="index_path = itemTo.view_path" class="index-item py-[5px] px-[10px] mr-[10px] rounded-[3px] cursor-pointer" :class="index_path == itemTo.view_path ? 'bg-primary text-[#fff]' : '' ">
|
||||
<span >{{ itemTo.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="submitIndex">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import { useRoute,useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
import { img, getToken } from '@/utils/common'
|
||||
|
||||
import storage from '@/utils/storage'
|
||||
import userInfo from './user-info.vue'
|
||||
import layoutSetting from './layout-setting.vue'
|
||||
import message from './message.vue'
|
||||
import useSystemStore from "@/stores/modules/system";
|
||||
|
||||
const showDialog = ref<boolean>(false)
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const systemStore = useSystemStore()
|
||||
const website = computed(() => {
|
||||
return systemStore.website
|
||||
})
|
||||
|
||||
// 检测登录 start
|
||||
const detectionLoginDialog = ref(false)
|
||||
const comparisonToken = ref('')
|
||||
const comparisonSiteId = ref('')
|
||||
if (storage.get('comparisonTokenStorage')) {
|
||||
comparisonToken.value = storage.get('comparisonTokenStorage')
|
||||
}
|
||||
if (storage.get('comparisonSiteIdStorage')) {
|
||||
comparisonSiteId.value = storage.get('comparisonSiteIdStorage')
|
||||
}
|
||||
// 监听标签页面切换
|
||||
document.addEventListener('visibilitychange', e => {
|
||||
if (document.visibilityState === 'visible' && (comparisonSiteId.value != storage.get('siteId') || comparisonToken.value != storage.get('token'))) {
|
||||
detectionLoginDialog.value = true
|
||||
}
|
||||
})
|
||||
|
||||
systemStore.toggleMenuCollapse(storage.get('menuiscollapse') || false)
|
||||
|
||||
const detectionLoginFn = () => {
|
||||
detectionLoginDialog.value = false
|
||||
location.reload()
|
||||
}
|
||||
// 检测登录 end
|
||||
|
||||
// 刷新路由
|
||||
const refreshRouter = () => {
|
||||
if (!appStore.routeRefreshTag) return
|
||||
appStore.refreshRouterView()
|
||||
}
|
||||
|
||||
// 面包屑导航
|
||||
const breadcrumb = computed(() => {
|
||||
const matched = route.matched.filter(item => { return item.meta.title })
|
||||
if (matched[0] && matched[0].path == '/') matched.splice(0, 1)
|
||||
return matched
|
||||
})
|
||||
storage.set({ key: 'currHeadMenuName', data: "" })
|
||||
|
||||
const toggleMenuCollapse = () => {
|
||||
systemStore.toggleMenuCollapse(!systemStore.menuIsCollapse)
|
||||
}
|
||||
const toHome = () => {
|
||||
if (!window.localStorage.getItem('site.token')) {
|
||||
window.localStorage.setItem('site.token', getToken())
|
||||
window.localStorage.setItem('site.comparisonTokenStorage', getToken())
|
||||
}
|
||||
if (!window.localStorage.getItem('site.userinfo')) {
|
||||
window.localStorage.setItem('site.userinfo', JSON.stringify(useUserStore().userInfo))
|
||||
}
|
||||
router.push({ path: '/home/index'})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-header{
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
border-bottom: 1px solid #e8e9eb;
|
||||
}
|
||||
.navbar-item {
|
||||
padding: 0 8px;
|
||||
}
|
||||
.index-item {
|
||||
border: 1px solid;
|
||||
border-color: var(--el-color-primary);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<icon name="element Setting" @click="drawer = true" />
|
||||
|
||||
<el-drawer v-model="drawer" :title="t('layout.layoutSetting')" size="300px">
|
||||
<el-scrollbar>
|
||||
<!-- 黑暗模式 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.darkMode') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="dark" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主题颜色 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.themeColor') }}</div>
|
||||
<div>
|
||||
<el-color-picker v-model="theme" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 布局风格 -->
|
||||
<div class="setting-item mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.layoutStyle') }}</div>
|
||||
<div class="flex mt-[10px] layout-style flex-wrap">
|
||||
<div class="relative w-[125px] h-[100px] border mr-[10px] mb-[10px] hover:border-primary"
|
||||
:class="{ 'border-primary': currLayout == item.key }" v-for="(item, index) in layouts"
|
||||
@click="handleSetLayout(item.key)">
|
||||
<img :src="item.image" alt="" class="w-full h-full">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { setThemeColor, img } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
import Storage from '@/utils/storage'
|
||||
|
||||
const layouts = ref([
|
||||
{ key: 'admin', image: img('static/resource/images/system/layout_bussiness.png') },
|
||||
{ key: 'admin_simplicity', image: img('static/resource/images/system/layout_darkside.png') }
|
||||
])
|
||||
const currLayout = ref(Storage.get('admin_layout') || 'admin')
|
||||
|
||||
const drawer = ref(false)
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
const dark = computed({
|
||||
get() {
|
||||
return systemStore.dark
|
||||
},
|
||||
set(val) {
|
||||
systemStore.setTheme('dark', val)
|
||||
toggleDark(val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const sidebar = computed({
|
||||
get() {
|
||||
return systemStore.sidebar
|
||||
},
|
||||
set(val) {
|
||||
systemStore.setTheme('sidebar', val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const theme = computed({
|
||||
get() {
|
||||
return systemStore.theme
|
||||
},
|
||||
set(val) {
|
||||
systemStore.setTheme('theme', val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const handleSetLayout = (key: string) => {
|
||||
Storage.set({ key: 'admin_layout', data: key })
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.layout-style {
|
||||
&>div:nth-child(2n+2) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<el-popover class="box-item" :width="322">
|
||||
<template #reference>
|
||||
<div class="relative">
|
||||
<icon name="iconfont iconFramec-1" />
|
||||
<span v-if="showRedDot" class="absolute top-[-3px] right-[-5px] w-[12px] h-[12px] rounded-full bg-[#DA203E] text-white text-[12px] flex justify-center items-center">1</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex items-center bg-[#F8FAFF] p-[10px] rounded-[8px]" v-if="showRedDot">
|
||||
<div class="w-[36px] h-[36px] rounded-full flex justify-center items-center">
|
||||
<img src="@/app/assets/images/index/cloud.png" alt="" class="w-[36px] h-[36px]" />
|
||||
</div>
|
||||
<div class="py-[3px] ml-[5px] flex-1">
|
||||
<div class="text-[16px] font-bold text-[#1D1F3A] mb-[5px]">云编译</div>
|
||||
<div class="text-[12px] text-[#4F516D] flex justify-between items-center">
|
||||
<span>有正在执行的编译任务</span>
|
||||
<span class="text-primary cursor-pointer ml-auto" @click="cloudBuildRef?.elNotificationClick()">点击查看</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center" v-else>
|
||||
<img src="@/app/assets/images/index/message_empty.png" alt="">
|
||||
</div>
|
||||
</el-popover>
|
||||
<!-- <i class="iconfont iconFramec-1 cursor-pointer" title="消息" v-else></i> -->
|
||||
<cloud-build ref="cloudBuildRef"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import CloudBuild from "@/app/components/cloud-build/index.vue"
|
||||
|
||||
const cloudBuildRef = ref<any>(null)
|
||||
|
||||
const showRedDot = ref(false)
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const startPolling = () => {
|
||||
if (pollTimer) return
|
||||
cloudBuildRef.value?.getCloudBuildTaskFn()
|
||||
|
||||
pollTimer = setInterval(() => {
|
||||
const startTime = localStorage.getItem('cloud_build_task')
|
||||
showRedDot.value = !!startTime
|
||||
if (!startTime) {
|
||||
stopPolling()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
}
|
||||
startPolling()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.box-item .el-popover.el-popper){
|
||||
padding: 13px 16px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-dropdown @command="switchLang" :tabindex="1">
|
||||
<icon name="iconfont iconfanyi" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="zh-cn" :disabled="systemStore.lang == 'zh-cn'">简体中文</el-dropdown-item>
|
||||
<el-dropdown-item command="en" :disabled="systemStore.lang == 'en'">English</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { language } from '@/lang'
|
||||
import { useRoute } from 'vue-router'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const route = useRoute()
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const switchLang = (command: string) => {
|
||||
systemStore.$patch((state) => {
|
||||
state.lang = command
|
||||
storage.set({ key: 'lang', data: command })
|
||||
})
|
||||
language.loadLocaleMessages(route.path, systemStore.lang)
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dropdown @command="clickEvent" :tabindex="1">
|
||||
<div class="userinfo flex h-full items-center">
|
||||
<el-avatar v-if="userStore.userInfo.head_img" :size="25" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
|
||||
<img v-else src="@/app/assets/images/member_head.png" class="w-[25px] rounded-full" />
|
||||
<div class="user-name pl-[8px]">{{ userStore.userInfo.username }}</div>
|
||||
<icon name="element ArrowDown" class="ml-[5px]" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<div class="p-[10px]">
|
||||
<div class="userinfo flex h-full items-center pb-[10px] border-b-[1px] border-solid border-[#e5e5e5]">
|
||||
<el-avatar v-if="userStore.userInfo.head_img" :size="45" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
|
||||
<img v-else src="@/app/assets/images/member_head.png" class="w-[45px] rounded-full" />
|
||||
<div>
|
||||
<div class="user-name pl-[8px] text-[14px]">{{ userStore.userInfo.username }}</div>
|
||||
<div class="pl-[8px] text-[13px] text-[#9699B6]">个人中心</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="getUserInfoFn" class="rounded-[4px]">
|
||||
<!-- <router-link to="/user/center"> -->
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconshezhi1 ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">账号设置</span>
|
||||
</div>
|
||||
<!-- </router-link> -->
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item class="rounded-[4px]">
|
||||
<router-link to="/tools/authorize">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconshouquanxinxi2 ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">授权信息</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="changePasswordDialog=true" class="rounded-[4px]">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconxiugai ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">修改密码</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="logout" class="rounded-[4px]">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont icontuichudenglu ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">退出登录</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码">
|
||||
<div>
|
||||
<el-form :model="saveInfo" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-form-item :label="t('originalPassword')" prop="original_password">
|
||||
<el-input v-model="saveInfo.original_password" type="password" :placeholder="t('originalPasswordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('newPassword')" prop="password">
|
||||
<el-input v-model="saveInfo.password" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
<div class="form-tip">{{t('passwordTip')}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('passwordCopy')" prop="password_copy">
|
||||
<el-input v-model="saveInfo.password_copy" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="changePasswordDialog = false">{{t('cancel')}}</el-button>
|
||||
<el-button type="primary" @click="submitForm(formRef)">{{t('save')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<user-info-edit ref="userInfoEditRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import type { FormInstance, FormRules, ElNotification } from 'element-plus'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { setUserInfo } from '@/app/api/personal'
|
||||
import { t } from '@/lang'
|
||||
import { img } from '@/utils/common'
|
||||
import userInfoEdit from '@/app/components/user-info-edit/index.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const clickEvent = (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
userStore.logout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userStore.logout()
|
||||
}
|
||||
const userInfoEditRef = ref(null)
|
||||
const getUserInfoFn = () => {
|
||||
userInfoEditRef.value?.open()
|
||||
}
|
||||
// 修改密码 --- start
|
||||
const changePasswordDialog = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
// 提交信息
|
||||
const saveInfo = reactive({
|
||||
original_password: '',
|
||||
password: '',
|
||||
password_copy: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
original_password: [
|
||||
{ required: true, message: t('originalPasswordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password_copy: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
let msg = ''
|
||||
if (saveInfo.password && !saveInfo.original_password) msg = t('originalPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && !saveInfo.password_copy) msg = t('newPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && saveInfo.password_copy && saveInfo.password != saveInfo.password_copy) msg = t('doubleCipherHint')
|
||||
if (msg) {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: msg
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setUserInfo(saveInfo).then((res: any) => {
|
||||
changePasswordDialog.value = false
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-popper .el-dropdown-menu{
|
||||
width: 150px;
|
||||
}
|
||||
</style>
|
||||
136
admin-vben/src/layout/admin_simplicity/components/tabs.vue
Normal file
136
admin-vben/src/layout/admin_simplicity/components/tabs.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="tab-wrap w-full px-[16px]">
|
||||
<el-tabs :closable="tabbarStore.tabLength > 1" :model-value="route.path" @tab-click="tabClick" @tab-remove="removeTab">
|
||||
<el-tab-pane v-for="(tab, key, index) in tabbarStore.tabs" :name="tab.path" :key="index">
|
||||
<template #label>
|
||||
<el-dropdown trigger="contextmenu" placement="bottom-start">
|
||||
<span :class="{ 'text-primary': route.path == tab.path }" class="tab-name">{{ tab.title }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Back" :disabled="index == 0" @click="closeLeft(tab.path)">{{t('tabs.closeLeft') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Right" :disabled="index == (tabbarStore.tabLength - 1)" @click="closeRight(tab.path)">{{t('tabs.closeRight') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Close" :disabled="tabbarStore.tabLength == 1" @click="closeOther(tab.path)">{{t('tabs.closeOther') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted } from 'vue'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const tabbarStore = useTabbarStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
tabbarStore.addTab(route)
|
||||
})
|
||||
|
||||
watch(route, (nval: any) => {
|
||||
tabbarStore.addTab(nval)
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加tab
|
||||
* @param content
|
||||
*/
|
||||
const tabClick = (content: any) => {
|
||||
const tabRoute = tabbarStore.tabs[content.props.name]
|
||||
router.push({ path: tabRoute.path, query: tabRoute.query })
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除tab
|
||||
* @param content
|
||||
*/
|
||||
const removeTab = (content: any) => {
|
||||
if (route.path == content) {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
router.push({ path: tabs[tabs.indexOf(content) - 1] })
|
||||
}
|
||||
tabbarStore.removeTab(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭左侧
|
||||
* @param path
|
||||
*/
|
||||
const closeLeft = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(path) - 1; i >= 0; i--) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ path })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧
|
||||
* @param path
|
||||
*/
|
||||
const closeRight = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(path) + 1; i < tabs.length; i++) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ path })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭其他
|
||||
* @param path
|
||||
*/
|
||||
const closeOther = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
tabs.forEach((key: string) => { key != path && delete tabbarStore.tabs[key] })
|
||||
router.push({ path })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs) {
|
||||
.el-tabs--border-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
margin-bottom: 0;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
display: inline-flex !important;
|
||||
padding: 0 20px !important;
|
||||
align-items: center;
|
||||
|
||||
.tab-name:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
41
admin-vben/src/layout/admin_simplicity/index.vue
Normal file
41
admin-vben/src/layout/admin_simplicity/index.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<el-container>
|
||||
<!-- 顶部 -->
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
<!-- 顶部 end -->
|
||||
<el-container :style="{height:'calc(100vh - 64px)'}">
|
||||
<!-- 左侧边栏 -->
|
||||
<layout-aside></layout-aside>
|
||||
<!-- 左侧边栏 end -->
|
||||
<!-- 主体 -->
|
||||
<el-main class="h-full p-0 bg-page">
|
||||
<el-scrollbar>
|
||||
<div class="p-[15px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<!-- 主体 end -->
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import layoutHeader from './components/header/index.vue'
|
||||
import layoutAside from './components/aside/index.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tabbarStore = useTabbarStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
34
admin-vben/src/layout/bussiness/components/aside/index.vue
Normal file
34
admin-vben/src/layout/bussiness/components/aside/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<el-aside class="h-screen layout-aside w-auto">
|
||||
<side class="hidden-xs-only" />
|
||||
</el-aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import side from './side.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const route = useRoute()
|
||||
watch(route, () => {
|
||||
systemStore.$patch(state => {
|
||||
state.menuDrawer = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.layout-aside {
|
||||
background-color: var(--side-dark-color, var(--el-bg-color));
|
||||
border-right: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.aside-drawer {
|
||||
.el-drawer__body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<template v-if="meta.show">
|
||||
<el-sub-menu v-if="routes.children" :index="String(routes.name)">
|
||||
<template #title>
|
||||
<span :class="['ml-[10px]']">{{ meta.title }}</span>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :key="index" />
|
||||
</el-sub-menu>
|
||||
<template v-else>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-if="meta.addon && meta.parent_route && meta.parent_route.addon == ''">
|
||||
<template #title>
|
||||
<span :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-else>
|
||||
<template #title>
|
||||
<span :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
<div v-if="routes.is_border" class="!border-0 !border-t-[1px] border-solid mx-[25px] bg-[#f7f7f7] my-[5px]"></div>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import { img } from '@/utils/common'
|
||||
import menuItem from './menu-item.vue'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
|
||||
const router = useRouter()
|
||||
const props = defineProps({
|
||||
routes: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const userStore = useUserStore()
|
||||
const siteInfo = userStore.siteInfo
|
||||
const meta = computed(() => props.routes.meta)
|
||||
|
||||
const addons = computed(() => {
|
||||
const addons:Record<string, any> = {}
|
||||
siteInfo?.apps.forEach((item: any) => { addons[item.key] = item })
|
||||
siteInfo?.site_addons.forEach((item: any) => { addons[item.key] = item })
|
||||
return addons
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-sub-menu{
|
||||
.el-icon{
|
||||
width: auto;
|
||||
}
|
||||
li{
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
335
admin-vben/src/layout/bussiness/components/aside/side.vue
Normal file
335
admin-vben/src/layout/bussiness/components/aside/side.vue
Normal file
@@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<el-container class="w-100 h-screen">
|
||||
<el-main class="p-0 flex">
|
||||
<div class="w-[64px] bg-[#282c34] h-screen one-menu">
|
||||
<el-header class="logo-wrap">
|
||||
<div class="logo flex items-center m-auto h-[64px]" v-if="!systemStore.menuIsCollapse">
|
||||
<el-image style="width: 40px; height: 40px" :src="img(logoUrl)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-[40px]"><img class="max-w-[40px]" src="@/app/assets/images/icon-addon-one.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="logo flex items-center justify-center h-[64px]" v-else>
|
||||
<i class="text-3xl iconfont iconyunkongjian"></i>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-scrollbar class="h-[calc( 100vh - 64px )]">
|
||||
<el-menu :default-active="oneMenuActive" :router="true" class="aside-menu" :unique-opened="true">
|
||||
<template v-for="(item, index) in oneMenuData" :key="index">
|
||||
<el-menu-item :index="item.original_name" @click="router.push({ name: item.name })" v-if="item.meta.show">
|
||||
<div v-if="item.meta.icon" class="w-[16px] h-[16px] relative flex justify-center">
|
||||
<icon :name="item.meta.icon" class="absolute top-[50%] -translate-y-[50%]" />
|
||||
</div>
|
||||
<template #title>
|
||||
<div class="relative">
|
||||
<span class="ml-[10px] text-[15px]">{{ item.meta.short_title || item.meta.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<el-scrollbar v-if="twoMenuData.length" class="two-menu w-[190px]">
|
||||
<div class="w-[190px] h-[64px] flex items-center justify-center text-[16px] border-0 border-b-[1px] border-solid border-[#eee]">{{ route.matched[1].meta.title }}</div>
|
||||
<el-menu :default-active="route.name" :router="true" class="aside-menu" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in twoMenuData" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, onMounted, watchEffect } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import menuItem from './menu-item.vue'
|
||||
import { getShowApp, getShowMarketing } from '@/app/api/site'
|
||||
import { img } from '@/utils/common'
|
||||
import { findFirstValidRoute } from '@/router/routers'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const siteInfo = userStore.siteInfo
|
||||
const routers = userStore.routers
|
||||
const addonIndexRoute = userStore.addonIndexRoute
|
||||
|
||||
const oneMenuData = ref<Record<string, any>[]>([])
|
||||
const twoMenuData = ref<Record<string, any>[]>([])
|
||||
const addonRouters: Record<string, any> = {}
|
||||
const logoUrl = computed(() => {
|
||||
return userStore.siteInfo.icon ? userStore.siteInfo.icon : systemStore.website.icon
|
||||
})
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon == '') {
|
||||
if (item.meta.attr == '') {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
}
|
||||
oneMenuData.value.push(item)
|
||||
}
|
||||
} else if (item.meta.addon != '' && siteInfo?.apps.length <= 1 && siteInfo?.apps[0].key == item.meta.addon && item.meta.show) {
|
||||
if (item.children) {
|
||||
item.children.forEach((citem: Record<string, any>) => {
|
||||
citem.original_name = citem.name
|
||||
if (citem.children && citem.children.length) {
|
||||
citem.name = findFirstValidRoute(citem.children)
|
||||
}
|
||||
})
|
||||
oneMenuData.value.unshift(...item.children)
|
||||
} else {
|
||||
oneMenuData.value.unshift(item)
|
||||
}
|
||||
} else {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// oneMenuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
})
|
||||
|
||||
// 多应用时将应用插入菜单
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
const routers:Record<string, any>[] = []
|
||||
siteInfo?.apps.forEach((item: Record<string, any>) => {
|
||||
if (addonRouters[item.key]) {
|
||||
addonRouters[item.key].name = addonIndexRoute[item.key]
|
||||
routers.push(addonRouters[item.key])
|
||||
}
|
||||
})
|
||||
oneMenuData.value.unshift(...routers)
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// oneMenuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
const oneMenuActive = ref(route.matched[1].name)
|
||||
|
||||
const appList = ref(null)
|
||||
const marketingList = ref(null)
|
||||
// const loading = ref(true);
|
||||
const getAppList = async () => {
|
||||
const res = await getShowApp()
|
||||
appList.value = res.data
|
||||
// loading.value = false;
|
||||
}
|
||||
const getMarketingList = async () => {
|
||||
const res = await getShowMarketing()
|
||||
marketingList.value = res.data
|
||||
}
|
||||
onMounted(async () => {
|
||||
await getAppList() // 确保数据先加载
|
||||
await getMarketingList()
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
// if (!appList.value || loading.value) return; // 确保数据加载完毕
|
||||
const addonKeys = appList.value?.addon?.list?.map(item => item.key) ?? []
|
||||
const toolKeys = appList.value?.tool?.list?.map(item => item.key) ?? []
|
||||
const allKeys = [...addonKeys, ...toolKeys]
|
||||
const marketingKeys = marketingList.value?.marketing?.list?.map(item => item.key) ?? []
|
||||
const matchedName = route.matched[1]?.name
|
||||
if (allKeys.includes(matchedName)) {
|
||||
oneMenuActive.value = 'addon'
|
||||
twoMenuData.value = route.matched[1]?.children ?? []
|
||||
} else if (marketingKeys.includes(matchedName)) {
|
||||
oneMenuActive.value = 'active'
|
||||
twoMenuData.value = route.matched[1]?.children ?? []
|
||||
} else if (route.meta.attr !== '') {
|
||||
oneMenuActive.value = route.matched[2]?.name
|
||||
twoMenuData.value = route.matched[1]?.children ?? []
|
||||
} else {
|
||||
// 多应用
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
twoMenuData.value = route.matched[1]?.children
|
||||
oneMenuActive.value = route.matched[1]?.name
|
||||
} else {
|
||||
// 单应用
|
||||
const oneMenu = route.matched[1]
|
||||
if (oneMenu.meta.addon === '') {
|
||||
oneMenuActive.value = route.matched[1]?.name
|
||||
twoMenuData.value = route.matched[1]?.children ?? []
|
||||
} else {
|
||||
if (oneMenu.meta.addon === siteInfo?.apps[0]?.key) {
|
||||
oneMenuActive.value = route.matched[2]?.name
|
||||
twoMenuData.value = route.matched[2]?.children ?? []
|
||||
} else {
|
||||
oneMenuActive.value = route.matched[1]?.name
|
||||
twoMenuData.value = route.matched[1]?.children ?? []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// watch(route, () => {
|
||||
// if (route.meta.attr != '') {
|
||||
// oneMenuActive.value = route.matched[2].name
|
||||
// twoMenuData.value = route.matched[1].children ?? []
|
||||
// } else {
|
||||
// // 多应用
|
||||
// if (siteInfo?.apps.length > 1) {
|
||||
// twoMenuData.value = route.matched[1].children
|
||||
// oneMenuActive.value = route.matched[1].name
|
||||
// } else {
|
||||
// // 单应用
|
||||
// const oneMenu = route.matched[1]
|
||||
// if (oneMenu.meta.addon == '') {
|
||||
// oneMenuActive.value = route.matched[1].name
|
||||
// twoMenuData.value = route.matched[1].children ?? []
|
||||
// } else {
|
||||
// if (oneMenu.meta.addon == siteInfo?.apps[0].key) {
|
||||
// oneMenuActive.value = route.matched[2].name
|
||||
// twoMenuData.value = route.matched[2].children ?? []
|
||||
// } else {
|
||||
// oneMenuActive.value = route.matched[1].name
|
||||
// twoMenuData.value = route.matched[1].children ?? []
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.one-menu{
|
||||
.aside-menu:not(.el-menu--collapse) {
|
||||
background-color: transparent;
|
||||
.el-menu-item{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
height: 54px;
|
||||
width: 54px;
|
||||
color: #b9b9bf;
|
||||
font-size: 14px;
|
||||
border-radius: 5px;
|
||||
line-height: 1;
|
||||
&>div{
|
||||
i{
|
||||
position: static;
|
||||
transform: inherit;
|
||||
}
|
||||
span{
|
||||
display: block;
|
||||
margin: 5px 0 0;
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
background-color: transparent;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
&.is-active{
|
||||
background-color: var(--el-color-primary) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
span{
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-menu{
|
||||
border: 0;
|
||||
}
|
||||
.el-scrollbar{
|
||||
height: calc(100vh - 65px);
|
||||
}
|
||||
}
|
||||
.two-menu{
|
||||
.aside-menu:not(.el-menu--collapse) {
|
||||
width: 190px;
|
||||
border: 0;
|
||||
padding-top: 10px;
|
||||
.el-menu-item{
|
||||
height: 40px;
|
||||
margin: 0 8px 2px;
|
||||
padding: 0 10px !important;
|
||||
border-radius: 2px;
|
||||
span{
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
&.is-active{
|
||||
background-color: #f3f3f3 !important;
|
||||
color: #333;
|
||||
}
|
||||
&:hover{
|
||||
background-color: transparent;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.el-sub-menu{
|
||||
.el-sub-menu__title{
|
||||
margin: 0 8px 2px;
|
||||
height: 40px;
|
||||
padding-left: 8px;
|
||||
border-radius: 2px;
|
||||
span{
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
&:hover{
|
||||
background-color: transparent;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.el-menu-item{
|
||||
padding-left: 20px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-wrap {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
|
||||
.logo {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: var(--el-font-size-base);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
284
admin-vben/src/layout/bussiness/components/header/index.vue
Normal file
284
admin-vben/src/layout/bussiness/components/header/index.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<el-container :class="['h-full px-[10px]',{'layout-header border-b border-color': !dark}]" >
|
||||
<el-row class="w-100 h-full w-full">
|
||||
<el-col :span="10">
|
||||
<div class="left-panel h-full flex items-center">
|
||||
<!-- 左侧菜单折叠 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element Expand" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element Fold" v-else />
|
||||
</div> -->
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element Refresh" />
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px] hidden-xs-only">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index" :to="route.path" class="inter">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<div class="right-panel h-full flex items-center justify-end">
|
||||
<div class="flex items-center flex-shrink-0 hidden-xs-only">
|
||||
<el-dropdown trigger="hover" :hide-on-click="false" popper-class="site-info-wrap" class="mr-[8px]">
|
||||
<!-- 状态 -->
|
||||
<div class="mx-[8px] bg-[#f6f6f6] border-[1px] border-solid border-[#eee] rounded-[4px] px-[9px] py-[6px] flex items-center">
|
||||
<span class="mr-[6px] text-[12px] !text-[#333]">{{siteInfo.site_name}}</span>
|
||||
<span class="!text-[10px] text-[#f56c6c]" :class="{'!text-[#67c23a]': siteInfo.status == 1, '!text-[#f56c6c]': siteInfo.status == 3}">{{ siteInfo.status_name }}</span>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
<!-- 站点id -->
|
||||
<div class="text-[14px]">站点编号:{{siteInfo.site_id}}</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<!-- 到期时间 -->
|
||||
<div v-if="siteInfo.expire_time == 0" class="text-[14px]">到期时间:永久</div>
|
||||
<div v-else class="text-[14px]">到期时间:{{ siteInfo.expire_time }}</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div class="flex items-center flex-shrink-0 hidden-xs-only">
|
||||
<el-popover placement="bottom" :width="330" trigger="click" v-model:visible="isMenuSearch" >
|
||||
<template #reference>
|
||||
<i class="iconfont icona-sousuoV6xx-36 cursor-pointer px-[8px] !text-[14px]"></i>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="flex items-center">
|
||||
<el-select v-model="selectedRoute" filterable class="!w-[250px] mr-[20px] menu-select" :teleported="false" clearable @change="handleRouteSelect">
|
||||
<el-option v-for="item in flatRoutes" :key="item.name" :label="item.full_title" :value="item.name" >
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button type="primary" link @click="isMenuSearch = false">{{t('取消')}}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
<!-- 预览 只有站点时展示-->
|
||||
<i class="iconfont iconicon_huojian1 cursor-pointer px-[8px]" :title="t('visitWap')" @click="toPreview"></i>
|
||||
<i class="iconfont iconlingdang-xianxing cursor-pointer px-[8px]" :title="t('newInfo')" v-if="appType == 'site'"></i>
|
||||
<!-- 切换语言 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<switch-lang />
|
||||
</div> -->
|
||||
<!-- 切换全屏 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleFullscreen">
|
||||
<icon name="iconfont icontuichuquanping" v-if="isFullscreen" />
|
||||
<icon name="iconfont iconquanping" v-else />
|
||||
</div> -->
|
||||
<!-- 布局设置 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detectionLoginFn">{{ t('layout.detectionLoginOperation') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import layoutSetting from './layout-setting.vue'
|
||||
import userInfo from './user-info.vue'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const appType = storage.get('app_type')
|
||||
const { toggle: toggleFullscreen, isFullscreen } = useFullscreen()
|
||||
const systemStore = useSystemStore()
|
||||
const appStore = useAppStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const screenWidth = ref(window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth)
|
||||
|
||||
const userStore = useUserStore()
|
||||
const siteInfo:any = computed(() => {
|
||||
return userStore.siteInfo
|
||||
})
|
||||
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
const isMenuSearch = ref(false)
|
||||
const routers = userStore.routers
|
||||
const getParentTitleChain=(meta:any) =>{
|
||||
let titles = []
|
||||
let current = meta?.parent_route
|
||||
|
||||
while (current) {
|
||||
if (current.short_title) {
|
||||
titles.unshift(current.short_title)
|
||||
}
|
||||
current = current.parent_route
|
||||
}
|
||||
|
||||
return titles.join(' - ')
|
||||
}
|
||||
const flattenRoutes = (routes:any, parent = null)=> {
|
||||
let flat = [];
|
||||
routes.forEach(route => {
|
||||
const { path, name, meta = {}, short_title, children } = route
|
||||
const isLeaf = meta.type ==1 && meta.show==1
|
||||
if(isLeaf){
|
||||
const title = meta.title || short_title || ''
|
||||
const parentTitleChain = getParentTitleChain(meta)
|
||||
const fullTitle = parentTitleChain ? `${parentTitleChain} - ${title}` : title
|
||||
const item = {
|
||||
path,
|
||||
name,
|
||||
title,
|
||||
parent_title: parentTitleChain,
|
||||
full_title: fullTitle
|
||||
};
|
||||
|
||||
flat.push(item);
|
||||
}
|
||||
if (children && children.length > 0) {
|
||||
flat = flat.concat(flattenRoutes(children, route))
|
||||
}
|
||||
});
|
||||
|
||||
return flat;
|
||||
}
|
||||
const flatRoutes = flattenRoutes(routers)
|
||||
const selectedRoute = ref('')
|
||||
const handleRouteSelect = (name:any) => {
|
||||
if (name) {
|
||||
router.push({ name })
|
||||
isMenuSearch.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 检测登录 start
|
||||
const detectionLoginDialog = ref(false)
|
||||
const comparisonToken = ref('')
|
||||
const comparisonSiteId = ref('')
|
||||
if (storage.get('comparisonTokenStorage')) {
|
||||
comparisonToken.value = storage.get('comparisonTokenStorage')
|
||||
// storage.remove(['comparisonTokenStorage']);
|
||||
}
|
||||
if (storage.get('comparisonSiteIdStorage')) {
|
||||
comparisonSiteId.value = storage.get('comparisonSiteIdStorage')
|
||||
// storage.remove(['comparisonSiteIdStorage']);
|
||||
}
|
||||
// 监听标签页面切换
|
||||
document.addEventListener('visibilitychange', e => {
|
||||
if (document.visibilityState === 'visible' && (comparisonSiteId.value != storage.get('siteId') || comparisonToken.value != storage.get('token'))) {
|
||||
detectionLoginDialog.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const detectionLoginFn = () => {
|
||||
detectionLoginDialog.value = false
|
||||
location.href = `${location.origin}/site/`
|
||||
}
|
||||
// 检测登录 end
|
||||
|
||||
onMounted(() => {
|
||||
// 监听窗体宽度变化
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
screenWidth.value = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
|
||||
})()
|
||||
}
|
||||
})
|
||||
|
||||
// watch(screenWidth, () => {
|
||||
// if (screenWidth.value < 992) {
|
||||
// if (!systemStore.menuIsCollapse) systemStore.toggleMenuCollapse(true)
|
||||
// } else {
|
||||
// if (systemStore.menuIsCollapse) systemStore.toggleMenuCollapse(false)
|
||||
// }
|
||||
// })
|
||||
|
||||
// 菜单栏展开折叠
|
||||
// const toggleMenuCollapse = () => {
|
||||
// systemStore.$patch((state) => {
|
||||
// if (screenWidth.value < 768) {
|
||||
// state.menuDrawer = true
|
||||
// state.menuIsCollapse = false
|
||||
// } else {
|
||||
// systemStore.toggleMenuCollapse(!systemStore.menuIsCollapse)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// 刷新路由
|
||||
const refreshRouter = () => {
|
||||
if (!appStore.routeRefreshTag) return
|
||||
appStore.refreshRouterView()
|
||||
}
|
||||
|
||||
// 面包屑导航
|
||||
const breadcrumb = computed(() => {
|
||||
const matched = route.matched.filter(item => { return item.meta.title })
|
||||
if (matched[0] && matched[0].path == '/') matched.splice(0, 1)
|
||||
return matched
|
||||
})
|
||||
|
||||
// 跳转去预览
|
||||
const toPreview = () => {
|
||||
const url = router.resolve({
|
||||
path: '/preview/wap',
|
||||
query: {
|
||||
page: '/'
|
||||
}
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-header{
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
box-shadow: 0 0 4px 0 rgba(0,145,255,0.1);
|
||||
}
|
||||
.navbar-item {
|
||||
padding: 0 8px;
|
||||
&:hover {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
}
|
||||
.index-item {
|
||||
border: 1px solid;
|
||||
border-color: var(--el-color-primary);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
:deep(.el-dropdown-menu__item) {
|
||||
&:focus {
|
||||
background-color: transparent !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<icon name="element Setting" @click="drawer = true" />
|
||||
|
||||
<el-drawer v-model="drawer" :title="t('layout.layoutSetting')" size="300px">
|
||||
<el-scrollbar>
|
||||
<!-- 黑暗模式 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.darkMode') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="dark" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主题颜色 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.themeColor') }}</div>
|
||||
<div>
|
||||
<el-color-picker v-model="theme" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 标签栏 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.tab') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="tab" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { setThemeColor } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
import storage from "@/utils/storage";
|
||||
|
||||
const drawer = ref(false)
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
const dark = computed({
|
||||
get () {
|
||||
return systemStore.dark
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('dark', val)
|
||||
toggleDark(val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const tab = computed({
|
||||
get () {
|
||||
return systemStore.tab
|
||||
},
|
||||
set (val) {
|
||||
systemStore.$patch((state) => {
|
||||
state.tab = val
|
||||
storage.set({ key: 'tab', data: val })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const theme = computed({
|
||||
get () {
|
||||
return systemStore.theme
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('theme', val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.layout-style {
|
||||
&>div:nth-child(2n+2) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-dropdown @command="switchLang" :tabindex="1">
|
||||
<icon name="iconfont iconfanyi" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="zh-cn" :disabled="systemStore.lang == 'zh-cn'">简体中文</el-dropdown-item>
|
||||
<el-dropdown-item command="en" :disabled="systemStore.lang == 'en'">English</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { language } from '@/lang'
|
||||
import { useRoute } from 'vue-router'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const route = useRoute()
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const switchLang = (command: string) => {
|
||||
systemStore.$patch((state) => {
|
||||
state.lang = command
|
||||
storage.set({ key: 'lang', data: command })
|
||||
})
|
||||
language.loadLocaleMessages(route.path, systemStore.lang)
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
160
admin-vben/src/layout/bussiness/components/header/user-info.vue
Normal file
160
admin-vben/src/layout/bussiness/components/header/user-info.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dropdown @command="clickEvent" :tabindex="1">
|
||||
<div class="userinfo flex h-full items-center">
|
||||
<el-avatar v-if="userStore.userInfo.head_img" :size="25" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
|
||||
<img v-else src="@/app/assets/images/member_head.png" class="w-[25px] rounded-full" />
|
||||
<div class="user-name pl-[8px]">{{ userStore.userInfo.username }}</div>
|
||||
<icon name="element ArrowDown" class="ml-[5px]" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<div class="p-[10px]">
|
||||
<div class="userinfo flex h-full items-center pb-[10px] border-b-[1px] border-solid border-[#e5e5e5]">
|
||||
<el-avatar v-if="userStore.userInfo.head_img" :size="45" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
|
||||
<img v-else src="@/app/assets/images/member_head.png" class="w-[45px] rounded-full" />
|
||||
<div>
|
||||
<div class="user-name pl-[8px] text-[14px]">{{ userStore.userInfo.username }}</div>
|
||||
<div class="pl-[8px] text-[13px] text-[#9699B6]">个人中心</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="toLink('/home/index')" v-if="isAllowChange">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconqiehuan ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">切换站点</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="getUserInfoFn">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconshezhi1 ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">账号设置</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="changePasswordDialog=true">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconxiugai ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">修改密码</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="logout">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont icontuichudenglu ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">退出登录</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码">
|
||||
<div>
|
||||
<el-form :model="saveInfo" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-form-item :label="t('originalPassword')" prop="original_password">
|
||||
<el-input v-model="saveInfo.original_password" type="password" :placeholder="t('originalPasswordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('newPassword')" prop="password">
|
||||
<el-input v-model="saveInfo.password" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
<div class="form-tip">{{t('passwordTip')}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('passwordCopy')" prop="password_copy">
|
||||
<el-input v-model="saveInfo.password_copy" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="changePasswordDialog = false">{{t('cancel')}}</el-button>
|
||||
<el-button type="primary" @click="submitForm(formRef)">{{t('save')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<user-info-edit ref="userInfoEditRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { FormInstance, FormRules, ElNotification } from 'element-plus'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { setUserInfo } from '@/app/api/personal'
|
||||
import { img } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
import userInfoEdit from '@/app/components/user-info-edit/index.vue'
|
||||
|
||||
const isAllowChange = localStorage.getItem('isAllowChange') === 'true';
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const clickEvent = (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
userStore.logout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userStore.logout()
|
||||
}
|
||||
const toLink = (link) => {
|
||||
router.push(link)
|
||||
}
|
||||
const userInfoEditRef = ref(null)
|
||||
const getUserInfoFn = () => {
|
||||
userInfoEditRef.value?.open()
|
||||
}
|
||||
// 修改密码 --- start
|
||||
const changePasswordDialog = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
// 提交信息
|
||||
const saveInfo = reactive({
|
||||
original_password: '',
|
||||
password: '',
|
||||
password_copy: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
original_password: [
|
||||
{ required: true, message: t('originalPasswordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password_copy: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
let msg = ''
|
||||
if (saveInfo.password && !saveInfo.original_password) msg = t('originalPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && !saveInfo.password_copy) msg = t('newPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && saveInfo.password_copy && saveInfo.password != saveInfo.password_copy) msg = t('doubleCipherHint')
|
||||
if (msg) {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: msg
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setUserInfo(saveInfo).then((res: any) => {
|
||||
changePasswordDialog.value = false
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-popper .el-dropdown-menu{
|
||||
width: 150px;
|
||||
}
|
||||
</style>
|
||||
142
admin-vben/src/layout/bussiness/components/tabs.vue
Normal file
142
admin-vben/src/layout/bussiness/components/tabs.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="tab-wrap w-full px-[16px]" v-show="systemStore.tab">
|
||||
<el-tabs :closable="tabbarStore.tabLength > 1" :model-value="route.name" @tab-click="tabClick" @tab-remove="removeTab">
|
||||
<el-tab-pane v-for="(tab, key, index) in tabbarStore.tabs" :name="tab.name" :key="index">
|
||||
<template #label>
|
||||
<el-dropdown trigger="contextmenu" placement="bottom-start">
|
||||
<span :class="{ 'text-primary': route.name == tab.name }" class="tab-name">{{ tab.title }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Back" :disabled="index == 0" @click="closeLeft(tab.name)">{{t('tabs.closeLeft') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Right" :disabled="index == (tabbarStore.tabLength - 1)" @click="closeRight(tab.name)">{{t('tabs.closeRight') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Close" :disabled="tabbarStore.tabLength == 1" @click="closeOther(tab.name)">{{t('tabs.closeOther') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted } from 'vue'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const tabbarStore = useTabbarStore()
|
||||
const systemStore = useSystemStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
tabbarStore.addTab(route)
|
||||
})
|
||||
|
||||
watch(route, (nval: any) => {
|
||||
tabbarStore.addTab(nval)
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加tab
|
||||
* @param content
|
||||
*/
|
||||
const tabClick = (content: any) => {
|
||||
const tabRoute = tabbarStore.tabs[content.props.name]
|
||||
router.push({ name: tabRoute.name, query: tabRoute.query })
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除tab
|
||||
* @param content
|
||||
*/
|
||||
const removeTab = (content: any) => {
|
||||
if (route.name == content) {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
if (tabs.indexOf(content) == 0) {
|
||||
router.push({ name: tabs[1] })
|
||||
} else {
|
||||
router.push({ name: tabs[tabs.indexOf(content) - 1] })
|
||||
}
|
||||
}
|
||||
tabbarStore.removeTab(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭左侧
|
||||
* @param name
|
||||
*/
|
||||
const closeLeft = (name: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(name) - 1; i >= 0; i--) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ name })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧
|
||||
* @param name
|
||||
*/
|
||||
const closeRight = (name: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(name) + 1; i < tabs.length; i++) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ name })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭其他
|
||||
* @param name
|
||||
*/
|
||||
const closeOther = (name: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
tabs.forEach((key: string) => { key != name && delete tabbarStore.tabs[key] })
|
||||
router.push({ name })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs) {
|
||||
.el-tabs--border-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
margin-bottom: 0;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
display: inline-flex !important;
|
||||
padding: 0 20px !important;
|
||||
align-items: center;
|
||||
|
||||
.tab-name:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
50
admin-vben/src/layout/bussiness/index.vue
Normal file
50
admin-vben/src/layout/bussiness/index.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<!-- 左侧边栏 -->
|
||||
<layout-aside></layout-aside>
|
||||
<!-- 左侧边栏 end -->
|
||||
|
||||
<el-container>
|
||||
<!-- 顶部 -->
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
<!-- 顶部 end -->
|
||||
|
||||
<layout-tab />
|
||||
|
||||
<!-- 主体 -->
|
||||
<el-main class="h-full p-0 bg-page">
|
||||
<el-scrollbar>
|
||||
<div class="p-[15px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<!-- 主体 end -->
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import layoutHeader from './components/header/index.vue'
|
||||
import layoutAside from './components/aside/index.vue'
|
||||
import layoutTab from './components/tabs.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tabbarStore = useTabbarStore()
|
||||
const systemStore = useSystemStore()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
4
admin-vben/src/layout/bussiness/layout.json
Normal file
4
admin-vben/src/layout/bussiness/layout.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"layout": "bussiness",
|
||||
"cover": "/static/resource/images/system/layout_bussiness.png"
|
||||
}
|
||||
71
admin-vben/src/layout/darkside/components/aside/index.vue
Normal file
71
admin-vben/src/layout/darkside/components/aside/index.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<el-aside class="layout-aside dark w-auto">
|
||||
<side class="hidden-xs-only slide" />
|
||||
</el-aside>
|
||||
|
||||
<el-drawer v-model="systemStore.menuDrawer" direction="ltr" :with-header="false" custom-class="aside-drawer" size="210px">
|
||||
<template #default>
|
||||
<side />
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import side from './side.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
watch(route, () => {
|
||||
systemStore.$patch(state => {
|
||||
state.menuDrawer = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.layout-aside {
|
||||
--side-dark-color: #141414;
|
||||
background-color: var(--side-dark-color, var(--el-bg-color));
|
||||
|
||||
&.bright {
|
||||
background-color: #F5F7F9;
|
||||
|
||||
li {
|
||||
background-color: #F5F7F9;
|
||||
|
||||
&.is-active:not(.is-opened) {
|
||||
position: relative;
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
background-color: var(--el-menu-active-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slide {
|
||||
border-right: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
}
|
||||
|
||||
.aside-drawer {
|
||||
.el-drawer__body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
119
admin-vben/src/layout/darkside/components/aside/menu-item.vue
Normal file
119
admin-vben/src/layout/darkside/components/aside/menu-item.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<template v-if="meta.show">
|
||||
<el-sub-menu v-if="routes.children" :index="String(routes.name)">
|
||||
<template #title>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :key="index" :level="props.level + 1" />
|
||||
<template v-if="routes.name == 'addon_list' || routes.name == 'marketing_list'">
|
||||
<template v-if="addonsMenus">
|
||||
<menu-item :routes="addonsMenus" :key="index" :level="props.level + 1"/>
|
||||
</template>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
<template v-else>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-if="meta.addon && meta.parent_route && meta.parent_route.addon == ''">
|
||||
<template #title>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-else>
|
||||
<template #title>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
<div v-if="routes.is_border" class="!border-0 !border-t-[1px] border-solid mx-[25px] bg-[#f7f7f7] my-[5px]"></div>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import menuItem from './menu-item.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const routers = useUserStore().routers
|
||||
const props = defineProps({
|
||||
routes: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
})
|
||||
const systemStore = useSystemStore()
|
||||
const meta = computed(() => props.routes.meta)
|
||||
|
||||
const addons = computed(() => {
|
||||
const addons:Record<string, any> = {}
|
||||
userStore.siteInfo?.apps.forEach((item: any) => { addons[item.key] = item })
|
||||
userStore.siteInfo?.site_addons.forEach((item: any) => { addons[item.key] = item })
|
||||
return addons
|
||||
})
|
||||
|
||||
const systemAddonKeys = computed(() => {
|
||||
return userStore.siteInfo?.site_addons.map((item: any) => item.key)
|
||||
})
|
||||
|
||||
const addonRouters: Record<string, any> = {}
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon) {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
if (item.meta.attr) {
|
||||
addonRouters[item.meta.attr] = item
|
||||
}
|
||||
})
|
||||
|
||||
const addonsMenus = ref(null)
|
||||
|
||||
watch(route, () => {
|
||||
if (props.routes.name == 'addon_list') {
|
||||
if (systemAddonKeys.value.includes(route.meta.addon) && addonRouters[route.meta.addon]) {
|
||||
addonsMenus.value = addonRouters[route.meta.addon]
|
||||
} else if (route.meta.attr && addonRouters[route.meta.attr]) {
|
||||
addonsMenus.value = addonRouters[route.meta.attr]
|
||||
} else {
|
||||
addonsMenus.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const marketingKeys = storage.get('darksideMarketingKeys')
|
||||
const matchedName = route.matched[1]?.name
|
||||
if (props.routes.name == 'marketing_list') {
|
||||
if (marketingKeys && marketingKeys.includes(matchedName)) {
|
||||
addonsMenus.value = route.matched[1] ?? []
|
||||
addonsMenus.value.meta.show = 1
|
||||
} else {
|
||||
addonsMenus.value = null
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-sub-menu{
|
||||
.el-icon{
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
151
admin-vben/src/layout/darkside/components/aside/side.vue
Normal file
151
admin-vben/src/layout/darkside/components/aside/side.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<el-container class="w-[200px] h-screen flex flex-col">
|
||||
<el-header class="logo-wrap flex items-center justify-center h-[64px]">
|
||||
<div class="logo flex items-center m-auto h-[64px]" v-if="!systemStore.menuIsCollapse">
|
||||
<el-image style="width: 40px; height: 40px" :src="img(logoUrl)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-[40px]"><img class="max-w-[40px]" src="@/app/assets/images/icon-addon-one.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="logo flex items-center justify-center h-[64px]" v-else>
|
||||
<i class="text-3xl iconfont iconyunkongjian"></i>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main class="menu-wrap">
|
||||
<el-scrollbar>
|
||||
<el-menu :default-active="route.name" :router="true" background-color="--side-dark-color" text-color="#fff" active-text-color="#fff" :unique-opened="true" :collapse="systemStore.menuIsCollapse" >
|
||||
<menu-item v-for="(route, index) in menuData" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import menuItem from './menu-item.vue'
|
||||
import { img } from '@/utils/common'
|
||||
import { findFirstValidRoute } from '@/router/routers'
|
||||
import { getShowMarketing } from '@/app/api/site'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const siteInfo = userStore.siteInfo
|
||||
const routers = userStore.routers
|
||||
const addonIndexRoute = userStore.addonIndexRoute
|
||||
const menuData = ref<Record<string, any>[]>([])
|
||||
const addonRouters: Record<string, any> = {}
|
||||
const logoUrl = computed(() => {
|
||||
return userStore.siteInfo.icon ? userStore.siteInfo.icon : systemStore.website.icon
|
||||
})
|
||||
|
||||
const getMarketingList = async () => {
|
||||
const res = await getShowMarketing()
|
||||
const marketingList = res.data
|
||||
const marketingKeys = marketingList?.marketing?.list?.map(item => item.key) ?? []
|
||||
// menuData.value.forEach((item, index, arr) => {
|
||||
// if (marketingKeys.includes(item.name)) {
|
||||
// arr.splice(index, 1)
|
||||
// }
|
||||
// })
|
||||
storage.set({ key: 'darksideMarketingKeys', data: marketingKeys })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getMarketingList()
|
||||
})
|
||||
|
||||
routers.forEach((item, index) => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon == '') {
|
||||
if (item.meta.attr == '' && item.name != 'sign' && item.name != 'verify') {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
}
|
||||
menuData.value.push(item)
|
||||
}
|
||||
} else if (item.meta.addon != '' && siteInfo?.apps.length == 1 && siteInfo?.apps[0].key == item.meta.addon && item.meta.show) {
|
||||
if (item.children) {
|
||||
item.children.forEach((citem: Record<string, any>) => {
|
||||
citem.original_name = citem.name
|
||||
if (citem.children && citem.children.length) {
|
||||
citem.name = findFirstValidRoute(citem.children)
|
||||
}
|
||||
})
|
||||
menuData.value.unshift(...item.children)
|
||||
} else {
|
||||
menuData.value.unshift(item)
|
||||
}
|
||||
} else {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// menuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
})
|
||||
|
||||
// 多应用时将应用插入菜单
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
const routers:Record<string, any>[] = []
|
||||
siteInfo?.apps.forEach((item: Record<string, any>) => {
|
||||
if (addonRouters[item.key]) {
|
||||
addonRouters[item.key].name = addonIndexRoute[item.key]
|
||||
routers.push(addonRouters[item.key])
|
||||
}
|
||||
})
|
||||
menuData.value.unshift(...routers)
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// menuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.menu-wrap {
|
||||
padding: 0!important;
|
||||
|
||||
.el-menu {
|
||||
border-right: 0!important;
|
||||
|
||||
.el-menu-item, .el-sub-menu__title {
|
||||
--el-menu-item-height: 40px;
|
||||
}
|
||||
|
||||
.el-sub-menu .el-menu-item {
|
||||
--el-menu-sub-item-height: 40px;
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
background-color: var(--el-color-primary)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
285
admin-vben/src/layout/darkside/components/header/index.vue
Normal file
285
admin-vben/src/layout/darkside/components/header/index.vue
Normal file
@@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<el-container :class="['h-full px-[10px]',{'layout-header border-b border-color': !dark}]" >
|
||||
<el-row class="w-100 h-full w-full">
|
||||
<el-col :span="10">
|
||||
<div class="left-panel h-full flex items-center">
|
||||
<!-- 左侧菜单折叠 -->
|
||||
<div class="hidden-sm-and-up navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element Expand" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element Fold" v-else />
|
||||
</div>
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element Refresh" />
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px]">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index" :to="route.path" class="inter">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<div class="right-panel h-full flex items-center justify-end">
|
||||
<div class="flex items-center flex-shrink-0 hidden-xs-only">
|
||||
<el-dropdown trigger="hover" :hide-on-click="false" popper-class="site-info-wrap" class="mr-[8px]">
|
||||
<!-- 状态 -->
|
||||
<div class="mx-[8px] bg-[#f6f6f6] border-[1px] border-solid border-[#eee] rounded-[4px] px-[9px] py-[6px] flex items-center">
|
||||
<span class="mr-[6px] text-[12px] !text-[#333]">{{siteInfo.site_name}}</span>
|
||||
<span class="!text-[10px] text-[#f56c6c]" :class="{'!text-[#67c23a]': siteInfo.status == 1, '!text-[#f56c6c]': siteInfo.status == 3}">{{ siteInfo.status_name }}</span>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
<!-- 站点id -->
|
||||
<div class="text-[14px]">站点编号:{{siteInfo.site_id}}</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<!-- 到期时间 -->
|
||||
<div v-if="siteInfo.expire_time == 0" class="text-[14px]">到期时间:永久</div>
|
||||
<div v-else class="text-[14px]">到期时间:{{ siteInfo.expire_time }}</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div class="flex items-center flex-shrink-0 hidden-xs-only">
|
||||
<el-popover placement="bottom" :width="330" trigger="click" v-model:visible="isMenuSearch" >
|
||||
<template #reference>
|
||||
<i class="iconfont icona-sousuoV6xx-36 cursor-pointer px-[8px] !text-[14px]"></i>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="flex items-center">
|
||||
<el-select v-model="selectedRoute" filterable class="!w-[250px] mr-[20px] menu-select" :teleported="false" clearable @change="handleRouteSelect">
|
||||
<el-option v-for="item in flatRoutes" :key="item.name" :label="item.full_title" :value="item.name" >
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button type="primary" link @click="isMenuSearch = false">{{t('取消')}}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
<!-- 预览 只有站点时展示-->
|
||||
<i class="iconfont iconicon_huojian1 cursor-pointer px-[8px]" :title="t('visitWap')" @click="toPreview"></i>
|
||||
<i class="iconfont iconlingdang-xianxing cursor-pointer px-[8px]" :title="t('newInfo')" v-if="appType == 'site'"></i>
|
||||
<!-- 切换语言 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<switch-lang />
|
||||
</div> -->
|
||||
<!-- 切换全屏 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleFullscreen">
|
||||
<icon name="iconfont icontuichuquanping" v-if="isFullscreen" />
|
||||
<icon name="iconfont iconquanping" v-else />
|
||||
</div> -->
|
||||
<!-- 布局设置 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detectionLoginFn">{{ t('layout.detectionLoginOperation') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import layoutSetting from './layout-setting.vue'
|
||||
import userInfo from './user-info.vue'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const appType = storage.get('app_type')
|
||||
const { toggle: toggleFullscreen } = useFullscreen()
|
||||
const systemStore = useSystemStore()
|
||||
const appStore = useAppStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const screenWidth = ref(window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth)
|
||||
|
||||
const userStore = useUserStore()
|
||||
const siteInfo:any = computed(() => {
|
||||
return userStore.siteInfo
|
||||
})
|
||||
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
const isMenuSearch = ref(false)
|
||||
const routers = userStore.routers
|
||||
const getParentTitleChain=(meta:any) =>{
|
||||
let titles = []
|
||||
let current = meta?.parent_route
|
||||
|
||||
while (current) {
|
||||
if (current.short_title) {
|
||||
titles.unshift(current.short_title)
|
||||
}
|
||||
current = current.parent_route
|
||||
}
|
||||
|
||||
return titles.join(' - ')
|
||||
}
|
||||
const flattenRoutes = (routes:any, parent = null)=> {
|
||||
let flat = [];
|
||||
routes.forEach(route => {
|
||||
const { path, name, meta = {}, short_title, children } = route
|
||||
const isLeaf = meta.type ==1 && meta.show==1
|
||||
if(isLeaf){
|
||||
const title = meta.title || short_title || ''
|
||||
const parentTitleChain = getParentTitleChain(meta)
|
||||
const fullTitle = parentTitleChain ? `${parentTitleChain} - ${title}` : title
|
||||
const item = {
|
||||
path,
|
||||
name,
|
||||
title,
|
||||
parent_title: parentTitleChain,
|
||||
full_title: fullTitle
|
||||
};
|
||||
|
||||
flat.push(item);
|
||||
}
|
||||
if (children && children.length > 0) {
|
||||
flat = flat.concat(flattenRoutes(children, route))
|
||||
}
|
||||
});
|
||||
|
||||
return flat;
|
||||
}
|
||||
const flatRoutes = flattenRoutes(routers)
|
||||
const selectedRoute = ref('')
|
||||
const handleRouteSelect = (name:any) => {
|
||||
if (name) {
|
||||
router.push({ name })
|
||||
isMenuSearch.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 检测登录 start
|
||||
const detectionLoginDialog = ref(false)
|
||||
const comparisonToken = ref('')
|
||||
const comparisonSiteId = ref('')
|
||||
if (storage.get('comparisonTokenStorage')) {
|
||||
comparisonToken.value = storage.get('comparisonTokenStorage')
|
||||
// storage.remove(['comparisonTokenStorage']);
|
||||
}
|
||||
if (storage.get('comparisonSiteIdStorage')) {
|
||||
comparisonSiteId.value = storage.get('comparisonSiteIdStorage')
|
||||
// storage.remove(['comparisonSiteIdStorage']);
|
||||
}
|
||||
// 监听标签页面切换
|
||||
document.addEventListener('visibilitychange', e => {
|
||||
if (document.visibilityState === 'visible' && (comparisonSiteId.value != storage.get('siteId') || comparisonToken.value != storage.get('token'))) {
|
||||
detectionLoginDialog.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const detectionLoginFn = () => {
|
||||
detectionLoginDialog.value = false
|
||||
location.href = `${location.origin}/site/`
|
||||
}
|
||||
// 检测登录 end
|
||||
|
||||
onMounted(() => {
|
||||
// 监听窗体宽度变化
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
screenWidth.value = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
|
||||
})()
|
||||
}
|
||||
})
|
||||
|
||||
// watch(screenWidth, () => {
|
||||
// if (screenWidth.value < 992) {
|
||||
// if (!systemStore.menuIsCollapse) systemStore.toggleMenuCollapse(true)
|
||||
// } else {
|
||||
// if (systemStore.menuIsCollapse) systemStore.toggleMenuCollapse(false)
|
||||
// }
|
||||
// })
|
||||
|
||||
// 菜单栏展开折叠
|
||||
const toggleMenuCollapse = () => {
|
||||
systemStore.$patch((state) => {
|
||||
if (screenWidth.value < 768) {
|
||||
state.menuDrawer = true
|
||||
state.menuIsCollapse = false
|
||||
} else {
|
||||
systemStore.toggleMenuCollapse(!systemStore.menuIsCollapse)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新路由
|
||||
const refreshRouter = () => {
|
||||
if (!appStore.routeRefreshTag) return
|
||||
appStore.refreshRouterView()
|
||||
}
|
||||
|
||||
// 面包屑导航
|
||||
const breadcrumb = computed(() => {
|
||||
const matched = route.matched.filter(item => { return item.meta.title })
|
||||
if (matched[0] && matched[0].path == '/') matched.splice(0, 1)
|
||||
return matched
|
||||
})
|
||||
|
||||
// 跳转去预览
|
||||
const toPreview = () => {
|
||||
const url = router.resolve({
|
||||
path: '/preview/wap',
|
||||
query: {
|
||||
page:'/'
|
||||
}
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-header{
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
box-shadow: 0 0 4px 0 rgba(0,145,255,0.1);
|
||||
}
|
||||
.navbar-item {
|
||||
padding: 0 8px;
|
||||
&:hover {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
}
|
||||
.index-item {
|
||||
border: 1px solid;
|
||||
border-color: var(--el-color-primary);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
:deep(.el-dropdown-menu__item) {
|
||||
&:focus {
|
||||
background-color: transparent !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<icon name="element Setting" @click="drawer = true" />
|
||||
|
||||
<el-drawer v-model="drawer" :title="t('layout.layoutSetting')" size="300px">
|
||||
<el-scrollbar>
|
||||
<!-- 黑暗模式 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.darkMode') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="dark" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主题颜色 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.themeColor') }}</div>
|
||||
<div>
|
||||
<el-color-picker v-model="theme" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 标签栏 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.tab') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="tab" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { setThemeColor } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
import storage from "@/utils/storage";
|
||||
|
||||
const drawer = ref(false)
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
const dark = computed({
|
||||
get () {
|
||||
return systemStore.dark
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('dark', val)
|
||||
toggleDark(val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const tab = computed({
|
||||
get () {
|
||||
return systemStore.tab
|
||||
},
|
||||
set (val) {
|
||||
systemStore.$patch((state) => {
|
||||
state.tab = val
|
||||
storage.set({ key: 'tab', data: val })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const theme = computed({
|
||||
get () {
|
||||
return systemStore.theme
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('theme', val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.layout-style {
|
||||
&>div:nth-child(2n+2) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-dropdown @command="switchLang" :tabindex="1">
|
||||
<icon name="iconfont iconfanyi" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="zh-cn" :disabled="systemStore.lang == 'zh-cn'">简体中文</el-dropdown-item>
|
||||
<el-dropdown-item command="en" :disabled="systemStore.lang == 'en'">English</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { language } from '@/lang'
|
||||
import { useRoute } from 'vue-router'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const route = useRoute()
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const switchLang = (command: string) => {
|
||||
systemStore.$patch((state) => {
|
||||
state.lang = command
|
||||
storage.set({ key: 'lang', data: command })
|
||||
})
|
||||
language.loadLocaleMessages(route.path, systemStore.lang)
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
159
admin-vben/src/layout/darkside/components/header/user-info.vue
Normal file
159
admin-vben/src/layout/darkside/components/header/user-info.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dropdown @command="clickEvent" :tabindex="1">
|
||||
<div class="userinfo flex h-full items-center">
|
||||
<el-avatar v-if="userStore.userInfo.head_img" :size="25" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
|
||||
<img v-else src="@/app/assets/images/member_head.png" class="w-[25px] rounded-full" />
|
||||
<div class="user-name pl-[8px]">{{ userStore.userInfo.username }}</div>
|
||||
<icon name="element ArrowDown" class="ml-[5px]" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<div class="p-[10px]">
|
||||
<div class="userinfo flex h-full items-center pb-[10px] border-b-[1px] border-solid border-[#e5e5e5]">
|
||||
<el-avatar v-if="userStore.userInfo.head_img" :size="45" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
|
||||
<img v-else src="@/app/assets/images/member_head.png" class="w-[45px] rounded-full" />
|
||||
<div>
|
||||
<div class="user-name pl-[8px] text-[14px]">{{ userStore.userInfo.username }}</div>
|
||||
<div class="pl-[8px] text-[13px] text-[#9699B6]">个人中心</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="toLink('/home/index')" v-if="isAllowChange">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconqiehuan ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">切换站点</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="getUserInfoFn">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconshezhi1 ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">账号设置</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="changePasswordDialog=true">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconxiugai ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">修改密码</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="logout">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont icontuichudenglu ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">退出登录</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码">
|
||||
<div>
|
||||
<el-form :model="saveInfo" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-form-item :label="t('originalPassword')" prop="original_password">
|
||||
<el-input v-model="saveInfo.original_password" type="password" :placeholder="t('originalPasswordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('newPassword')" prop="password">
|
||||
<el-input v-model="saveInfo.password" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
<div class="form-tip">{{t('passwordTip')}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('passwordCopy')" prop="password_copy">
|
||||
<el-input v-model="saveInfo.password_copy" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="changePasswordDialog = false">{{t('cancel')}}</el-button>
|
||||
<el-button type="primary" @click="submitForm(formRef)">{{t('save')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<user-info-edit ref="userInfoEditRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { FormInstance, FormRules, ElNotification } from 'element-plus'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { setUserInfo } from '@/app/api/personal'
|
||||
import { t } from '@/lang'
|
||||
import userInfoEdit from '@/app/components/user-info-edit/index.vue'
|
||||
import { img } from '@/utils/common'
|
||||
|
||||
const isAllowChange = localStorage.getItem('isAllowChange') === 'true';
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const clickEvent = (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
userStore.logout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userStore.logout()
|
||||
}
|
||||
const toLink = (link) => {
|
||||
router.push(link)
|
||||
}
|
||||
const userInfoEditRef = ref(null)
|
||||
const getUserInfoFn = () => {
|
||||
userInfoEditRef.value?.open()
|
||||
}
|
||||
// 修改密码 --- start
|
||||
const changePasswordDialog = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
// 提交信息
|
||||
const saveInfo = reactive({
|
||||
original_password: '',
|
||||
password: '',
|
||||
password_copy: ''
|
||||
})
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
original_password: [
|
||||
{ required: true, message: t('originalPasswordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password_copy: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
let msg = ''
|
||||
if (saveInfo.password && !saveInfo.original_password) msg = t('originalPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && !saveInfo.password_copy) msg = t('newPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && saveInfo.password_copy && saveInfo.password != saveInfo.password_copy) msg = t('doubleCipherHint')
|
||||
if (msg) {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: msg
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setUserInfo(saveInfo).then((res: any) => {
|
||||
changePasswordDialog.value = false
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-popper .el-dropdown-menu{
|
||||
width: 150px;
|
||||
}
|
||||
</style>
|
||||
142
admin-vben/src/layout/darkside/components/tabs.vue
Normal file
142
admin-vben/src/layout/darkside/components/tabs.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="tab-wrap w-full px-[16px]" v-show="systemStore.tab">
|
||||
<el-tabs :closable="tabbarStore.tabLength > 1" :model-value="route.name" @tab-click="tabClick" @tab-remove="removeTab">
|
||||
<el-tab-pane v-for="(tab, key, index) in tabbarStore.tabs" :name="tab.name" :key="index">
|
||||
<template #label>
|
||||
<el-dropdown trigger="contextmenu" placement="bottom-start">
|
||||
<span :class="{ 'text-primary': route.name == tab.name }" class="tab-name">{{ tab.title }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Back" :disabled="index == 0" @click="closeLeft(tab.name)">{{t('tabs.closeLeft') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Right" :disabled="index == (tabbarStore.tabLength - 1)" @click="closeRight(tab.name)">{{t('tabs.closeRight') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Close" :disabled="tabbarStore.tabLength == 1" @click="closeOther(tab.name)">{{t('tabs.closeOther') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted } from 'vue'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const tabbarStore = useTabbarStore()
|
||||
const systemStore = useSystemStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
tabbarStore.addTab(route)
|
||||
})
|
||||
|
||||
watch(route, (nval: any) => {
|
||||
tabbarStore.addTab(nval)
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加tab
|
||||
* @param content
|
||||
*/
|
||||
const tabClick = (content: any) => {
|
||||
const tabRoute = tabbarStore.tabs[content.props.name]
|
||||
router.push({ name: tabRoute.name, query: tabRoute.query })
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除tab
|
||||
* @param content
|
||||
*/
|
||||
const removeTab = (content: any) => {
|
||||
if (route.name == content) {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
if (tabs.indexOf(content) == 0) {
|
||||
router.push({ name: tabs[1] })
|
||||
} else {
|
||||
router.push({ name: tabs[tabs.indexOf(content) - 1] })
|
||||
}
|
||||
}
|
||||
tabbarStore.removeTab(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭左侧
|
||||
* @param name
|
||||
*/
|
||||
const closeLeft = (name: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(name) - 1; i >= 0; i--) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ name })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧
|
||||
* @param name
|
||||
*/
|
||||
const closeRight = (name: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(name) + 1; i < tabs.length; i++) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ name })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭其他
|
||||
* @param name
|
||||
*/
|
||||
const closeOther = (name: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
tabs.forEach((key: string) => { key != name && delete tabbarStore.tabs[key] })
|
||||
router.push({ name })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs) {
|
||||
.el-tabs--border-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
margin-bottom: 0;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
display: inline-flex !important;
|
||||
padding: 0 20px !important;
|
||||
align-items: center;
|
||||
|
||||
.tab-name:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
50
admin-vben/src/layout/darkside/index.vue
Normal file
50
admin-vben/src/layout/darkside/index.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<!-- 左侧边栏 -->
|
||||
<layout-aside></layout-aside>
|
||||
<!-- 左侧边栏 end -->
|
||||
|
||||
<el-container>
|
||||
<!-- 顶部 -->
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
<!-- 顶部 end -->
|
||||
|
||||
<layout-tab />
|
||||
|
||||
<!-- 主体 -->
|
||||
<el-main class="h-full p-0 bg-page">
|
||||
<el-scrollbar>
|
||||
<div class="p-[15px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<!-- 主体 end -->
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import layoutHeader from './components/header/index.vue'
|
||||
import layoutAside from './components/aside/index.vue'
|
||||
import layoutTab from './components/tabs.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tabbarStore = useTabbarStore()
|
||||
const systemStore = useSystemStore()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
4
admin-vben/src/layout/darkside/layout.json
Normal file
4
admin-vben/src/layout/darkside/layout.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"layout": "darkside",
|
||||
"cover": "/static/resource/images/system/layout_darkside.png"
|
||||
}
|
||||
10
admin-vben/src/layout/decorate/index.vue
Normal file
10
admin-vben/src/layout/decorate/index.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<el-container class="w-screen h-screen min-w-[1200px]">
|
||||
<router-view></router-view>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
68
admin-vben/src/layout/default/components/aside/index.vue
Normal file
68
admin-vben/src/layout/default/components/aside/index.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<el-aside class="layout-aside w-auto">
|
||||
<side class="hidden-xs-only slide" />
|
||||
</el-aside>
|
||||
|
||||
<el-drawer v-model="systemStore.menuDrawer" direction="ltr" :with-header="false" custom-class="aside-drawer" size="210px">
|
||||
<template #default>
|
||||
<side />
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import side from './side.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
watch(route, () => {
|
||||
systemStore.$patch(state => {
|
||||
state.menuDrawer = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.layout-aside {
|
||||
&.bright {
|
||||
background-color: #F5F7F9;
|
||||
|
||||
li {
|
||||
background-color: #F5F7F9;
|
||||
|
||||
&.is-active:not(.is-opened) {
|
||||
position: relative;
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
background-color: var(--el-menu-active-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slide {
|
||||
border-right: 1px solid var(--el-border-color-extra-light);
|
||||
}
|
||||
}
|
||||
|
||||
.aside-drawer {
|
||||
.el-drawer__body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
119
admin-vben/src/layout/default/components/aside/menu-item.vue
Normal file
119
admin-vben/src/layout/default/components/aside/menu-item.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<template v-if="meta.show">
|
||||
<el-sub-menu v-if="routes.children" :index="String(routes.name)">
|
||||
<template #title>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :key="index" :level="props.level + 1" />
|
||||
<template v-if="routes.name == 'addon_list' || routes.name == 'marketing_list'">
|
||||
<template v-if="addonsMenus">
|
||||
<menu-item :routes="addonsMenus" :key="index" :level="props.level + 1"/>
|
||||
</template>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
<template v-else>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-if="meta.addon && meta.parent_route && meta.parent_route.addon == ''">
|
||||
<template #title>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-else>
|
||||
<template #title>
|
||||
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
|
||||
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="ml-[10px]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
<div v-if="routes.is_border" class="!border-0 !border-t-[1px] border-solid mx-[25px] bg-[#f7f7f7] my-[5px]"></div>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import menuItem from './menu-item.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const routers = useUserStore().routers
|
||||
const props = defineProps({
|
||||
routes: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
})
|
||||
const systemStore = useSystemStore()
|
||||
const meta = computed(() => props.routes.meta)
|
||||
|
||||
const addons = computed(() => {
|
||||
const addons:Record<string, any> = {}
|
||||
userStore.siteInfo?.apps.forEach((item: any) => { addons[item.key] = item })
|
||||
userStore.siteInfo?.site_addons.forEach((item: any) => { addons[item.key] = item })
|
||||
return addons
|
||||
})
|
||||
|
||||
const systemAddonKeys = computed(() => {
|
||||
return userStore.siteInfo?.site_addons.map((item: any) => item.key)
|
||||
})
|
||||
|
||||
const addonRouters: Record<string, any> = {}
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon) {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
if (item.meta.attr) {
|
||||
addonRouters[item.meta.attr] = item
|
||||
}
|
||||
})
|
||||
|
||||
const addonsMenus = ref(null)
|
||||
|
||||
watch(route, () => {
|
||||
if (props.routes.name == 'addon_list') {
|
||||
if (systemAddonKeys.value.includes(route.meta.addon) && addonRouters[route.meta.addon]) {
|
||||
addonsMenus.value = addonRouters[route.meta.addon]
|
||||
} else if (route.meta.attr && addonRouters[route.meta.attr]) {
|
||||
addonsMenus.value = addonRouters[route.meta.attr]
|
||||
} else {
|
||||
addonsMenus.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const marketingKeys = storage.get('defaultMarketingKeys')
|
||||
const matchedName = route.matched[1]?.name
|
||||
if (props.routes.name == 'marketing_list') {
|
||||
if (marketingKeys && marketingKeys.includes(matchedName)) {
|
||||
addonsMenus.value = route.matched[1] ?? []
|
||||
addonsMenus.value.meta.show = 1
|
||||
} else {
|
||||
addonsMenus.value = null
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-sub-menu{
|
||||
.el-icon{
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
146
admin-vben/src/layout/default/components/aside/side.vue
Normal file
146
admin-vben/src/layout/default/components/aside/side.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<el-container class="w-[200px] h-screen layout-aside flex flex-col">
|
||||
<el-header class="logo-wrap flex items-center justify-center h-[64px]">
|
||||
<div class="logo flex items-center m-auto h-[64px]" v-if="!systemStore.menuIsCollapse">
|
||||
<el-image style="width: 40px; height: 40px" :src="img(logoUrl)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-[40px]"><img class="max-w-[40px]" src="@/app/assets/images/icon-addon-one.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="logo flex items-center justify-center h-[64px]" v-else>
|
||||
<i class="text-3xl iconfont iconyunkongjian"></i>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main class="menu-wrap">
|
||||
<el-scrollbar>
|
||||
<el-menu :default-active="route.name" :router="true" class="aside-menu h-full" :unique-opened="true" :collapse="systemStore.menuIsCollapse" >
|
||||
<menu-item v-for="(route, index) in menuData" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import menuItem from './menu-item.vue'
|
||||
import { img } from '@/utils/common'
|
||||
import { findFirstValidRoute } from '@/router/routers'
|
||||
import { getShowMarketing } from '@/app/api/site'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const siteInfo = userStore.siteInfo
|
||||
const routers = userStore.routers
|
||||
const addonIndexRoute = userStore.addonIndexRoute
|
||||
const menuData = ref<Record<string, any>[]>([])
|
||||
const addonRouters: Record<string, any> = {}
|
||||
const logoUrl = computed(() => {
|
||||
return userStore.siteInfo.icon ? userStore.siteInfo.icon : systemStore.website.icon
|
||||
})
|
||||
|
||||
const getMarketingList = async () => {
|
||||
const res = await getShowMarketing()
|
||||
const marketingList = res.data
|
||||
const marketingKeys = marketingList?.marketing?.list?.map(item => item.key) ?? []
|
||||
// menuData.value.forEach((item, index, arr) => {
|
||||
// if (marketingKeys.includes(item.name)) {
|
||||
// arr.splice(index, 1)
|
||||
// }
|
||||
// })
|
||||
storage.set({ key: 'defaultMarketingKeys', data: marketingKeys })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getMarketingList()
|
||||
})
|
||||
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon == '') {
|
||||
if (item.meta.attr == '' && item.name != 'sign' && item.name != 'verify') {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
}
|
||||
menuData.value.push(item)
|
||||
}
|
||||
} else if (item.meta.addon != '' && siteInfo?.apps.length == 1 && siteInfo?.apps[0].key == item.meta.addon && item.meta.show) {
|
||||
if (item.children) {
|
||||
item.children.forEach((citem: Record<string, any>) => {
|
||||
citem.original_name = citem.name
|
||||
if (citem.children && citem.children.length) {
|
||||
citem.name = findFirstValidRoute(citem.children)
|
||||
}
|
||||
})
|
||||
menuData.value.unshift(...item.children)
|
||||
} else {
|
||||
menuData.value.unshift(item)
|
||||
}
|
||||
} else {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// menuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
})
|
||||
|
||||
// 多应用时将应用插入菜单
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
const routers:Record<string, any>[] = []
|
||||
siteInfo?.apps.forEach((item: Record<string, any>) => {
|
||||
if (addonRouters[item.key]) {
|
||||
addonRouters[item.key].name = addonIndexRoute[item.key]
|
||||
routers.push(addonRouters[item.key])
|
||||
}
|
||||
})
|
||||
menuData.value.unshift(...routers)
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// menuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.menu-wrap {
|
||||
padding: 0!important;
|
||||
|
||||
.el-menu {
|
||||
border-right: 0!important;
|
||||
|
||||
.el-menu-item, .el-sub-menu__title {
|
||||
--el-menu-item-height: 40px;
|
||||
}
|
||||
|
||||
.el-sub-menu .el-menu-item {
|
||||
--el-menu-sub-item-height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
288
admin-vben/src/layout/default/components/header/index.vue
Normal file
288
admin-vben/src/layout/default/components/header/index.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<el-container :class="['h-full px-[10px]',{'layout-header border-b border-color': !dark}]" >
|
||||
<el-row class="w-100 h-full w-full">
|
||||
<el-col :span="10">
|
||||
<div class="left-panel h-full flex items-center">
|
||||
<!-- 左侧菜单折叠 -->
|
||||
<div class="hidden-sm-and-up navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element Expand" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element Fold" v-else />
|
||||
</div>
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element Refresh" />
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px]">
|
||||
<el-breadcrumb separator="/">
|
||||
<!-- :to="route.path" class="inter" 这种修改方式导致部分跳转不对,需要重新调整菜单才可以 -->
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :to="route.path" class="inter" :key="index">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<div class="right-panel h-full flex items-center justify-end">
|
||||
<div class="flex items-center flex-shrink-0 hidden-xs-only">
|
||||
<el-dropdown trigger="hover" :hide-on-click="false" popper-class="site-info-wrap" class="mr-[8px]">
|
||||
<!-- 状态 -->
|
||||
<div class="mx-[8px] bg-[#f6f6f6] border-[1px] border-solid border-[#eee] rounded-[4px] px-[9px] py-[6px] flex items-center">
|
||||
<span class="mr-[6px] text-[12px] !text-[#333]">{{siteInfo.site_name}}</span>
|
||||
<span class="!text-[10px] text-[#f56c6c]" :class="{'!text-[#67c23a]': siteInfo.status == 1, '!text-[#f56c6c]': siteInfo.status == 3}">{{ siteInfo.status_name }}</span>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
<!-- 站点id -->
|
||||
<div class="text-[14px]">站点编号:{{siteInfo.site_id}}</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<!-- 到期时间 -->
|
||||
<div v-if="siteInfo.expire_time == 0" class="text-[14px]">到期时间:永久</div>
|
||||
<div v-else class="text-[14px]">到期时间:{{ siteInfo.expire_time }}</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div class="flex items-center flex-shrink-0 hidden-xs-only">
|
||||
<el-popover placement="bottom" :width="330" trigger="click" v-model:visible="isMenuSearch" >
|
||||
<template #reference>
|
||||
<i class="iconfont icona-sousuoV6xx-36 cursor-pointer px-[8px] !text-[14px]"></i>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="flex items-center">
|
||||
<el-select v-model="selectedRoute" filterable class="!w-[250px] mr-[20px] menu-select" :teleported="false" clearable @change="handleRouteSelect">
|
||||
<el-option v-for="item in flatRoutes" :key="item.name" :label="item.full_title" :value="item.name" >
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button type="primary" link @click="isMenuSearch = false">{{t('取消')}}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
<!-- 预览 只有站点时展示-->
|
||||
<i class="iconfont iconicon_huojian1 cursor-pointer px-[8px]" :title="t('visitWap')" @click="toPreview"></i>
|
||||
<i class="iconfont iconlingdang-xianxing cursor-pointer px-[8px]" :title="t('newInfo')" v-if="appType == 'site'"></i>
|
||||
<!-- 切换语言 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<switch-lang />
|
||||
</div> -->
|
||||
<!-- 切换全屏 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleFullscreen">
|
||||
<icon name="iconfont icontuichuquanping" v-if="isFullscreen" />
|
||||
<icon name="iconfont iconquanping" v-else />
|
||||
</div> -->
|
||||
<!-- 布局设置 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detectionLoginFn">{{ t('layout.detectionLoginOperation') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import layoutSetting from './layout-setting.vue'
|
||||
import userInfo from './user-info.vue'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const appType = storage.get('app_type')
|
||||
const { toggle: toggleFullscreen } = useFullscreen()
|
||||
const systemStore = useSystemStore()
|
||||
const appStore = useAppStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const screenWidth = ref(window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth)
|
||||
|
||||
const userStore = useUserStore()
|
||||
const siteInfo:any = computed(() => {
|
||||
return userStore.siteInfo
|
||||
})
|
||||
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
const isMenuSearch = ref(false)
|
||||
const routers = userStore.routers
|
||||
const getParentTitleChain=(meta:any) =>{
|
||||
let titles = []
|
||||
let current = meta?.parent_route
|
||||
|
||||
while (current) {
|
||||
if (current.short_title) {
|
||||
titles.unshift(current.short_title)
|
||||
}
|
||||
current = current.parent_route
|
||||
}
|
||||
|
||||
return titles.join(' - ')
|
||||
}
|
||||
const flattenRoutes = (routes:any, parent = null)=> {
|
||||
let flat = [];
|
||||
routes.forEach(route => {
|
||||
const { path, name, meta = {}, short_title, children } = route
|
||||
const isLeaf = meta.type ==1 && meta.show==1
|
||||
if(isLeaf){
|
||||
const title = meta.title || short_title || ''
|
||||
const parentTitleChain = getParentTitleChain(meta)
|
||||
const fullTitle = parentTitleChain ? `${parentTitleChain} - ${title}` : title
|
||||
const item = {
|
||||
path,
|
||||
name,
|
||||
title,
|
||||
parent_title: parentTitleChain,
|
||||
full_title: fullTitle
|
||||
};
|
||||
|
||||
flat.push(item);
|
||||
}
|
||||
if (children && children.length > 0) {
|
||||
flat = flat.concat(flattenRoutes(children, route))
|
||||
}
|
||||
});
|
||||
|
||||
return flat;
|
||||
}
|
||||
const flatRoutes = flattenRoutes(routers)
|
||||
const selectedRoute = ref('')
|
||||
const handleRouteSelect = (name:any) => {
|
||||
if (name) {
|
||||
router.push({ name })
|
||||
isMenuSearch.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 检测登录 start
|
||||
const detectionLoginDialog = ref(false)
|
||||
const comparisonToken = ref('')
|
||||
const comparisonSiteId = ref('')
|
||||
if (storage.get('comparisonTokenStorage')) {
|
||||
comparisonToken.value = storage.get('comparisonTokenStorage')
|
||||
// storage.remove(['comparisonTokenStorage']);
|
||||
}
|
||||
if (storage.get('comparisonSiteIdStorage')) {
|
||||
comparisonSiteId.value = storage.get('comparisonSiteIdStorage')
|
||||
// storage.remove(['comparisonSiteIdStorage']);
|
||||
}
|
||||
// 监听标签页面切换
|
||||
document.addEventListener('visibilitychange', e => {
|
||||
if (document.visibilityState === 'visible' && (comparisonSiteId.value != storage.get('siteId') || comparisonToken.value != storage.get('token'))) {
|
||||
detectionLoginDialog.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const detectionLoginFn = () => {
|
||||
detectionLoginDialog.value = false
|
||||
location.href = `${location.origin}/site/`
|
||||
}
|
||||
// 检测登录 end
|
||||
|
||||
onMounted(() => {
|
||||
// 监听窗体宽度变化
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
screenWidth.value = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
|
||||
})()
|
||||
}
|
||||
})
|
||||
|
||||
// watch(screenWidth, () => {
|
||||
// if (screenWidth.value < 992) {
|
||||
// if (!systemStore.menuIsCollapse) systemStore.toggleMenuCollapse(true)
|
||||
// } else {
|
||||
// if (systemStore.menuIsCollapse) systemStore.toggleMenuCollapse(false)
|
||||
// }
|
||||
// })
|
||||
|
||||
// 菜单栏展开折叠
|
||||
const toggleMenuCollapse = () => {
|
||||
systemStore.$patch((state) => {
|
||||
if (screenWidth.value < 768) {
|
||||
state.menuDrawer = true
|
||||
state.menuIsCollapse = false
|
||||
} else {
|
||||
systemStore.toggleMenuCollapse(!systemStore.menuIsCollapse)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新路由
|
||||
const refreshRouter = () => {
|
||||
if (!appStore.routeRefreshTag) return
|
||||
appStore.refreshRouterView()
|
||||
}
|
||||
|
||||
// 面包屑导航
|
||||
const breadcrumb = computed(() => {
|
||||
const matched = route.matched.filter(item => { return item.meta.title })
|
||||
if (matched[0] && matched[0].path == '/') matched.splice(0, 1)
|
||||
return matched
|
||||
})
|
||||
|
||||
// 跳转去预览
|
||||
const toPreview = () => {
|
||||
const url = router.resolve({
|
||||
path: '/preview/wap',
|
||||
query: {
|
||||
page:'/'
|
||||
}
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-header{
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
box-shadow: 0 0 4px 0 rgba(0,145,255,0.1);
|
||||
}
|
||||
.navbar-item {
|
||||
padding: 0 8px;
|
||||
&:hover {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
}
|
||||
.index-item {
|
||||
border: 1px solid;
|
||||
border-color: var(--el-color-primary);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
:deep(.el-dropdown-menu__item) {
|
||||
&:focus {
|
||||
background-color: transparent !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
:deep(.inter .el-breadcrumb__inner){
|
||||
font-weight: inherit !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<icon name="element Setting" @click="drawer = true" />
|
||||
|
||||
<el-drawer v-model="drawer" :title="t('layout.layoutSetting')" size="300px">
|
||||
<el-scrollbar>
|
||||
<!-- 黑暗模式 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.darkMode') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="dark" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主题颜色 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.themeColor') }}</div>
|
||||
<div>
|
||||
<el-color-picker v-model="theme" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 标签栏 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.tab') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="tab" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { setThemeColor } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
import storage from "@/utils/storage";
|
||||
|
||||
const drawer = ref(false)
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
const dark = computed({
|
||||
get () {
|
||||
return systemStore.dark
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('dark', val)
|
||||
toggleDark(val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const tab = computed({
|
||||
get () {
|
||||
return systemStore.tab
|
||||
},
|
||||
set (val) {
|
||||
systemStore.$patch((state) => {
|
||||
state.tab = val
|
||||
storage.set({ key: 'tab', data: val })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const theme = computed({
|
||||
get () {
|
||||
return systemStore.theme
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('theme', val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.layout-style {
|
||||
&>div:nth-child(2n+2) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-dropdown @command="switchLang" :tabindex="1">
|
||||
<icon name="iconfont iconfanyi" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="zh-cn" :disabled="systemStore.lang == 'zh-cn'">简体中文</el-dropdown-item>
|
||||
<el-dropdown-item command="en" :disabled="systemStore.lang == 'en'">English</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { language } from '@/lang'
|
||||
import { useRoute } from 'vue-router'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const route = useRoute()
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const switchLang = (command: string) => {
|
||||
systemStore.$patch((state) => {
|
||||
state.lang = command
|
||||
storage.set({ key: 'lang', data: command })
|
||||
})
|
||||
language.loadLocaleMessages(route.path, systemStore.lang)
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
159
admin-vben/src/layout/default/components/header/user-info.vue
Normal file
159
admin-vben/src/layout/default/components/header/user-info.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dropdown @command="clickEvent" :tabindex="1">
|
||||
<div class="userinfo flex h-full items-center">
|
||||
<el-avatar v-if="userStore.userInfo.head_img" :size="25" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
|
||||
<img v-else src="@/app/assets/images/member_head.png" class="w-[25px] rounded-full" />
|
||||
<div class="user-name pl-[8px]">{{ userStore.userInfo.username }}</div>
|
||||
<icon name="element ArrowDown" class="ml-[5px]" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<div class="p-[10px]">
|
||||
<div class="userinfo flex h-full items-center pb-[10px] border-b-[1px] border-solid border-[#e5e5e5]">
|
||||
<el-avatar v-if="userStore.userInfo.head_img" :size="45" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
|
||||
<img v-else src="@/app/assets/images/member_head.png" class="w-[45px] rounded-full" />
|
||||
<div>
|
||||
<div class="user-name pl-[8px] text-[14px]">{{ userStore.userInfo.username }}</div>
|
||||
<div class="pl-[8px] text-[13px] text-[#9699B6]">个人中心</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="toLink('/home/index')" v-if="isAllowChange">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconqiehuan ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">切换站点</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="getUserInfoFn">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconshezhi1 ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">账号设置</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="changePasswordDialog=true">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconxiugai ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">修改密码</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="logout">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont icontuichudenglu ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">退出登录</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码">
|
||||
<div>
|
||||
<el-form :model="saveInfo" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-form-item :label="t('originalPassword')" prop="original_password">
|
||||
<el-input v-model="saveInfo.original_password" type="password" :placeholder="t('originalPasswordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('newPassword')" prop="password">
|
||||
<el-input v-model="saveInfo.password" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
<div class="form-tip">{{t('passwordTip')}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('passwordCopy')" prop="password_copy">
|
||||
<el-input v-model="saveInfo.password_copy" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="changePasswordDialog = false">{{t('cancel')}}</el-button>
|
||||
<el-button type="primary" @click="submitForm(formRef)">{{t('save')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<user-info-edit ref="userInfoEditRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { FormInstance, FormRules, ElNotification } from 'element-plus'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { setUserInfo } from '@/app/api/personal'
|
||||
import { t } from '@/lang'
|
||||
import userInfoEdit from '@/app/components/user-info-edit/index.vue'
|
||||
import { img } from '@/utils/common'
|
||||
|
||||
const isAllowChange = localStorage.getItem('isAllowChange') === 'true';
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const clickEvent = (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
userStore.logout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userStore.logout()
|
||||
}
|
||||
const toLink = (link) => {
|
||||
router.push(link)
|
||||
}
|
||||
const userInfoEditRef = ref(null)
|
||||
const getUserInfoFn = () => {
|
||||
userInfoEditRef.value?.open()
|
||||
}
|
||||
// 修改密码 --- start
|
||||
const changePasswordDialog = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
// 提交信息
|
||||
const saveInfo = reactive({
|
||||
original_password: '',
|
||||
password: '',
|
||||
password_copy: ''
|
||||
})
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
original_password: [
|
||||
{ required: true, message: t('originalPasswordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password_copy: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
let msg = ''
|
||||
if (saveInfo.password && !saveInfo.original_password) msg = t('originalPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && !saveInfo.password_copy) msg = t('newPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && saveInfo.password_copy && saveInfo.password != saveInfo.password_copy) msg = t('doubleCipherHint')
|
||||
if (msg) {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: msg
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setUserInfo(saveInfo).then((res: any) => {
|
||||
changePasswordDialog.value = false
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-popper .el-dropdown-menu{
|
||||
width: 150px;
|
||||
}
|
||||
</style>
|
||||
142
admin-vben/src/layout/default/components/tabs.vue
Normal file
142
admin-vben/src/layout/default/components/tabs.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="tab-wrap w-full px-[16px]" v-show="systemStore.tab">
|
||||
<el-tabs :closable="tabbarStore.tabLength > 1" :model-value="route.name" @tab-click="tabClick" @tab-remove="removeTab">
|
||||
<el-tab-pane v-for="(tab, key, index) in tabbarStore.tabs" :name="tab.name" :key="index">
|
||||
<template #label>
|
||||
<el-dropdown trigger="contextmenu" placement="bottom-start">
|
||||
<span :class="{ 'text-primary': route.name == tab.name }" class="tab-name">{{ tab.title }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Back" :disabled="index == 0" @click="closeLeft(tab.name)">{{t('tabs.closeLeft') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Right" :disabled="index == (tabbarStore.tabLength - 1)" @click="closeRight(tab.name)">{{t('tabs.closeRight') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Close" :disabled="tabbarStore.tabLength == 1" @click="closeOther(tab.name)">{{t('tabs.closeOther') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted } from 'vue'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const tabbarStore = useTabbarStore()
|
||||
const systemStore = useSystemStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
tabbarStore.addTab(route)
|
||||
})
|
||||
|
||||
watch(route, (nval: any) => {
|
||||
tabbarStore.addTab(nval)
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加tab
|
||||
* @param content
|
||||
*/
|
||||
const tabClick = (content: any) => {
|
||||
const tabRoute = tabbarStore.tabs[content.props.name]
|
||||
router.push({ name: tabRoute.name, query: tabRoute.query })
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除tab
|
||||
* @param content
|
||||
*/
|
||||
const removeTab = (content: any) => {
|
||||
if (route.name == content) {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
if (tabs.indexOf(content) == 0) {
|
||||
router.push({ name: tabs[1] })
|
||||
} else {
|
||||
router.push({ name: tabs[tabs.indexOf(content) - 1] })
|
||||
}
|
||||
}
|
||||
tabbarStore.removeTab(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭左侧
|
||||
* @param name
|
||||
*/
|
||||
const closeLeft = (name: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(name) - 1; i >= 0; i--) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ name })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧
|
||||
* @param name
|
||||
*/
|
||||
const closeRight = (name: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(name) + 1; i < tabs.length; i++) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ name })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭其他
|
||||
* @param name
|
||||
*/
|
||||
const closeOther = (name: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
tabs.forEach((key: string) => { key != name && delete tabbarStore.tabs[key] })
|
||||
router.push({ name })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs) {
|
||||
.el-tabs--border-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
margin-bottom: 0;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
display: inline-flex !important;
|
||||
padding: 0 20px !important;
|
||||
align-items: center;
|
||||
|
||||
.tab-name:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
50
admin-vben/src/layout/default/index.vue
Normal file
50
admin-vben/src/layout/default/index.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<!-- 左侧边栏 -->
|
||||
<layout-aside></layout-aside>
|
||||
<!-- 左侧边栏 end -->
|
||||
|
||||
<el-container>
|
||||
<!-- 顶部 -->
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
<!-- 顶部 end -->
|
||||
|
||||
<layout-tab />
|
||||
|
||||
<!-- 主体 -->
|
||||
<el-main class="h-full p-0 bg-page">
|
||||
<el-scrollbar>
|
||||
<div class="p-[15px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<!-- 主体 end -->
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import layoutHeader from './components/header/index.vue'
|
||||
import layoutAside from './components/aside/index.vue'
|
||||
import layoutTab from './components/tabs.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tabbarStore = useTabbarStore()
|
||||
const systemStore = useSystemStore()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
4
admin-vben/src/layout/default/layout.json
Normal file
4
admin-vben/src/layout/default/layout.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"layout": "default",
|
||||
"cover": "/static/resource/images/system/layout_default.png"
|
||||
}
|
||||
53
admin-vben/src/layout/index.vue
Normal file
53
admin-vben/src/layout/index.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<component :is="layout" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, markRaw, defineAsyncComponent, provide } from 'vue'
|
||||
import { getAppType } from '@/utils/common'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import Storage from '@/utils/storage'
|
||||
|
||||
const sysLayout = import.meta.glob('./*/index.vue')
|
||||
const addonLayout = import.meta.glob('@/addon/**/layout/index.vue')
|
||||
const modules = Object.assign(sysLayout, addonLayout)
|
||||
|
||||
let siteLayout = 'default'
|
||||
switch (getAppType()) {
|
||||
case 'admin':
|
||||
siteLayout = Storage.get('admin_layout') || 'admin'
|
||||
break
|
||||
default:
|
||||
const siteInfo = useUserStore().siteInfo
|
||||
if (siteInfo && siteInfo.apps) {
|
||||
const layouts = useSystemStore().layoutConfig
|
||||
if (siteInfo.apps.length == 1) {
|
||||
layouts[siteInfo.apps[0].key] != undefined && (siteLayout = layouts[siteInfo.apps[0].key])
|
||||
} else {
|
||||
layouts.system != undefined && (siteLayout = layouts.system)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const layout = ref<any>(null)
|
||||
|
||||
Object.keys(modules).forEach(key => {
|
||||
key.indexOf(`/${siteLayout}/`) !== -1 && (layout.value = markRaw(defineAsyncComponent(modules[key])))
|
||||
})
|
||||
|
||||
!layout.value && (layout.value = markRaw(defineAsyncComponent(modules['./default/index.vue'])))
|
||||
|
||||
/**
|
||||
* 监听某些页面需要独立配置布局
|
||||
*/
|
||||
provide('setLayout', (name: any) => {
|
||||
if (siteLayout == name) return
|
||||
siteLayout = name
|
||||
Object.keys(modules).forEach(key => {
|
||||
key.indexOf(`/${name}/`) !== -1 && (layout.value = markRaw(defineAsyncComponent(modules[key])))
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
43
admin-vben/src/layout/profession/components/aside/index.vue
Normal file
43
admin-vben/src/layout/profession/components/aside/index.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<el-aside :class="['layout-aside w-auto h-screen', { 'bright': !dark }]">
|
||||
<side class="hidden-xs-only" />
|
||||
</el-aside>
|
||||
|
||||
<el-drawer v-model="systemStore.menuDrawer" direction="ltr" :with-header="false" custom-class="aside-drawer" size="210px">
|
||||
<template #default>
|
||||
<side />
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import side from './side.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const systemStore = useSystemStore()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
|
||||
watch(route, () => {
|
||||
systemStore.$patch(state => {
|
||||
state.menuDrawer = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.layout-aside {
|
||||
border-right: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.aside-drawer {
|
||||
|
||||
.el-drawer__body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<template v-if="meta.show">
|
||||
<el-sub-menu v-if="routes.children" :index="String(routes.name)">
|
||||
<template #title>
|
||||
<span :class="['ml-[10px]']">{{ meta.title }}</span>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :key="index" />
|
||||
</el-sub-menu>
|
||||
<template v-else>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-if="meta.addon && meta.parent_route && meta.parent_route.addon == ''">
|
||||
<template #title>
|
||||
<span :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })" v-else>
|
||||
<template #title>
|
||||
<span :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
<div v-if="routes.is_border" class="!border-0 !border-t-[1px] border-solid mx-[25px] bg-[#f7f7f7] my-[5px]"></div>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import menuItem from './menu-item.vue'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
|
||||
const router = useRouter()
|
||||
const props = defineProps({
|
||||
routes: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const userStore = useUserStore()
|
||||
const siteInfo = userStore.siteInfo
|
||||
const meta = computed(() => props.routes.meta)
|
||||
|
||||
const addons = computed(() => {
|
||||
const addons:Record<string, any> = {}
|
||||
siteInfo?.apps.forEach((item: any) => { addons[item.key] = item })
|
||||
siteInfo?.site_addons.forEach((item: any) => { addons[item.key] = item })
|
||||
return addons
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-sub-menu {
|
||||
|
||||
.el-icon {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
361
admin-vben/src/layout/profession/components/aside/side.vue
Normal file
361
admin-vben/src/layout/profession/components/aside/side.vue
Normal file
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<el-container class="w-100 h-screen">
|
||||
<el-main class="flex p-0">
|
||||
<div class="one-menu w-[124px] h-screen px-[8px] bg-[#1c2233]">
|
||||
<el-header class="logo-wrap">
|
||||
<div class="logo flex items-center m-auto h-[64px]" v-if="!systemStore.menuIsCollapse">
|
||||
<el-image style="width: 40px; height: 40px" :src="img(logoUrl)" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-[40px]">
|
||||
<img class="max-w-[40px]" src="@/app/assets/images/icon-addon-one.png" alt="" object-fit="contain">
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="logo flex items-center justify-center h-[64px]" v-else>
|
||||
<i class="text-3xl iconfont iconyunkongjian"></i>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<el-scrollbar class="h-[calc( 100vh - 64px )]">
|
||||
<el-menu :default-active="oneMenuActive" :router="true" class="aside-menu" :unique-opened="true" :collapse="systemStore.menuIsCollapse">
|
||||
<template v-for="(item, index) in oneMenuData" :key="index">
|
||||
<el-menu-item :index="item.original_name" @click="router.push({ name: item.name })" v-if="item.meta.show">
|
||||
<div v-if="item.meta.icon" class="w-[16px] h-[16px] relative flex justify-center">
|
||||
<el-image class="w-[16px] h-[16px] rounded-[50%] overflow-hidden" :src="item.meta.icon" fit="fill" v-if="isUrl(item.meta.icon)"/>
|
||||
<icon :name="item.meta.icon" class="absolute top-[50%] -translate-y-[50%]" v-else />
|
||||
</div>
|
||||
<div v-else class="w-[16px] h-[16px]"></div>
|
||||
<template #title>
|
||||
<div class="relative flex-1 w-0">
|
||||
<span class="ml-[10px] w-full truncate">{{ item.meta.short_title || item.meta.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
<el-scrollbar v-if="twoMenuData.length" class="two-menu w-[132px]">
|
||||
<div class="w-[132px] h-[64px] flex items-center justify-center text-[16px] border-b-[1px] border-solid border-[var(--el-border-color-lighter)]">
|
||||
{{ route.matched[1].meta.title }}
|
||||
</div>
|
||||
|
||||
<el-menu class="aside-menu" :default-active="route.name" :default-openeds="menuOption" :router="true" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in twoMenuData" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, onMounted, watchEffect } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { getShowApp, getShowMarketing } from '@/app/api/site'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { img, isUrl } from '@/utils/common'
|
||||
import { findFirstValidRoute } from '@/router/routers'
|
||||
import menuItem from './menu-item.vue'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const siteInfo = userStore.siteInfo
|
||||
const routers = userStore.routers
|
||||
const addonIndexRoute = userStore.addonIndexRoute
|
||||
|
||||
const oneMenuData = ref<Record<string, any>[]>([])
|
||||
const twoMenuData = ref<Record<string, any>[]>([])
|
||||
const addonRouters: Record<string, any> = {}
|
||||
const logoUrl = computed(() => {
|
||||
return userStore.siteInfo.icon ? userStore.siteInfo.icon : systemStore.website.icon
|
||||
})
|
||||
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon == '') {
|
||||
if (item.meta.attr == '') {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
}
|
||||
oneMenuData.value.push(item)
|
||||
}
|
||||
} else if (item.meta.addon != '' && siteInfo?.apps.length <= 1 && siteInfo?.apps[0].key == item.meta.addon && item.meta.show) {
|
||||
if (item.children) {
|
||||
item.children.forEach((citem: Record<string, any>) => {
|
||||
citem.original_name = citem.name
|
||||
if (citem.children && citem.children.length) {
|
||||
citem.name = findFirstValidRoute(citem.children)
|
||||
}
|
||||
})
|
||||
oneMenuData.value.unshift(...item.children)
|
||||
} else {
|
||||
oneMenuData.value.unshift(item)
|
||||
}
|
||||
} else {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// oneMenuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
})
|
||||
// 多应用时将应用插入菜单
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
const routers:Record<string, any>[] = []
|
||||
siteInfo?.apps.forEach((item: Record<string, any>) => {
|
||||
if (addonRouters[item.key]) {
|
||||
addonRouters[item.key].name = addonIndexRoute[item.key]
|
||||
routers.push(addonRouters[item.key])
|
||||
}
|
||||
})
|
||||
oneMenuData.value.unshift(...routers)
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// oneMenuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
const appList = ref(null)
|
||||
const marketingList = ref(null)
|
||||
// const loading = ref(true);
|
||||
const oneMenuActive = ref(route.matched[1].name)
|
||||
|
||||
const getAppList = async () => {
|
||||
const res = await getShowApp()
|
||||
appList.value = res.data
|
||||
// loading.value = false;
|
||||
}
|
||||
const getMarketingList = async () => {
|
||||
const res = await getShowMarketing()
|
||||
marketingList.value = res.data
|
||||
}
|
||||
onMounted(async () => {
|
||||
await getAppList() // 确保数据先加载
|
||||
await getMarketingList()
|
||||
})
|
||||
|
||||
// 让二级菜单默认展开
|
||||
const menuOption = ref([])
|
||||
const secondMenuShowWayFn = () => {
|
||||
menuOption.value = []
|
||||
if (oneMenuActive.value !== 'active' && oneMenuActive.value !== 'addon' && twoMenuData.value && Object.values(twoMenuData.value).length) {
|
||||
const data = cloneDeep(twoMenuData.value)
|
||||
for (const key in data) {
|
||||
menuOption.value.push(data[key].name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
// if (!appList.value || loading.value) return; // 确保数据加载完毕
|
||||
const addonKeys = appList.value?.addon?.list?.map(item => item.key) ?? []
|
||||
const toolKeys = appList.value?.tool?.list?.map(item => item.key) ?? []
|
||||
const allKeys = [...addonKeys, ...toolKeys]
|
||||
const marketingKeys = marketingList.value?.marketing?.list?.map(item => item.key) ?? []
|
||||
const matchedName = route.matched[1]?.name
|
||||
if (allKeys.includes(matchedName)) {
|
||||
oneMenuActive.value = 'addon'
|
||||
twoMenuData.value = route.matched[1]?.children ?? []
|
||||
} else if (marketingKeys.includes(matchedName)) {
|
||||
oneMenuActive.value = 'active'
|
||||
twoMenuData.value = route.matched[1]?.children ?? []
|
||||
} else if (route.meta.attr !== '') {
|
||||
oneMenuActive.value = route.matched[2]?.name
|
||||
twoMenuData.value = route.matched[1]?.children ?? []
|
||||
} else {
|
||||
// 多应用
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
twoMenuData.value = route.matched[1]?.children
|
||||
oneMenuActive.value = route.matched[1]?.name
|
||||
} else {
|
||||
// 单应用
|
||||
const oneMenu = route.matched[1]
|
||||
if (oneMenu.meta.addon === '') {
|
||||
oneMenuActive.value = route.matched[1]?.name
|
||||
twoMenuData.value = route.matched[1]?.children ?? []
|
||||
} else {
|
||||
if (oneMenu.meta.addon === siteInfo?.apps[0]?.key) {
|
||||
oneMenuActive.value = route.matched[2]?.name
|
||||
twoMenuData.value = route.matched[2]?.children ?? []
|
||||
} else {
|
||||
oneMenuActive.value = route.matched[1]?.name
|
||||
twoMenuData.value = route.matched[1]?.children ?? []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
secondMenuShowWayFn()
|
||||
})
|
||||
|
||||
// watch(route, () => {
|
||||
|
||||
// if (route.meta.attr != '') {
|
||||
// oneMenuActive.value = route.matched[2].name
|
||||
// twoMenuData.value = route.matched[1].children ?? []
|
||||
// } else {
|
||||
// // 多应用
|
||||
// if (siteInfo?.apps.length > 1) {
|
||||
// twoMenuData.value = route.matched[1].children
|
||||
// oneMenuActive.value = route.matched[1].name
|
||||
// } else {
|
||||
// // 单应用
|
||||
// const oneMenu = route.matched[1]
|
||||
// if (oneMenu.meta.addon == '') {
|
||||
// oneMenuActive.value = route.matched[1].name
|
||||
// twoMenuData.value = route.matched[1].children ?? []
|
||||
// } else {
|
||||
// if (oneMenu.meta.addon == siteInfo?.apps[0].key) {
|
||||
// oneMenuActive.value = route.matched[2].name
|
||||
// twoMenuData.value = route.matched[2].children ?? []
|
||||
// } else {
|
||||
// oneMenuActive.value = route.matched[1].name
|
||||
// twoMenuData.value = route.matched[1].children ?? []
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.one-menu {
|
||||
|
||||
.aside-menu:not(.el-menu--collapse) {
|
||||
background-color: transparent;
|
||||
|
||||
.el-menu-item {
|
||||
font-size: 14px;
|
||||
height: 40px;
|
||||
margin-bottom: 4px;
|
||||
padding-left: 12px !important;
|
||||
color: rgba(255,255,255,.7);
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: #fff !important;
|
||||
background-color: var(--el-color-primary) !important;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.el-scrollbar {
|
||||
height: calc(100vh - 65px);
|
||||
}
|
||||
}
|
||||
|
||||
.two-menu {
|
||||
|
||||
.aside-menu:not(.el-menu--collapse) {
|
||||
width: 132px;
|
||||
padding-top: 16px;
|
||||
border: 0;
|
||||
|
||||
.el-menu-item {
|
||||
height: 36px;
|
||||
margin: 0 12px 4px;
|
||||
padding: 0 !important;
|
||||
border-radius: 2px;
|
||||
|
||||
span {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-sub-menu {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.el-sub-menu__title {
|
||||
height: 36px;
|
||||
margin: 0 8px 4px;
|
||||
padding-left: 0;
|
||||
border-radius: 2px;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
height: 36px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
.el-icon.el-sub-menu__icon-arrow {
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item {
|
||||
padding-left: 20px !important;
|
||||
span{
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-wrap {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
|
||||
.logo {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
291
admin-vben/src/layout/profession/components/header/index.vue
Normal file
291
admin-vben/src/layout/profession/components/header/index.vue
Normal file
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<el-container :class="['h-full px-[10px]',{'layout-header border-b border-color': !dark}]" >
|
||||
<el-row class="w-100 h-full w-full">
|
||||
<el-col :span="10">
|
||||
<div class="left-panel h-full flex items-center">
|
||||
<!-- 左侧菜单折叠 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element Expand" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element Fold" v-else />
|
||||
</div> -->
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element Refresh" />
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px] hidden-xs-only">
|
||||
<el-breadcrumb separator="/">
|
||||
<!-- :to="route.path" class="inter" 这种修改方式导致部分跳转不对,需要重新调整菜单才可以 -->
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index" :to="route.path" class="inter">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<div class="right-panel h-full flex items-center justify-end">
|
||||
<div class="flex items-center flex-shrink-0 hidden-xs-only">
|
||||
<el-dropdown trigger="hover" :hide-on-click="false" popper-class="site-info-wrap" class="mr-[8px]">
|
||||
<!-- 状态 -->
|
||||
<div class="mx-[8px] bg-[#f6f6f6] border-[1px] border-solid border-[#eee] rounded-[4px] px-[9px] py-[6px] flex items-center">
|
||||
<span class="mr-[6px] text-[12px] !text-[#333]">{{siteInfo.site_name}}</span>
|
||||
<span class="!text-[10px] text-[#f56c6c]" :class="{'!text-[#67c23a]': siteInfo.status == 1, '!text-[#f56c6c]': siteInfo.status == 3}">{{ siteInfo.status_name }}</span>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
<!-- 站点id -->
|
||||
<div class="text-[14px]">站点编号:{{siteInfo.site_id}}</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<!-- 到期时间 -->
|
||||
<div v-if="siteInfo.expire_time == 0" class="text-[14px]">到期时间:永久</div>
|
||||
<div v-else class="text-[14px]">到期时间:{{ siteInfo.expire_time }}</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div class="flex items-center flex-shrink-0 hidden-xs-only">
|
||||
<el-popover placement="bottom" :width="330" trigger="click" v-model:visible="isMenuSearch" >
|
||||
<template #reference>
|
||||
<i class="iconfont icona-sousuoV6xx-36 cursor-pointer px-[8px] !text-[14px]"></i>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="flex items-center">
|
||||
<el-select v-model="selectedRoute" filterable class="!w-[250px] mr-[20px] menu-select" :teleported="false" clearable @change="handleRouteSelect">
|
||||
<el-option v-for="item in flatRoutes" :key="item.name" :label="item.full_title" :value="item.name" >
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button type="primary" link @click="isMenuSearch = false">{{t('取消')}}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
|
||||
<!-- 预览 只有站点时展示-->
|
||||
<i class="iconfont iconicon_huojian1 cursor-pointer px-[8px]" :title="t('visitWap')" @click="toPreview"></i>
|
||||
<i class="iconfont iconlingdang-xianxing cursor-pointer px-[8px]" :title="t('newInfo')" v-if="appType == 'site'"></i>
|
||||
<!-- 切换语言 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<switch-lang />
|
||||
</div> -->
|
||||
<!-- 切换全屏 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleFullscreen">
|
||||
<icon name="iconfont icontuichuquanping" v-if="isFullscreen" />
|
||||
<icon name="iconfont iconquanping" v-else />
|
||||
</div> -->
|
||||
<!-- 布局设置 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detectionLoginFn">{{ t('layout.detectionLoginOperation') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import layoutSetting from './layout-setting.vue'
|
||||
import userInfo from './user-info.vue'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const appType = storage.get('app_type')
|
||||
const { toggle: toggleFullscreen } = useFullscreen()
|
||||
const systemStore = useSystemStore()
|
||||
const appStore = useAppStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const screenWidth = ref(window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth)
|
||||
|
||||
const userStore = useUserStore()
|
||||
const siteInfo:any = computed(() => {
|
||||
return userStore.siteInfo
|
||||
})
|
||||
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
const isMenuSearch = ref(false)
|
||||
const routers = userStore.routers
|
||||
const getParentTitleChain=(meta:any) =>{
|
||||
let titles = []
|
||||
let current = meta?.parent_route
|
||||
|
||||
while (current) {
|
||||
if (current.short_title) {
|
||||
titles.unshift(current.short_title)
|
||||
}
|
||||
current = current.parent_route
|
||||
}
|
||||
|
||||
return titles.join(' - ')
|
||||
}
|
||||
const flattenRoutes = (routes:any, parent = null)=> {
|
||||
let flat = [];
|
||||
routes.forEach(route => {
|
||||
const { path, name, meta = {}, short_title, children } = route
|
||||
const isLeaf = meta.type ==1 && meta.show==1
|
||||
if(isLeaf){
|
||||
const title = meta.title || short_title || ''
|
||||
const parentTitleChain = getParentTitleChain(meta)
|
||||
const fullTitle = parentTitleChain ? `${parentTitleChain} - ${title}` : title
|
||||
const item = {
|
||||
path,
|
||||
name,
|
||||
title,
|
||||
parent_title: parentTitleChain,
|
||||
full_title: fullTitle
|
||||
};
|
||||
|
||||
flat.push(item);
|
||||
}
|
||||
if (children && children.length > 0) {
|
||||
flat = flat.concat(flattenRoutes(children, route))
|
||||
}
|
||||
});
|
||||
|
||||
return flat;
|
||||
}
|
||||
const flatRoutes = flattenRoutes(routers)
|
||||
const selectedRoute = ref('')
|
||||
const handleRouteSelect = (name:any) => {
|
||||
if (name) {
|
||||
router.push({ name })
|
||||
isMenuSearch.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 检测登录 start
|
||||
const detectionLoginDialog = ref(false)
|
||||
const comparisonToken = ref('')
|
||||
const comparisonSiteId = ref('')
|
||||
if (storage.get('comparisonTokenStorage')) {
|
||||
comparisonToken.value = storage.get('comparisonTokenStorage')
|
||||
// storage.remove(['comparisonTokenStorage']);
|
||||
}
|
||||
if (storage.get('comparisonSiteIdStorage')) {
|
||||
comparisonSiteId.value = storage.get('comparisonSiteIdStorage')
|
||||
// storage.remove(['comparisonSiteIdStorage']);
|
||||
}
|
||||
// 监听标签页面切换
|
||||
document.addEventListener('visibilitychange', e => {
|
||||
if (document.visibilityState === 'visible' && (comparisonSiteId.value != storage.get('siteId') || comparisonToken.value != storage.get('token'))) {
|
||||
detectionLoginDialog.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const detectionLoginFn = () => {
|
||||
detectionLoginDialog.value = false
|
||||
location.href = `${location.origin}/site/`
|
||||
}
|
||||
// 检测登录 end
|
||||
|
||||
onMounted(() => {
|
||||
// 监听窗体宽度变化
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
screenWidth.value = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
|
||||
})()
|
||||
}
|
||||
})
|
||||
|
||||
// watch(screenWidth, () => {
|
||||
// if (screenWidth.value < 992) {
|
||||
// if (!systemStore.menuIsCollapse) systemStore.toggleMenuCollapse(true)
|
||||
// } else {
|
||||
// if (systemStore.menuIsCollapse) systemStore.toggleMenuCollapse(false)
|
||||
// }
|
||||
// })
|
||||
|
||||
// 菜单栏展开折叠
|
||||
// const toggleMenuCollapse = () => {
|
||||
// systemStore.$patch((state) => {
|
||||
// if (screenWidth.value < 768) {
|
||||
// state.menuDrawer = true
|
||||
// state.menuIsCollapse = false
|
||||
// } else {
|
||||
// systemStore.toggleMenuCollapse(!systemStore.menuIsCollapse)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// 刷新路由
|
||||
const refreshRouter = () => {
|
||||
if (!appStore.routeRefreshTag) return
|
||||
appStore.refreshRouterView()
|
||||
}
|
||||
|
||||
// 面包屑导航
|
||||
const breadcrumb = computed(() => {
|
||||
// console.log(route.matched)
|
||||
const matched = route.matched.filter(item => { return item.meta.title })
|
||||
if (matched[0] && matched[0].path == '/') matched.splice(0, 1)
|
||||
return matched
|
||||
})
|
||||
|
||||
// 跳转去预览
|
||||
const toPreview = () => {
|
||||
const url = router.resolve({
|
||||
path: '/preview/wap',
|
||||
query: {
|
||||
page:'/'
|
||||
}
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-header{
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
box-shadow: 0 0 4px 0 rgba(0,145,255,0.1);
|
||||
}
|
||||
.navbar-item {
|
||||
padding: 0 8px;
|
||||
&:hover {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
}
|
||||
.index-item {
|
||||
border: 1px solid;
|
||||
border-color: var(--el-color-primary);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
:deep(.el-dropdown-menu__item) {
|
||||
&:focus {
|
||||
background-color: transparent !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
:deep(.inter .el-breadcrumb__inner){
|
||||
font-weight: inherit !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<icon name="element Setting" @click="drawer = true" />
|
||||
|
||||
<el-drawer v-model="drawer" :title="t('layout.layoutSetting')" size="300px">
|
||||
<el-scrollbar>
|
||||
<!-- 黑暗模式 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.darkMode') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="dark" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主题颜色 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.themeColor') }}</div>
|
||||
<div>
|
||||
<el-color-picker v-model="theme" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 标签栏 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.tab') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="tab" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { setThemeColor } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
import storage from "@/utils/storage";
|
||||
|
||||
const drawer = ref(false)
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
const dark = computed({
|
||||
get () {
|
||||
return systemStore.dark
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('dark', val)
|
||||
toggleDark(val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const tab = computed({
|
||||
get () {
|
||||
return systemStore.tab
|
||||
},
|
||||
set (val) {
|
||||
systemStore.$patch((state) => {
|
||||
state.tab = val
|
||||
storage.set({ key: 'tab', data: val })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const theme = computed({
|
||||
get () {
|
||||
return systemStore.theme
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('theme', val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.layout-style {
|
||||
&>div:nth-child(2n+2) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-dropdown @command="switchLang" :tabindex="1">
|
||||
<icon name="iconfont iconfanyi" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="zh-cn" :disabled="systemStore.lang == 'zh-cn'">简体中文</el-dropdown-item>
|
||||
<el-dropdown-item command="en" :disabled="systemStore.lang == 'en'">English</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { language } from '@/lang'
|
||||
import { useRoute } from 'vue-router'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const route = useRoute()
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const switchLang = (command: string) => {
|
||||
systemStore.$patch((state) => {
|
||||
state.lang = command
|
||||
storage.set({ key: 'lang', data: command })
|
||||
})
|
||||
language.loadLocaleMessages(route.path, systemStore.lang)
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
158
admin-vben/src/layout/profession/components/header/user-info.vue
Normal file
158
admin-vben/src/layout/profession/components/header/user-info.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dropdown @command="clickEvent" :tabindex="1">
|
||||
<div class="userinfo flex h-full items-center">
|
||||
<el-avatar v-if="userStore.userInfo.head_img" :size="25" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
|
||||
<img v-else src="@/app/assets/images/member_head.png" class="w-[25px] rounded-full" />
|
||||
<div class="user-name pl-[8px]">{{ userStore.userInfo.username }}</div>
|
||||
<icon name="element ArrowDown" class="ml-[5px]" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<div class="p-[10px]">
|
||||
<div class="userinfo flex h-full items-center pb-[10px] border-b-[1px] border-solid border-[#e5e5e5]">
|
||||
<el-avatar v-if="userStore.userInfo.head_img" :size="45" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
|
||||
<img v-else src="@/app/assets/images/member_head.png" class="w-[45px] rounded-full" />
|
||||
<div>
|
||||
<div class="user-name pl-[8px] text-[14px]">{{ userStore.userInfo.username }}</div>
|
||||
<div class="pl-[8px] text-[13px] text-[#9699B6]">个人中心</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="toLink('/home/index')" v-if="isAllowChange">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconqiehuan ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">切换站点</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="getUserInfoFn">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconshezhi1 ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">账号设置</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="changePasswordDialog=true">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconxiugai ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">修改密码</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="logout">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont icontuichudenglu ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">退出登录</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码">
|
||||
<div>
|
||||
<el-form :model="saveInfo" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-form-item :label="t('originalPassword')" prop="original_password">
|
||||
<el-input v-model="saveInfo.original_password" type="password" :placeholder="t('originalPasswordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('newPassword')" prop="password">
|
||||
<el-input v-model="saveInfo.password" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
<div class="form-tip">{{t('passwordTip')}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('passwordCopy')" prop="password_copy">
|
||||
<el-input v-model="saveInfo.password_copy" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="changePasswordDialog = false">{{t('cancel')}}</el-button>
|
||||
<el-button type="primary" @click="submitForm(formRef)">{{t('save')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<user-info-edit ref="userInfoEditRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { FormInstance, FormRules, ElNotification } from 'element-plus'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { setUserInfo } from '@/app/api/personal'
|
||||
import { t } from '@/lang'
|
||||
import userInfoEdit from '@/app/components/user-info-edit/index.vue'
|
||||
import { img } from '@/utils/common'
|
||||
|
||||
const isAllowChange = localStorage.getItem('isAllowChange') === 'true';
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const clickEvent = (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
userStore.logout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userStore.logout()
|
||||
}
|
||||
const toLink = (link) => {
|
||||
router.push(link)
|
||||
}
|
||||
const userInfoEditRef = ref(null)
|
||||
const getUserInfoFn = () => {
|
||||
userInfoEditRef.value?.open()
|
||||
}
|
||||
// 修改密码 --- start
|
||||
const changePasswordDialog = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
// 提交信息
|
||||
const saveInfo = reactive({
|
||||
original_password: '',
|
||||
password: '',
|
||||
password_copy: ''
|
||||
})
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
original_password: [
|
||||
{ required: true, message: t('originalPasswordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password_copy: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
let msg = ''
|
||||
if (saveInfo.password && !saveInfo.original_password) msg = t('originalPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && !saveInfo.password_copy) msg = t('newPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && saveInfo.password_copy && saveInfo.password != saveInfo.password_copy) msg = t('doubleCipherHint')
|
||||
if (msg) {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: msg
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setUserInfo(saveInfo).then((res: any) => {
|
||||
changePasswordDialog.value = false
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-popper .el-dropdown-menu{
|
||||
width: 150px;
|
||||
}
|
||||
</style>
|
||||
142
admin-vben/src/layout/profession/components/tabs.vue
Normal file
142
admin-vben/src/layout/profession/components/tabs.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="tab-wrap w-full px-[16px]" v-show="systemStore.tab">
|
||||
<el-tabs :closable="tabbarStore.tabLength > 1" :model-value="route.name" @tab-click="tabClick" @tab-remove="removeTab">
|
||||
<el-tab-pane v-for="(tab, key, index) in tabbarStore.tabs" :name="tab.name" :key="index">
|
||||
<template #label>
|
||||
<el-dropdown trigger="contextmenu" placement="bottom-start">
|
||||
<span :class="{ 'text-primary': route.name == tab.name }" class="tab-name">{{ tab.title }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Back" :disabled="index == 0" @click="closeLeft(tab.name)">{{t('tabs.closeLeft') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Right" :disabled="index == (tabbarStore.tabLength - 1)" @click="closeRight(tab.name)">{{t('tabs.closeRight') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Close" :disabled="tabbarStore.tabLength == 1" @click="closeOther(tab.name)">{{t('tabs.closeOther') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted } from 'vue'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const tabbarStore = useTabbarStore()
|
||||
const systemStore = useSystemStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
tabbarStore.addTab(route)
|
||||
})
|
||||
|
||||
watch(route, (nval: any) => {
|
||||
tabbarStore.addTab(nval)
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加tab
|
||||
* @param content
|
||||
*/
|
||||
const tabClick = (content: any) => {
|
||||
const tabRoute = tabbarStore.tabs[content.props.name]
|
||||
router.push({ name: tabRoute.name, query: tabRoute.query })
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除tab
|
||||
* @param content
|
||||
*/
|
||||
const removeTab = (content: any) => {
|
||||
if (route.name == content) {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
if (tabs.indexOf(content) == 0) {
|
||||
router.push({ name: tabs[1] })
|
||||
} else {
|
||||
router.push({ name: tabs[tabs.indexOf(content) - 1] })
|
||||
}
|
||||
}
|
||||
tabbarStore.removeTab(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭左侧
|
||||
* @param name
|
||||
*/
|
||||
const closeLeft = (name: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(name) - 1; i >= 0; i--) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ name })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧
|
||||
* @param name
|
||||
*/
|
||||
const closeRight = (name: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(name) + 1; i < tabs.length; i++) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ name })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭其他
|
||||
* @param name
|
||||
*/
|
||||
const closeOther = (name: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
tabs.forEach((key: string) => { key != name && delete tabbarStore.tabs[key] })
|
||||
router.push({ name })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs) {
|
||||
.el-tabs--border-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
margin-bottom: 0;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
display: inline-flex !important;
|
||||
padding: 0 20px !important;
|
||||
align-items: center;
|
||||
|
||||
.tab-name:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
50
admin-vben/src/layout/profession/index.vue
Normal file
50
admin-vben/src/layout/profession/index.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<!-- 左侧边栏 -->
|
||||
<layout-aside></layout-aside>
|
||||
<!-- 左侧边栏 end -->
|
||||
|
||||
<el-container>
|
||||
<!-- 顶部 -->
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
<!-- 顶部 end -->
|
||||
|
||||
<layout-tab />
|
||||
|
||||
<!-- 主体 -->
|
||||
<el-main class="h-full p-0 bg-page">
|
||||
<el-scrollbar>
|
||||
<div class="p-[15px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<!-- 主体 end -->
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import layoutHeader from './components/header/index.vue'
|
||||
import layoutAside from './components/aside/index.vue'
|
||||
import layoutTab from './components/tabs.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tabbarStore = useTabbarStore()
|
||||
const systemStore = useSystemStore()
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
4
admin-vben/src/layout/profession/layout.json
Normal file
4
admin-vben/src/layout/profession/layout.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"layout": "profession",
|
||||
"cover": "/static/resource/images/system/layout_profession.png"
|
||||
}
|
||||
Reference in New Issue
Block a user