🧹 清理重复配置文件
- 删除根目录中重复的 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>
|
||||
Reference in New Issue
Block a user